deeafe239f
前端: - 新增 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>
356 lines
12 KiB
Vue
356 lines
12 KiB
Vue
<script setup lang="ts">
|
|
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 }[] }
|
|
|
|
const menuGroups = computed<MenuGroup[]>(() => {
|
|
const groups: MenuGroup[] = [
|
|
{
|
|
label: '汇总',
|
|
items: [
|
|
{ path: '/', label: '仪表盘', icon: '<polyline points="4 7 12 3 20 7"></polyline><polyline points="20 7 20 21 4 21 4 7"></polyline><line x1="8" y1="21" x2="8" y2="12"></line><line x1="16" y1="21" x2="16" y2="12"></line>' },
|
|
{ path: '/weekly-report', label: '周报', icon: '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line>' },
|
|
{ path: '/light-board', label: '亮灯表', icon: '<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>' },
|
|
],
|
|
},
|
|
{
|
|
label: '工作',
|
|
items: [
|
|
{ path: '/work-plans', label: '工作计划', icon: '<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line><polyline points="8 14 12 18 16 14"></polyline>' },
|
|
{ path: '/mini-business', label: '商机跟单', icon: '<line x1="12" y1="1" x2="12" y2="23"></line><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path>' },
|
|
{ path: '/key-visits', label: '要客拜访', icon: '<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>' },
|
|
],
|
|
},
|
|
]
|
|
if (auth.isManager || auth.isDirector) {
|
|
groups.push({
|
|
label: undefined,
|
|
items: [
|
|
{ path: '/workspace', label: '我的数据', icon: '<line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line>' },
|
|
],
|
|
})
|
|
}
|
|
groups.push({
|
|
label: '管理',
|
|
items: [
|
|
{ path: '/customers', label: '客户管理', icon: '<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle>' },
|
|
],
|
|
})
|
|
if (auth.isDirector) {
|
|
groups[groups.length - 1].items.push(
|
|
{ path: '/users', label: '用户管理', icon: '<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path>' },
|
|
{ path: '/settings', label: '系统设置', icon: '<circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>' },
|
|
)
|
|
}
|
|
return groups
|
|
})
|
|
|
|
const roleLabel = computed(() => {
|
|
if (auth.isDirector) return '支局长'
|
|
if (auth.isLeader) return '分管领导'
|
|
return '客户经理'
|
|
})
|
|
|
|
function cycleTheme() {
|
|
const next = themeStore.currentTheme === 'editorial' ? 'light' : 'editorial'
|
|
themeStore.setTheme(next)
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="desktop-layout">
|
|
<!-- ═══ Sidebar — Ink & Gold Editorial ═══ -->
|
|
<aside class="sidebar" :class="{ 'sidebar--collapsed': collapsed }">
|
|
<div class="sidebar-brand">
|
|
<h1 class="brand-title">企迹</h1>
|
|
<p class="brand-sub">政企周报管理</p>
|
|
<div class="brand-rule"></div>
|
|
</div>
|
|
|
|
<nav class="sidebar-nav">
|
|
<template v-for="group in menuGroups" :key="group.label || 'nogroup'">
|
|
<div v-if="group.label" class="nav-group-label">{{ group.label }}</div>
|
|
<router-link
|
|
v-for="item in group.items"
|
|
:key="item.path"
|
|
:to="item.path"
|
|
class="nav-item"
|
|
:class="{ 'nav-item--active': route.path === item.path }"
|
|
>
|
|
<svg class="nav-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" v-html="item.icon"></svg>
|
|
<span class="nav-label">{{ item.label }}</span>
|
|
</router-link>
|
|
</template>
|
|
</nav>
|
|
|
|
<div class="sidebar-footer">
|
|
<span class="footer-role">{{ roleLabel }}</span>
|
|
<span class="footer-name">{{ auth.name }}</span>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- ═══ Main Area ═══ -->
|
|
<div class="main-area">
|
|
<header class="topbar">
|
|
<button class="collapse-btn" @click="collapsed = !collapsed" :title="collapsed ? '展开菜单' : '折叠菜单'">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<polyline :points="collapsed ? '13 17 18 12 13 7' : '11 7 6 12 11 17'"></polyline>
|
|
</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>
|
|
<line x1="12" y1="18" x2="12.01" y2="18"></line>
|
|
</svg>
|
|
移动端
|
|
</button>
|
|
<button class="topbar-btn topbar-btn--logout" @click="auth.logout(); router.push('/login')">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
|
|
<polyline points="16 17 21 12 16 7"></polyline>
|
|
<line x1="21" y1="12" x2="9" y2="12"></line>
|
|
</svg>
|
|
退出
|
|
</button>
|
|
</div>
|
|
</header>
|
|
<main class="content">
|
|
<router-view />
|
|
</main>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
/* ═══ Layout ═══ */
|
|
.desktop-layout {
|
|
display: flex;
|
|
height: 100vh;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* ═══ Sidebar ═══ */
|
|
.sidebar {
|
|
width: 240px;
|
|
background: var(--sidebar-bg);
|
|
color: var(--sidebar-color);
|
|
display: flex;
|
|
flex-direction: column;
|
|
flex-shrink: 0;
|
|
overflow: hidden;
|
|
transition: width 0.25s ease;
|
|
}
|
|
.sidebar--collapsed {
|
|
width: 64px;
|
|
}
|
|
.sidebar--collapsed .brand-sub,
|
|
.sidebar--collapsed .brand-rule,
|
|
.sidebar--collapsed .nav-label,
|
|
.sidebar--collapsed .footer-role,
|
|
.sidebar--collapsed .footer-name { display: none; }
|
|
.sidebar--collapsed .brand-title { font-size: 20px; text-align: center; }
|
|
.sidebar--collapsed .nav-item { justify-content: center; padding: 11px 0; }
|
|
.sidebar--collapsed .nav-icon { font-size: 20px; margin: 0; }
|
|
.sidebar--collapsed .sidebar-footer { text-align: center; padding: 10px; }
|
|
|
|
.sidebar-brand {
|
|
padding: 26px 22px 18px;
|
|
border-bottom: 1px solid var(--sidebar-divider);
|
|
}
|
|
|
|
.brand-title {
|
|
margin: 0;
|
|
font-family: var(--font-heading);
|
|
font-size: 24px;
|
|
font-weight: 400;
|
|
letter-spacing: 0.12em;
|
|
color: var(--sidebar-brand-color);
|
|
line-height: 1.2;
|
|
}
|
|
|
|
.brand-sub {
|
|
margin: 2px 0 0;
|
|
font-family: var(--font-body);
|
|
font-size: 10px;
|
|
color: var(--sidebar-brand-sub-color);
|
|
letter-spacing: 0.1em;
|
|
}
|
|
|
|
.brand-rule {
|
|
width: 28px;
|
|
height: 3px;
|
|
background: var(--sidebar-accent);
|
|
margin-top: 14px;
|
|
}
|
|
|
|
/* ═══ Navigation ═══ */
|
|
.sidebar-nav {
|
|
flex: 1;
|
|
padding: 12px 10px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.nav-group-label {
|
|
font-family: var(--font-mono);
|
|
font-size: 10px;
|
|
letter-spacing: 0.15em;
|
|
color: var(--sidebar-group-label-color);
|
|
padding: 12px 14px 4px;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.sidebar--collapsed .nav-group-label { display: none; }
|
|
|
|
.nav-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 11px 14px;
|
|
color: var(--sidebar-nav-text);
|
|
text-decoration: none;
|
|
font-family: var(--font-body);
|
|
font-size: 14px;
|
|
letter-spacing: 0.04em;
|
|
transition: all 0.22s;
|
|
border-left: 2px solid transparent;
|
|
}
|
|
|
|
.nav-item:hover {
|
|
color: var(--sidebar-nav-text-hover);
|
|
background: var(--sidebar-nav-bg-hover);
|
|
}
|
|
|
|
.nav-item--active {
|
|
color: var(--sidebar-nav-text-active);
|
|
background: var(--sidebar-nav-bg-active);
|
|
border-left-color: var(--sidebar-nav-border-active);
|
|
}
|
|
|
|
.nav-icon {
|
|
font-size: 18px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.nav-label {
|
|
font-weight: 500;
|
|
}
|
|
|
|
/* ═══ Sidebar Footer ═══ */
|
|
.sidebar-footer {
|
|
padding: 14px 22px;
|
|
border-top: 1px solid var(--sidebar-divider);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
}
|
|
|
|
.footer-role {
|
|
font-family: var(--font-body);
|
|
font-size: 10px;
|
|
color: var(--sidebar-footer-role-color);
|
|
letter-spacing: 0.1em;
|
|
}
|
|
|
|
.footer-name {
|
|
font-family: var(--font-heading);
|
|
font-size: 13px;
|
|
color: var(--sidebar-footer-name-color);
|
|
letter-spacing: 0.04em;
|
|
}
|
|
|
|
/* ═══ Main Area ═══ */
|
|
.main-area {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: var(--paper);
|
|
min-width: 0;
|
|
}
|
|
|
|
/* ═══ Topbar ═══ */
|
|
.topbar {
|
|
background: var(--surface);
|
|
padding: 10px 24px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
border-bottom: 1px solid var(--warm-border);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.topbar-greeting {
|
|
font-family: var(--font-heading);
|
|
font-size: 14px;
|
|
color: var(--ink);
|
|
letter-spacing: 0.04em;
|
|
}
|
|
|
|
.collapse-btn {
|
|
display: flex; align-items: center; justify-content: center;
|
|
width: 28px; height: 28px;
|
|
background: none; border: none;
|
|
color: var(--warm-gray); cursor: pointer;
|
|
transition: all 0.2s; flex-shrink: 0; margin-right: 8px;
|
|
opacity: 0.5;
|
|
}
|
|
.collapse-btn:hover { color: var(--ink); opacity: 1; }
|
|
|
|
.topbar-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.topbar-btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
padding: 6px 12px;
|
|
background: none;
|
|
border: 1px solid var(--warm-border);
|
|
color: var(--warm-gray);
|
|
font-family: var(--font-body);
|
|
font-size: 12px;
|
|
letter-spacing: 0.03em;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.topbar-btn:hover {
|
|
color: var(--ink);
|
|
border-color: var(--ink);
|
|
}
|
|
|
|
.topbar-btn--logout:hover {
|
|
color: var(--vermilion);
|
|
border-color: var(--vermilion);
|
|
}
|
|
|
|
/* ═══ Content ═══ */
|
|
.content {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 24px;
|
|
}
|
|
</style>
|