feat: 双主题系统 — 编辑风 + 极简亮色

前端:
- 新增 Pinia theme store,localStorage 持久化,data-theme 切换
- App.vue: :root 新增 --font-*/--sidebar-* CSS 变量;新增 :root[data-theme=light] 完整配色(极简黑白金 + Noto Sans SC)
- DesktopLayout: 侧边栏 15 个变量化 + 顶栏主题切换按钮
- MobileLayout: 装饰色变量化
- Settings: 新增「界面主题」卡片
- 21 个组件 font-family 统一替换为 var(--font-*)
- 新增 utils/managerColor.ts — 12 色调色板 + 自适应文字色 + 后端自定义色优先
- WorkPlans/MiniBusiness/KeyVisits/CustomerManage: 客户经理标签色统一走共享工具
- UserManage: 编辑弹窗新增颜色选择器(仅 manager)
- LightBoard: 去标题星标图标
- ManagerWorkspace/WorkPlans/MiniBusiness/KeyVisits: 删除按钮改为实心红底白字

后端:
- User 模型新增 color 列 + lifespan 自动迁移
- users API 支持 color 字段读写
- UserOut schema 新增 color

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-06 16:03:13 +08:00
parent 336b60b3af
commit deeafe239f
37 changed files with 2275 additions and 218 deletions
+33 -4
View File
@@ -2,6 +2,9 @@
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import api from '@/api/index'
import { useThemeStore, themeLabels, type Theme } from '@/stores/theme'
const themeStore = useThemeStore()
const managers = ref<any[]>([])
const selectedUserIds = ref<string[]>([])
@@ -107,6 +110,24 @@ async function handleImportPreview() {
<div class="page-rule"></div>
</div>
<!-- Theme -->
<el-card class="setting-card">
<template #header>
<div class="card-header-title">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right:6px; color: var(--gold)">
<circle cx="12" cy="12" r="5"></circle><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"></path>
</svg>
界面主题
</div>
</template>
<div class="theme-options">
<label v-for="t in (['editorial', 'light'] as Theme[])" :key="t" class="theme-opt" :class="{ 'theme-opt--active': themeStore.currentTheme === t }" @click="themeStore.setTheme(t)">
<span class="theme-opt-radio">{{ themeStore.currentTheme === t ? '●' : '○' }}</span>
<span class="theme-opt-label">{{ themeLabels[t] }}</span>
</label>
</div>
</el-card>
<!-- Manual Remind -->
<el-card class="setting-card">
<template #header>
@@ -206,7 +227,7 @@ async function handleImportPreview() {
<el-button type="success" :loading="importLoading" @click="handleImportPreview" :disabled="!importFile">预览并导入</el-button>
</el-form>
<div v-if="importPreview" style="margin-top:16px">
<h4 style="font-family: 'ZCOOL XiaoWei', STSong, serif; color: var(--ink)">文件预览</h4>
<h4 style="font-family: var(--font-heading); color: var(--ink)">文件预览</h4>
<el-table :data="Object.entries(importPreview)" stripe>
<el-table-column prop="0" label="Sheet" />
<el-table-column prop="1.row_count" label="数据行数" />
@@ -249,7 +270,7 @@ async function handleImportPreview() {
.page-head { margin-bottom: 24px; }
.page-title {
margin: 0;
font-family: 'ZCOOL XiaoWei', STSong, serif;
font-family: var(--font-heading);
font-size: 22px; font-weight: 400;
color: var(--ink); letter-spacing: 0.06em;
}
@@ -258,10 +279,18 @@ async function handleImportPreview() {
.setting-card { margin-bottom: 16px; }
.card-header-title {
display: flex; align-items: center;
font-family: 'ZCOOL XiaoWei', STSong, serif;
font-family: var(--font-heading);
font-size: 15px; color: var(--ink); letter-spacing: 0.05em;
}
.setting-row { display: flex; gap: 32px; align-items: flex-start; flex-wrap: wrap; }
.setting-col { display: flex; flex-direction: column; gap: 8px; }
.setting-label { font-family: 'Noto Serif SC', STSong, serif; font-size: 13px; color: var(--ink); }
.setting-label { font-family: var(--font-body); font-size: 13px; color: var(--ink); }
/* ── Theme Selector ── */
.theme-options { display: flex; gap: 16px; }
.theme-opt { display: flex; align-items: center; gap: 8px; cursor: pointer; padding: 8px 16px; border: 1px solid var(--warm-border); transition: all var(--transition); }
.theme-opt:hover { border-color: var(--ink); }
.theme-opt--active { border-color: var(--ink); background: var(--c-primary-bg); }
.theme-opt-radio { font-size: 14px; color: var(--ink); }
.theme-opt-label { font-family: var(--font-body); font-size: 14px; color: var(--c-text); }
</style>