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
+55
View File
@@ -0,0 +1,55 @@
import { reactive } from 'vue'
// 12-color palettes — wide hue spacing for max distinction
const COLORS_DARK = ['#C62828','#D84315','#EF6C00','#B8860B','#558B2F','#00897B','#0277BD','#C2185B','#4E342E','#00695C','#5D4037','#2E7D32']
const COLORS_LIGHT = ['#FB7185','#FB923C','#FBBF24','#A3E635','#34D399','#2DD4BF','#38BDF8','#F472B6','#F97316','#84CC16','#22D3EE','#FDA4AF']
// Reactive name→color overrides (populated from backend user.color)
export const managerColorOverrides: Record<string, string> = reactive({})
let loaded = false
export async function loadManagerColors() {
if (loaded) return
try {
const { default: api } = await import('@/api/index')
const res = await api.get('/users/')
for (const u of res.data) {
if (u.color) managerColorOverrides[u.name] = u.color
}
loaded = true
} catch (_) {}
}
export function setManagerColorOverride(name: string, color: string | null) {
if (color) {
managerColorOverrides[name] = color
} else {
delete managerColorOverrides[name]
}
}
export function getManagerColor(name: string, isLight: boolean): string {
if (!name) return '#909399'
// Stored override takes priority
if (managerColorOverrides[name]) return managerColorOverrides[name]
// Fallback: deterministic hash from name
let h = 0
for (let i = 0; i < name.length; i++) h = name.charCodeAt(i) + ((h << 5) - h)
const colors = isLight ? COLORS_LIGHT : COLORS_DARK
return colors[Math.abs(h) % colors.length]
}
/** Return white or dark text color based on background luminance */
export function getMgrTextColor(bgHex: string): string {
const r = parseInt(bgHex.slice(1, 3), 16)
const g = parseInt(bgHex.slice(3, 5), 16)
const b = parseInt(bgHex.slice(5, 7), 16)
const lum = 0.299 * r + 0.587 * g + 0.114 * b
return lum > 150 ? '#1A1A1A' : '#FFFFFF'
}
/** Return { background, color } style object for a manager tag */
export function getMgrStyle(name: string, isLight: boolean) {
const bg = getManagerColor(name, isLight)
return { background: bg, color: getMgrTextColor(bg) }
}