解决了深色模式下tab栏和left-nav的选中状态非深色的问题,以及下拉框 padding 为 6px 的问题,以及深色模式头像 hover-card 的 下拉框头部和 下拉框 item hover 效果问题

This commit is contained in:
TermiNexus committed 2026-08-04 02:11:01 +08:00
1 parent 41b982c7b5
commit 39debab2b3
20 files changed
+1010 -143

No files matched your search

+7
View File
@@ -36,6 +36,12 @@ const props = defineProps<{
* 关闭时表格自适应内容高度(详情页嵌套场景,随页面滚动)。
*/
stretch?: boolean
/**
* 表格最大高度(px,可选)。
* 详情页嵌套子表场景传入(如 400),行数超过时表格内部滚动,
* 避免所有行撑开导致页面无限加长。不传则不限制。
*/
maxHeight?: number
}>()
const options: UseCrudTableOptions<T> = {
@@ -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 }"
+64 -10
View File
@@ -22,19 +22,73 @@ defineProps<{
<template>
<!-- 信息网格:columns 控制列数,title 控制标题,border 控制边框 -->
<el-descriptions
:column="columns || 2"
:title="title"
:border="border ?? true"
class="info-grid"
>
<slot />
</el-descriptions>
<el-card shadow="never" class="info-grid-card">
<template v-if="title" #header>
<div class="info-grid-header">
<span class="info-grid-title-dot" />
<span class="info-grid-title">{{ title }}</span>
</div>
</template>
<el-descriptions
:column="columns || 2"
:border="border ?? true"
class="info-grid"
>
<slot />
</el-descriptions>
</el-card>
</template>
<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 {
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>
+10 -3
View File
@@ -122,8 +122,8 @@ onBeforeUnmount(() => {
<style scoped lang="scss">
.tab-bar {
height: 36px;
background: #fff;
border-bottom: 1px solid var(--gerp-border-color);
background: var(--el-bg-color);
border-bottom: 1px solid var(--el-border-color-light);
display: flex;
align-items: center;
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 {
font-size: 14px;
flex-shrink: 0;
@@ -218,7 +225,7 @@ onBeforeUnmount(() => {
margin: 0;
padding: 4px 0;
list-style: none;
background: #fff;
background: var(--el-bg-color-overlay);
border: 1px solid var(--el-border-color-light);
border-radius: 6px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
+26 -11
View File
@@ -328,8 +328,8 @@ const isFaIcon = (icon: string | undefined | null): boolean => {
justify-content: space-between;
height: 56px;
padding: 0 16px;
background: #fff;
border-bottom: 1px solid var(--gerp-border-color);
background: var(--el-bg-color);
border-bottom: 1px solid var(--el-border-color-light);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
z-index: 100;
}
@@ -354,7 +354,7 @@ const isFaIcon = (icon: string | undefined | null): boolean => {
&-text {
font-size: 16px;
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;
&:hover {
background: #f5f7fa;
background: var(--el-fill-color-light);
}
}
@@ -397,7 +397,7 @@ const isFaIcon = (icon: string | undefined | null): boolean => {
.gerp-user-name {
font-size: 14px;
color: #303133;
color: var(--el-text-color-primary);
}
.gerp-main {
@@ -409,8 +409,8 @@ const isFaIcon = (icon: string | undefined | null): boolean => {
.gerp-sidebar {
width: 220px;
flex-shrink: 0;
background: #fff;
border-right: 1px solid var(--gerp-border-color);
background: var(--el-bg-color);
border-right: 1px solid var(--el-border-color-light);
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
display: flex;
@@ -611,12 +611,13 @@ const isFaIcon = (icon: string | undefined | null): boolean => {
// 侧边栏底部(版本号 + 设置按钮)
.gerp-sidebar-footer {
padding: 8px 16px;
border-top: 1px solid var(--gerp-border-color);
border-top: 1px solid var(--el-border-color-light);
display: flex;
align-items: center;
justify-content: space-between;
min-height: 40px;
flex-shrink: 0;
background: var(--el-bg-color);
.gerp-version {
font-size: 11px;
@@ -630,7 +631,7 @@ const isFaIcon = (icon: string | undefined | null): boolean => {
overflow: hidden;
display: flex;
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;
}
/* 用户信息头部:主色淡背景 + 大头像 */
/* 用户信息头部:主色淡背景 + 大头像(暗色下自动变暗) */
.user-dropdown-header {
display: flex;
align-items: center;
gap: 12px;
padding: 16px 20px;
background: var(--el-color-primary-light-9);
background: var(--el-fill-color-light);
.user-dropdown-avatar {
width: 48px;
@@ -782,4 +783,18 @@ const isFaIcon = (icon: string | undefined | null): boolean => {
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>
+2
View File
@@ -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'
+112 -9
View File
@@ -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<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 状态 */
export const useAppStore = defineStore('app', () => {
/** 侧边栏是否折叠 */
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')
@@ -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<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 恢复) */
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 {
+76 -2
View File
@@ -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);
}
}
+21 -3
View File
@@ -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
+1 -1
View File
@@ -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 {
+333 -47
View File
@@ -41,84 +41,370 @@ async function handleLogin(): Promise<void> {
<template>
<div class="login-page">
<div class="login-card">
<div class="login-header">
<img src="/favicon.svg" alt="logo" class="login-logo" />
<h1 class="login-title">GERP 管理系统</h1>
<p class="login-subtitle">登录到后台管理</p>
<!-- 背景装饰:浮动光斑 -->
<div class="bg-orb orb-a" />
<div class="bg-orb orb-b" />
<div class="bg-orb orb-c" />
<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>
<el-form :model="form" :rules="rules" label-position="top" size="large" @submit.prevent="handleLogin">
<el-form-item label="用户名" prop="UserName">
<el-input v-model="form.UserName" placeholder="请输入用户名" :prefix-icon="'User'" clearable />
</el-form-item>
<!-- 右:登录表单区 -->
<div class="form-panel">
<div class="form-inner">
<div class="form-header">
<h2 class="form-title">欢迎回来</h2>
<p class="form-subtitle">登录您的账户以继续</p>
</div>
<el-form-item label="密码" prop="UserPassword">
<el-input
v-model="form.UserPassword"
type="password"
placeholder="请输入密码"
:prefix-icon="'Lock'"
show-password
@keyup.enter="handleLogin"
/>
</el-form-item>
<el-form
:model="form"
:rules="rules"
label-position="top"
size="large"
class="login-form"
@submit.prevent="handleLogin"
>
<el-form-item label="用户名" prop="UserName">
<el-input
v-model="form.UserName"
placeholder="请输入用户名"
:prefix-icon="'User'"
clearable
/>
</el-form-item>
<el-form-item>
<el-checkbox v-model="form.RememberMe">记住我</el-checkbox>
</el-form-item>
<el-form-item label="密码" prop="UserPassword">
<el-input
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">
登 录
</el-button>
</el-form>
<div class="form-options">
<el-checkbox v-model="form.RememberMe">记住我</el-checkbox>
</div>
<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>
</template>
<style scoped lang="scss">
.login-page {
position: relative;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
overflow: hidden;
background: #0b1220;
}
.login-card {
width: 400px;
max-width: 90vw;
padding: 40px;
background: #fff;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
/* ===== 背景装饰 ===== */
.bg-orb {
position: absolute;
border-radius: 50%;
filter: blur(90px);
pointer-events: none;
animation: orb-float 12s ease-in-out infinite alternate;
}
.login-header {
text-align: center;
margin-bottom: 32px;
.orb-a {
width: 480px;
height: 480px;
top: -140px;
left: -120px;
background: radial-gradient(circle, rgba(99, 102, 241, 0.55), transparent 70%);
}
.login-logo {
width: 56px;
height: 56px;
margin-bottom: 12px;
.orb-b {
width: 420px;
height: 420px;
bottom: -160px;
right: -100px;
background: radial-gradient(circle, rgba(16, 185, 129, 0.4), transparent 70%);
animation-delay: -4s;
}
.login-title {
margin: 0 0 8px;
font-size: 24px;
.orb-c {
width: 300px;
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;
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;
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 {
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>
+60 -2
View File
@@ -1,9 +1,56 @@
<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 { getAvatarUrl } from '@/api/auth'
const userStore = useUserStore()
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>
<template>
@@ -16,12 +63,23 @@ const avatarUrl = getAvatarUrl()
<img :src="avatarUrl" alt="头像" class="avatar" />
<el-descriptions :column="1" border>
<el-descriptions-item label="用户名">{{ userStore.userInfo?.UserName || '-' }}</el-descriptions-item>
<el-descriptions-item label="显示名">{{ userStore.userInfo?.DisplayName || '-' }}</el-descriptions-item>
<el-descriptions-item label="邮箱">{{ userStore.userInfo?.Email || '-' }}</el-descriptions-item>
<el-descriptions-item label="邮箱">{{ 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="是否管理员">
<el-tag :type="userStore.isAdmin ? 'danger' : 'info'">{{ userStore.isAdmin ? '是' : '否' }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ userStore.userInfo?.UserCreateTime || '-' }}</el-descriptions-item>
</el-descriptions>
</div>
</el-card>
+159 -22
View File
@@ -26,7 +26,6 @@ import { ref, reactive, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import DetailPage from '@/components/DetailPage.vue'
import InfoGrid from '@/components/InfoGrid.vue'
import CrudTable from '@/components/CrudTable.vue'
import { formApi } from '@/api/form'
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)
</script>
@@ -214,58 +223,104 @@ onMounted(loadCompany)
<!-- 错误提示(缺参 / 加载失败) -->
<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-descriptions-item label="公司名称">{{ asStr(companyData.CompanyName) || '-' }}</el-descriptions-item>
<el-descriptions-item label="公司简称">{{ asStr(companyData.CompanyShort) || '-' }}</el-descriptions-item>
<el-descriptions-item label="公司状态">
<el-tag :type="companyStatusMap[String(companyData.CompanyStatus)]?.type || 'info'" size="small">
{{ companyStatusMap[String(companyData.CompanyStatus)]?.text ?? companyData.CompanyStatus }}
</el-tag>
</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>
</InfoGrid>
<!-- 主表基本信息:公司头部卡 -->
<el-card v-if="companyData" shadow="never" class="company-hero">
<div class="company-hero-body">
<div class="company-avatar">
<el-icon :size="26"><OfficeBuilding /></el-icon>
</div>
<div class="company-hero-main">
<div class="company-hero-title-row">
<h2 class="company-hero-title">{{ asStr(companyData.CompanyName) || '-' }}</h2>
<el-tag :type="companyStatusMap[String(companyData.CompanyStatus)]?.type || 'info'" size="small" effect="light">
{{ companyStatusMap[String(companyData.CompanyStatus)]?.text ?? companyData.CompanyStatus }}
</el-tag>
</div>
<p class="company-hero-sub">{{ asStr(companyData.CompanyShort) ? '简称 ' + asStr(companyData.CompanyShort) : '暂无简称' }}</p>
</div>
</div>
<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:部门 -->
<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
table="Department"
title="部门"
:query="{ CompanyUUID: companyUUID }"
:selectable="true"
:pageable="true"
:max-height="400"
pk-field="DeptUUID"
:on-view="goDeptDetail"
/>
</el-card>
<!-- 子表 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
table="Workgroup"
title="工作组"
:query="{ CompanyUUID: companyUUID }"
:selectable="true"
:pageable="true"
:max-height="400"
pk-field="WorkgroupUUID"
:on-view="goWorkgroupDetail"
/>
</el-card>
<!-- 子表 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
table="UserRole"
title="员工"
:query="{ CompanyUUID: companyUUID }"
:selectable="true"
:pageable="true"
:max-height="400"
pk-field="UserRoleUUID"
:on-view="goEmployeeDetail"
/>
@@ -361,5 +416,87 @@ onMounted(loadCompany)
</template>
<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>
+8 -2
View File
@@ -189,8 +189,13 @@ onMounted(async () => {
</InfoGrid>
<!-- 部门员工子表 -->
<el-card shadow="never">
<template #header>部门员工</template>
<el-card shadow="never" class="detail-card">
<template #header>
<div class="detail-card-header">
<el-icon><User /></el-icon>
<span>部门员工</span>
</div>
</template>
<CrudTable
v-if="deptUUID"
table="UserRole"
@@ -199,6 +204,7 @@ onMounted(async () => {
:pk-field="'UserRoleUUID'"
selectable
pageable
:max-height="400"
:on-view="goUserDetail"
/>
</el-card>
+52 -4
View File
@@ -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<string, { text: string; type: 'success' | 'danger' }> = {
/** 账号状态映射:0=禁用,1=启用(对齐原版 profile.html:1 正常,0 已封禁) */
const accountStatusMap: Record<string, { text: string; type: 'success' | 'danger' | 'info' }> = {
'0': { text: '禁用', type: 'danger' },
'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 拦截器) */
const avatarUrl = computed(() => {
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.UserEmail) || '-' }}</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-tag :type="accountStatusMap[String(userData.UserAccountLicense)]?.type || 'info'" size="small">
{{ accountStatusMap[String(userData.UserAccountLicense)]?.text ?? userData.UserAccountLicense }}
</el-tag>
</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="是否员工">
<span :style="{ color: isTruthy(userData.IsStaff) ? '#166534' : '#991b1b' }">
{{ isTruthy(userData.IsStaff) ? '是' : '否' }}
@@ -274,25 +308,39 @@ onMounted(loadUser)
</InfoGrid>
<!-- 子表 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
table="UserRole"
title="角色与组织"
:query="{ UserUUID: userUUID }"
:selectable="true"
:pageable="true"
:max-height="400"
pk-field="UserRoleUUID"
/>
</el-card>
<!-- 子表 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
table="UserDept"
title="部门关联"
:query="{ UserUUID: userUUID }"
:selectable="true"
:pageable="true"
:max-height="400"
pk-field="UserDeptUUID"
:on-view="goDeptDetail"
/>
+2 -2
View File
@@ -221,7 +221,7 @@ onMounted(loadRole)
</el-alert>
<!-- 子表 1:权限列表 -->
<el-card v-if="roleData" shadow="never">
<el-card v-if="roleData" shadow="never" class="detail-card">
<CrudTable
table="RolePermission"
title="权限列表"
@@ -233,7 +233,7 @@ onMounted(loadRole)
</el-card>
<!-- 子表 2:关联用户 -->
<el-card v-if="roleData" shadow="never">
<el-card v-if="roleData" shadow="never" class="detail-card">
<CrudTable
table="UserRole"
title="关联用户"
+7 -2
View File
@@ -185,8 +185,13 @@ onMounted(async () => {
</InfoGrid>
<!-- 关联公司子表 -->
<el-card shadow="never">
<template #header>关联公司</template>
<el-card shadow="never" class="detail-card">
<template #header>
<div class="detail-card-header">
<el-icon><OfficeBuilding /></el-icon>
<span>关联公司</span>
</div>
</template>
<CrudTable
v-if="tenantUUID"
table="Company"
+7 -2
View File
@@ -164,8 +164,13 @@ onMounted(async () => {
</InfoGrid>
<!-- 工作组角色子表 -->
<el-card shadow="never">
<template #header>工作组角色</template>
<el-card shadow="never" class="detail-card">
<template #header>
<div class="detail-card-header">
<el-icon><Avatar /></el-icon>
<span>工作组角色</span>
</div>
</template>
<CrudTable
v-if="workgroupUUID"
table="WorkgroupRole"
+14 -4
View File
@@ -218,8 +218,13 @@ onMounted(async () => {
</InfoGrid>
<!-- 关联用户子表 -->
<el-card shadow="never">
<template #header>关联用户</template>
<el-card shadow="never" class="detail-card">
<template #header>
<div class="detail-card-header">
<el-icon><Avatar /></el-icon>
<span>关联用户</span>
</div>
</template>
<CrudTable
v-if="roleUUID"
table="UserRole"
@@ -233,8 +238,13 @@ onMounted(async () => {
</el-card>
<!-- 角色权限子表 -->
<el-card shadow="never">
<template #header>角色权限</template>
<el-card shadow="never" class="detail-card">
<template #header>
<div class="detail-card-header">
<el-icon><Lock /></el-icon>
<span>角色权限</span>
</div>
</template>
<CrudTable
v-if="roleUUID"
table="RolePermission"
+8 -11
View File
@@ -19,8 +19,8 @@
* - 【废弃 GERPThemeSync】:不再调用 window.GERPThemeSync.init(),主题改由 Pinia appStore 统一管理
* - 【移除 postMessage】:删除 SETTINGS_UPDATE / GET_THEME / THEME_CHANGE 全部跨窗口通信,
* Vue 单页应用内主题与设置直接通过 store / 响应式数据驱动,无需 iframe 消息桥
* - 主题管理用 useAppStore:appStore 当前仅支持 light / dark,选择这两项时调用 appStore.setTheme;
* 其余主题(default / green / purple / red)仅本地记录,TODO 待 appStore 扩展多主题支持
* - 主题管理用 useAppStore:appStore 支持 default / light / dark / green / purple / red 全部主题,
* setTheme 统一驱动 html.dark 与 Element Plus 主色 CSS 变量(--el-color-primary 系列)
* - 侧边栏折叠改用 appStore.sidebarCollapsed / toggleSidebar(替代 GERPUI.SetSidebarCollapsed)
* - 动画开关通过 watch 设置 CSS 变量 --TransitionSpeed / --AnimationDuration(替代原生 DOM 操作)
* - server / admin_verify 由 request.ts 拦截器统一注入
@@ -152,37 +152,34 @@ function applySettingsToRuntime(s: Settings): void {
appStore.sidebarCollapsed = !s.sidebarExpanded
}
// 主题:appStore 仅支持 light / dark,其余主题 TODO
// 主题:由 appStore 统一管理
applyTheme(s.themeMode)
}
/** 应用主题:light/dark 走 appStore,其余主题 TODO 待 appStore 扩展 */
/** 应用主题:全部 6 种主题由 appStore 统一管理(驱动 html.dark + Element Plus 主色变量) */
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)
return
}
// TODO: appStore 当前仅支持 light/dark,default/green/purple/red 等主题待扩展后接入
// default 暂按 light 处理,避免无主题
if (mode === 'default') {
appStore.setTheme('light')
}
}
/** 主题切换处理:立即应用并即时保存 */
function onThemeChange(value: string): void {
settings.themeMode = value
applyTheme(value)
debounceSave(300)
}
/** 侧边栏切换处理:立即生效并即时保存 */
function onSidebarChange(value: string | number | boolean): void {
settings.sidebarExpanded = Boolean(value)
appStore.sidebarCollapsed = !value
debounceSave(200)
}
/** 动画切换处理:立即生效并即时保存 */
function onAnimationsChange(value: string | number | boolean): void {
settings.animations = Boolean(value)
const root = document.documentElement
root.style.setProperty('--TransitionSpeed', value ? '0.2s' : '0s')
root.style.setProperty('--AnimationDuration', value ? '0.3s' : '0s')
+41 -6
View File
@@ -5,7 +5,7 @@
* 迁移自 TempLate/GERP/pages/system/user-detail.html
*
* 业务结构:
* 1. 基本信息(InfoGrid):用户名 / 邮箱 / 电话 / 性别 / 账号状态 / 是否员工 / 是否超级用户 / 创建时间 / 最后登录
* 1. 基本信息(InfoGrid):用户名 / 邮箱 / 邮箱验证 / 电话 / 性别 / 账号状态 / 职位状态 / 是否员工 / 是否超级用户 / 创建时间 / 最后登录
* 2. 2FA 双因素信息卡(TODO:GetUser2FAInfo 接口契约确认后补全字段渲染)
* 3. 角色与组织子表(CrudTable: UserRole,按 UserUUID 过滤)
* 4. 部门关联子表(CrudTable: UserDept,按 UserUUID 过滤)
@@ -67,13 +67,28 @@ const isolationHint = computed(() =>
// === 显示格式化 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 () => {
<el-descriptions-item label="用户名">{{ data?.UserName || '-' }}</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="邮箱验证">
<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="账号状态">
<el-tag :type="accountLicenseTag(data?.UserAccountLicense).type" size="small">
{{ accountLicenseTag(data?.UserAccountLicense).text }}
</el-tag>
</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?.IsSuperuser) }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ formatTime(data?.UserCreateTime) }}</el-descriptions-item>
@@ -238,8 +263,13 @@ onMounted(async () => {
</InfoGrid>
<!-- 角色与组织子表 -->
<el-card shadow="never">
<template #header>角色与组织</template>
<el-card shadow="never" class="detail-card">
<template #header>
<div class="detail-card-header">
<el-icon><Avatar /></el-icon>
<span>角色与组织</span>
</div>
</template>
<CrudTable
v-if="userUUID"
table="UserRole"
@@ -253,8 +283,13 @@ onMounted(async () => {
</el-card>
<!-- 部门关联子表 -->
<el-card shadow="never">
<template #header>部门关联</template>
<el-card shadow="never" class="detail-card">
<template #header>
<div class="detail-card-header">
<el-icon><Share /></el-icon>
<span>部门关联</span>
</div>
</template>
<CrudTable
v-if="userUUID"
table="UserDept"