diff --git a/src/components/CrudTable.vue b/src/components/CrudTable.vue index d5eb251..c237795 100644 --- a/src/components/CrudTable.vue +++ b/src/components/CrudTable.vue @@ -36,6 +36,12 @@ const props = defineProps<{ * 关闭时表格自适应内容高度(详情页嵌套场景,随页面滚动)。 */ stretch?: boolean + /** + * 表格最大高度(px,可选)。 + * 详情页嵌套子表场景传入(如 400),行数超过时表格内部滚动, + * 避免所有行撑开导致页面无限加长。不传则不限制。 + */ + maxHeight?: number }>() const options: UseCrudTableOptions = { @@ -172,6 +178,7 @@ function renderUuid(value: unknown): string { stripe round :height="props.stretch ? '100%' : 'auto'" + :max-height="props.maxHeight" :row-config="{ isHover: true }" :column-config="{ resizable: true }" :checkbox-config="{ range: true }" diff --git a/src/components/InfoGrid.vue b/src/components/InfoGrid.vue index f286ea0..3942f5a 100644 --- a/src/components/InfoGrid.vue +++ b/src/components/InfoGrid.vue @@ -22,19 +22,73 @@ defineProps<{ diff --git a/src/components/TabBar.vue b/src/components/TabBar.vue index 7be59c2..092e067 100644 --- a/src/components/TabBar.vue +++ b/src/components/TabBar.vue @@ -122,8 +122,8 @@ onBeforeUnmount(() => { diff --git a/src/main.ts b/src/main.ts index 25d0ac6..aba2c79 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,8 @@ import { createApp } from 'vue' import ElementPlus from 'element-plus' 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 VxeUITable from 'vxe-table' import 'vxe-table/lib/style.css' diff --git a/src/stores/app.ts b/src/stores/app.ts index ee6f4d0..5af6738 100644 --- a/src/stores/app.ts +++ b/src/stores/app.ts @@ -1,12 +1,99 @@ import { defineStore } from 'pinia' 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 = { + 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 状态 */ export const useAppStore = defineStore('app', () => { /** 侧边栏是否折叠 */ const sidebarCollapsed = ref(false) - /** 当前主题:light / dark */ - const theme = ref<'light' | 'dark'>('light') + /** 当前主题 */ + const theme = ref('default') /** 当前语言 */ const locale = ref<'zh-CN' | 'en-US'>('zh-CN') @@ -15,22 +102,38 @@ export const useAppStore = defineStore('app', () => { sidebarCollapsed.value = !sidebarCollapsed.value } - /** 设置主题 */ - function setTheme(value: 'light' | 'dark'): void { + /** 设置主题:驱动 html.dark 与 Element Plus 主色 CSS 变量 */ + function setTheme(value: ThemeMode): void { 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 = { + '--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 恢复) */ function initTheme(): void { - const saved = localStorage.getItem('gerp-theme') as 'light' | 'dark' | null - if (saved) setTheme(saved) + const saved = localStorage.getItem('gerp-theme') as ThemeMode | null + if (saved && THEME_CONFIGS[saved]) setTheme(saved) } - /** 切换主题 */ + /** 顶栏快捷切换:浅色主题 ↔ 深色主题 */ function toggleTheme(): void { - setTheme(theme.value === 'light' ? 'dark' : 'light') + setTheme(theme.value === 'dark' ? 'light' : 'dark') } return { diff --git a/src/styles/global.scss b/src/styles/global.scss index 3ac100f..63acb89 100644 --- a/src/styles/global.scss +++ b/src/styles/global.scss @@ -20,8 +20,8 @@ body { font-family: 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif; font-size: 14px; line-height: 1.5; - color: #303133; - background-color: #f5f7fa; + color: var(--el-text-color-primary); + background-color: var(--el-bg-color-page); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } @@ -66,3 +66,77 @@ html.dark { --gerp-bg-color: #1d1e1f; --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); + } +} diff --git a/src/types/api.ts b/src/types/api.ts index a03a849..68abaa1 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -208,15 +208,33 @@ export interface MenuItem { Permission?: string } -/** 当前登录用户信息 */ +/** 当前登录用户信息(对齐后端 /Query/GetELTUserInfo 返回结构) */ export interface UserInfo { /** 用户 UUID */ UserUUID: 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 /** 头像 URL */ Avatar?: string diff --git a/src/views/LegacyPage.vue b/src/views/LegacyPage.vue index a94bb16..7fa1a85 100644 --- a/src/views/LegacyPage.vue +++ b/src/views/LegacyPage.vue @@ -39,7 +39,7 @@ function onLoad(): void { height: 100%; width: 100%; overflow: hidden; - background: var(--gerp-bg-color, #f5f7fa); + background: var(--el-bg-color-page); } .legacy-page-skeleton { diff --git a/src/views/Login.vue b/src/views/Login.vue index 2029339..13bf3b8 100644 --- a/src/views/Login.vue +++ b/src/views/Login.vue @@ -41,84 +41,370 @@ async function handleLogin(): Promise { diff --git a/src/views/Profile.vue b/src/views/Profile.vue index 6068d69..410bc11 100644 --- a/src/views/Profile.vue +++ b/src/views/Profile.vue @@ -1,9 +1,56 @@ diff --git a/src/views/company/DepartmentDetail.vue b/src/views/company/DepartmentDetail.vue index b81edac..5959517 100644 --- a/src/views/company/DepartmentDetail.vue +++ b/src/views/company/DepartmentDetail.vue @@ -189,8 +189,13 @@ onMounted(async () => { - - + + { :pk-field="'UserRoleUUID'" selectable pageable + :max-height="400" :on-view="goUserDetail" /> diff --git a/src/views/company/EmployeeDetail.vue b/src/views/company/EmployeeDetail.vue index 6614fe8..72a297a 100644 --- a/src/views/company/EmployeeDetail.vue +++ b/src/views/company/EmployeeDetail.vue @@ -40,6 +40,9 @@ interface UserRecord { UserEmail: string UserTele: string UserAccountLicense: number + UserEmailVerify: number + UserGender: number + UserPositionStatus: number IsStaff: number | boolean UserCreateTime: string [key: string]: unknown @@ -75,12 +78,32 @@ const userUUID = (route.query.UserUUID as string) || '' /** 用户主键字段名 */ const PK_FIELD = 'UserUUID' -/** 账号状态映射:0=禁用,1=启用 */ -const accountStatusMap: Record = { +/** 账号状态映射:0=禁用,1=启用(对齐原版 profile.html:1 正常,0 已封禁) */ +const accountStatusMap: Record = { '0': { text: '禁用', type: 'danger' }, '1': { text: '启用', type: 'success' }, } +/** 邮箱验证映射:0=未认证,1=已认证(对齐原版 profile.html) */ +const emailVerifyMap: Record = { + '0': { text: '未认证', type: 'danger' }, + '1': { text: '已认证', type: 'success' }, +} + +/** 职位状态映射:1=在职,0=停职(对齐原版 profile.html) */ +const positionStatusMap: Record = { + '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 拦截器) */ const avatarUrl = computed(() => { const params = new URLSearchParams() @@ -236,11 +259,22 @@ onMounted(loadUser) {{ asStr(userData.UserRealName) || '-' }} {{ asStr(userData.UserEmail) || '-' }} {{ asStr(userData.UserTele) || '-' }} + + + {{ emailVerifyMap[String(userData.UserEmailVerify)]?.text ?? userData.UserEmailVerify }} + + + {{ genderText(userData.UserGender) }} {{ accountStatusMap[String(userData.UserAccountLicense)]?.text ?? userData.UserAccountLicense }} + + + {{ positionStatusMap[String(userData.UserPositionStatus)]?.text ?? userData.UserPositionStatus }} + + {{ isTruthy(userData.IsStaff) ? '是' : '否' }} @@ -274,25 +308,39 @@ onMounted(loadUser) - + + - + + diff --git a/src/views/company/RoleDetail.vue b/src/views/company/RoleDetail.vue index b4dca63..657c8bd 100644 --- a/src/views/company/RoleDetail.vue +++ b/src/views/company/RoleDetail.vue @@ -221,7 +221,7 @@ onMounted(loadRole) - + - + { - - + + { - - + + { - - + + { - - + + // === 显示格式化 helper === -/** 性别:1→男,0→女 */ +/** 性别:-1=保密,1=男,0=女(对齐原版 profile.html 性别按钮) */ function genderText(g: unknown): string { if (g === 1 || g === '1') return '男' if (g === 0 || g === '0') return '女' + if (g === -1 || g === '-1') 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 { return val ? '是' : '否' @@ -213,12 +228,22 @@ onMounted(async () => { {{ data?.UserName || '-' }} {{ data?.UserEmail || '-' }} {{ data?.UserTele || '-' }} + + + {{ emailVerifyTag(data?.UserEmailVerify).text }} + + {{ genderText(data?.UserGender) }} {{ accountLicenseTag(data?.UserAccountLicense).text }} + + + {{ positionStatusTag(data?.UserPositionStatus).text }} + + {{ yesNoText(data?.IsStaff) }} {{ yesNoText(data?.IsSuperuser) }} {{ formatTime(data?.UserCreateTime) }} @@ -238,8 +263,13 @@ onMounted(async () => { - - + + { - - + +