解决了深色模式下tab栏和left-nav的选中状态非深色的问题,以及下拉框 padding 为 6px 的问题,以及深色模式头像 hover-card 的 下拉框头部和 下拉框 item hover 效果问题
This commit is contained in:
1 parent
41b982c7b5
commit
39debab2b3
20 files changed
+977
-110
No files matched your search
@@ -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 }"
|
||||
|
||||
@@ -22,19 +22,73 @@ defineProps<{
|
||||
|
||||
<template>
|
||||
<!-- 信息网格:columns 控制列数,title 控制标题,border 控制边框 -->
|
||||
<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"
|
||||
:title="title"
|
||||
: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>
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+317
-31
@@ -41,16 +41,65 @@ 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">
|
||||
<!-- 右:登录表单区 -->
|
||||
<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
|
||||
: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-input
|
||||
v-model="form.UserName"
|
||||
placeholder="请输入用户名"
|
||||
:prefix-icon="'User'"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="密码" prop="UserPassword">
|
||||
@@ -64,61 +113,298 @@ async function handleLogin(): Promise<void> {
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<div class="form-options">
|
||||
<el-checkbox v-model="form.RememberMe">记住我</el-checkbox>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<el-button type="primary" size="large" class="login-btn" :loading="loading" @click="handleLogin">
|
||||
<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
@@ -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>
|
||||
|
||||
@@ -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,14 +223,38 @@ 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">
|
||||
<!-- 主表基本信息:公司头部卡 -->
|
||||
<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) || '-' }}
|
||||
@@ -230,42 +263,64 @@ onMounted(loadCompany)
|
||||
{{ resolvedNames.tenant?.[companyData.TenantUUID] || asStr(companyData.TenantUUID) || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ formatTime(companyData.CompanyCreateTime) }}</el-descriptions-item>
|
||||
</InfoGrid>
|
||||
</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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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="关联用户"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user