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:
TermiNexus committed 2026-08-11 01:03:18 +08:00
1 parent a813e34a4f
commit 4f4508083c
10 files changed
+714 -298

No files matched your search

+2 -4
View File
@@ -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
View File
@@ -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);
}
+3 -2
View File
@@ -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;
}
+57 -73
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 {
+9 -1
View File
@@ -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;
+133 -3
View File
@@ -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>