style(gerp): 整体企业化改造(阿里云式克制风格、配色可配置、Win11 侧边栏)
- global.scss 重构为设计令牌体系:细边框/克制圆角/浅阴影/紧凑字号 - 统一覆盖 Element Plus 与 vxe-table 企业化样式(表头底色、行间细横线、紧凑行高) - app store 支持自定义主色:自动派生 light-N/dark-2 色阶,配色与明暗主题解耦 - Settings 新增"主题与配色"区:预设色板 + 自定义取色器 - 布局头部改面包屑式页名;侧边栏改 Win11 胶囊菜单(无指示条,hover/选中整行高亮,折叠图标居中,菜单项补 title 提示) - TabBar 还原圆角选中卡片式样式 - Dashboard 重做:关键指标 KPI 区 + 快捷入口 + 系统信息(去除装饰性渐变) - Login 配色改为跟随主题主色(去掉硬编码渐变) - 内容区内边距收敛到各视图容器,避免双层 padding
This commit is contained in:
1 parent
a813e34a4f
commit
4f4508083c
10 files changed
+714
-298
No files matched your search
@@ -552,11 +552,9 @@ function isCardSelected(row: Record<string, unknown>): boolean {
|
||||
<vxe-table
|
||||
v-if="!cardMode"
|
||||
ref="xTableRef"
|
||||
class="gerp-table"
|
||||
:data="dataList"
|
||||
v-loading="loading"
|
||||
border
|
||||
stripe
|
||||
round
|
||||
:height="tableFixedHeight"
|
||||
:max-height="tableLimitHeight"
|
||||
:row-config="{ isHover: true }"
|
||||
@@ -1026,7 +1024,7 @@ function isCardSelected(row: Record<string, unknown>): boolean {
|
||||
}
|
||||
|
||||
.table-title {
|
||||
font-size: 16px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
|
||||
+10
-10
@@ -40,24 +40,24 @@ defineProps<{
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
// 信息网格卡片:圆角 + 淡边框 + hover 阴影
|
||||
// 信息网格面板:细边框 + 轻阴影(企业风格,无大圆角)
|
||||
.info-grid-card {
|
||||
border-radius: 10px;
|
||||
border-radius: var(--gerp-radius-sm);
|
||||
overflow: hidden;
|
||||
transition: box-shadow 0.25s;
|
||||
transition: box-shadow 0.2s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
|
||||
box-shadow: var(--gerp-shadow-hover);
|
||||
}
|
||||
|
||||
:deep(.el-card__header) {
|
||||
padding: 14px 20px;
|
||||
padding: 10px 16px;
|
||||
background: var(--el-fill-color-light);
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 16px 20px;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,15 +66,15 @@ defineProps<{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 15px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.info-grid-title-dot {
|
||||
width: 6px;
|
||||
height: 16px;
|
||||
border-radius: 3px;
|
||||
width: 3px;
|
||||
height: 14px;
|
||||
border-radius: 2px;
|
||||
background: var(--el-color-primary);
|
||||
}
|
||||
|
||||
|
||||
@@ -141,8 +141,9 @@ onBeforeUnmount(() => {
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* 标签栏:圆角选中卡片式(还原原样式,保持企业风一致的紧凑高度) */
|
||||
.tab-bar {
|
||||
height: 36px;
|
||||
height: var(--gerp-tabbar-height);
|
||||
background: var(--el-bg-color);
|
||||
border-bottom: 1px solid var(--el-border-color-light);
|
||||
display: flex;
|
||||
@@ -166,7 +167,7 @@ onBeforeUnmount(() => {
|
||||
.tab-list {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 36px;
|
||||
height: var(--gerp-tabbar-height);
|
||||
padding: 0 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -219,7 +219,11 @@ const isFaIcon = (icon: string | undefined | null): boolean => {
|
||||
</div>
|
||||
|
||||
<div class="gerp-header-center">
|
||||
<h1 class="gerp-nav-title">{{ navTitle }}</h1>
|
||||
<span class="gerp-breadcrumb">
|
||||
<span class="gerp-breadcrumb-root">GERP</span>
|
||||
<el-icon class="gerp-breadcrumb-sep"><ArrowRight /></el-icon>
|
||||
<span class="gerp-breadcrumb-current">{{ navTitle }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="gerp-header-right">
|
||||
@@ -293,12 +297,13 @@ const isFaIcon = (icon: string | undefined | null): boolean => {
|
||||
<template #title>
|
||||
<i v-if="menu.Icon && isFaIcon(menu.Icon)" :class="menu.Icon" class="gerp-menu-icon" />
|
||||
<el-icon v-else-if="menu.Icon"><component :is="menu.Icon" /></el-icon>
|
||||
<span>{{ menu.Title }}</span>
|
||||
<span :title="menu.Title">{{ menu.Title }}</span>
|
||||
</template>
|
||||
<el-menu-item
|
||||
v-for="child in menu.Children"
|
||||
:key="child.Path || child.Title"
|
||||
:index="child.Path || ''"
|
||||
:title="child.Title"
|
||||
>
|
||||
<i v-if="child.Icon && isFaIcon(child.Icon)" :class="child.Icon" class="gerp-menu-icon" />
|
||||
<el-icon v-else-if="child.Icon"><component :is="child.Icon" /></el-icon>
|
||||
@@ -307,7 +312,7 @@ const isFaIcon = (icon: string | undefined | null): boolean => {
|
||||
</el-sub-menu>
|
||||
|
||||
<!-- 无子菜单 -->
|
||||
<el-menu-item v-else :index="menu.Path || ''">
|
||||
<el-menu-item v-else :index="menu.Path || ''" :title="menu.Title">
|
||||
<i v-if="menu.Icon && isFaIcon(menu.Icon)" :class="menu.Icon" class="gerp-menu-icon" />
|
||||
<el-icon v-else-if="menu.Icon"><component :is="menu.Icon" /></el-icon>
|
||||
<template #title><span>{{ menu.Title }}</span></template>
|
||||
@@ -370,12 +375,12 @@ const isFaIcon = (icon: string | undefined | null): boolean => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 56px;
|
||||
height: var(--gerp-header-height);
|
||||
padding: 0 16px;
|
||||
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;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.gerp-header-left,
|
||||
@@ -406,10 +411,26 @@ const isFaIcon = (icon: string | undefined | null): boolean => {
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.gerp-nav-title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
/* 面包屑式页名(企业控制台惯用:根级 + 分隔 + 当前页) */
|
||||
.gerp-breadcrumb {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 14px;
|
||||
|
||||
&-root {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
&-sep {
|
||||
color: var(--el-text-color-placeholder);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&-current {
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.gerp-header-actions {
|
||||
@@ -451,7 +472,7 @@ const isFaIcon = (icon: string | undefined | null): boolean => {
|
||||
}
|
||||
|
||||
.gerp-sidebar {
|
||||
width: 220px;
|
||||
width: var(--gerp-sidebar-width);
|
||||
flex-shrink: 0;
|
||||
background: var(--el-bg-color);
|
||||
border-right: 1px solid var(--el-border-color-light);
|
||||
@@ -506,32 +527,30 @@ const isFaIcon = (icon: string | undefined | null): boolean => {
|
||||
// 折叠态:宽度变小 + 文字淡出收缩 + 箭头隐藏 + 二级菜单收起
|
||||
// 注意:icon 保持与展开态完全相同的位置(padding/margin 不变),仅宽度收缩 + 文字淡出
|
||||
&.collapsed {
|
||||
width: 64px;
|
||||
width: var(--gerp-sidebar-collapsed-width);
|
||||
// 一级菜单(sub-menu)宽度锁定为容器宽
|
||||
:deep(.el-sub-menu) {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
// sub-menu 标题:padding-left 与 el-menu-item 对齐(icon 原地不动),
|
||||
// padding-right 收紧(默认 44px 为箭头预留,会撑破 64px)
|
||||
// 折叠态:菜单项与 sub-menu 标题图标居中(icon 18px,(64-18)/2=23)
|
||||
:deep(.el-menu-item) {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding-left: 23px;
|
||||
padding-right: 23px;
|
||||
}
|
||||
:deep(.el-sub-menu__title) {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
padding-left: 23px;
|
||||
padding-right: 23px;
|
||||
}
|
||||
// 二级菜单容器宽度锁定为容器宽
|
||||
:deep(.el-menu--inline) {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
// 隐藏左侧指示条
|
||||
:deep(.el-menu-item),
|
||||
:deep(.el-sub-menu__title) {
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
:deep(.el-menu-item span),
|
||||
:deep(.el-sub-menu__title span) {
|
||||
opacity: 0;
|
||||
@@ -553,84 +572,48 @@ const isFaIcon = (icon: string | undefined | null): boolean => {
|
||||
}
|
||||
}
|
||||
|
||||
// 菜单项:对齐原版 GERP 的左侧指示条 + hover/active 效果
|
||||
// 菜单项:Win11 风格胶囊(无指示条,hover/选中整行圆角高亮)
|
||||
:deep(.el-menu-item) {
|
||||
position: relative;
|
||||
margin: 2px 8px;
|
||||
margin: 2px 10px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
transition: padding 0.4s cubic-bezier(0.25, 0.1, 0.25, 1), background-color 0.2s ease, color 0.2s ease;
|
||||
|
||||
// 左侧指示条(hover/active 时显示)
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 3px;
|
||||
height: 60%;
|
||||
background: var(--el-color-primary);
|
||||
border-radius: 0 3px 3px 0;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
transition: padding 0.4s cubic-bezier(0.25, 0.1, 0.25, 1), background-color 0.15s ease, color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--el-fill-color-light);
|
||||
|
||||
&::before {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background: var(--el-color-primary-light-9);
|
||||
color: var(--el-color-primary);
|
||||
font-weight: 500;
|
||||
|
||||
&::before {
|
||||
opacity: 1;
|
||||
}
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
// 子菜单标题
|
||||
// 子菜单标题:同样胶囊 hover
|
||||
:deep(.el-sub-menu__title) {
|
||||
margin: 2px 8px;
|
||||
margin: 2px 10px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
transition: padding 0.4s cubic-bezier(0.25, 0.1, 0.25, 1), background-color 0.2s ease, color 0.2s ease;
|
||||
transition: padding 0.4s cubic-bezier(0.25, 0.1, 0.25, 1), background-color 0.15s ease, color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
}
|
||||
|
||||
// 子菜单项:层级缩进 + 字号略小 + 左侧圆点指示,明确层级
|
||||
// 子菜单项:层级缩进 + 字号略小,选中同样胶囊高亮
|
||||
:deep(.el-menu--inline .el-menu-item) {
|
||||
font-size: 13px;
|
||||
min-width: auto;
|
||||
padding-left: 52px !important;
|
||||
margin: 1px 10px;
|
||||
padding-left: 46px !important;
|
||||
color: var(--el-text-color-secondary);
|
||||
position: relative;
|
||||
|
||||
// 子项左侧圆点指示(区分层级)
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 32px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 50%;
|
||||
background: var(--el-text-color-placeholder);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
color: var(--el-color-primary);
|
||||
&::after {
|
||||
background: var(--el-color-primary);
|
||||
}
|
||||
background: var(--el-color-primary-light-9);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -680,11 +663,12 @@ const isFaIcon = (icon: string | undefined | null): boolean => {
|
||||
|
||||
// 路由内容体:撑满剩余空间,内部滚动
|
||||
// 多 tab-pane 叠放,active 的撑满并独立滚动,非 active 的 v-show:none 不占空间
|
||||
// 内边距由各视图自身容器控制(避免双层 padding),这里置 0
|
||||
.gerp-content-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
padding: 16px;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
+76
-21
@@ -24,7 +24,7 @@ interface ThemePalette {
|
||||
dark2: string
|
||||
}
|
||||
|
||||
/** 各主题色板配置 */
|
||||
/** 各主题色板配置(预设配色;自定义配色经 setCustomColor 覆盖主色) */
|
||||
const THEME_CONFIGS: Record<ThemeMode, ThemePalette> = {
|
||||
default: {
|
||||
dark: false,
|
||||
@@ -88,12 +88,57 @@ const THEME_CONFIGS: Record<ThemeMode, ThemePalette> = {
|
||||
},
|
||||
}
|
||||
|
||||
/** 应用状态:侧边栏折叠、主题、语言等全局 UI 状态 */
|
||||
/** 本地存储键 */
|
||||
const THEME_KEY = 'gerp-theme'
|
||||
const PRIMARY_KEY = 'gerp-primary'
|
||||
|
||||
/**
|
||||
* 十六进制颜色与白/黑按比例混合(对齐 Element Plus light-N/dark-2 派生规则)
|
||||
* light-N:主色混白 N*10%;dark-2:主色混黑 20%
|
||||
*/
|
||||
function mixColor(hex: string, mix: string, weight: number): string {
|
||||
const parse = (c: string): [number, number, number] => {
|
||||
const h = c.replace('#', '')
|
||||
const full = h.length === 3 ? h.split('').map((x) => x + x).join('') : h
|
||||
return [
|
||||
parseInt(full.slice(0, 2), 16),
|
||||
parseInt(full.slice(2, 4), 16),
|
||||
parseInt(full.slice(4, 6), 16),
|
||||
]
|
||||
}
|
||||
const [r1, g1, b1] = parse(hex)
|
||||
const [r2, g2, b2] = parse(mix)
|
||||
const r = Math.round(r1 * (1 - weight) + r2 * weight)
|
||||
const g = Math.round(g1 * (1 - weight) + g2 * weight)
|
||||
const b = Math.round(b1 * (1 - weight) + b2 * weight)
|
||||
return `#${[r, g, b].map((v) => v.toString(16).padStart(2, '0')).join('')}`
|
||||
}
|
||||
|
||||
/** 把主色应用到 Element Plus CSS 变量(含全部衍生色阶) */
|
||||
function applyPrimaryColor(color: string): void {
|
||||
const root = document.documentElement
|
||||
const vars: Record<string, string> = {
|
||||
'--el-color-primary': color,
|
||||
'--el-color-primary-light-3': mixColor(color, '#ffffff', 0.3),
|
||||
'--el-color-primary-light-5': mixColor(color, '#ffffff', 0.5),
|
||||
'--el-color-primary-light-7': mixColor(color, '#ffffff', 0.7),
|
||||
'--el-color-primary-light-8': mixColor(color, '#ffffff', 0.8),
|
||||
'--el-color-primary-light-9': mixColor(color, '#ffffff', 0.9),
|
||||
'--el-color-primary-dark-2': mixColor(color, '#000000', 0.2),
|
||||
}
|
||||
for (const [k, v] of Object.entries(vars)) {
|
||||
root.style.setProperty(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
/** 应用状态:侧边栏折叠、主题、配色、语言等全局 UI 状态 */
|
||||
export const useAppStore = defineStore('app', () => {
|
||||
/** 侧边栏是否折叠 */
|
||||
const sidebarCollapsed = ref(false)
|
||||
/** 当前主题 */
|
||||
/** 当前主题(明暗 + 预设配色) */
|
||||
const theme = ref<ThemeMode>('default')
|
||||
/** 自定义主色(hex);设置后覆盖预设配色,null 表示用预设 */
|
||||
const customPrimary = ref<string | null>(null)
|
||||
/** 当前语言 */
|
||||
const locale = ref<'zh-CN' | 'en-US'>('zh-CN')
|
||||
|
||||
@@ -102,7 +147,7 @@ export const useAppStore = defineStore('app', () => {
|
||||
sidebarCollapsed.value = !sidebarCollapsed.value
|
||||
}
|
||||
|
||||
/** 设置主题:驱动 html.dark 与 Element Plus 主色 CSS 变量 */
|
||||
/** 设置主题:驱动 html.dark 与 Element Plus 主色 CSS 变量(自定义配色优先) */
|
||||
function setTheme(value: ThemeMode): void {
|
||||
theme.value = value
|
||||
const cfg = THEME_CONFIGS[value] ?? THEME_CONFIGS.default
|
||||
@@ -110,28 +155,36 @@ export const useAppStore = defineStore('app', () => {
|
||||
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)
|
||||
applyPrimaryColor(customPrimary.value ?? cfg.primary)
|
||||
localStorage.setItem(THEME_KEY, value)
|
||||
}
|
||||
|
||||
/** 初始化主题(从 localStorage 恢复) */
|
||||
/**
|
||||
* 设置自定义主色(hex),null 恢复预设配色。
|
||||
* 配色与主题解耦:明暗切换(toggleTheme)不改变自定义配色。
|
||||
*/
|
||||
function setCustomColor(color: string | null): void {
|
||||
customPrimary.value = color
|
||||
const cfg = THEME_CONFIGS[theme.value] ?? THEME_CONFIGS.default
|
||||
applyPrimaryColor(color ?? cfg.primary)
|
||||
if (color) localStorage.setItem(PRIMARY_KEY, color)
|
||||
else localStorage.removeItem(PRIMARY_KEY)
|
||||
}
|
||||
|
||||
/** 初始化主题与配色(从 localStorage 恢复) */
|
||||
function initTheme(): void {
|
||||
const saved = localStorage.getItem('gerp-theme') as ThemeMode | null
|
||||
if (saved && THEME_CONFIGS[saved]) setTheme(saved)
|
||||
const saved = localStorage.getItem(THEME_KEY) as ThemeMode | null
|
||||
if (saved && THEME_CONFIGS[saved]) theme.value = saved
|
||||
const savedColor = localStorage.getItem(PRIMARY_KEY)
|
||||
if (savedColor) customPrimary.value = savedColor
|
||||
const cfg = THEME_CONFIGS[theme.value] ?? THEME_CONFIGS.default
|
||||
const root = document.documentElement
|
||||
root.classList.toggle('dark', cfg.dark)
|
||||
root.setAttribute('data-vxe-ui-theme', cfg.dark ? 'dark' : 'light')
|
||||
applyPrimaryColor(customPrimary.value ?? cfg.primary)
|
||||
}
|
||||
|
||||
/** 顶栏快捷切换:浅色主题 ↔ 深色主题 */
|
||||
/** 顶栏快捷切换:浅色主题 ↔ 深色主题(保持当前配色) */
|
||||
function toggleTheme(): void {
|
||||
setTheme(theme.value === 'dark' ? 'light' : 'dark')
|
||||
}
|
||||
@@ -139,9 +192,11 @@ export const useAppStore = defineStore('app', () => {
|
||||
return {
|
||||
sidebarCollapsed,
|
||||
theme,
|
||||
customPrimary,
|
||||
locale,
|
||||
toggleSidebar,
|
||||
setTheme,
|
||||
setCustomColor,
|
||||
initTheme,
|
||||
toggleTheme,
|
||||
}
|
||||
|
||||
+270
-48
@@ -1,6 +1,16 @@
|
||||
@use 'sass:meta';
|
||||
|
||||
/* ===== 全局重置与基础样式 ===== */
|
||||
/* ============================================================
|
||||
* GERP 全局样式 · 企业控制台风格
|
||||
*
|
||||
* 视觉基调:参考阿里云控制台的克制、清晰与高信息密度。
|
||||
* - 细边框(1px 浅灰)、克制的圆角(4px)、浅阴影
|
||||
* - 单一强调色:全部走 --el-color-primary 系列 CSS 变量,
|
||||
* 配色在「系统设置 → 主题与配色」中切换,本文件不写死任何主色
|
||||
* - 面板以「细边框 + 分区」承载信息,卡片仅用于承载交互
|
||||
* ============================================================ */
|
||||
|
||||
/* ---------- 1. 基础重置 ---------- */
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
@@ -17,8 +27,8 @@ body,
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||
font-size: 14px;
|
||||
font-family: var(--gerp-font-family);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: var(--el-text-color-primary);
|
||||
background-color: var(--el-bg-color-page);
|
||||
@@ -31,7 +41,73 @@ a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* ===== 滚动条样式 ===== */
|
||||
/* ---------- 2. 设计令牌 ---------- */
|
||||
:root {
|
||||
/* 字体栈:企业级跨平台 */
|
||||
--gerp-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue',
|
||||
'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', sans-serif;
|
||||
|
||||
/* 圆角(克制) */
|
||||
--gerp-radius-xs: 2px;
|
||||
--gerp-radius-sm: 4px;
|
||||
--gerp-radius-md: 6px;
|
||||
--gerp-radius-lg: 8px;
|
||||
|
||||
/* 边框(浅灰细线) */
|
||||
--gerp-border-color: #e4e7ed;
|
||||
--gerp-border-color-light: #ebedf0;
|
||||
|
||||
/* 阴影(浅、克制,去除装饰性大阴影) */
|
||||
--gerp-shadow-card: 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||
--gerp-shadow-hover: 0 2px 6px rgba(15, 23, 42, 0.08);
|
||||
--gerp-shadow-popover: 0 4px 14px rgba(15, 23, 42, 0.12);
|
||||
|
||||
/* 布局尺寸(全局唯一来源,供布局组件引用) */
|
||||
--gerp-header-height: 52px;
|
||||
--gerp-tabbar-height: 36px;
|
||||
--gerp-sidebar-width: 220px;
|
||||
--gerp-sidebar-collapsed-width: 64px;
|
||||
--gerp-content-padding: 16px;
|
||||
}
|
||||
|
||||
html.dark {
|
||||
--gerp-border-color: #3d4046;
|
||||
--gerp-border-color-light: #35383d;
|
||||
--gerp-shadow-card: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
--gerp-shadow-hover: 0 2px 6px rgba(0, 0, 0, 0.42);
|
||||
--gerp-shadow-popover: 0 4px 14px rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
|
||||
/* ---------- 3. Element Plus 基础变量覆盖 ---------- */
|
||||
:root {
|
||||
/* 更紧凑的基础字号(企业控制台信息密度) */
|
||||
--el-font-size-base: 13px;
|
||||
--el-font-size-small: 12px;
|
||||
--el-font-size-large: 15px;
|
||||
|
||||
/* 统一圆角 */
|
||||
--el-border-radius-base: var(--gerp-radius-sm);
|
||||
--el-border-radius-small: var(--gerp-radius-xs);
|
||||
--el-border-radius-round: 999px;
|
||||
|
||||
/* 统一阴影:克制 */
|
||||
--el-box-shadow-light: var(--gerp-shadow-card);
|
||||
--el-box-shadow: var(--gerp-shadow-hover);
|
||||
--el-box-shadow-dark: var(--gerp-shadow-popover);
|
||||
|
||||
/* 卡片:细边框、无默认大阴影、更紧凑内边距 */
|
||||
--el-card-border-color: var(--gerp-border-color);
|
||||
--el-card-border-radius: var(--gerp-radius-sm);
|
||||
--el-card-padding: 14px 16px;
|
||||
--el-card-box-shadow: none;
|
||||
|
||||
/* 弹窗:圆角 + 轻阴影 + 更紧凑标题 */
|
||||
--el-dialog-border-radius: var(--gerp-radius-md);
|
||||
--el-dialog-padding-primary: 16px;
|
||||
--el-dialog-title-font-size: 15px;
|
||||
}
|
||||
|
||||
/* ---------- 4. 滚动条 ---------- */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
@@ -42,15 +118,14 @@ a {
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #c0c4cc;
|
||||
background: #c2c6cc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #909399;
|
||||
background: #9aa0a8;
|
||||
}
|
||||
|
||||
/* 深色主题滚动条:深灰 thumb,hover 提亮 */
|
||||
html.dark {
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #4c4d4f;
|
||||
@@ -61,27 +136,96 @@ html.dark {
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 主题 CSS 变量(兼容旧 GERP 主题系统) ===== */
|
||||
/* ---------- 5. 兼容旧 GERP 主题变量(业务代码可能引用) ---------- */
|
||||
:root {
|
||||
--gerp-primary: #409eff;
|
||||
--gerp-success: #67c23a;
|
||||
--gerp-warning: #e6a23c;
|
||||
--gerp-danger: #f56c6c;
|
||||
--gerp-info: #909399;
|
||||
--gerp-bg-color: #f5f7fa;
|
||||
--gerp-border-color: #dcdfe6;
|
||||
--gerp-primary: var(--el-color-primary);
|
||||
--gerp-success: var(--el-color-success);
|
||||
--gerp-warning: var(--el-color-warning);
|
||||
--gerp-danger: var(--el-color-danger);
|
||||
--gerp-info: var(--el-color-info);
|
||||
--gerp-bg-color: var(--el-bg-color-page);
|
||||
}
|
||||
|
||||
/* ===== 暗色主题(通过 html.dark 切换) ===== */
|
||||
html.dark {
|
||||
--gerp-bg-color: #1d1e1f;
|
||||
--gerp-border-color: #414243;
|
||||
/* ---------- 6. 面板与卡片 ---------- */
|
||||
.el-card {
|
||||
/* 卡片默认即细边框无阴影;hover 阴影由 is-hover-shadow 类单独给 */
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* ===== 下拉面板首尾无多余空隙 ===== */
|
||||
/* el-select 下拉选项列表:去掉默认 padding: 6px 0,选项首尾贴边 */
|
||||
/* 加 !important:Element Plus select 样式按需动态加载(css chunk 后注入),
|
||||
不加 important 会被其后加载的同特异性规则覆盖 */
|
||||
.el-card.is-hover-shadow:hover {
|
||||
box-shadow: var(--gerp-shadow-hover);
|
||||
}
|
||||
|
||||
.el-card__header {
|
||||
padding: 12px 16px;
|
||||
border-bottom-color: var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
/* 页面级分段面板(企业控制台惯用:细边框 + 分区标题,避免卡片套卡片) */
|
||||
.gerp-panel {
|
||||
background: var(--el-bg-color);
|
||||
border: 1px solid var(--gerp-border-color);
|
||||
border-radius: var(--gerp-radius-sm);
|
||||
padding: 16px;
|
||||
|
||||
.gerp-panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 14px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
|
||||
.el-icon {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- 7. 按钮 ---------- */
|
||||
.el-button {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* 文本按钮(表格操作列):主色、更紧凑 */
|
||||
.el-button.is-link {
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
/* ---------- 8. 输入类控件:统一圆角与聚焦态 ---------- */
|
||||
.el-input__wrapper,
|
||||
.el-textarea__inner,
|
||||
.el-select__wrapper {
|
||||
border-radius: var(--gerp-radius-sm);
|
||||
}
|
||||
|
||||
/* ---------- 9. 弹窗 / 消息框 ---------- */
|
||||
.el-dialog {
|
||||
border-radius: var(--gerp-radius-md);
|
||||
box-shadow: var(--gerp-shadow-popover);
|
||||
}
|
||||
|
||||
.el-dialog__header {
|
||||
padding: 14px 16px 10px;
|
||||
margin-right: 0;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.el-dialog__footer {
|
||||
padding: 10px 16px 14px;
|
||||
}
|
||||
|
||||
.el-message-box {
|
||||
border-radius: var(--gerp-radius-md);
|
||||
box-shadow: var(--gerp-shadow-popover);
|
||||
}
|
||||
|
||||
/* ---------- 10. 下拉面板:无首尾空隙(保留原有修复) ---------- */
|
||||
.el-select-dropdown__list {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
@@ -89,52 +233,105 @@ html.dark {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
/* el-dropdown 菜单:去掉默认 padding: 5px 0,菜单项首尾贴边 */
|
||||
/* 用完整 padding:0 简写 + !important,避免与 Element Plus 默认规则合并时被吞 */
|
||||
.el-dropdown__popper .el-dropdown-menu {
|
||||
padding: 0 !important;
|
||||
padding: 4px 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 背景在深色下过亮,改用半透明主色) ===== */
|
||||
/* ---------- 11. 描述列表(详情页信息网格) ---------- */
|
||||
.el-descriptions__label {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ---------- 12. 标签(克制:小号、细边框) ---------- */
|
||||
.el-tag {
|
||||
border-radius: var(--gerp-radius-xs);
|
||||
}
|
||||
|
||||
/* ---------- 13. 分页 ---------- */
|
||||
.el-pagination {
|
||||
--el-pagination-button-bg-color: transparent;
|
||||
--el-pagination-hover-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
/* ---------- 14. 表格(vxe-table 企业化) ---------- */
|
||||
:root {
|
||||
/* 行高:紧凑(默认 48 → 40) */
|
||||
--vxe-ui-table-row-height-default: 40px;
|
||||
--vxe-ui-table-row-height-medium: 40px;
|
||||
|
||||
/* 表头:浅灰底 + 中等字重,区分数据区 */
|
||||
--vxe-ui-table-header-background-color: var(--el-fill-color-light);
|
||||
--vxe-ui-table-header-font-color: var(--el-text-color-regular);
|
||||
--vxe-ui-table-header-font-weight: 600;
|
||||
|
||||
/* 边框:细线,与主题色联动 */
|
||||
--vxe-ui-table-border-color: var(--el-border-color-lighter);
|
||||
--vxe-ui-table-border-radius: 0;
|
||||
--vxe-ui-table-border-width: 1px;
|
||||
|
||||
/* 字号:跟随基础字号 */
|
||||
--vxe-ui-table-font-size: 13px;
|
||||
|
||||
/* hover 行 */
|
||||
--vxe-ui-table-row-hover-background-color: var(--el-fill-color-light);
|
||||
--vxe-ui-table-row-striped-background-color: var(--el-fill-color-lighter);
|
||||
--vxe-ui-table-row-current-background-color: var(--el-color-primary-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);
|
||||
--vxe-ui-table-header-background-color: var(--el-fill-color-darker);
|
||||
}
|
||||
|
||||
/* 企业表格:不画竖线,仅表头底色 + 行间细横线(gerp-table 类由 CrudTable 挂载) */
|
||||
.gerp-table {
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
|
||||
.vxe-header--column,
|
||||
.vxe-body--column,
|
||||
.vxe-footer--column {
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
/* 页面加载遮罩:暗色半透明黑(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;
|
||||
/* 最后一列底部不需要多余横线 */
|
||||
.vxe-body--last-row .vxe-body--column,
|
||||
.vxe-table--border-bottom .vxe-body--column {
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 详情页公共样式 ===== */
|
||||
/* 详情子表卡片:圆角 + hover 阴影(详情页统一观感) */
|
||||
/* 操作列按钮:链接按钮组更紧凑 */
|
||||
.gerp-table .vxe-body--column .el-button.is-link {
|
||||
padding: 0 2px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* ---------- 15. 表单 ---------- */
|
||||
.el-form-item {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.el-form-item__label {
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
/* ---------- 16. 详情页公共样式 ---------- */
|
||||
/* 详情子表卡片:细边框 + hover 浅阴影(去除大圆角) */
|
||||
.detail-card {
|
||||
border-radius: 10px;
|
||||
transition: box-shadow 0.25s;
|
||||
border-radius: var(--gerp-radius-sm);
|
||||
transition: box-shadow 0.2s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
|
||||
box-shadow: var(--gerp-shadow-hover);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,7 +340,7 @@ html.dark {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 15px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
|
||||
@@ -151,3 +348,28 @@ html.dark {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- 17. 深色模式适配 ---------- */
|
||||
html.dark {
|
||||
/* 侧边栏菜单选中项:半透明主色(light-9 在深色下过亮) */
|
||||
.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);
|
||||
}
|
||||
|
||||
/* 页面加载遮罩 */
|
||||
.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;
|
||||
}
|
||||
|
||||
/* 卡片阴影加深 */
|
||||
.el-card.is-hover-shadow:hover {
|
||||
box-shadow: var(--gerp-shadow-hover);
|
||||
}
|
||||
}
|
||||
+139
-92
@@ -1,6 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { get } from '@/api/request'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import type { MenuItem } from '@/types/api'
|
||||
|
||||
interface DashboardStats {
|
||||
UserCount: number
|
||||
@@ -9,6 +12,9 @@ interface DashboardStats {
|
||||
ActiveTenants: number
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const stats = ref<DashboardStats>({
|
||||
UserCount: 0,
|
||||
OrderCount: 0,
|
||||
@@ -26,68 +32,89 @@ onMounted(async () => {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
/** 关键指标(企业控制台 KPI 数值区) */
|
||||
const statItems = computed(() => [
|
||||
{ label: '用户总数', value: String(stats.value.UserCount ?? 0) },
|
||||
{ label: '订单总数', value: String(stats.value.OrderCount ?? 0) },
|
||||
{ label: '今日收入', value: `¥${(Number(stats.value.TodayRevenue) || 0).toFixed(2)}` },
|
||||
{ label: '活跃租户', value: String(stats.value.ActiveTenants ?? 0) },
|
||||
])
|
||||
|
||||
/** 快捷入口:取菜单一级项;含子菜单时跳首个子页 */
|
||||
const quickMenus = computed<MenuItem[]>(() => userStore.menus || [])
|
||||
|
||||
/** 判断图标是否为 FontAwesome 类名(与布局菜单一致) */
|
||||
const isFaIcon = (icon: string | undefined | null): boolean => {
|
||||
if (!icon) return false
|
||||
const trimmed = icon.trim()
|
||||
return trimmed.includes(' ') || trimmed.startsWith('fa-') || /^fa[brsl]?\b/.test(trimmed)
|
||||
}
|
||||
|
||||
/** 跳转菜单对应路由(子菜单取首个子页) */
|
||||
function goMenu(menu: MenuItem): void {
|
||||
const target = menu.Path || menu.Children?.[0]?.Path
|
||||
if (target) router.push(target)
|
||||
}
|
||||
|
||||
/** 当前角色文案 */
|
||||
const roleLabel = computed(() => {
|
||||
if (userStore.isAdmin) return '管理员'
|
||||
return userStore.currentRole || '普通用户'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-loading="loading" class="dashboard">
|
||||
<el-row :gutter="16">
|
||||
<el-col :xs="24" :sm="12" :lg="6">
|
||||
<el-card shadow="hover" class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon user"><el-icon :size="32"><User /></el-icon></div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ stats.UserCount }}</div>
|
||||
<div class="stat-label">用户总数</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<!-- 关键指标 -->
|
||||
<section class="gerp-panel stat-panel">
|
||||
<div class="gerp-panel-header">
|
||||
<el-icon><DataAnalysis /></el-icon>
|
||||
<span>关键指标</span>
|
||||
</div>
|
||||
<div class="stat-grid">
|
||||
<div v-for="item in statItems" :key="item.label" class="stat-cell">
|
||||
<div class="stat-value">{{ item.value }}</div>
|
||||
<div class="stat-label">{{ item.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-col :xs="24" :sm="12" :lg="6">
|
||||
<el-card shadow="hover" class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon order"><el-icon :size="32"><ShoppingCart /></el-icon></div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ stats.OrderCount }}</div>
|
||||
<div class="stat-label">订单总数</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<!-- 快捷入口 -->
|
||||
<section v-if="quickMenus.length" class="gerp-panel">
|
||||
<div class="gerp-panel-header">
|
||||
<el-icon><Grid /></el-icon>
|
||||
<span>快捷入口</span>
|
||||
</div>
|
||||
<div class="quick-grid">
|
||||
<div
|
||||
v-for="menu in quickMenus"
|
||||
:key="menu.Path || menu.Title"
|
||||
class="quick-item"
|
||||
@click="goMenu(menu)"
|
||||
>
|
||||
<i v-if="menu.Icon && isFaIcon(menu.Icon)" :class="menu.Icon" class="quick-item-icon" />
|
||||
<el-icon v-else-if="menu.Icon" class="quick-item-icon"><component :is="menu.Icon" /></el-icon>
|
||||
<span class="quick-item-label">{{ menu.Title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-col :xs="24" :sm="12" :lg="6">
|
||||
<el-card shadow="hover" class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon revenue"><el-icon :size="32"><Money /></el-icon></div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">¥{{ (Number(stats.TodayRevenue) || 0).toFixed(2) }}</div>
|
||||
<div class="stat-label">今日收入</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :xs="24" :sm="12" :lg="6">
|
||||
<el-card shadow="hover" class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-icon tenant"><el-icon :size="32"><OfficeBuilding /></el-icon></div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ stats.ActiveTenants }}</div>
|
||||
<div class="stat-label">活跃租户</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-card class="welcome-card" shadow="never">
|
||||
<template #header>
|
||||
<span>欢迎使用 GERP 管理系统</span>
|
||||
</template>
|
||||
<p>当前技术栈:Vue 3 + TypeScript + Vite + Element Plus + vxe-table + FormKit</p>
|
||||
<p>后端:Django + DRF(已就绪,无需修改)</p>
|
||||
<p>路由模式:SPA(已移除 iframe)</p>
|
||||
</el-card>
|
||||
<!-- 系统信息 -->
|
||||
<section class="gerp-panel">
|
||||
<div class="gerp-panel-header">
|
||||
<el-icon><Monitor /></el-icon>
|
||||
<span>系统信息</span>
|
||||
</div>
|
||||
<el-descriptions :column="2" border size="small">
|
||||
<el-descriptions-item label="当前用户">{{ userStore.displayName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="当前角色">{{ roleLabel }}</el-descriptions-item>
|
||||
<el-descriptions-item label="前端技术栈">Vue 3 + TypeScript + Element Plus</el-descriptions-item>
|
||||
<el-descriptions-item label="数据表格">vxe-table · FormKit 表单</el-descriptions-item>
|
||||
<el-descriptions-item label="后端服务">Django + DRF</el-descriptions-item>
|
||||
<el-descriptions-item label="系统版本">v1.0.0</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -96,57 +123,77 @@ onMounted(async () => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
margin-bottom: 16px;
|
||||
/* 关键指标:KPI 数值区(无卡片、无装饰渐变,靠分隔线分区) */
|
||||
.stat-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
.stat-cell {
|
||||
padding: 4px 20px;
|
||||
|
||||
.stat-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
|
||||
&.user {
|
||||
background: linear-gradient(135deg, #409eff, #66b1ff);
|
||||
}
|
||||
&.order {
|
||||
background: linear-gradient(135deg, #67c23a, #85ce61);
|
||||
}
|
||||
&.revenue {
|
||||
background: linear-gradient(135deg, #e6a23c, #f0b78a);
|
||||
}
|
||||
&.tenant {
|
||||
background: linear-gradient(135deg, #f56c6c, #f89898);
|
||||
& + .stat-cell {
|
||||
border-left: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-size: 26px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
line-height: 1.2;
|
||||
color: var(--el-text-color-primary);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
margin-top: 6px;
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
margin-top: 4px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.welcome-card {
|
||||
p {
|
||||
margin: 8px 0;
|
||||
color: #606266;
|
||||
/* 快捷入口:图标宫格 */
|
||||
.quick-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.quick-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 16px 8px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--gerp-radius-sm);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, border-color 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: var(--el-fill-color-light);
|
||||
border-color: var(--el-border-color-lighter);
|
||||
}
|
||||
}
|
||||
|
||||
.quick-item-icon {
|
||||
font-size: 22px;
|
||||
color: var(--el-color-primary);
|
||||
text-align: center;
|
||||
width: 22px;
|
||||
}
|
||||
|
||||
.quick-item-label {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-regular);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
+15
-44
@@ -41,10 +41,8 @@ async function handleLogin(): Promise<void> {
|
||||
|
||||
<template>
|
||||
<div class="login-page">
|
||||
<!-- 背景装饰:浮动光斑 -->
|
||||
<div class="bg-orb orb-a" />
|
||||
<div class="bg-orb orb-b" />
|
||||
<div class="bg-orb orb-c" />
|
||||
<!-- 背景装饰:克制的网格纹理 + 单一主色光晕 -->
|
||||
<div class="bg-orb" />
|
||||
<div class="bg-grid" />
|
||||
|
||||
<div class="login-shell">
|
||||
@@ -149,41 +147,14 @@ async function handleLogin(): Promise<void> {
|
||||
/* ===== 背景装饰 ===== */
|
||||
.bg-orb {
|
||||
position: absolute;
|
||||
width: 520px;
|
||||
height: 520px;
|
||||
top: -180px;
|
||||
right: -120px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, color-mix(in srgb, var(--el-color-primary) 22%, transparent), transparent 70%);
|
||||
filter: blur(90px);
|
||||
pointer-events: none;
|
||||
animation: orb-float 12s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
.orb-a {
|
||||
width: 480px;
|
||||
height: 480px;
|
||||
top: -140px;
|
||||
left: -120px;
|
||||
background: radial-gradient(circle, rgba(99, 102, 241, 0.55), transparent 70%);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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 {
|
||||
@@ -205,7 +176,7 @@ async function handleLogin(): Promise<void> {
|
||||
width: 920px;
|
||||
max-width: 94vw;
|
||||
min-height: 560px;
|
||||
border-radius: 20px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: rgba(17, 24, 39, 0.72);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
@@ -226,7 +197,7 @@ async function handleLogin(): Promise<void> {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background:
|
||||
radial-gradient(120% 120% at 0% 0%, rgba(99, 102, 241, 0.18), transparent 60%),
|
||||
radial-gradient(120% 120% at 0% 0%, color-mix(in srgb, var(--el-color-primary) 16%, transparent), 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);
|
||||
}
|
||||
@@ -245,7 +216,7 @@ async function handleLogin(): Promise<void> {
|
||||
.brand-logo {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
filter: drop-shadow(0 4px 12px rgba(99, 102, 241, 0.4));
|
||||
filter: drop-shadow(0 4px 12px color-mix(in srgb, var(--el-color-primary) 40%, transparent));
|
||||
}
|
||||
|
||||
.brand-name {
|
||||
@@ -347,8 +318,8 @@ async function handleLogin(): Promise<void> {
|
||||
|
||||
.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);
|
||||
border-color: var(--el-color-primary);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--el-color-primary) 18%, transparent);
|
||||
}
|
||||
|
||||
.login-form :deep(.el-input__inner) {
|
||||
@@ -367,18 +338,18 @@ async function handleLogin(): Promise<void> {
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.2em;
|
||||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||
background: var(--el-color-primary);
|
||||
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);
|
||||
box-shadow: 0 10px 28px color-mix(in srgb, var(--el-color-primary) 35%, transparent);
|
||||
}
|
||||
|
||||
.login-btn:active {
|
||||
|
||||
@@ -54,7 +54,8 @@ const positionStatus = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<div class="profile-page">
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<span>个人中心</span>
|
||||
</template>
|
||||
@@ -83,9 +84,16 @@ const positionStatus = computed(() => {
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.profile-page {
|
||||
padding: 16px;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
}
|
||||
.profile {
|
||||
display: flex;
|
||||
gap: 32px;
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
* - server / admin_verify 由 request.ts 拦截器统一注入
|
||||
* - 确认弹窗改用 ElMessageBox.confirm(替代 GERPUI.Confirm)
|
||||
*/
|
||||
import { ref, reactive, watch, onMounted } from 'vue'
|
||||
import { ref, reactive, computed, watch, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { get, post } from '@/api/request'
|
||||
import { setPageTitle } from '@/utils/serverScope'
|
||||
@@ -86,6 +86,53 @@ const themeOptions = [
|
||||
{ value: 'red', label: '红色主题' },
|
||||
]
|
||||
|
||||
/** 各主题预设主色(用于"跟随主题预设"色块预览) */
|
||||
const presetThemeColors: Record<string, string> = {
|
||||
default: '#409eff',
|
||||
light: '#409eff',
|
||||
dark: '#409eff',
|
||||
green: '#67c23a',
|
||||
purple: '#8b5cf6',
|
||||
red: '#f56c6c',
|
||||
}
|
||||
|
||||
/** 预设配色色板(企业常用色系,点击即作为自定义主色生效) */
|
||||
const colorPresets = [
|
||||
{ label: '主题蓝', color: '#409eff' },
|
||||
{ label: '湖畔青', color: '#2f9e8f' },
|
||||
{ label: '薄荷绿', color: '#67c23a' },
|
||||
{ label: '活力橙', color: '#ff9f0a' },
|
||||
{ label: '优雅紫', color: '#8b5cf6' },
|
||||
{ label: '珊瑚红', color: '#f56c6c' },
|
||||
{ label: '石墨灰', color: '#5b6472' },
|
||||
]
|
||||
|
||||
/** 是否已启用自定义配色(false = 跟随主题预设) */
|
||||
const hasCustomColor = computed(() => appStore.customPrimary !== null)
|
||||
|
||||
/** 当前生效主色(预览用) */
|
||||
const currentPrimary = computed(
|
||||
() => appStore.customPrimary ?? presetThemeColors[settings.themeMode] ?? '#409eff',
|
||||
)
|
||||
|
||||
/** 恢复为跟随主题预设配色 */
|
||||
function onFollowTheme(): void {
|
||||
appStore.setCustomColor(null)
|
||||
debounceSave(300)
|
||||
}
|
||||
|
||||
/** 选择预设色板 */
|
||||
function onPickPreset(color: string): void {
|
||||
appStore.setCustomColor(color)
|
||||
debounceSave(300)
|
||||
}
|
||||
|
||||
/** 自定义取色器(el-color-picker change 回调:取消返回 null) */
|
||||
function onCustomColor(color: string | null): void {
|
||||
appStore.setCustomColor(color || null)
|
||||
debounceSave(300)
|
||||
}
|
||||
|
||||
/** 当前设置(响应式) */
|
||||
const settings = reactive<Settings>({ ...defaultSettings })
|
||||
/** 是否已加载完成(避免初始化期间触发即时保存) */
|
||||
@@ -229,6 +276,7 @@ async function onResetSettings(): Promise<void> {
|
||||
/* 忽略 */
|
||||
}
|
||||
applyTheme('default')
|
||||
appStore.setCustomColor(null) // 重置配色色系(恢复跟随主题预设)
|
||||
applySettingsToRuntime(settings)
|
||||
ElMessage.success('设置已重置')
|
||||
}
|
||||
@@ -275,11 +323,11 @@ watch(
|
||||
|
||||
<!-- 外观设置 -->
|
||||
<div class="settings-section">
|
||||
<h2 class="settings-section-title">外观设置</h2>
|
||||
<h2 class="settings-section-title">主题与外观</h2>
|
||||
<div class="settings-item">
|
||||
<div class="settings-item-info">
|
||||
<div class="settings-item-title">主题模式</div>
|
||||
<div class="settings-item-desc">选择系统的主题风格</div>
|
||||
<div class="settings-item-desc">选择系统的明暗风格与预设配色</div>
|
||||
</div>
|
||||
<div class="settings-item-control">
|
||||
<el-select
|
||||
@@ -291,6 +339,47 @@ watch(
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-item">
|
||||
<div class="settings-item-info">
|
||||
<div class="settings-item-title">配色色系</div>
|
||||
<div class="settings-item-desc">自定义系统主色,独立于明暗主题生效</div>
|
||||
</div>
|
||||
<div class="settings-item-control">
|
||||
<div class="color-scheme">
|
||||
<!-- 跟随主题预设 -->
|
||||
<div
|
||||
class="color-swatch"
|
||||
:class="{ active: !hasCustomColor }"
|
||||
:style="{ background: presetThemeColors[settings.themeMode] ?? '#409eff' }"
|
||||
title="跟随主题预设"
|
||||
@click="onFollowTheme"
|
||||
>
|
||||
<el-icon v-if="!hasCustomColor"><Check /></el-icon>
|
||||
</div>
|
||||
<!-- 预设色板 -->
|
||||
<div
|
||||
v-for="p in colorPresets"
|
||||
:key="p.color"
|
||||
class="color-swatch"
|
||||
:class="{ active: hasCustomColor && appStore.customPrimary === p.color }"
|
||||
:style="{ background: p.color }"
|
||||
:title="p.label"
|
||||
@click="onPickPreset(p.color)"
|
||||
>
|
||||
<el-icon v-if="hasCustomColor && appStore.customPrimary === p.color"><Check /></el-icon>
|
||||
</div>
|
||||
<!-- 自定义取色器 -->
|
||||
<el-color-picker
|
||||
:model-value="appStore.customPrimary ?? undefined"
|
||||
:predefine="colorPresets.map((p) => p.color)"
|
||||
size="small"
|
||||
@change="onCustomColor"
|
||||
/>
|
||||
<!-- 当前主色预览 -->
|
||||
<span class="primary-preview" :style="{ color: currentPrimary }">{{ currentPrimary }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-item">
|
||||
<div class="settings-item-info">
|
||||
<div class="settings-item-title">侧边栏默认展开</div>
|
||||
@@ -502,4 +591,45 @@ watch(
|
||||
margin-left: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 配色色系 ===== */
|
||||
.color-scheme {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.color-swatch {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: var(--gerp-radius-sm);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
border: 2px solid transparent;
|
||||
transition: transform 0.15s, box-shadow 0.15s;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.12);
|
||||
box-shadow: var(--gerp-shadow-hover);
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: var(--el-text-color-primary);
|
||||
box-shadow: 0 0 0 2px var(--el-bg-color) inset;
|
||||
}
|
||||
}
|
||||
|
||||
.primary-preview {
|
||||
font-size: 12px;
|
||||
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid var(--gerp-border-color);
|
||||
border-radius: var(--gerp-radius-xs);
|
||||
background: var(--el-fill-color-lighter);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user