解决了深色模式下tab栏和left-nav的选中状态非深色的问题,以及下拉框 padding 为 6px 的问题,以及深色模式头像 hover-card 的 下拉框头部和 下拉框 item hover 效果问题
This commit is contained in:
1 parent
41b982c7b5
commit
39debab2b3
20 files changed
+1010
-143
No files matched your search
@@ -36,6 +36,12 @@ const props = defineProps<{
|
|||||||
* 关闭时表格自适应内容高度(详情页嵌套场景,随页面滚动)。
|
* 关闭时表格自适应内容高度(详情页嵌套场景,随页面滚动)。
|
||||||
*/
|
*/
|
||||||
stretch?: boolean
|
stretch?: boolean
|
||||||
|
/**
|
||||||
|
* 表格最大高度(px,可选)。
|
||||||
|
* 详情页嵌套子表场景传入(如 400),行数超过时表格内部滚动,
|
||||||
|
* 避免所有行撑开导致页面无限加长。不传则不限制。
|
||||||
|
*/
|
||||||
|
maxHeight?: number
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const options: UseCrudTableOptions<T> = {
|
const options: UseCrudTableOptions<T> = {
|
||||||
@@ -172,6 +178,7 @@ function renderUuid(value: unknown): string {
|
|||||||
stripe
|
stripe
|
||||||
round
|
round
|
||||||
:height="props.stretch ? '100%' : 'auto'"
|
:height="props.stretch ? '100%' : 'auto'"
|
||||||
|
:max-height="props.maxHeight"
|
||||||
:row-config="{ isHover: true }"
|
:row-config="{ isHover: true }"
|
||||||
:column-config="{ resizable: true }"
|
:column-config="{ resizable: true }"
|
||||||
:checkbox-config="{ range: true }"
|
:checkbox-config="{ range: true }"
|
||||||
|
|||||||
+64
-10
@@ -22,19 +22,73 @@ defineProps<{
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- 信息网格:columns 控制列数,title 控制标题,border 控制边框 -->
|
<!-- 信息网格:columns 控制列数,title 控制标题,border 控制边框 -->
|
||||||
<el-descriptions
|
<el-card shadow="never" class="info-grid-card">
|
||||||
:column="columns || 2"
|
<template v-if="title" #header>
|
||||||
:title="title"
|
<div class="info-grid-header">
|
||||||
:border="border ?? true"
|
<span class="info-grid-title-dot" />
|
||||||
class="info-grid"
|
<span class="info-grid-title">{{ title }}</span>
|
||||||
>
|
</div>
|
||||||
<slot />
|
</template>
|
||||||
</el-descriptions>
|
<el-descriptions
|
||||||
|
:column="columns || 2"
|
||||||
|
:border="border ?? true"
|
||||||
|
class="info-grid"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</el-descriptions>
|
||||||
|
</el-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
// 信息网格底部留白,与后续内容分隔
|
// 信息网格卡片:圆角 + 淡边框 + hover 阴影
|
||||||
|
.info-grid-card {
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: box-shadow 0.25s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-card__header) {
|
||||||
|
padding: 14px 20px;
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-card__body) {
|
||||||
|
padding: 16px 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标题栏:左侧主色圆点 + 标题文字
|
||||||
|
.info-grid-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-grid-title-dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 描述网格:标签区更紧凑
|
||||||
.info-grid {
|
.info-grid {
|
||||||
margin-bottom: 16px;
|
:deep(.el-descriptions__label) {
|
||||||
|
width: 100px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
background: var(--el-fill-color-lighter);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-descriptions__content) {
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -122,8 +122,8 @@ onBeforeUnmount(() => {
|
|||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.tab-bar {
|
.tab-bar {
|
||||||
height: 36px;
|
height: 36px;
|
||||||
background: #fff;
|
background: var(--el-bg-color);
|
||||||
border-bottom: 1px solid var(--gerp-border-color);
|
border-bottom: 1px solid var(--el-border-color-light);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
@@ -177,6 +177,13 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 深色模式:选中 tab 用半透明主色背景(light-9 在深色下过亮,跟随主题主色) */
|
||||||
|
html.dark .tab-item.active {
|
||||||
|
background: color-mix(in srgb, var(--el-color-primary) 16%, transparent);
|
||||||
|
color: var(--el-color-primary-light-3);
|
||||||
|
border-color: color-mix(in srgb, var(--el-color-primary) 40%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
.tab-icon {
|
.tab-icon {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
@@ -218,7 +225,7 @@ onBeforeUnmount(() => {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 4px 0;
|
padding: 4px 0;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
background: #fff;
|
background: var(--el-bg-color-overlay);
|
||||||
border: 1px solid var(--el-border-color-light);
|
border: 1px solid var(--el-border-color-light);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
|
||||||
|
|||||||
@@ -328,8 +328,8 @@ const isFaIcon = (icon: string | undefined | null): boolean => {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
height: 56px;
|
height: 56px;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
background: #fff;
|
background: var(--el-bg-color);
|
||||||
border-bottom: 1px solid var(--gerp-border-color);
|
border-bottom: 1px solid var(--el-border-color-light);
|
||||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
@@ -354,7 +354,7 @@ const isFaIcon = (icon: string | undefined | null): boolean => {
|
|||||||
&-text {
|
&-text {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #303133;
|
color: var(--el-text-color-primary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,7 +384,7 @@ const isFaIcon = (icon: string | undefined | null): boolean => {
|
|||||||
transition: background 0.2s;
|
transition: background 0.2s;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: #f5f7fa;
|
background: var(--el-fill-color-light);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,7 +397,7 @@ const isFaIcon = (icon: string | undefined | null): boolean => {
|
|||||||
|
|
||||||
.gerp-user-name {
|
.gerp-user-name {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #303133;
|
color: var(--el-text-color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.gerp-main {
|
.gerp-main {
|
||||||
@@ -409,8 +409,8 @@ const isFaIcon = (icon: string | undefined | null): boolean => {
|
|||||||
.gerp-sidebar {
|
.gerp-sidebar {
|
||||||
width: 220px;
|
width: 220px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
background: #fff;
|
background: var(--el-bg-color);
|
||||||
border-right: 1px solid var(--gerp-border-color);
|
border-right: 1px solid var(--el-border-color-light);
|
||||||
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -611,12 +611,13 @@ const isFaIcon = (icon: string | undefined | null): boolean => {
|
|||||||
// 侧边栏底部(版本号 + 设置按钮)
|
// 侧边栏底部(版本号 + 设置按钮)
|
||||||
.gerp-sidebar-footer {
|
.gerp-sidebar-footer {
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
border-top: 1px solid var(--gerp-border-color);
|
border-top: 1px solid var(--el-border-color-light);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
background: var(--el-bg-color);
|
||||||
|
|
||||||
.gerp-version {
|
.gerp-version {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
@@ -630,7 +631,7 @@ const isFaIcon = (icon: string | undefined | null): boolean => {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: var(--gerp-bg-color);
|
background: var(--el-bg-color-page);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 路由内容体:撑满剩余空间,内部滚动
|
// 路由内容体:撑满剩余空间,内部滚动
|
||||||
@@ -668,13 +669,13 @@ const isFaIcon = (icon: string | undefined | null): boolean => {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 用户信息头部:主色淡背景 + 大头像 */
|
/* 用户信息头部:主色淡背景 + 大头像(暗色下自动变暗) */
|
||||||
.user-dropdown-header {
|
.user-dropdown-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 16px 20px;
|
padding: 16px 20px;
|
||||||
background: var(--el-color-primary-light-9);
|
background: var(--el-fill-color-light);
|
||||||
|
|
||||||
.user-dropdown-avatar {
|
.user-dropdown-avatar {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
@@ -782,4 +783,18 @@ const isFaIcon = (icon: string | undefined | null): boolean => {
|
|||||||
color: var(--el-color-danger);
|
color: var(--el-color-danger);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 用户下拉菜单项 hover:暗色下用 fill 变量(Element Plus 默认浅色 hover 在暗色下突兀) */
|
||||||
|
.gerp-user-dropdown .el-dropdown-menu__item {
|
||||||
|
&:not(.is-disabled):hover,
|
||||||
|
&:not(.is-disabled):focus {
|
||||||
|
background-color: var(--el-fill-color-light);
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 用户下拉菜单整体背景:跟随暗色 */
|
||||||
|
.el-dropdown__popper .el-dropdown-menu.gerp-user-dropdown {
|
||||||
|
background-color: var(--el-bg-color-overlay);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import ElementPlus from 'element-plus'
|
import ElementPlus from 'element-plus'
|
||||||
import 'element-plus/dist/index.css'
|
import 'element-plus/dist/index.css'
|
||||||
|
// Element Plus 暗色主题 CSS 变量(html.dark 时生效:卡片/导航/输入框等自动变暗)
|
||||||
|
import 'element-plus/theme-chalk/dark/css-vars.css'
|
||||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||||
import VxeUITable from 'vxe-table'
|
import VxeUITable from 'vxe-table'
|
||||||
import 'vxe-table/lib/style.css'
|
import 'vxe-table/lib/style.css'
|
||||||
|
|||||||
+112
-9
@@ -1,12 +1,99 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
/** 主题类型:default / light / dark / green / purple / red */
|
||||||
|
export type ThemeMode = 'default' | 'light' | 'dark' | 'green' | 'purple' | 'red'
|
||||||
|
|
||||||
|
/** 主题色板:主色 + Element Plus 衍生色(light-N 为主色混白,dark-2 为主色混黑) */
|
||||||
|
interface ThemePalette {
|
||||||
|
/** 是否深色模式(驱动 html.dark) */
|
||||||
|
dark: boolean
|
||||||
|
/** 主色 */
|
||||||
|
primary: string
|
||||||
|
/** light-3 */
|
||||||
|
light3: string
|
||||||
|
/** light-5 */
|
||||||
|
light5: string
|
||||||
|
/** light-7 */
|
||||||
|
light7: string
|
||||||
|
/** light-8 */
|
||||||
|
light8: string
|
||||||
|
/** light-9 */
|
||||||
|
light9: string
|
||||||
|
/** dark-2 */
|
||||||
|
dark2: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 各主题色板配置 */
|
||||||
|
const THEME_CONFIGS: Record<ThemeMode, ThemePalette> = {
|
||||||
|
default: {
|
||||||
|
dark: false,
|
||||||
|
primary: '#409eff',
|
||||||
|
light3: '#79bbff',
|
||||||
|
light5: '#a0cfff',
|
||||||
|
light7: '#c6e2ff',
|
||||||
|
light8: '#d9ecff',
|
||||||
|
light9: '#ecf5ff',
|
||||||
|
dark2: '#337ecc',
|
||||||
|
},
|
||||||
|
light: {
|
||||||
|
dark: false,
|
||||||
|
primary: '#409eff',
|
||||||
|
light3: '#79bbff',
|
||||||
|
light5: '#a0cfff',
|
||||||
|
light7: '#c6e2ff',
|
||||||
|
light8: '#d9ecff',
|
||||||
|
light9: '#ecf5ff',
|
||||||
|
dark2: '#337ecc',
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
dark: true,
|
||||||
|
primary: '#409eff',
|
||||||
|
light3: '#79bbff',
|
||||||
|
light5: '#a0cfff',
|
||||||
|
light7: '#c6e2ff',
|
||||||
|
light8: '#d9ecff',
|
||||||
|
light9: '#ecf5ff',
|
||||||
|
dark2: '#337ecc',
|
||||||
|
},
|
||||||
|
green: {
|
||||||
|
dark: false,
|
||||||
|
primary: '#67c23a',
|
||||||
|
light3: '#95d475',
|
||||||
|
light5: '#b3e19d',
|
||||||
|
light7: '#d1edc4',
|
||||||
|
light8: '#e1f3d8',
|
||||||
|
light9: '#f0f9eb',
|
||||||
|
dark2: '#529b2e',
|
||||||
|
},
|
||||||
|
purple: {
|
||||||
|
dark: false,
|
||||||
|
primary: '#8b5cf6',
|
||||||
|
light3: '#ae8df9',
|
||||||
|
light5: '#c5aefb',
|
||||||
|
light7: '#dccefc',
|
||||||
|
light8: '#e8defd',
|
||||||
|
light9: '#f3effe',
|
||||||
|
dark2: '#6f4ac5',
|
||||||
|
},
|
||||||
|
red: {
|
||||||
|
dark: false,
|
||||||
|
primary: '#f56c6c',
|
||||||
|
light3: '#f89898',
|
||||||
|
light5: '#fab6b6',
|
||||||
|
light7: '#fcd3d3',
|
||||||
|
light8: '#fde2e2',
|
||||||
|
light9: '#fef0f0',
|
||||||
|
dark2: '#c45656',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
/** 应用状态:侧边栏折叠、主题、语言等全局 UI 状态 */
|
/** 应用状态:侧边栏折叠、主题、语言等全局 UI 状态 */
|
||||||
export const useAppStore = defineStore('app', () => {
|
export const useAppStore = defineStore('app', () => {
|
||||||
/** 侧边栏是否折叠 */
|
/** 侧边栏是否折叠 */
|
||||||
const sidebarCollapsed = ref(false)
|
const sidebarCollapsed = ref(false)
|
||||||
/** 当前主题:light / dark */
|
/** 当前主题 */
|
||||||
const theme = ref<'light' | 'dark'>('light')
|
const theme = ref<ThemeMode>('default')
|
||||||
/** 当前语言 */
|
/** 当前语言 */
|
||||||
const locale = ref<'zh-CN' | 'en-US'>('zh-CN')
|
const locale = ref<'zh-CN' | 'en-US'>('zh-CN')
|
||||||
|
|
||||||
@@ -15,22 +102,38 @@ export const useAppStore = defineStore('app', () => {
|
|||||||
sidebarCollapsed.value = !sidebarCollapsed.value
|
sidebarCollapsed.value = !sidebarCollapsed.value
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 设置主题 */
|
/** 设置主题:驱动 html.dark 与 Element Plus 主色 CSS 变量 */
|
||||||
function setTheme(value: 'light' | 'dark'): void {
|
function setTheme(value: ThemeMode): void {
|
||||||
theme.value = value
|
theme.value = value
|
||||||
document.documentElement.classList.toggle('dark', value === 'dark')
|
const cfg = THEME_CONFIGS[value] ?? THEME_CONFIGS.default
|
||||||
|
const root = document.documentElement
|
||||||
|
root.classList.toggle('dark', cfg.dark)
|
||||||
|
// vxe-table 暗色主题([data-vxe-ui-theme="dark"] 触发其内置暗色变量)
|
||||||
|
root.setAttribute('data-vxe-ui-theme', cfg.dark ? 'dark' : 'light')
|
||||||
|
const vars: Record<string, string> = {
|
||||||
|
'--el-color-primary': cfg.primary,
|
||||||
|
'--el-color-primary-light-3': cfg.light3,
|
||||||
|
'--el-color-primary-light-5': cfg.light5,
|
||||||
|
'--el-color-primary-light-7': cfg.light7,
|
||||||
|
'--el-color-primary-light-8': cfg.light8,
|
||||||
|
'--el-color-primary-light-9': cfg.light9,
|
||||||
|
'--el-color-primary-dark-2': cfg.dark2,
|
||||||
|
}
|
||||||
|
for (const [k, v] of Object.entries(vars)) {
|
||||||
|
root.style.setProperty(k, v)
|
||||||
|
}
|
||||||
localStorage.setItem('gerp-theme', value)
|
localStorage.setItem('gerp-theme', value)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 初始化主题(从 localStorage 恢复) */
|
/** 初始化主题(从 localStorage 恢复) */
|
||||||
function initTheme(): void {
|
function initTheme(): void {
|
||||||
const saved = localStorage.getItem('gerp-theme') as 'light' | 'dark' | null
|
const saved = localStorage.getItem('gerp-theme') as ThemeMode | null
|
||||||
if (saved) setTheme(saved)
|
if (saved && THEME_CONFIGS[saved]) setTheme(saved)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 切换主题 */
|
/** 顶栏快捷切换:浅色主题 ↔ 深色主题 */
|
||||||
function toggleTheme(): void {
|
function toggleTheme(): void {
|
||||||
setTheme(theme.value === 'light' ? 'dark' : 'light')
|
setTheme(theme.value === 'dark' ? 'light' : 'dark')
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
+76
-2
@@ -20,8 +20,8 @@ body {
|
|||||||
font-family: 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
font-family: 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
color: #303133;
|
color: var(--el-text-color-primary);
|
||||||
background-color: #f5f7fa;
|
background-color: var(--el-bg-color-page);
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
@@ -66,3 +66,77 @@ html.dark {
|
|||||||
--gerp-bg-color: #1d1e1f;
|
--gerp-bg-color: #1d1e1f;
|
||||||
--gerp-border-color: #414243;
|
--gerp-border-color: #414243;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== 下拉面板首尾无多余空隙 ===== */
|
||||||
|
/* el-select 下拉选项列表:去掉默认 padding: 6px 0,选项首尾贴边 */
|
||||||
|
/* 加 !important:Element Plus select 样式按需动态加载(css chunk 后注入),
|
||||||
|
不加 important 会被其后加载的同特异性规则覆盖 */
|
||||||
|
.el-select-dropdown__list {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 !important;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* el-dropdown 菜单:去掉默认 padding: 5px 0,菜单项首尾贴边 */
|
||||||
|
/* 用完整 padding:0 简写 + !important,避免与 Element Plus 默认规则合并时被吞 */
|
||||||
|
.el-dropdown__popper .el-dropdown-menu {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* el-cascader 下拉面板(含多级列):同样去掉首尾 padding */
|
||||||
|
.el-cascader__dropdown .el-scrollbar__wrap {
|
||||||
|
padding-top: 0 !important;
|
||||||
|
padding-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* el-date-picker 下拉面板:去掉首尾 padding */
|
||||||
|
.el-picker__popper .el-picker-panel {
|
||||||
|
padding-top: 0 !important;
|
||||||
|
padding-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 深色模式:选中态适配(light-9 背景在深色下过亮,改用半透明主色) ===== */
|
||||||
|
html.dark {
|
||||||
|
/* 侧边栏菜单选中项(含二级子菜单) */
|
||||||
|
.gerp-sidebar .el-menu-item.is-active,
|
||||||
|
.gerp-sidebar .el-menu--inline .el-menu-item.is-active {
|
||||||
|
background: color-mix(in srgb, var(--el-color-primary) 16%, transparent);
|
||||||
|
color: var(--el-color-primary-light-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页面加载遮罩:暗色半透明黑(Element Plus 默认遮罩偏白) */
|
||||||
|
.el-loading-mask {
|
||||||
|
background-color: rgba(13, 13, 15, 0.72);
|
||||||
|
}
|
||||||
|
.el-loading-spinner .el-loading-text,
|
||||||
|
.el-loading-spinner .circular .path {
|
||||||
|
color: #a8a8b3;
|
||||||
|
stroke: #a8a8b3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 详情页公共样式 ===== */
|
||||||
|
/* 详情子表卡片:圆角 + hover 阴影(详情页统一观感) */
|
||||||
|
.detail-card {
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: box-shadow 0.25s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 详情卡片标题:主色图标 + 文字 */
|
||||||
|
.detail-card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
|
||||||
|
.el-icon {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
-3
@@ -208,15 +208,33 @@ export interface MenuItem {
|
|||||||
Permission?: string
|
Permission?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 当前登录用户信息 */
|
/** 当前登录用户信息(对齐后端 /Query/GetELTUserInfo 返回结构) */
|
||||||
export interface UserInfo {
|
export interface UserInfo {
|
||||||
/** 用户 UUID */
|
/** 用户 UUID */
|
||||||
UserUUID: string
|
UserUUID: string
|
||||||
/** 用户名 */
|
/** 用户名 */
|
||||||
UserName: string
|
UserName: string
|
||||||
/** 显示名 */
|
|
||||||
DisplayName?: string
|
|
||||||
/** 邮箱 */
|
/** 邮箱 */
|
||||||
|
UserEmail?: string
|
||||||
|
/** 邮箱验证:0=未认证,1=已认证 */
|
||||||
|
UserEmailVerify?: number
|
||||||
|
/** 电话 */
|
||||||
|
UserTele?: string
|
||||||
|
/** 性别:-1=保密,1=男,0=女 */
|
||||||
|
UserGender?: number
|
||||||
|
/** 账户许可:0=禁用,1=启用 */
|
||||||
|
UserAccountLicense?: number
|
||||||
|
/** 职位状态:1=在职,0=停职 */
|
||||||
|
UserPositionStatus?: number
|
||||||
|
/** 个人签名 */
|
||||||
|
UserPersonalSignature?: string
|
||||||
|
/** 创建时间 */
|
||||||
|
UserCreateTime?: string
|
||||||
|
/** 最后登录时间 */
|
||||||
|
UserLastLoginDate?: string
|
||||||
|
/** 显示名(兼容旧字段,后端未返回时回退 UserName) */
|
||||||
|
DisplayName?: string
|
||||||
|
/** 邮箱(兼容旧字段,后端实际返回 UserEmail) */
|
||||||
Email?: string
|
Email?: string
|
||||||
/** 头像 URL */
|
/** 头像 URL */
|
||||||
Avatar?: string
|
Avatar?: string
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ function onLoad(): void {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: var(--gerp-bg-color, #f5f7fa);
|
background: var(--el-bg-color-page);
|
||||||
}
|
}
|
||||||
|
|
||||||
.legacy-page-skeleton {
|
.legacy-page-skeleton {
|
||||||
|
|||||||
+333
-47
@@ -41,84 +41,370 @@ async function handleLogin(): Promise<void> {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="login-page">
|
<div class="login-page">
|
||||||
<div class="login-card">
|
<!-- 背景装饰:浮动光斑 -->
|
||||||
<div class="login-header">
|
<div class="bg-orb orb-a" />
|
||||||
<img src="/favicon.svg" alt="logo" class="login-logo" />
|
<div class="bg-orb orb-b" />
|
||||||
<h1 class="login-title">GERP 管理系统</h1>
|
<div class="bg-orb orb-c" />
|
||||||
<p class="login-subtitle">登录到后台管理</p>
|
<div class="bg-grid" />
|
||||||
|
|
||||||
|
<div class="login-shell">
|
||||||
|
<!-- 左:品牌叙事区 -->
|
||||||
|
<div class="brand-panel">
|
||||||
|
<div class="brand-inner">
|
||||||
|
<div class="brand-badge">
|
||||||
|
<img src="/favicon.svg" alt="logo" class="brand-logo" />
|
||||||
|
<span class="brand-name">GERP</span>
|
||||||
|
</div>
|
||||||
|
<h1 class="brand-headline">企业级业务<br />统一管理平台</h1>
|
||||||
|
<p class="brand-desc">
|
||||||
|
一站式管理您的组织、业务与数据资产,<br />
|
||||||
|
让每一次决策都有据可依。
|
||||||
|
</p>
|
||||||
|
<div class="brand-footer">
|
||||||
|
<div class="brand-stat">
|
||||||
|
<span class="brand-stat-num">多租户</span>
|
||||||
|
<span class="brand-stat-label">SaaS 架构</span>
|
||||||
|
</div>
|
||||||
|
<div class="brand-stat">
|
||||||
|
<span class="brand-stat-num">角色化</span>
|
||||||
|
<span class="brand-stat-label">精细权限</span>
|
||||||
|
</div>
|
||||||
|
<div class="brand-stat">
|
||||||
|
<span class="brand-stat-num">可扩展</span>
|
||||||
|
<span class="brand-stat-label">模块生态</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-form :model="form" :rules="rules" label-position="top" size="large" @submit.prevent="handleLogin">
|
<!-- 右:登录表单区 -->
|
||||||
<el-form-item label="用户名" prop="UserName">
|
<div class="form-panel">
|
||||||
<el-input v-model="form.UserName" placeholder="请输入用户名" :prefix-icon="'User'" clearable />
|
<div class="form-inner">
|
||||||
</el-form-item>
|
<div class="form-header">
|
||||||
|
<h2 class="form-title">欢迎回来</h2>
|
||||||
|
<p class="form-subtitle">登录您的账户以继续</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-form-item label="密码" prop="UserPassword">
|
<el-form
|
||||||
<el-input
|
:model="form"
|
||||||
v-model="form.UserPassword"
|
:rules="rules"
|
||||||
type="password"
|
label-position="top"
|
||||||
placeholder="请输入密码"
|
size="large"
|
||||||
:prefix-icon="'Lock'"
|
class="login-form"
|
||||||
show-password
|
@submit.prevent="handleLogin"
|
||||||
@keyup.enter="handleLogin"
|
>
|
||||||
/>
|
<el-form-item label="用户名" prop="UserName">
|
||||||
</el-form-item>
|
<el-input
|
||||||
|
v-model="form.UserName"
|
||||||
|
placeholder="请输入用户名"
|
||||||
|
:prefix-icon="'User'"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item>
|
<el-form-item label="密码" prop="UserPassword">
|
||||||
<el-checkbox v-model="form.RememberMe">记住我</el-checkbox>
|
<el-input
|
||||||
</el-form-item>
|
v-model="form.UserPassword"
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入密码"
|
||||||
|
:prefix-icon="'Lock'"
|
||||||
|
show-password
|
||||||
|
@keyup.enter="handleLogin"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
<el-button type="primary" size="large" class="login-btn" :loading="loading" @click="handleLogin">
|
<div class="form-options">
|
||||||
登 录
|
<el-checkbox v-model="form.RememberMe">记住我</el-checkbox>
|
||||||
</el-button>
|
</div>
|
||||||
</el-form>
|
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
class="login-btn"
|
||||||
|
:loading="loading"
|
||||||
|
@click="handleLogin"
|
||||||
|
>
|
||||||
|
登 录
|
||||||
|
</el-button>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<p class="form-footnote">© {{ new Date().getFullYear() }} GVS Distributed System · 安全登录</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.login-page {
|
.login-page {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
overflow: hidden;
|
||||||
|
background: #0b1220;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-card {
|
/* ===== 背景装饰 ===== */
|
||||||
width: 400px;
|
.bg-orb {
|
||||||
max-width: 90vw;
|
position: absolute;
|
||||||
padding: 40px;
|
border-radius: 50%;
|
||||||
background: #fff;
|
filter: blur(90px);
|
||||||
border-radius: 12px;
|
pointer-events: none;
|
||||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
|
animation: orb-float 12s ease-in-out infinite alternate;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-header {
|
.orb-a {
|
||||||
text-align: center;
|
width: 480px;
|
||||||
margin-bottom: 32px;
|
height: 480px;
|
||||||
|
top: -140px;
|
||||||
|
left: -120px;
|
||||||
|
background: radial-gradient(circle, rgba(99, 102, 241, 0.55), transparent 70%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-logo {
|
.orb-b {
|
||||||
width: 56px;
|
width: 420px;
|
||||||
height: 56px;
|
height: 420px;
|
||||||
margin-bottom: 12px;
|
bottom: -160px;
|
||||||
|
right: -100px;
|
||||||
|
background: radial-gradient(circle, rgba(16, 185, 129, 0.4), transparent 70%);
|
||||||
|
animation-delay: -4s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-title {
|
.orb-c {
|
||||||
margin: 0 0 8px;
|
width: 300px;
|
||||||
font-size: 24px;
|
height: 300px;
|
||||||
|
top: 40%;
|
||||||
|
left: 55%;
|
||||||
|
background: radial-gradient(circle, rgba(56, 189, 248, 0.35), transparent 70%);
|
||||||
|
animation-delay: -8s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes orb-float {
|
||||||
|
from { transform: translate(0, 0) scale(1); }
|
||||||
|
to { transform: translate(30px, 20px) scale(1.12); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-grid {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(rgba(255, 255, 255, 0.03) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(255, 255, 255, 0.03) 1px, transparent 1px);
|
||||||
|
background-size: 48px 48px;
|
||||||
|
mask-image: radial-gradient(ellipse at center, rgba(0, 0, 0, 0.9), transparent 75%);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 主壳:分屏 ===== */
|
||||||
|
.login-shell {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
width: 920px;
|
||||||
|
max-width: 94vw;
|
||||||
|
min-height: 560px;
|
||||||
|
border-radius: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: rgba(17, 24, 39, 0.72);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.5);
|
||||||
|
backdrop-filter: blur(24px);
|
||||||
|
animation: shell-in 0.6s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shell-in {
|
||||||
|
from { opacity: 0; transform: translateY(24px) scale(0.98); }
|
||||||
|
to { opacity: 1; transform: translateY(0) scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 左:品牌区 ===== */
|
||||||
|
.brand-panel {
|
||||||
|
flex: 1;
|
||||||
|
padding: 48px 44px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background:
|
||||||
|
radial-gradient(120% 120% at 0% 0%, rgba(99, 102, 241, 0.18), transparent 60%),
|
||||||
|
linear-gradient(160deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0.01));
|
||||||
|
border-right: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-inner {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-badge {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-logo {
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
filter: drop-shadow(0 4px 12px rgba(99, 102, 241, 0.4));
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-name {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-headline {
|
||||||
|
margin: 0 0 18px;
|
||||||
|
font-size: 32px;
|
||||||
|
line-height: 1.3;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #303133;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-subtitle {
|
.brand-desc {
|
||||||
|
margin: 0 0 40px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.9;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-stat {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-stat-num {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-stat-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 右:表单区 ===== */
|
||||||
|
.form-panel {
|
||||||
|
width: 420px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 48px 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-inner {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-header {
|
||||||
|
margin-bottom: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-title {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-subtitle {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #909399;
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-options {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form :deep(.el-form-item__label) {
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form :deep(.el-input__wrapper) {
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
box-shadow: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: border-color 0.25s, background 0.25s, box-shadow 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form :deep(.el-input__wrapper:hover) {
|
||||||
|
border-color: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form :deep(.el-input__wrapper.is-focus) {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border-color: rgba(99, 102, 241, 0.7);
|
||||||
|
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form :deep(.el-input__inner) {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form :deep(.el-input__inner::placeholder) {
|
||||||
|
color: rgba(255, 255, 255, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form :deep(.el-checkbox__label) {
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-btn {
|
.login-btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 44px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||||
|
transition: transform 0.2s, box-shadow 0.25s, filter 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
filter: brightness(1.08);
|
||||||
|
box-shadow: 0 10px 28px rgba(99, 102, 241, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-footnote {
|
||||||
|
margin: 24px 0 0;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 响应式 ===== */
|
||||||
|
@media (max-width: 820px) {
|
||||||
|
.brand-panel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-panel {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-shell {
|
||||||
|
width: 420px;
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
+60
-2
@@ -1,9 +1,56 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* 个人中心
|
||||||
|
*
|
||||||
|
* 数据源:userStore.userInfo(后端 /Query/GetELTUserInfo,含邮箱验证 / 性别 / 账号 / 职位等真实状态码)
|
||||||
|
* 展示规则对齐原版 GERP profile.html:
|
||||||
|
* - UserEmailVerify:0=未认证,1=已认证
|
||||||
|
* - UserGender:-1=保密,1=男,0=女
|
||||||
|
* - UserAccountLicense:0=禁用,1=启用
|
||||||
|
* - UserPositionStatus:1=在职,0=停职
|
||||||
|
*/
|
||||||
|
import { computed } from 'vue'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import { getAvatarUrl } from '@/api/auth'
|
import { getAvatarUrl } from '@/api/auth'
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const avatarUrl = getAvatarUrl()
|
const avatarUrl = getAvatarUrl()
|
||||||
|
|
||||||
|
/** 邮箱:后端返回 UserEmail(兼容旧字段 Email) */
|
||||||
|
const email = computed(() => userStore.userInfo?.UserEmail || userStore.userInfo?.Email || '-')
|
||||||
|
|
||||||
|
/** 性别文本 */
|
||||||
|
const genderText = computed(() => {
|
||||||
|
const g = userStore.userInfo?.UserGender
|
||||||
|
if (g === 1) return '男'
|
||||||
|
if (g === 0) return '女'
|
||||||
|
if (g === -1) return '保密'
|
||||||
|
return '-'
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 邮箱验证标签 */
|
||||||
|
const emailVerify = computed(() => {
|
||||||
|
const v = userStore.userInfo?.UserEmailVerify
|
||||||
|
if (v === 1) return { text: '已认证', type: 'success' as const }
|
||||||
|
if (v === 0) return { text: '未认证', type: 'danger' as const }
|
||||||
|
return { text: '-', type: 'info' as const }
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 账号状态标签 */
|
||||||
|
const accountStatus = computed(() => {
|
||||||
|
const v = userStore.userInfo?.UserAccountLicense
|
||||||
|
if (v === 1) return { text: '启用', type: 'success' as const }
|
||||||
|
if (v === 0) return { text: '禁用', type: 'danger' as const }
|
||||||
|
return { text: '-', type: 'info' as const }
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 职位状态标签 */
|
||||||
|
const positionStatus = computed(() => {
|
||||||
|
const v = userStore.userInfo?.UserPositionStatus
|
||||||
|
if (v === 1) return { text: '在职', type: 'success' as const }
|
||||||
|
if (v === 0) return { text: '停职', type: 'danger' as const }
|
||||||
|
return { text: '-', type: 'info' as const }
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -16,12 +63,23 @@ const avatarUrl = getAvatarUrl()
|
|||||||
<img :src="avatarUrl" alt="头像" class="avatar" />
|
<img :src="avatarUrl" alt="头像" class="avatar" />
|
||||||
<el-descriptions :column="1" border>
|
<el-descriptions :column="1" border>
|
||||||
<el-descriptions-item label="用户名">{{ userStore.userInfo?.UserName || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="用户名">{{ userStore.userInfo?.UserName || '-' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="显示名">{{ userStore.userInfo?.DisplayName || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="邮箱">{{ email }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="邮箱">{{ userStore.userInfo?.Email || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="电话">{{ userStore.userInfo?.UserTele || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="性别">{{ genderText }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="邮箱验证">
|
||||||
|
<el-tag :type="emailVerify.type" size="small">{{ emailVerify.text }}</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="账号状态">
|
||||||
|
<el-tag :type="accountStatus.type" size="small">{{ accountStatus.text }}</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="职位状态">
|
||||||
|
<el-tag :type="positionStatus.type" size="small">{{ positionStatus.text }}</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="当前角色">{{ userStore.currentRole || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="当前角色">{{ userStore.currentRole || '-' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="是否管理员">
|
<el-descriptions-item label="是否管理员">
|
||||||
<el-tag :type="userStore.isAdmin ? 'danger' : 'info'">{{ userStore.isAdmin ? '是' : '否' }}</el-tag>
|
<el-tag :type="userStore.isAdmin ? 'danger' : 'info'">{{ userStore.isAdmin ? '是' : '否' }}</el-tag>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建时间">{{ userStore.userInfo?.UserCreateTime || '-' }}</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import { ref, reactive, computed, onMounted } from 'vue'
|
|||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import DetailPage from '@/components/DetailPage.vue'
|
import DetailPage from '@/components/DetailPage.vue'
|
||||||
import InfoGrid from '@/components/InfoGrid.vue'
|
|
||||||
import CrudTable from '@/components/CrudTable.vue'
|
import CrudTable from '@/components/CrudTable.vue'
|
||||||
import { formApi } from '@/api/form'
|
import { formApi } from '@/api/form'
|
||||||
import { useResolveUUIDs } from '@/composables/useResolveUUIDs'
|
import { useResolveUUIDs } from '@/composables/useResolveUUIDs'
|
||||||
@@ -199,6 +198,16 @@ async function saveEdit(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 复制 UUID 到剪贴板 */
|
||||||
|
async function copyUUID(uuid: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(uuid)
|
||||||
|
ElMessage.success('UUID 已复制')
|
||||||
|
} catch {
|
||||||
|
ElMessage.warning('复制失败,请手动选择复制')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(loadCompany)
|
onMounted(loadCompany)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -214,58 +223,104 @@ onMounted(loadCompany)
|
|||||||
<!-- 错误提示(缺参 / 加载失败) -->
|
<!-- 错误提示(缺参 / 加载失败) -->
|
||||||
<el-alert v-if="errorMsg" type="error" :title="errorMsg" show-icon :closable="false" />
|
<el-alert v-if="errorMsg" type="error" :title="errorMsg" show-icon :closable="false" />
|
||||||
|
|
||||||
<!-- 主表基本信息(InfoGrid 直接置于 DetailPage,不包 el-card) -->
|
<!-- 主表基本信息:公司头部卡 -->
|
||||||
<InfoGrid v-if="companyData" title="基本信息" :columns="2">
|
<el-card v-if="companyData" shadow="never" class="company-hero">
|
||||||
<el-descriptions-item label="公司名称">{{ asStr(companyData.CompanyName) || '-' }}</el-descriptions-item>
|
<div class="company-hero-body">
|
||||||
<el-descriptions-item label="公司简称">{{ asStr(companyData.CompanyShort) || '-' }}</el-descriptions-item>
|
<div class="company-avatar">
|
||||||
<el-descriptions-item label="公司状态">
|
<el-icon :size="26"><OfficeBuilding /></el-icon>
|
||||||
<el-tag :type="companyStatusMap[String(companyData.CompanyStatus)]?.type || 'info'" size="small">
|
</div>
|
||||||
{{ companyStatusMap[String(companyData.CompanyStatus)]?.text ?? companyData.CompanyStatus }}
|
<div class="company-hero-main">
|
||||||
</el-tag>
|
<div class="company-hero-title-row">
|
||||||
</el-descriptions-item>
|
<h2 class="company-hero-title">{{ asStr(companyData.CompanyName) || '-' }}</h2>
|
||||||
<el-descriptions-item label="上级公司">
|
<el-tag :type="companyStatusMap[String(companyData.CompanyStatus)]?.type || 'info'" size="small" effect="light">
|
||||||
{{ resolvedNames.parentCompany?.[companyData.ParentCompanyUUID] || asStr(companyData.ParentCompanyUUID) || '-' }}
|
{{ companyStatusMap[String(companyData.CompanyStatus)]?.text ?? companyData.CompanyStatus }}
|
||||||
</el-descriptions-item>
|
</el-tag>
|
||||||
<el-descriptions-item label="租户">
|
</div>
|
||||||
{{ resolvedNames.tenant?.[companyData.TenantUUID] || asStr(companyData.TenantUUID) || '-' }}
|
<p class="company-hero-sub">{{ asStr(companyData.CompanyShort) ? '简称 ' + asStr(companyData.CompanyShort) : '暂无简称' }}</p>
|
||||||
</el-descriptions-item>
|
</div>
|
||||||
<el-descriptions-item label="创建时间">{{ formatTime(companyData.CompanyCreateTime) }}</el-descriptions-item>
|
</div>
|
||||||
</InfoGrid>
|
|
||||||
|
<el-descriptions class="company-meta" :column="3" border>
|
||||||
|
<el-descriptions-item label="公司 UUID">
|
||||||
|
<span class="uuid-cell">
|
||||||
|
<span class="uuid-text">{{ asStr(companyData.CompanyUUID) || '-' }}</span>
|
||||||
|
<el-button
|
||||||
|
v-if="asStr(companyData.CompanyUUID)"
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
title="复制 UUID"
|
||||||
|
@click="copyUUID(asStr(companyData.CompanyUUID))"
|
||||||
|
>
|
||||||
|
<el-icon><CopyDocument /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="上级公司">
|
||||||
|
{{ resolvedNames.parentCompany?.[companyData.ParentCompanyUUID] || asStr(companyData.ParentCompanyUUID) || '-' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="租户">
|
||||||
|
{{ resolvedNames.tenant?.[companyData.TenantUUID] || asStr(companyData.TenantUUID) || '-' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建时间">{{ formatTime(companyData.CompanyCreateTime) }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
<!-- 子表 1:部门 -->
|
<!-- 子表 1:部门 -->
|
||||||
<el-card v-if="companyData" shadow="never">
|
<el-card v-if="companyData" shadow="never" class="detail-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="detail-card-header">
|
||||||
|
<el-icon><Share /></el-icon>
|
||||||
|
<span>部门</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<CrudTable
|
<CrudTable
|
||||||
table="Department"
|
table="Department"
|
||||||
title="部门"
|
title="部门"
|
||||||
:query="{ CompanyUUID: companyUUID }"
|
:query="{ CompanyUUID: companyUUID }"
|
||||||
:selectable="true"
|
:selectable="true"
|
||||||
:pageable="true"
|
:pageable="true"
|
||||||
|
:max-height="400"
|
||||||
pk-field="DeptUUID"
|
pk-field="DeptUUID"
|
||||||
:on-view="goDeptDetail"
|
:on-view="goDeptDetail"
|
||||||
/>
|
/>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<!-- 子表 2:工作组 -->
|
<!-- 子表 2:工作组 -->
|
||||||
<el-card v-if="companyData" shadow="never">
|
<el-card v-if="companyData" shadow="never" class="detail-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="detail-card-header">
|
||||||
|
<el-icon><Avatar /></el-icon>
|
||||||
|
<span>工作组</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<CrudTable
|
<CrudTable
|
||||||
table="Workgroup"
|
table="Workgroup"
|
||||||
title="工作组"
|
title="工作组"
|
||||||
:query="{ CompanyUUID: companyUUID }"
|
:query="{ CompanyUUID: companyUUID }"
|
||||||
:selectable="true"
|
:selectable="true"
|
||||||
:pageable="true"
|
:pageable="true"
|
||||||
|
:max-height="400"
|
||||||
pk-field="WorkgroupUUID"
|
pk-field="WorkgroupUUID"
|
||||||
:on-view="goWorkgroupDetail"
|
:on-view="goWorkgroupDetail"
|
||||||
/>
|
/>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<!-- 子表 3:员工(UserRole 关联表) -->
|
<!-- 子表 3:员工(UserRole 关联表) -->
|
||||||
<el-card v-if="companyData" shadow="never">
|
<el-card v-if="companyData" shadow="never" class="detail-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="detail-card-header">
|
||||||
|
<el-icon><User /></el-icon>
|
||||||
|
<span>员工</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<CrudTable
|
<CrudTable
|
||||||
table="UserRole"
|
table="UserRole"
|
||||||
title="员工"
|
title="员工"
|
||||||
:query="{ CompanyUUID: companyUUID }"
|
:query="{ CompanyUUID: companyUUID }"
|
||||||
:selectable="true"
|
:selectable="true"
|
||||||
:pageable="true"
|
:pageable="true"
|
||||||
|
:max-height="400"
|
||||||
pk-field="UserRoleUUID"
|
pk-field="UserRoleUUID"
|
||||||
:on-view="goEmployeeDetail"
|
:on-view="goEmployeeDetail"
|
||||||
/>
|
/>
|
||||||
@@ -361,5 +416,87 @@ onMounted(loadCompany)
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
// 子表卡片之间通过 DetailPage 的 detail-content gap 控制间距,此处无需额外样式
|
// ===== 公司头部卡 =====
|
||||||
|
.company-hero {
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: box-shadow 0.25s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-card__body) {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.company-hero-body {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 公司图标:主色淡底圆角块
|
||||||
|
.company-avatar {
|
||||||
|
width: 52px;
|
||||||
|
height: 52px;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
background: var(--el-color-primary-light-9);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.company-hero-main {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.company-hero-title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.company-hero-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.company-hero-sub {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 公司元信息网格
|
||||||
|
.company-meta {
|
||||||
|
:deep(.el-descriptions__label) {
|
||||||
|
width: 90px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UUID 单元格:等宽字体 + 复制按钮
|
||||||
|
.uuid-cell {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uuid-text {
|
||||||
|
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 子表卡片 =====
|
||||||
|
// 使用全局 .detail-card / .detail-card-header(见 global.scss),此处无需重复定义
|
||||||
</style>
|
</style>
|
||||||
@@ -189,8 +189,13 @@ onMounted(async () => {
|
|||||||
</InfoGrid>
|
</InfoGrid>
|
||||||
|
|
||||||
<!-- 部门员工子表 -->
|
<!-- 部门员工子表 -->
|
||||||
<el-card shadow="never">
|
<el-card shadow="never" class="detail-card">
|
||||||
<template #header>部门员工</template>
|
<template #header>
|
||||||
|
<div class="detail-card-header">
|
||||||
|
<el-icon><User /></el-icon>
|
||||||
|
<span>部门员工</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<CrudTable
|
<CrudTable
|
||||||
v-if="deptUUID"
|
v-if="deptUUID"
|
||||||
table="UserRole"
|
table="UserRole"
|
||||||
@@ -199,6 +204,7 @@ onMounted(async () => {
|
|||||||
:pk-field="'UserRoleUUID'"
|
:pk-field="'UserRoleUUID'"
|
||||||
selectable
|
selectable
|
||||||
pageable
|
pageable
|
||||||
|
:max-height="400"
|
||||||
:on-view="goUserDetail"
|
:on-view="goUserDetail"
|
||||||
/>
|
/>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ interface UserRecord {
|
|||||||
UserEmail: string
|
UserEmail: string
|
||||||
UserTele: string
|
UserTele: string
|
||||||
UserAccountLicense: number
|
UserAccountLicense: number
|
||||||
|
UserEmailVerify: number
|
||||||
|
UserGender: number
|
||||||
|
UserPositionStatus: number
|
||||||
IsStaff: number | boolean
|
IsStaff: number | boolean
|
||||||
UserCreateTime: string
|
UserCreateTime: string
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
@@ -75,12 +78,32 @@ const userUUID = (route.query.UserUUID as string) || ''
|
|||||||
/** 用户主键字段名 */
|
/** 用户主键字段名 */
|
||||||
const PK_FIELD = 'UserUUID'
|
const PK_FIELD = 'UserUUID'
|
||||||
|
|
||||||
/** 账号状态映射:0=禁用,1=启用 */
|
/** 账号状态映射:0=禁用,1=启用(对齐原版 profile.html:1 正常,0 已封禁) */
|
||||||
const accountStatusMap: Record<string, { text: string; type: 'success' | 'danger' }> = {
|
const accountStatusMap: Record<string, { text: string; type: 'success' | 'danger' | 'info' }> = {
|
||||||
'0': { text: '禁用', type: 'danger' },
|
'0': { text: '禁用', type: 'danger' },
|
||||||
'1': { text: '启用', type: 'success' },
|
'1': { text: '启用', type: 'success' },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 邮箱验证映射:0=未认证,1=已认证(对齐原版 profile.html) */
|
||||||
|
const emailVerifyMap: Record<string, { text: string; type: 'success' | 'danger' | 'info' }> = {
|
||||||
|
'0': { text: '未认证', type: 'danger' },
|
||||||
|
'1': { text: '已认证', type: 'success' },
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 职位状态映射:1=在职,0=停职(对齐原版 profile.html) */
|
||||||
|
const positionStatusMap: Record<string, { text: string; type: 'success' | 'danger' | 'info' }> = {
|
||||||
|
'1': { text: '在职', type: 'success' },
|
||||||
|
'0': { text: '停职', type: 'danger' },
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 性别:-1=保密,1=男,0=女(对齐原版 profile.html 性别按钮) */
|
||||||
|
function genderText(val: unknown): string {
|
||||||
|
if (val === 1 || val === '1') return '男'
|
||||||
|
if (val === 0 || val === '0') return '女'
|
||||||
|
if (val === -1 || val === '-1') return '保密'
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
|
||||||
/** 头像 URL:手动附加 server / admin_verify 作用域参数(el-avatar 不走 axios 拦截器) */
|
/** 头像 URL:手动附加 server / admin_verify 作用域参数(el-avatar 不走 axios 拦截器) */
|
||||||
const avatarUrl = computed(() => {
|
const avatarUrl = computed(() => {
|
||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
@@ -236,11 +259,22 @@ onMounted(loadUser)
|
|||||||
<el-descriptions-item label="真实姓名">{{ asStr(userData.UserRealName) || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="真实姓名">{{ asStr(userData.UserRealName) || '-' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="邮箱">{{ asStr(userData.UserEmail) || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="邮箱">{{ asStr(userData.UserEmail) || '-' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="电话">{{ asStr(userData.UserTele) || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="电话">{{ asStr(userData.UserTele) || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="邮箱验证">
|
||||||
|
<el-tag :type="emailVerifyMap[String(userData.UserEmailVerify)]?.type || 'info'" size="small">
|
||||||
|
{{ emailVerifyMap[String(userData.UserEmailVerify)]?.text ?? userData.UserEmailVerify }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="性别">{{ genderText(userData.UserGender) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="账号状态">
|
<el-descriptions-item label="账号状态">
|
||||||
<el-tag :type="accountStatusMap[String(userData.UserAccountLicense)]?.type || 'info'" size="small">
|
<el-tag :type="accountStatusMap[String(userData.UserAccountLicense)]?.type || 'info'" size="small">
|
||||||
{{ accountStatusMap[String(userData.UserAccountLicense)]?.text ?? userData.UserAccountLicense }}
|
{{ accountStatusMap[String(userData.UserAccountLicense)]?.text ?? userData.UserAccountLicense }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="职位状态">
|
||||||
|
<el-tag :type="positionStatusMap[String(userData.UserPositionStatus)]?.type || 'info'" size="small">
|
||||||
|
{{ positionStatusMap[String(userData.UserPositionStatus)]?.text ?? userData.UserPositionStatus }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="是否员工">
|
<el-descriptions-item label="是否员工">
|
||||||
<span :style="{ color: isTruthy(userData.IsStaff) ? '#166534' : '#991b1b' }">
|
<span :style="{ color: isTruthy(userData.IsStaff) ? '#166534' : '#991b1b' }">
|
||||||
{{ isTruthy(userData.IsStaff) ? '是' : '否' }}
|
{{ isTruthy(userData.IsStaff) ? '是' : '否' }}
|
||||||
@@ -274,25 +308,39 @@ onMounted(loadUser)
|
|||||||
</InfoGrid>
|
</InfoGrid>
|
||||||
|
|
||||||
<!-- 子表 1:角色与组织(UserRole) -->
|
<!-- 子表 1:角色与组织(UserRole) -->
|
||||||
<el-card v-if="userData" shadow="never">
|
<el-card v-if="userData" shadow="never" class="detail-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="detail-card-header">
|
||||||
|
<el-icon><Avatar /></el-icon>
|
||||||
|
<span>角色与组织</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<CrudTable
|
<CrudTable
|
||||||
table="UserRole"
|
table="UserRole"
|
||||||
title="角色与组织"
|
title="角色与组织"
|
||||||
:query="{ UserUUID: userUUID }"
|
:query="{ UserUUID: userUUID }"
|
||||||
:selectable="true"
|
:selectable="true"
|
||||||
:pageable="true"
|
:pageable="true"
|
||||||
|
:max-height="400"
|
||||||
pk-field="UserRoleUUID"
|
pk-field="UserRoleUUID"
|
||||||
/>
|
/>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<!-- 子表 2:部门关联(UserDept) -->
|
<!-- 子表 2:部门关联(UserDept) -->
|
||||||
<el-card v-if="userData" shadow="never">
|
<el-card v-if="userData" shadow="never" class="detail-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="detail-card-header">
|
||||||
|
<el-icon><Share /></el-icon>
|
||||||
|
<span>部门关联</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<CrudTable
|
<CrudTable
|
||||||
table="UserDept"
|
table="UserDept"
|
||||||
title="部门关联"
|
title="部门关联"
|
||||||
:query="{ UserUUID: userUUID }"
|
:query="{ UserUUID: userUUID }"
|
||||||
:selectable="true"
|
:selectable="true"
|
||||||
:pageable="true"
|
:pageable="true"
|
||||||
|
:max-height="400"
|
||||||
pk-field="UserDeptUUID"
|
pk-field="UserDeptUUID"
|
||||||
:on-view="goDeptDetail"
|
:on-view="goDeptDetail"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ onMounted(loadRole)
|
|||||||
</el-alert>
|
</el-alert>
|
||||||
|
|
||||||
<!-- 子表 1:权限列表 -->
|
<!-- 子表 1:权限列表 -->
|
||||||
<el-card v-if="roleData" shadow="never">
|
<el-card v-if="roleData" shadow="never" class="detail-card">
|
||||||
<CrudTable
|
<CrudTable
|
||||||
table="RolePermission"
|
table="RolePermission"
|
||||||
title="权限列表"
|
title="权限列表"
|
||||||
@@ -233,7 +233,7 @@ onMounted(loadRole)
|
|||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<!-- 子表 2:关联用户 -->
|
<!-- 子表 2:关联用户 -->
|
||||||
<el-card v-if="roleData" shadow="never">
|
<el-card v-if="roleData" shadow="never" class="detail-card">
|
||||||
<CrudTable
|
<CrudTable
|
||||||
table="UserRole"
|
table="UserRole"
|
||||||
title="关联用户"
|
title="关联用户"
|
||||||
|
|||||||
@@ -185,8 +185,13 @@ onMounted(async () => {
|
|||||||
</InfoGrid>
|
</InfoGrid>
|
||||||
|
|
||||||
<!-- 关联公司子表 -->
|
<!-- 关联公司子表 -->
|
||||||
<el-card shadow="never">
|
<el-card shadow="never" class="detail-card">
|
||||||
<template #header>关联公司</template>
|
<template #header>
|
||||||
|
<div class="detail-card-header">
|
||||||
|
<el-icon><OfficeBuilding /></el-icon>
|
||||||
|
<span>关联公司</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<CrudTable
|
<CrudTable
|
||||||
v-if="tenantUUID"
|
v-if="tenantUUID"
|
||||||
table="Company"
|
table="Company"
|
||||||
|
|||||||
@@ -164,8 +164,13 @@ onMounted(async () => {
|
|||||||
</InfoGrid>
|
</InfoGrid>
|
||||||
|
|
||||||
<!-- 工作组角色子表 -->
|
<!-- 工作组角色子表 -->
|
||||||
<el-card shadow="never">
|
<el-card shadow="never" class="detail-card">
|
||||||
<template #header>工作组角色</template>
|
<template #header>
|
||||||
|
<div class="detail-card-header">
|
||||||
|
<el-icon><Avatar /></el-icon>
|
||||||
|
<span>工作组角色</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<CrudTable
|
<CrudTable
|
||||||
v-if="workgroupUUID"
|
v-if="workgroupUUID"
|
||||||
table="WorkgroupRole"
|
table="WorkgroupRole"
|
||||||
|
|||||||
@@ -218,8 +218,13 @@ onMounted(async () => {
|
|||||||
</InfoGrid>
|
</InfoGrid>
|
||||||
|
|
||||||
<!-- 关联用户子表 -->
|
<!-- 关联用户子表 -->
|
||||||
<el-card shadow="never">
|
<el-card shadow="never" class="detail-card">
|
||||||
<template #header>关联用户</template>
|
<template #header>
|
||||||
|
<div class="detail-card-header">
|
||||||
|
<el-icon><Avatar /></el-icon>
|
||||||
|
<span>关联用户</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<CrudTable
|
<CrudTable
|
||||||
v-if="roleUUID"
|
v-if="roleUUID"
|
||||||
table="UserRole"
|
table="UserRole"
|
||||||
@@ -233,8 +238,13 @@ onMounted(async () => {
|
|||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<!-- 角色权限子表 -->
|
<!-- 角色权限子表 -->
|
||||||
<el-card shadow="never">
|
<el-card shadow="never" class="detail-card">
|
||||||
<template #header>角色权限</template>
|
<template #header>
|
||||||
|
<div class="detail-card-header">
|
||||||
|
<el-icon><Lock /></el-icon>
|
||||||
|
<span>角色权限</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<CrudTable
|
<CrudTable
|
||||||
v-if="roleUUID"
|
v-if="roleUUID"
|
||||||
table="RolePermission"
|
table="RolePermission"
|
||||||
|
|||||||
@@ -19,8 +19,8 @@
|
|||||||
* - 【废弃 GERPThemeSync】:不再调用 window.GERPThemeSync.init(),主题改由 Pinia appStore 统一管理
|
* - 【废弃 GERPThemeSync】:不再调用 window.GERPThemeSync.init(),主题改由 Pinia appStore 统一管理
|
||||||
* - 【移除 postMessage】:删除 SETTINGS_UPDATE / GET_THEME / THEME_CHANGE 全部跨窗口通信,
|
* - 【移除 postMessage】:删除 SETTINGS_UPDATE / GET_THEME / THEME_CHANGE 全部跨窗口通信,
|
||||||
* Vue 单页应用内主题与设置直接通过 store / 响应式数据驱动,无需 iframe 消息桥
|
* Vue 单页应用内主题与设置直接通过 store / 响应式数据驱动,无需 iframe 消息桥
|
||||||
* - 主题管理用 useAppStore:appStore 当前仅支持 light / dark,选择这两项时调用 appStore.setTheme;
|
* - 主题管理用 useAppStore:appStore 支持 default / light / dark / green / purple / red 全部主题,
|
||||||
* 其余主题(default / green / purple / red)仅本地记录,TODO 待 appStore 扩展多主题支持
|
* setTheme 统一驱动 html.dark 与 Element Plus 主色 CSS 变量(--el-color-primary 系列)
|
||||||
* - 侧边栏折叠改用 appStore.sidebarCollapsed / toggleSidebar(替代 GERPUI.SetSidebarCollapsed)
|
* - 侧边栏折叠改用 appStore.sidebarCollapsed / toggleSidebar(替代 GERPUI.SetSidebarCollapsed)
|
||||||
* - 动画开关通过 watch 设置 CSS 变量 --TransitionSpeed / --AnimationDuration(替代原生 DOM 操作)
|
* - 动画开关通过 watch 设置 CSS 变量 --TransitionSpeed / --AnimationDuration(替代原生 DOM 操作)
|
||||||
* - server / admin_verify 由 request.ts 拦截器统一注入
|
* - server / admin_verify 由 request.ts 拦截器统一注入
|
||||||
@@ -152,37 +152,34 @@ function applySettingsToRuntime(s: Settings): void {
|
|||||||
appStore.sidebarCollapsed = !s.sidebarExpanded
|
appStore.sidebarCollapsed = !s.sidebarExpanded
|
||||||
}
|
}
|
||||||
|
|
||||||
// 主题:appStore 仅支持 light / dark,其余主题 TODO
|
// 主题:由 appStore 统一管理
|
||||||
applyTheme(s.themeMode)
|
applyTheme(s.themeMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 应用主题:light/dark 走 appStore,其余主题 TODO 待 appStore 扩展 */
|
/** 应用主题:全部 6 种主题由 appStore 统一管理(驱动 html.dark + Element Plus 主色变量) */
|
||||||
function applyTheme(mode: string): void {
|
function applyTheme(mode: string): void {
|
||||||
if (mode === 'light' || mode === 'dark') {
|
if (mode === 'light' || mode === 'dark' || mode === 'green' || mode === 'purple' || mode === 'red' || mode === 'default') {
|
||||||
appStore.setTheme(mode)
|
appStore.setTheme(mode)
|
||||||
return
|
|
||||||
}
|
|
||||||
// TODO: appStore 当前仅支持 light/dark,default/green/purple/red 等主题待扩展后接入
|
|
||||||
// default 暂按 light 处理,避免无主题
|
|
||||||
if (mode === 'default') {
|
|
||||||
appStore.setTheme('light')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 主题切换处理:立即应用并即时保存 */
|
/** 主题切换处理:立即应用并即时保存 */
|
||||||
function onThemeChange(value: string): void {
|
function onThemeChange(value: string): void {
|
||||||
|
settings.themeMode = value
|
||||||
applyTheme(value)
|
applyTheme(value)
|
||||||
debounceSave(300)
|
debounceSave(300)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 侧边栏切换处理:立即生效并即时保存 */
|
/** 侧边栏切换处理:立即生效并即时保存 */
|
||||||
function onSidebarChange(value: string | number | boolean): void {
|
function onSidebarChange(value: string | number | boolean): void {
|
||||||
|
settings.sidebarExpanded = Boolean(value)
|
||||||
appStore.sidebarCollapsed = !value
|
appStore.sidebarCollapsed = !value
|
||||||
debounceSave(200)
|
debounceSave(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 动画切换处理:立即生效并即时保存 */
|
/** 动画切换处理:立即生效并即时保存 */
|
||||||
function onAnimationsChange(value: string | number | boolean): void {
|
function onAnimationsChange(value: string | number | boolean): void {
|
||||||
|
settings.animations = Boolean(value)
|
||||||
const root = document.documentElement
|
const root = document.documentElement
|
||||||
root.style.setProperty('--TransitionSpeed', value ? '0.2s' : '0s')
|
root.style.setProperty('--TransitionSpeed', value ? '0.2s' : '0s')
|
||||||
root.style.setProperty('--AnimationDuration', value ? '0.3s' : '0s')
|
root.style.setProperty('--AnimationDuration', value ? '0.3s' : '0s')
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* 迁移自 TempLate/GERP/pages/system/user-detail.html
|
* 迁移自 TempLate/GERP/pages/system/user-detail.html
|
||||||
*
|
*
|
||||||
* 业务结构:
|
* 业务结构:
|
||||||
* 1. 基本信息(InfoGrid):用户名 / 邮箱 / 电话 / 性别 / 账号状态 / 是否员工 / 是否超级用户 / 创建时间 / 最后登录
|
* 1. 基本信息(InfoGrid):用户名 / 邮箱 / 邮箱验证 / 电话 / 性别 / 账号状态 / 职位状态 / 是否员工 / 是否超级用户 / 创建时间 / 最后登录
|
||||||
* 2. 2FA 双因素信息卡(TODO:GetUser2FAInfo 接口契约确认后补全字段渲染)
|
* 2. 2FA 双因素信息卡(TODO:GetUser2FAInfo 接口契约确认后补全字段渲染)
|
||||||
* 3. 角色与组织子表(CrudTable: UserRole,按 UserUUID 过滤)
|
* 3. 角色与组织子表(CrudTable: UserRole,按 UserUUID 过滤)
|
||||||
* 4. 部门关联子表(CrudTable: UserDept,按 UserUUID 过滤)
|
* 4. 部门关联子表(CrudTable: UserDept,按 UserUUID 过滤)
|
||||||
@@ -67,13 +67,28 @@ const isolationHint = computed(() =>
|
|||||||
|
|
||||||
// === 显示格式化 helper ===
|
// === 显示格式化 helper ===
|
||||||
|
|
||||||
/** 性别:1→男,0→女 */
|
/** 性别:-1=保密,1=男,0=女(对齐原版 profile.html 性别按钮) */
|
||||||
function genderText(g: unknown): string {
|
function genderText(g: unknown): string {
|
||||||
if (g === 1 || g === '1') return '男'
|
if (g === 1 || g === '1') return '男'
|
||||||
if (g === 0 || g === '0') return '女'
|
if (g === 0 || g === '0') return '女'
|
||||||
|
if (g === -1 || g === '-1') return '保密'
|
||||||
return '-'
|
return '-'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 邮箱验证:0=未认证,1=已认证(对齐原版 profile.html) */
|
||||||
|
function emailVerifyTag(v: unknown): { text: string; type: 'success' | 'danger' | 'info' } {
|
||||||
|
if (v === 1 || v === '1') return { text: '已认证', type: 'success' }
|
||||||
|
if (v === 0 || v === '0') return { text: '未认证', type: 'danger' }
|
||||||
|
return { text: String(v ?? '-'), type: 'info' }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 职位状态:1=在职,0=停职(对齐原版 profile.html) */
|
||||||
|
function positionStatusTag(v: unknown): { text: string; type: 'success' | 'danger' | 'info' } {
|
||||||
|
if (v === 1 || v === '1') return { text: '在职', type: 'success' }
|
||||||
|
if (v === 0 || v === '0') return { text: '停职', type: 'danger' }
|
||||||
|
return { text: String(v ?? '-'), type: 'info' }
|
||||||
|
}
|
||||||
|
|
||||||
/** 是 / 否 文本 */
|
/** 是 / 否 文本 */
|
||||||
function yesNoText(val: unknown): string {
|
function yesNoText(val: unknown): string {
|
||||||
return val ? '是' : '否'
|
return val ? '是' : '否'
|
||||||
@@ -213,12 +228,22 @@ onMounted(async () => {
|
|||||||
<el-descriptions-item label="用户名">{{ data?.UserName || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="用户名">{{ data?.UserName || '-' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="邮箱">{{ data?.UserEmail || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="邮箱">{{ data?.UserEmail || '-' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="电话">{{ data?.UserTele || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="电话">{{ data?.UserTele || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="邮箱验证">
|
||||||
|
<el-tag :type="emailVerifyTag(data?.UserEmailVerify).type" size="small">
|
||||||
|
{{ emailVerifyTag(data?.UserEmailVerify).text }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="性别">{{ genderText(data?.UserGender) }}</el-descriptions-item>
|
<el-descriptions-item label="性别">{{ genderText(data?.UserGender) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="账号状态">
|
<el-descriptions-item label="账号状态">
|
||||||
<el-tag :type="accountLicenseTag(data?.UserAccountLicense).type" size="small">
|
<el-tag :type="accountLicenseTag(data?.UserAccountLicense).type" size="small">
|
||||||
{{ accountLicenseTag(data?.UserAccountLicense).text }}
|
{{ accountLicenseTag(data?.UserAccountLicense).text }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="职位状态">
|
||||||
|
<el-tag :type="positionStatusTag(data?.UserPositionStatus).type" size="small">
|
||||||
|
{{ positionStatusTag(data?.UserPositionStatus).text }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="是否员工">{{ yesNoText(data?.IsStaff) }}</el-descriptions-item>
|
<el-descriptions-item label="是否员工">{{ yesNoText(data?.IsStaff) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="是否超级用户">{{ yesNoText(data?.IsSuperuser) }}</el-descriptions-item>
|
<el-descriptions-item label="是否超级用户">{{ yesNoText(data?.IsSuperuser) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="创建时间">{{ formatTime(data?.UserCreateTime) }}</el-descriptions-item>
|
<el-descriptions-item label="创建时间">{{ formatTime(data?.UserCreateTime) }}</el-descriptions-item>
|
||||||
@@ -238,8 +263,13 @@ onMounted(async () => {
|
|||||||
</InfoGrid>
|
</InfoGrid>
|
||||||
|
|
||||||
<!-- 角色与组织子表 -->
|
<!-- 角色与组织子表 -->
|
||||||
<el-card shadow="never">
|
<el-card shadow="never" class="detail-card">
|
||||||
<template #header>角色与组织</template>
|
<template #header>
|
||||||
|
<div class="detail-card-header">
|
||||||
|
<el-icon><Avatar /></el-icon>
|
||||||
|
<span>角色与组织</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<CrudTable
|
<CrudTable
|
||||||
v-if="userUUID"
|
v-if="userUUID"
|
||||||
table="UserRole"
|
table="UserRole"
|
||||||
@@ -253,8 +283,13 @@ onMounted(async () => {
|
|||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<!-- 部门关联子表 -->
|
<!-- 部门关联子表 -->
|
||||||
<el-card shadow="never">
|
<el-card shadow="never" class="detail-card">
|
||||||
<template #header>部门关联</template>
|
<template #header>
|
||||||
|
<div class="detail-card-header">
|
||||||
|
<el-icon><Share /></el-icon>
|
||||||
|
<span>部门关联</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<CrudTable
|
<CrudTable
|
||||||
v-if="userUUID"
|
v-if="userUUID"
|
||||||
table="UserDept"
|
table="UserDept"
|
||||||
|
|||||||
Reference in new issue
Block a user