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
+13 -9
View File
@@ -1,11 +1,15 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { useAuthStore } from '@/stores/auth'
import { useThemeStore } from '@/stores/theme'
import { customersApi } from '@/api/customers'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getManagerColor as mgrColorFn, getMgrTextColor, loadManagerColors } from '@/utils/managerColor'
import api from '@/api/index'
const auth = useAuthStore()
const themeStore = useThemeStore()
const isLight = computed(() => themeStore.currentTheme === 'light')
const loading = ref(false)
const search = ref('')
const filterIndustry = ref('')
@@ -43,7 +47,7 @@ const importLoading = ref(false)
const importResult = ref<any>(null)
onMounted(async () => {
await loadCustomers()
await Promise.all([loadCustomers(), loadManagerColors()])
try {
const res = await api.get('/users/')
managers.value = (res.data || []).filter((u: any) => u.role !== 'leader')
@@ -193,11 +197,11 @@ async function removeExistingContact(contactId: string) {
} catch (e: any) { if (e !== 'cancel') ElMessage.error('删除失败') }
}
const mgrColors = ['#1C3738','#4A6741','#5B7FA5','#7B7568','#8B6F47','#6B5B4F','#3D5A5C','#5C4A3D']
function mgrColor(name: string) {
if (!name) return '#909399'
let h = 0; for (let i=0;i<name.length;i++) h = name.charCodeAt(i) + ((h<<5)-h)
return mgrColors[Math.abs(h) % mgrColors.length]
return mgrColorFn(name, isLight.value)
}
function mgrTextColor(name: string) {
return getMgrTextColor(mgrColor(name))
}
async function openDetail(customer: any) {
@@ -339,7 +343,7 @@ async function handleImport() {
<el-table-column prop="in_use_services" label="在用业务" min-width="150" />
<el-table-column label="客户经理" width="90">
<template #default="{ row }">
<el-tag :color="mgrColor(row.primary_manager_name)" effect="dark" size="small" style="border:none;color:#fff">{{ row.primary_manager_name || '-' }}</el-tag>
<el-tag :color="mgrColor(row.primary_manager_name)" effect="dark" size="small" :style="{ border:'none', color: mgrTextColor(row.primary_manager_name) }">{{ row.primary_manager_name || '-' }}</el-tag>
</template>
</el-table-column>
</el-table>
@@ -410,7 +414,7 @@ async function handleImport() {
<el-descriptions-item label="备注">{{ detailCustomer.remarks || '-' }}</el-descriptions-item>
<el-descriptions-item label="客户经理">{{ detailCustomer.primary_manager_name || '-' }}</el-descriptions-item>
</el-descriptions>
<h4 style="margin-top:16px; font-family: 'ZCOOL XiaoWei', STSong, serif; color: var(--ink)">联系人</h4>
<h4 style="margin-top:16px; font-family: var(--font-heading); color: var(--ink)">联系人</h4>
<div v-if="detailCustomer.contacts?.length">
<el-tag v-for="c in detailCustomer.contacts" :key="c.id" style="margin:4px">
{{ c.name }}{{ c.phone ? ' · '+c.phone : '' }}{{ c.role_desc ? ' ('+c.role_desc+')' : '' }}
@@ -489,7 +493,7 @@ async function handleImport() {
.page-head-row { display: flex; justify-content: space-between; align-items: center; }
.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;
}