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:
@@ -2,10 +2,12 @@
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useThemeStore, themeLabels } from '@/stores/theme'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
const themeStore = useThemeStore()
|
||||
const collapsed = ref(false)
|
||||
|
||||
interface MenuGroup { label?: string; items: { path: string; label: string; icon: string }[] }
|
||||
@@ -57,6 +59,11 @@ const roleLabel = computed(() => {
|
||||
if (auth.isLeader) return '分管领导'
|
||||
return '客户经理'
|
||||
})
|
||||
|
||||
function cycleTheme() {
|
||||
const next = themeStore.currentTheme === 'editorial' ? 'light' : 'editorial'
|
||||
themeStore.setTheme(next)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -100,6 +107,13 @@ const roleLabel = computed(() => {
|
||||
</svg>
|
||||
</button>
|
||||
<div class="topbar-actions">
|
||||
<button class="topbar-btn" @click="cycleTheme" :title="'切换至' + themeLabels[themeStore.currentTheme === 'editorial' ? 'light' : 'editorial']">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle v-if="themeStore.currentTheme === 'editorial'" cx="12" cy="12" r="5"></circle>
|
||||
<path v-if="themeStore.currentTheme === 'editorial'" 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>
|
||||
<path v-if="themeStore.currentTheme === 'light'" d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="topbar-btn" @click="router.push('/m')" title="移动端">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="5" y="2" width="14" height="20" rx="2" ry="2"></rect>
|
||||
@@ -135,8 +149,8 @@ const roleLabel = computed(() => {
|
||||
/* ═══ Sidebar ═══ */
|
||||
.sidebar {
|
||||
width: 240px;
|
||||
background: var(--ink);
|
||||
color: #fff;
|
||||
background: var(--sidebar-bg);
|
||||
color: var(--sidebar-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
@@ -158,31 +172,31 @@ const roleLabel = computed(() => {
|
||||
|
||||
.sidebar-brand {
|
||||
padding: 26px 22px 18px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.06);
|
||||
border-bottom: 1px solid var(--sidebar-divider);
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
margin: 0;
|
||||
font-family: 'ZCOOL XiaoWei', STSong, Songti SC, serif;
|
||||
font-family: var(--font-heading);
|
||||
font-size: 24px;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.12em;
|
||||
color: #fff;
|
||||
color: var(--sidebar-brand-color);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.brand-sub {
|
||||
margin: 2px 0 0;
|
||||
font-family: 'Noto Serif SC', STSong, serif;
|
||||
font-family: var(--font-body);
|
||||
font-size: 10px;
|
||||
color: rgba(255,255,255,0.4);
|
||||
color: var(--sidebar-brand-sub-color);
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.brand-rule {
|
||||
width: 28px;
|
||||
height: 3px;
|
||||
background: var(--gold);
|
||||
background: var(--sidebar-accent);
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
@@ -197,10 +211,10 @@ const roleLabel = computed(() => {
|
||||
}
|
||||
|
||||
.nav-group-label {
|
||||
font-family: 'JetBrains Mono', 'SF Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.15em;
|
||||
color: rgba(255,255,255,0.25);
|
||||
color: var(--sidebar-group-label-color);
|
||||
padding: 12px 14px 4px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
@@ -212,9 +226,9 @@ const roleLabel = computed(() => {
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 11px 14px;
|
||||
color: rgba(255,255,255,0.55);
|
||||
color: var(--sidebar-nav-text);
|
||||
text-decoration: none;
|
||||
font-family: 'Noto Serif SC', STSong, serif;
|
||||
font-family: var(--font-body);
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.04em;
|
||||
transition: all 0.22s;
|
||||
@@ -222,14 +236,14 @@ const roleLabel = computed(() => {
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
color: rgba(255,255,255,0.85);
|
||||
background: rgba(255,255,255,0.04);
|
||||
color: var(--sidebar-nav-text-hover);
|
||||
background: var(--sidebar-nav-bg-hover);
|
||||
}
|
||||
|
||||
.nav-item--active {
|
||||
color: #fff;
|
||||
background: rgba(255,255,255,0.06);
|
||||
border-left-color: var(--gold);
|
||||
color: var(--sidebar-nav-text-active);
|
||||
background: var(--sidebar-nav-bg-active);
|
||||
border-left-color: var(--sidebar-nav-border-active);
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
@@ -244,23 +258,23 @@ const roleLabel = computed(() => {
|
||||
/* ═══ Sidebar Footer ═══ */
|
||||
.sidebar-footer {
|
||||
padding: 14px 22px;
|
||||
border-top: 1px solid rgba(255,255,255,0.06);
|
||||
border-top: 1px solid var(--sidebar-divider);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.footer-role {
|
||||
font-family: 'Noto Serif SC', STSong, serif;
|
||||
font-family: var(--font-body);
|
||||
font-size: 10px;
|
||||
color: var(--gold);
|
||||
color: var(--sidebar-footer-role-color);
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.footer-name {
|
||||
font-family: 'ZCOOL XiaoWei', STSong, serif;
|
||||
font-family: var(--font-heading);
|
||||
font-size: 13px;
|
||||
color: rgba(255,255,255,0.7);
|
||||
color: var(--sidebar-footer-name-color);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
@@ -285,7 +299,7 @@ const roleLabel = computed(() => {
|
||||
}
|
||||
|
||||
.topbar-greeting {
|
||||
font-family: 'ZCOOL XiaoWei', STSong, serif;
|
||||
font-family: var(--font-heading);
|
||||
font-size: 14px;
|
||||
color: var(--ink);
|
||||
letter-spacing: 0.04em;
|
||||
@@ -315,7 +329,7 @@ const roleLabel = computed(() => {
|
||||
background: none;
|
||||
border: 1px solid var(--warm-border);
|
||||
color: var(--warm-gray);
|
||||
font-family: 'Noto Serif SC', STSong, serif;
|
||||
font-family: var(--font-body);
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.03em;
|
||||
cursor: pointer;
|
||||
|
||||
@@ -125,14 +125,14 @@ function labelFor(field: string) {
|
||||
}
|
||||
|
||||
.log-editor {
|
||||
font-family: 'ZCOOL XiaoWei', STSong, serif;
|
||||
font-family: var(--font-heading);
|
||||
font-size: 13px;
|
||||
color: var(--ink);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
font-family: 'JetBrains Mono', 'SF Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
color: var(--c-text-muted);
|
||||
}
|
||||
|
||||
@@ -266,7 +266,7 @@ const imgStyle = computed(() => {
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
cursor: pointer;
|
||||
font-family: 'Noto Serif SC', STSong, serif;
|
||||
font-family: var(--font-body);
|
||||
font-size: 12px;
|
||||
transition: all 0.15s;
|
||||
white-space: nowrap;
|
||||
@@ -287,7 +287,7 @@ const imgStyle = computed(() => {
|
||||
}
|
||||
|
||||
.fit-label {
|
||||
font-family: 'JetBrains Mono', 'SF Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ const activeTab = computed(() => {
|
||||
|
||||
.header-title {
|
||||
margin: 0;
|
||||
font-family: 'ZCOOL XiaoWei', STSong, Songti SC, serif;
|
||||
font-family: var(--font-heading);
|
||||
font-size: 26px;
|
||||
font-weight: 400;
|
||||
color: var(--ink);
|
||||
@@ -138,7 +138,7 @@ const activeTab = computed(() => {
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
font-family: 'Noto Serif SC', STSong, Songti SC, serif;
|
||||
font-family: var(--font-body);
|
||||
font-size: 11px;
|
||||
color: var(--warm-gray);
|
||||
letter-spacing: 0.12em;
|
||||
@@ -178,7 +178,7 @@ const activeTab = computed(() => {
|
||||
}
|
||||
|
||||
.header-user {
|
||||
font-family: 'Noto Serif SC', STSong, serif;
|
||||
font-family: var(--font-body);
|
||||
font-size: 13px;
|
||||
color: var(--warm-gray);
|
||||
letter-spacing: 0.03em;
|
||||
@@ -201,7 +201,7 @@ const activeTab = computed(() => {
|
||||
right: 0;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background: linear-gradient(135deg, transparent 60%, rgba(196,147,74,0.06) 60%);
|
||||
background: linear-gradient(135deg, transparent 60%, var(--page-accent-color) 60%);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
@@ -238,7 +238,7 @@ const activeTab = computed(() => {
|
||||
cursor: pointer;
|
||||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
white-space: nowrap;
|
||||
font-family: 'Noto Serif SC', STSong, serif;
|
||||
font-family: var(--font-body);
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user