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
+118 -10
View File
@@ -67,6 +67,114 @@ const isMobile = computed(() => {
--radius-sm: 2px;
--radius-xs: 2px;
--transition: 0.22s cubic-bezier(0.4, 0, 0.2, 1);
/* ── Sidebar — dark editorial ── */
--sidebar-bg: var(--ink);
--sidebar-color: #fff;
--sidebar-accent: var(--gold);
--sidebar-nav-text: rgba(255,255,255,0.55);
--sidebar-nav-text-hover: rgba(255,255,255,0.85);
--sidebar-nav-text-active: #fff;
--sidebar-nav-bg-hover: rgba(255,255,255,0.04);
--sidebar-nav-bg-active: rgba(255,255,255,0.06);
--sidebar-nav-border-active: var(--gold);
--sidebar-divider: rgba(255,255,255,0.06);
--sidebar-brand-color: #fff;
--sidebar-brand-sub-color: rgba(255,255,255,0.4);
--sidebar-group-label-color: rgba(255,255,255,0.25);
--sidebar-footer-role-color: var(--gold);
--sidebar-footer-name-color: rgba(255,255,255,0.7);
/* ── Mobile page accent ── */
--page-accent-color: rgba(196,147,74,0.06);
/* ── Manager chip tags ── */
--mgr-chip-text: #fff;
/* ── Fonts — editorial Chinese ── */
--font-body: 'Noto Serif SC', STSong, Songti SC, '宋体', serif;
--font-heading: 'ZCOOL XiaoWei', STSong, Songti SC, serif;
--font-mono: 'JetBrains Mono', 'SF Mono', monospace;
}
/* ═══════════════════════════════════════════════════════════
THEME: Light — 极简功能主义 / Brutally Minimal
═══════════════════════════════════════════════════════════ */
:root[data-theme="light"] {
/* ── Core palette ── */
--ink: #1A1A1A;
--ink-light: #3D3D3D;
--ink-dark: #0A0A0A;
--vermilion: #DC2626;
--vermilion-light: #EF4444;
--vermilion-dark: #B91C1C;
--gold: #D4AF37;
--gold-light: #E5C955;
--gold-dark: #B8941F;
--paper: #F5F5F5;
--paper-dark: #EBEBEB;
--surface: #FFFFFF;
--sage: #16A34A;
--amber: #D97706;
--warm-gray: #666666;
--warm-border: #DDDDDD;
/* ── Semantic tokens ── */
--c-primary: var(--ink);
--c-primary-light: var(--ink-light);
--c-primary-bg: #F5F5F5;
--c-accent: var(--vermilion);
--c-accent-light: var(--vermilion-light);
--c-gold: var(--gold);
--c-success: var(--sage);
--c-warning: var(--amber);
--c-danger: var(--vermilion);
--c-info: #2563EB;
--c-text: #1A1A1A;
--c-text-secondary: #666666;
--c-text-muted: #999999;
--c-bg: var(--paper);
--c-bg-card: var(--surface);
--c-border: var(--warm-border);
/* ── Effects ── */
--shadow-sm: 0 1px 2px rgba(0,0,0,0.04);
--shadow: 0 1px 3px rgba(0,0,0,0.08);
--shadow-md: 0 2px 6px rgba(0,0,0,0.10);
--shadow-lg: 0 4px 12px rgba(0,0,0,0.12);
--radius: 0px;
--radius-sm: 0px;
--radius-xs: 0px;
--transition: 0.18s cubic-bezier(0.4, 0, 0.2, 1);
/* ── Sidebar — light minimal ── */
--sidebar-bg: #FFFFFF;
--sidebar-color: #1A1A1A;
--sidebar-accent: var(--gold);
--sidebar-nav-text: #777777;
--sidebar-nav-text-hover: #1A1A1A;
--sidebar-nav-text-active: #1A1A1A;
--sidebar-nav-bg-hover: #F5F5F5;
--sidebar-nav-bg-active: #F0F0F0;
--sidebar-nav-border-active: var(--gold);
--sidebar-divider: #EEEEEE;
--sidebar-brand-color: #1A1A1A;
--sidebar-brand-sub-color: #999999;
--sidebar-group-label-color: #AAAAAA;
--sidebar-footer-role-color: var(--gold);
--sidebar-footer-name-color: #777777;
/* ── Mobile page accent ── */
--page-accent-color: rgba(212,175,55,0.04);
/* ── Manager chip tags ── */
--mgr-chip-text: #1A1A1A;
/* ── Fonts — sans-serif minimal ── */
--font-body: 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
--font-heading: 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
--font-mono: 'JetBrains Mono', monospace;
}
/* ── Reset & Base ── */
@@ -76,7 +184,7 @@ html, body, #app {
margin: 0; padding: 0; height: 100%;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
font-family: 'Noto Serif SC', STSong, Songti SC, '宋体', serif;
font-family: var(--font-body);
font-size: 15px;
line-height: 1.7;
color: var(--c-text);
@@ -87,7 +195,7 @@ html, body, #app {
/* ── Editorial heading style ── */
h1, h2, h3, h4, h5, h6 {
font-family: 'ZCOOL XiaoWei', STSong, Songti SC, serif;
font-family: var(--font-heading);
letter-spacing: 0.02em;
}
@@ -107,7 +215,7 @@ h1, h2, h3, h4, h5, h6 {
.el-card__header {
border-bottom: 2px solid var(--gold) !important;
padding: 16px 20px !important;
font-family: 'ZCOOL XiaoWei', STSong, serif;
font-family: var(--font-heading);
font-weight: 600;
font-size: 16px;
color: var(--ink);
@@ -121,7 +229,7 @@ h1, h2, h3, h4, h5, h6 {
font-weight: 500;
letter-spacing: 0.02em;
transition: all var(--transition);
font-family: 'Noto Serif SC', STSong, Songti SC, serif;
font-family: var(--font-body);
}
.el-button--primary {
background: var(--ink) !important;
@@ -149,14 +257,14 @@ h1, h2, h3, h4, h5, h6 {
.el-tag {
border-radius: 2px !important;
font-weight: 500;
font-family: 'Noto Serif SC', STSong, serif;
font-family: var(--font-body);
letter-spacing: 0.02em;
}
/* ── Tabs ── */
.el-tabs__item {
font-weight: 500;
font-family: 'ZCOOL XiaoWei', STSong, serif;
font-family: var(--font-heading);
}
.el-tabs__active-bar { background: var(--gold) !important; }
.el-tabs__item.is-active { color: var(--ink) !important; }
@@ -167,7 +275,7 @@ h1, h2, h3, h4, h5, h6 {
color: var(--ink);
font-weight: 600;
font-size: 13px;
font-family: 'ZCOOL XiaoWei', STSong, serif;
font-family: var(--font-heading);
letter-spacing: 0.03em;
}
.el-table--striped .el-table__body tr.el-table__row--striped td.el-table__cell {
@@ -184,7 +292,7 @@ h1, h2, h3, h4, h5, h6 {
.el-dialog__header {
padding: 20px 24px 0 !important;
font-weight: 600;
font-family: 'ZCOOL XiaoWei', STSong, serif;
font-family: var(--font-heading);
}
.el-dialog__body { padding: 20px 24px !important; }
@@ -225,7 +333,7 @@ h1, h2, h3, h4, h5, h6 {
/* ── Textarea ── */
.el-textarea__inner {
border-radius: 2px !important;
font-family: 'Noto Serif SC', STSong, serif;
font-family: var(--font-body);
}
/* ── Radio ── */
@@ -258,7 +366,7 @@ h1, h2, h3, h4, h5, h6 {
/* ── Divider ── */
.el-divider__text {
font-family: 'ZCOOL XiaoWei', STSong, serif;
font-family: var(--font-heading);
color: var(--warm-gray);
}
+38 -24
View File
@@ -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;
+2 -2
View File
@@ -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);
}
+2 -2
View File
@@ -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;
}
+5 -5
View File
@@ -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;
}
+30
View File
@@ -0,0 +1,30 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
export type Theme = 'editorial' | 'light'
export const themeLabels: Record<Theme, string> = {
editorial: '编辑风',
light: '极简亮色',
}
export const useThemeStore = defineStore('theme', () => {
const currentTheme = ref<Theme>(
(localStorage.getItem('theme') as Theme) || 'editorial'
)
function applyTheme(theme: Theme) {
document.documentElement.setAttribute('data-theme', theme)
}
function setTheme(theme: Theme) {
currentTheme.value = theme
localStorage.setItem('theme', theme)
applyTheme(theme)
}
// Apply on init
applyTheme(currentTheme.value)
return { currentTheme, setTheme }
})
+2 -2
View File
@@ -1,5 +1,5 @@
/* ── Google Fonts import for Editorial Chinese fonts ── */
@import url('https://fonts.googleapis.com/css2?family=ZCOOL+XiaoWei&family=Noto+Serif+SC:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
/* ── Google Fonts import ── */
@import url('https://fonts.googleapis.com/css2?family=ZCOOL+XiaoWei&family=Noto+Serif+SC:wght@300;400;500;600;700&family=Noto+Sans+SC:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
@tailwind base;
@tailwind components;
+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) }
}
+1 -1
View File
@@ -25,6 +25,6 @@ onMounted(() => {
height: 100vh;
font-size: 16px;
color: var(--ink);
font-family: 'Noto Serif SC', STSong, serif;
font-family: var(--font-body);
}
</style>
+1 -1
View File
@@ -100,7 +100,7 @@ async function doBind() {
.wecom-bind-page { max-width: 400px; margin: 80px auto; padding: 32px 24px; text-align: center; }
.bind-error { color: var(--vermilion); font-size: 15px; }
.bind-icon { font-size: 56px; margin-bottom: 16px; }
.bind-title { margin: 0 0 8px; font-family: 'ZCOOL XiaoWei', STSong, serif; font-size: 22px; font-weight: 400; color: var(--ink); }
.bind-title { margin: 0 0 8px; font-family: var(--font-heading); font-size: 22px; font-weight: 400; color: var(--ink); }
.bind-desc { margin: 0 0 28px; font-size: 14px; color: var(--c-text-muted); }
.bind-info { text-align: left; background: var(--c-bg-light, #faf9f6); border-radius: 8px; padding: 16px 20px; margin-bottom: 28px; }
.info-row { display: flex; justify-content: space-between; align-items: center; padding: 8px 0; }
+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;
}
+9 -9
View File
@@ -210,7 +210,7 @@ function rowState(p: any): 'full' | 'catching' | 'missing' {
.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;
}
@@ -218,7 +218,7 @@ function rowState(p: any): 'full' | 'catching' | 'missing' {
.week-nav-btn { background: var(--surface); border: 1px solid var(--warm-border); padding: 4px 10px; cursor: pointer; color: var(--warm-gray); font-size: 12px; }
.week-nav-btn:hover:not(:disabled) { color: var(--ink); border-color: var(--ink); }
.week-nav-btn:disabled { opacity: 0.3; cursor: not-allowed; }
.week-label { font-family: 'JetBrains Mono', 'SF Mono', monospace; font-size: 12px; color: var(--ink); letter-spacing: 0.04em; }
.week-label { font-family: var(--font-mono); font-size: 12px; color: var(--ink); letter-spacing: 0.04em; }
.week-nav-reset { background: none; border: 1px solid var(--gold); color: var(--gold); padding: 4px 10px; cursor: pointer; font-size: 12px; }
.week-nav-reset:hover { background: var(--gold); color: #fff; }
.page-rule { width: 32px; height: 3px; background: var(--gold); margin-top: 12px; }
@@ -256,11 +256,11 @@ function rowState(p: any): 'full' | 'catching' | 'missing' {
.stat-body { display: flex; flex-direction: column; }
.stat-num {
font-family: 'ZCOOL XiaoWei', STSong, serif;
font-family: var(--font-heading);
font-size: 28px; color: var(--ink); line-height: 1.1; letter-spacing: 0.04em;
}
.stat-label {
font-family: 'Noto Serif SC', STSong, serif;
font-family: var(--font-body);
font-size: 12px; color: var(--warm-gray); letter-spacing: 0.04em;
}
@@ -271,7 +271,7 @@ function rowState(p: any): 'full' | 'catching' | 'missing' {
.progress-card { margin-top: 4px; }
.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;
}
@@ -339,7 +339,7 @@ function rowState(p: any): 'full' | 'catching' | 'missing' {
}
.seal-char {
font-family: 'ZCOOL XiaoWei', STSong, serif;
font-family: var(--font-heading);
font-size: 20px;
font-weight: 400;
line-height: 1.1;
@@ -356,7 +356,7 @@ function rowState(p: any): 'full' | 'catching' | 'missing' {
/* Inline status text (replaces el-tag) */
.progress-status {
font-family: 'Noto Serif SC', STSong, serif;
font-family: var(--font-body);
font-size: 12px;
letter-spacing: 0.05em;
}
@@ -365,10 +365,10 @@ function rowState(p: any): 'full' | 'catching' | 'missing' {
.progress-status--full { color: var(--sage); }
.progress-count {
font-family: 'JetBrains Mono', 'SF Mono', monospace;
font-family: var(--font-mono);
font-size: 13px; color: var(--warm-gray); letter-spacing: 0.03em;
position: relative; z-index: 1;
}
.empty { text-align: center; color: var(--c-text-muted); padding: 40px 0; font-family: 'Noto Serif SC', STSong, serif; }
.empty { text-align: center; color: var(--c-text-muted); padding: 40px 0; font-family: var(--font-body); }
</style>
+14 -17
View File
@@ -1,13 +1,17 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useAuthStore } from '@/stores/auth'
import { useThemeStore } from '@/stores/theme'
import { ElMessage, ElMessageBox } from 'element-plus'
import { todayStr } from '@/utils'
import { getMgrStyle, loadManagerColors } from '@/utils/managerColor'
import api from '@/api/index'
import EditLogPanel from '@/components/EditLogPanel.vue'
const auth = useAuthStore()
const themeStore = useThemeStore()
const loading = ref(false)
const isLight = computed(() => themeStore.currentTheme === 'light')
const keyVisits = ref<any[]>([])
const customers = ref<any[]>([])
const allUsers = ref<any[]>([])
@@ -19,13 +23,6 @@ const form = ref<any>({})
const statusPick = ref<Record<string, string>>({})
const keyStatuses = ['未开始', '进行中', '已完成']
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]
}
const managerSummary = computed(() => {
const map: Record<string, number> = {}
keyVisits.value.forEach((k: any) => {
@@ -36,7 +33,7 @@ const managerSummary = computed(() => {
})
onMounted(async () => {
await Promise.all([loadItems(), loadCustomers(), loadUsers()])
await Promise.all([loadItems(), loadCustomers(), loadUsers(), loadManagerColors()])
})
async function loadUsers() {
@@ -135,7 +132,7 @@ async function quickStatusChange(row: any, newStatus: string) {
<div class="page-rule"></div>
<div v-if="managerSummary.length" class="summary-bar">
<span class="summary-label">客户经理汇总</span>
<span v-for="[name, count] in managerSummary" :key="name" class="summary-chip" :style="{ background: mgrColor(name) }">{{ name }} · {{ count }}</span>
<span v-for="[name, count] in managerSummary" :key="name" class="summary-chip" :style="getMgrStyle(name, isLight)">{{ name }} · {{ count }}</span>
</div>
</div>
@@ -153,7 +150,7 @@ async function quickStatusChange(row: any, newStatus: string) {
</el-table-column>
<el-table-column label="客户经理" width="100">
<template #default="{ row }">
<span class="mgr-tag" :style="{ background: mgrColor(row.manager_name) }">{{ row.manager_name || '-' }}</span>
<span class="mgr-tag" :style="getMgrStyle(row.manager_name, isLight)">{{ row.manager_name || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="urgency_level" label="重要度" width="90">
@@ -181,7 +178,7 @@ async function quickStatusChange(row: any, newStatus: string) {
<el-table-column prop="visit_target" label="拜访对象" width="110" />
<el-table-column label="操作" width="80" fixed="right">
<template #default="{ row }">
<el-button text size="small" type="danger" @click="handleDelete(row.id)">删除</el-button>
<el-button size="small" type="danger" @click="handleDelete(row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
@@ -234,13 +231,13 @@ async function quickStatusChange(row: any, newStatus: string) {
.edit-indicator { font-size: 12px; margin-left: 3px; opacity: 0.5; cursor: help; }
.page-head { margin-bottom: 20px; }
.page-head-row { display: flex; justify-content: space-between; align-items: flex-start; }
.page-title { margin: 0; font-family: 'ZCOOL XiaoWei', STSong, serif; font-size: 22px; font-weight: 400; color: var(--ink); letter-spacing: 0.06em; }
.page-desc { margin: 4px 0 0; font-size: 13px; color: var(--c-text-muted); font-family: 'Noto Serif SC', STSong, serif; }
.page-title { margin: 0; font-family: var(--font-heading); font-size: 22px; font-weight: 400; color: var(--ink); letter-spacing: 0.06em; }
.page-desc { margin: 4px 0 0; font-size: 13px; color: var(--c-text-muted); font-family: var(--font-body); }
.page-rule { width: 32px; height: 3px; background: var(--gold); margin-top: 12px; }
.summary-bar { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-top: 14px; padding: 10px 14px; background: var(--c-bg-light, #faf9f6); border-radius: 6px; border: 1px solid var(--c-border, #e8e5df); }
.summary-label { font-size: 12px; color: var(--c-text-muted); font-family: 'Noto Serif SC', STSong, serif; margin-right: 4px; }
.summary-chip { display: inline-block; padding: 2px 10px; border-radius: 12px; font-size: 12px; color: #fff; white-space: nowrap; }
.mgr-tag { display: inline-block; padding: 1px 9px; border-radius: 10px; font-size: 12px; color: #fff; white-space: nowrap; }
.summary-label { font-size: 12px; color: var(--c-text-muted); font-family: var(--font-body); margin-right: 4px; }
.summary-chip { display: inline-block; padding: 2px 10px; border-radius: 12px; font-size: 12px; color: var(--mgr-chip-text); white-space: nowrap; }
.mgr-tag { display: inline-block; padding: 1px 9px; border-radius: 10px; font-size: 12px; color: var(--mgr-chip-text); white-space: nowrap; }
.method-grid { display: flex; gap: 8px; }
.empty { text-align: center; color: var(--c-text-muted); padding: 48px 0; font-family: 'Noto Serif SC', STSong, serif; }
.empty { text-align: center; color: var(--c-text-muted); padding: 48px 0; font-family: var(--font-body); }
</style>
+18 -23
View File
@@ -111,12 +111,7 @@ const statusLabel: Record<string, string> = { green: '近30天已拜访', yellow
<div class="page-head">
<div class="page-head-row">
<div>
<h2 class="page-title">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" style="margin-right:8px; color: var(--gold); vertical-align: -4px;">
<path d="M12 2l2.4 7.2h7.6l-6 4.8 2.4 7.2-6.4-4.8-6.4 4.8 2.4-7.2-6-4.8h7.6z"/>
</svg>
客户拜访亮灯表
</h2>
<h2 class="page-title">客户拜访亮灯表</h2>
<div class="month-nav">
<button class="month-nav-btn" @click="changeMonth(-1)"></button>
<span class="month-label">{{ referenceMonth }}</span>
@@ -277,12 +272,12 @@ const statusLabel: Record<string, string> = { green: '近30天已拜访', yellow
/* ═══ Page Header ═══ */
.page-head { margin-bottom: 20px; }
.page-head-row { display: flex; justify-content: space-between; align-items: flex-start; }
.page-title { margin: 0; font-family: 'ZCOOL XiaoWei', STSong, serif; font-size: 22px; font-weight: 400; color: var(--ink); letter-spacing: 0.06em; }
.page-title { margin: 0; font-family: var(--font-heading); font-size: 22px; font-weight: 400; color: var(--ink); letter-spacing: 0.06em; }
.month-nav { display: flex; align-items: center; gap: 8px; margin: 4px 0 6px; }
.month-nav-btn { background: var(--surface); border: 1px solid var(--warm-border); padding: 3px 8px; cursor: pointer; color: var(--warm-gray); font-size: 12px; }
.month-nav-btn:hover:not(:disabled) { color: var(--ink); border-color: var(--ink); }
.month-nav-btn:disabled { opacity: 0.3; cursor: not-allowed; }
.month-label { font-family: 'JetBrains Mono', monospace; font-size: 14px; color: var(--ink); letter-spacing: 0.04em; }
.month-label { font-family: var(--font-mono); font-size: 14px; color: var(--ink); letter-spacing: 0.04em; }
.month-nav-reset { background: none; border: 1px solid var(--gold); color: var(--gold); padding: 3px 8px; cursor: pointer; font-size: 12px; }
.month-nav-reset:hover { background: var(--gold); color: #fff; }
.page-rule { width: 32px; height: 3px; background: var(--gold); margin-top: 12px; }
@@ -291,8 +286,8 @@ const statusLabel: Record<string, string> = { green: '近30天已拜访', yellow
.team-bar { display: flex; gap: 0; background: var(--surface); border: 1px solid var(--warm-border); margin-bottom: 20px; }
.team-stat { flex: 1; text-align: center; padding: 16px 8px; border-right: 1px solid var(--warm-border); display: flex; flex-direction: column; gap: 4px; }
.team-stat:last-child { border-right: none; }
.team-stat-num { font-family: 'ZCOOL XiaoWei', STSong, serif; font-size: 26px; color: var(--ink); line-height: 1.1; }
.team-stat-label { font-family: 'Noto Serif SC', STSong, serif; font-size: 11px; color: var(--warm-gray); letter-spacing: 0.04em; }
.team-stat-num { font-family: var(--font-heading); font-size: 26px; color: var(--ink); line-height: 1.1; }
.team-stat-label { font-family: var(--font-body); font-size: 11px; color: var(--warm-gray); letter-spacing: 0.04em; }
.team-stat--green .team-stat-num { color: var(--sage); }
.team-stat--yellow .team-stat-num { color: var(--gold); }
.team-stat--red .team-stat-num { color: var(--vermilion); }
@@ -306,18 +301,18 @@ const statusLabel: Record<string, string> = { green: '近30天已拜访', yellow
.manager-summary:hover { background: rgba(196,147,74,0.03); }
.manager-info { display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0; }
.manager-expand { font-size: 10px; color: var(--warm-gray); width: 14px; flex-shrink: 0; }
.manager-name { font-family: 'ZCOOL XiaoWei', STSong, serif; font-size: 15px; color: var(--ink); letter-spacing: 0.04em; cursor: pointer; }
.manager-name { font-family: var(--font-heading); font-size: 15px; color: var(--ink); letter-spacing: 0.04em; cursor: pointer; }
.manager-name:hover { color: var(--gold); }
.manager-count { font-family: 'Noto Serif SC', STSong, serif; font-size: 12px; color: var(--warm-gray); }
.manager-count { font-family: var(--font-body); font-size: 12px; color: var(--warm-gray); }
.manager-lights { display: flex; gap: 6px; }
.light-dot { display: inline-flex; align-items: center; justify-content: center; min-width: 26px; height: 26px; border-radius: 4px; font-family: 'JetBrains Mono', monospace; font-size: 12px; font-weight: 600; cursor: help; }
.light-dot { display: inline-flex; align-items: center; justify-content: center; min-width: 26px; height: 26px; border-radius: 4px; font-family: var(--font-mono); font-size: 12px; font-weight: 600; cursor: help; }
.light-dot--green { background: #EDF2EC; color: var(--sage); }
.light-dot--yellow { background: #FBF6EE; color: var(--gold); }
.light-dot--red { background: #FBF1EE; color: var(--vermilion); }
.manager-coverage { display: flex; align-items: center; gap: 10px; width: 180px; flex-shrink: 0; }
.coverage-bar { flex: 1; height: 6px; background: var(--paper-dark); border-radius: 3px; overflow: hidden; }
.coverage-fill { height: 100%; border-radius: 3px; transition: width 0.5s ease; }
.coverage-pct { font-family: 'JetBrains Mono', monospace; font-size: 13px; font-weight: 600; width: 40px; text-align: right; }
.coverage-pct { font-family: var(--font-mono); font-size: 13px; font-weight: 600; width: 40px; text-align: right; }
/* ═══ Customer Grid ═══ */
.customer-grid { display: flex; flex-wrap: wrap; gap: 10px; padding: 0 18px 16px; border-top: 1px solid var(--warm-border); padding-top: 14px; }
@@ -340,22 +335,22 @@ const statusLabel: Record<string, string> = { green: '近30天已拜访', yellow
.status-circle--yellow { background: conic-gradient(var(--gold) 50%, transparent 50%); box-shadow: inset 0 0 0 1.5px var(--gold); }
.status-circle--red { background: transparent; border: 2px solid var(--vermilion); box-shadow: 0 0 0 2px rgba(184,71,46,0.08); }
.status-circle--gray { background: transparent; border: 1.5px dashed var(--warm-gray); }
.card-name { font-family: 'ZCOOL XiaoWei', STSong, serif; font-size: 13px; color: var(--ink); letter-spacing: 0.03em; line-height: 1.4; word-break: break-all; }
.card-name { font-family: var(--font-heading); font-size: 13px; color: var(--ink); letter-spacing: 0.03em; line-height: 1.4; word-break: break-all; }
.card-meta { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 6px; }
.card-industry,.card-services { font-family: 'Noto Serif SC', STSong, serif; font-size: 10px; color: var(--warm-gray); }
.card-industry,.card-services { font-family: var(--font-body); font-size: 10px; color: var(--warm-gray); }
.card-visit-info { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
.card-last-visit { font-family: 'JetBrains Mono', monospace; font-size: 10px; color: var(--warm-gray); }
.card-last-visit { font-family: var(--font-mono); font-size: 10px; color: var(--warm-gray); }
.card-last-visit--never { color: var(--vermilion); }
.card-method { font-family: 'Noto Serif SC', STSong, serif; font-size: 10px; padding: 1px 6px; border: 1px solid var(--warm-border); color: var(--warm-gray); }
.card-method { font-family: var(--font-body); font-size: 10px; padding: 1px 6px; border: 1px solid var(--warm-border); color: var(--warm-gray); }
.card-plans { display: flex; gap: 4px; flex-wrap: wrap; }
.plan-badge { font-family: 'JetBrains Mono', monospace; font-size: 9px; padding: 2px 6px; border-radius: 8px; background: #EDF2EC; color: var(--sage); }
.plan-badge { font-family: var(--font-mono); font-size: 9px; padding: 2px 6px; border-radius: 8px; background: #EDF2EC; color: var(--sage); }
.plan-badge.plan-overdue { background: #FBF1EE; color: var(--vermilion); animation: pulse-warn 2s ease-in-out infinite; }
.card-warning { margin-top: 6px; font-family: 'Noto Serif SC', STSong, serif; font-size: 10px; color: var(--vermilion); }
.card-warning { margin-top: 6px; font-family: var(--font-body); font-size: 10px; color: var(--vermilion); }
.unassigned-section { border-style: dashed; border-color: var(--warm-gray); }
.unassigned-summary { cursor: pointer; }
.empty-state { text-align: center; padding: 48px 0; }
.empty-text { font-family: 'Noto Serif SC', STSong, serif; font-size: 15px; color: var(--warm-gray); }
.empty { text-align: center; padding: 20px; color: var(--c-text-muted); font-family: 'Noto Serif SC', STSong, serif; }
.empty-text { font-family: var(--font-body); font-size: 15px; color: var(--warm-gray); }
.empty { text-align: center; padding: 20px; color: var(--c-text-muted); font-family: var(--font-body); }
/* ═══ Dialog ═══ */
.dlg-status { padding: 8px 12px; border-radius: 6px; font-size: 13px; font-weight: 600; margin-bottom: 12px; }
@@ -369,7 +364,7 @@ const statusLabel: Record<string, string> = { green: '近30天已拜访', yellow
.dlg-section { border-top: 1px solid var(--warm-border); padding-top: 10px; margin-top: 10px; }
.dlg-section-title { margin: 0 0 8px; font-size: 13px; color: var(--ink); }
.dlg-visit-item { display: flex; gap: 8px; padding: 4px 0; font-size: 12px; }
.dlg-vdate { font-family: 'JetBrains Mono', monospace; color: var(--warm-gray); white-space: nowrap; }
.dlg-vdate { font-family: var(--font-mono); color: var(--warm-gray); white-space: nowrap; }
.dlg-vmethod { color: var(--sage); white-space: nowrap; }
.dlg-vcontent { color: var(--c-text-muted); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dlg-plan-item { display: flex; align-items: center; gap: 8px; padding: 4px 0; font-size: 12px; }
@@ -302,7 +302,7 @@ const notesByDate = computed(() => {
<el-table-column prop="communication_content" label="沟通内容" min-width="200" show-overflow-tooltip />
<el-table-column label="操作" width="70">
<template #default="{ row }">
<el-button text size="small" type="danger" @click="handleDelete('visit', row.id)">删除</el-button>
<el-button size="small" type="danger" @click="handleDelete('visit', row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
@@ -328,7 +328,7 @@ const notesByDate = computed(() => {
<el-table-column prop="time_range" label="时间" width="100" />
<el-table-column label="操作" width="70">
<template #default="{ row }">
<el-button text size="small" type="danger" @click="handleDelete('note', row.id)">删除</el-button>
<el-button size="small" type="danger" @click="handleDelete('note', row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
@@ -361,7 +361,7 @@ const notesByDate = computed(() => {
</el-table-column>
<el-table-column label="操作" width="70">
<template #default="{ row }">
<el-button text size="small" type="danger" @click="handleDelete('plan', row.id)">删除</el-button>
<el-button size="small" type="danger" @click="handleDelete('plan', row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
@@ -395,7 +395,7 @@ const notesByDate = computed(() => {
<el-table-column prop="expected_revenue_date" label="预计列收" width="110" />
<el-table-column label="操作" width="70">
<template #default="{ row }">
<el-button text size="small" type="danger" @click="handleDelete('mini', row.id)">删除</el-button>
<el-button size="small" type="danger" @click="handleDelete('mini', row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
@@ -430,7 +430,7 @@ const notesByDate = computed(() => {
<el-table-column prop="visit_target" label="拜访对象" width="100" />
<el-table-column label="操作" width="70">
<template #default="{ row }">
<el-button text size="small" type="danger" @click="handleDelete('key', row.id)">删除</el-button>
<el-button size="small" type="danger" @click="handleDelete('key', row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
@@ -534,9 +534,9 @@ const notesByDate = computed(() => {
<style scoped>
.page-head { margin-bottom: 20px; }
.page-title { margin: 0; font-family: 'ZCOOL XiaoWei', STSong, serif; font-size: 22px; font-weight: 400; color: var(--ink); letter-spacing: 0.06em; }
.page-title { margin: 0; font-family: var(--font-heading); font-size: 22px; font-weight: 400; color: var(--ink); letter-spacing: 0.06em; }
.page-rule { width: 32px; height: 3px; background: var(--gold); margin-top: 12px; }
.date-group { margin-bottom: 20px; }
.date-title { display: flex; align-items: center; margin: 12px 0 8px; font-family: 'ZCOOL XiaoWei', STSong, serif; font-size: 14px; color: var(--ink); letter-spacing: 0.04em; }
.empty { text-align: center; color: var(--c-text-muted); padding: 40px 0; font-family: 'Noto Serif SC', STSong, serif; }
.date-title { display: flex; align-items: center; margin: 12px 0 8px; font-family: var(--font-heading); font-size: 14px; color: var(--ink); letter-spacing: 0.04em; }
.empty { text-align: center; color: var(--c-text-muted); padding: 40px 0; font-family: var(--font-body); }
</style>
+14 -17
View File
@@ -1,13 +1,17 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useAuthStore } from '@/stores/auth'
import { useThemeStore } from '@/stores/theme'
import { ElMessage, ElMessageBox } from 'element-plus'
import { todayStr } from '@/utils'
import { getMgrStyle, loadManagerColors } from '@/utils/managerColor'
import api from '@/api/index'
import EditLogPanel from '@/components/EditLogPanel.vue'
const auth = useAuthStore()
const themeStore = useThemeStore()
const loading = ref(false)
const isLight = computed(() => themeStore.currentTheme === 'light')
const miniBusiness = ref<any[]>([])
const customers = ref<any[]>([])
@@ -17,13 +21,6 @@ const form = ref<any>({})
const statusPick = ref<Record<string, string>>({})
const miniStatuses = ['跟进中', '已签约', '已流失']
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]
}
const managerSummary = computed(() => {
const map: Record<string, number> = {}
miniBusiness.value.forEach((m: any) => {
@@ -34,7 +31,7 @@ const managerSummary = computed(() => {
})
onMounted(async () => {
await Promise.all([loadItems(), loadCustomers()])
await Promise.all([loadItems(), loadCustomers(), loadManagerColors()])
})
async function loadCustomers(q?: string) {
@@ -120,7 +117,7 @@ async function quickStatusChange(row: any, newStatus: string) {
<div class="page-rule"></div>
<div v-if="managerSummary.length" class="summary-bar">
<span class="summary-label">客户经理汇总</span>
<span v-for="[name, count] in managerSummary" :key="name" class="summary-chip" :style="{ background: mgrColor(name) }">{{ name }} · {{ count }}</span>
<span v-for="[name, count] in managerSummary" :key="name" class="summary-chip" :style="getMgrStyle(name, isLight)">{{ name }} · {{ count }}</span>
</div>
</div>
@@ -138,7 +135,7 @@ async function quickStatusChange(row: any, newStatus: string) {
</el-table-column>
<el-table-column label="客户经理" width="100">
<template #default="{ row }">
<span class="mgr-tag" :style="{ background: mgrColor(row.manager_name) }">{{ row.manager_name || '-' }}</span>
<span class="mgr-tag" :style="getMgrStyle(row.manager_name, isLight)">{{ row.manager_name || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="product_type" label="产品类型" width="130" />
@@ -161,7 +158,7 @@ async function quickStatusChange(row: any, newStatus: string) {
<el-table-column prop="expected_revenue_date" label="预计列收" width="110" />
<el-table-column label="操作" width="80" fixed="right">
<template #default="{ row }">
<el-button text size="small" type="danger" @click="handleDelete(row.id)">删除</el-button>
<el-button size="small" type="danger" @click="handleDelete(row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
@@ -203,12 +200,12 @@ async function quickStatusChange(row: any, newStatus: string) {
.edit-indicator { font-size: 12px; margin-left: 3px; opacity: 0.5; cursor: help; }
.page-head { margin-bottom: 20px; }
.page-head-row { display: flex; justify-content: space-between; align-items: flex-start; }
.page-title { margin: 0; font-family: 'ZCOOL XiaoWei', STSong, serif; font-size: 22px; font-weight: 400; color: var(--ink); letter-spacing: 0.06em; }
.page-desc { margin: 4px 0 0; font-size: 13px; color: var(--c-text-muted); font-family: 'Noto Serif SC', STSong, serif; }
.page-title { margin: 0; font-family: var(--font-heading); font-size: 22px; font-weight: 400; color: var(--ink); letter-spacing: 0.06em; }
.page-desc { margin: 4px 0 0; font-size: 13px; color: var(--c-text-muted); font-family: var(--font-body); }
.page-rule { width: 32px; height: 3px; background: var(--gold); margin-top: 12px; }
.summary-bar { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-top: 14px; padding: 10px 14px; background: var(--c-bg-light, #faf9f6); border-radius: 6px; border: 1px solid var(--c-border, #e8e5df); }
.summary-label { font-size: 12px; color: var(--c-text-muted); font-family: 'Noto Serif SC', STSong, serif; margin-right: 4px; }
.summary-chip { display: inline-block; padding: 2px 10px; border-radius: 12px; font-size: 12px; color: #fff; white-space: nowrap; }
.mgr-tag { display: inline-block; padding: 1px 9px; border-radius: 10px; font-size: 12px; color: #fff; white-space: nowrap; }
.empty { text-align: center; color: var(--c-text-muted); padding: 48px 0; font-family: 'Noto Serif SC', STSong, serif; }
.summary-label { font-size: 12px; color: var(--c-text-muted); font-family: var(--font-body); margin-right: 4px; }
.summary-chip { display: inline-block; padding: 2px 10px; border-radius: 12px; font-size: 12px; color: var(--mgr-chip-text); white-space: nowrap; }
.mgr-tag { display: inline-block; padding: 1px 9px; border-radius: 10px; font-size: 12px; color: var(--mgr-chip-text); white-space: nowrap; }
.empty { text-align: center; color: var(--c-text-muted); padding: 48px 0; font-family: var(--font-body); }
</style>
+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>
+13 -3
View File
@@ -12,6 +12,7 @@ const editUser = ref<any>(null)
const editRole = ref('')
const editDepartment = ref('')
const editWecomId = ref('')
const editColor = ref('')
const editRequireReport = ref(true)
const roleOptions = [
@@ -44,13 +45,14 @@ function openEdit(user: any) {
editDepartment.value = user.department || ''
editWecomId.value = user.wecom_userid || ''
editRequireReport.value = user.require_report !== false // default true
editColor.value = user.color || ''
editDialogVisible.value = true
}
async function handleSave() {
if (!editUser.value) return
try {
const payload: any = { role: editRole.value, department: editDepartment.value, require_report: editRequireReport.value }
const payload: any = { role: editRole.value, department: editDepartment.value, require_report: editRequireReport.value, color: editColor.value || null }
await api.put(`/users/${editUser.value.id}/role`, payload)
const newWecomId = editWecomId.value.trim()
if (newWecomId !== (editUser.value.wecom_userid || '')) {
@@ -127,6 +129,14 @@ async function handleDelete(row: any) {
<el-switch v-model="editRequireReport" active-text="是" inactive-text="否" />
<div class="form-hint">关闭后该用户不计入填报统计不接收催办提醒</div>
</el-form-item>
<el-form-item v-if="editRole === 'manager'" label="标签颜色">
<div style="display:flex;align-items:center;gap:8px">
<input type="color" v-model="editColor" style="width:36px;height:32px;border:none;cursor:pointer;padding:0" />
<span style="font-family:var(--font-mono);font-size:13px;color:var(--c-text-secondary)">{{ editColor || '默认(自动分配)' }}</span>
<el-button v-if="editColor" size="small" text @click="editColor = ''">清除</el-button>
</div>
<div class="form-hint">仅客户经理可用留空则自动分配颜色</div>
</el-form-item>
</el-form>
</template>
<template #footer>
@@ -144,12 +154,12 @@ async function handleDelete(row: any) {
.page-head { margin-bottom: 20px; }
.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;
}
.page-rule { width: 32px; height: 3px; background: var(--gold); margin-top: 12px; }
.wecom-id { font-family: 'JetBrains Mono', monospace; font-size: 12px; color: var(--sage); background: var(--c-bg-light, #f0ede5); padding: 2px 8px; border-radius: 4px; }
.wecom-id { font-family: var(--font-mono); font-size: 12px; color: var(--sage); background: var(--c-bg-light, #f0ede5); padding: 2px 8px; border-radius: 4px; }
.form-hint { font-size: 12px; color: var(--c-text-muted); margin-top: 4px; }
.tag-yes { font-size: 12px; color: var(--sage); }
.tag-no { font-size: 12px; color: var(--c-text-muted); }
+11 -11
View File
@@ -392,7 +392,7 @@ const notesByDate = computed(() => {
.page-head-row { display: flex; justify-content: space-between; align-items: flex-start; }
.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;
}
@@ -400,7 +400,7 @@ const notesByDate = computed(() => {
.week-nav-btn { background: var(--surface); border: 1px solid var(--warm-border); padding: 3px 8px; cursor: pointer; color: var(--warm-gray); font-size: 12px; }
.week-nav-btn:hover:not(:disabled) { color: var(--ink); border-color: var(--ink); }
.week-nav-btn:disabled { opacity: 0.3; cursor: not-allowed; }
.week-label { font-family: 'JetBrains Mono', 'SF Mono', monospace; font-size: 12px; color: var(--ink); }
.week-label { font-family: var(--font-mono); font-size: 12px; color: var(--ink); }
.week-nav-reset { background: none; border: 1px solid var(--gold); color: var(--gold); padding: 3px 8px; cursor: pointer; font-size: 12px; }
.week-nav-reset:hover { background: var(--gold); color: #fff; }
@@ -411,13 +411,13 @@ const notesByDate = computed(() => {
.date-title {
display: flex; align-items: center;
margin: 12px 0 8px;
font-family: 'ZCOOL XiaoWei', STSong, serif;
font-family: var(--font-heading);
font-size: 14px; color: var(--ink); letter-spacing: 0.04em;
}
.photo-cell { display: flex; gap: 4px; }
.mini-thumb { width: 36px; height: 36px; object-fit: cover; cursor: pointer; }
.edit-indicator { font-size: 12px; margin-left: 3px; opacity: 0.5; cursor: help; }
.empty { text-align: center; color: var(--c-text-muted); padding: 40px 0; font-family: 'Noto Serif SC', STSong, serif; }
.empty { text-align: center; color: var(--c-text-muted); padding: 40px 0; font-family: var(--font-body); }
/* ═══ AI Summary Panel ═══ */
.ai-summary-card {
@@ -441,11 +441,11 @@ const notesByDate = computed(() => {
display: flex; align-items: center;
}
.ai-header-title {
font-family: 'ZCOOL XiaoWei', STSong, serif;
font-family: var(--font-heading);
font-size: 15px; color: var(--ink); letter-spacing: 0.05em;
}
.ai-header-hint {
font-family: 'Noto Serif SC', STSong, serif;
font-family: var(--font-body);
font-size: 11px; color: var(--warm-gray); margin-left: 10px;
}
.ai-header-actions {
@@ -455,7 +455,7 @@ const notesByDate = computed(() => {
text-align: center; padding: 32px 0;
}
.ai-loading-text {
font-family: 'ZCOOL XiaoWei', STSong, serif;
font-family: var(--font-heading);
font-size: 15px; color: var(--warm-gray);
animation: pulse-text 1.8s ease-in-out infinite;
}
@@ -465,22 +465,22 @@ const notesByDate = computed(() => {
}
.ai-error {
color: var(--vermilion);
font-family: 'Noto Serif SC', STSong, serif;
font-family: var(--font-body);
padding: 12px 0;
}
.ai-content {
font-family: 'Noto Serif SC', STSong, serif;
font-family: var(--font-body);
line-height: 1.85;
color: var(--c-text);
}
.ai-content :deep(.ai-h3) {
font-family: 'ZCOOL XiaoWei', STSong, serif;
font-family: var(--font-heading);
font-size: 16px; color: var(--ink);
margin: 16px 0 8px; letter-spacing: 0.04em;
border-left: 3px solid var(--gold); padding-left: 10px;
}
.ai-content :deep(.ai-h4) {
font-family: 'ZCOOL XiaoWei', STSong, serif;
font-family: var(--font-heading);
font-size: 14px; color: var(--ink);
margin: 12px 0 6px; letter-spacing: 0.03em;
}
+14 -16
View File
@@ -1,12 +1,15 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useAuthStore } from '@/stores/auth'
import { useThemeStore } from '@/stores/theme'
import { ElMessage, ElMessageBox } from 'element-plus'
import { todayStr } from '@/utils'
import { getMgrStyle, loadManagerColors } from '@/utils/managerColor'
import api from '@/api/index'
import EditLogPanel from '@/components/EditLogPanel.vue'
const auth = useAuthStore()
const themeStore = useThemeStore()
const loading = ref(false)
const workPlans = ref<any[]>([])
const customers = ref<any[]>([])
@@ -17,12 +20,7 @@ const form = ref<any>({})
const statusPick = ref<Record<string, string>>({})
const planStatuses = ['计划中', '已完成', '已取消']
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]
}
const isLight = computed(() => themeStore.currentTheme === 'light')
const managerSummary = computed(() => {
const map: Record<string, number> = {}
@@ -34,7 +32,7 @@ const managerSummary = computed(() => {
})
onMounted(async () => {
await Promise.all([loadPlans(), loadCustomers()])
await Promise.all([loadPlans(), loadCustomers(), loadManagerColors()])
})
async function loadCustomers(q?: string) {
@@ -120,7 +118,7 @@ async function quickStatusChange(row: any, newStatus: string) {
<div class="page-rule"></div>
<div v-if="managerSummary.length" class="summary-bar">
<span class="summary-label">客户经理汇总</span>
<span v-for="[name, count] in managerSummary" :key="name" class="summary-chip" :style="{ background: mgrColor(name) }">{{ name }} · {{ count }}</span>
<span v-for="[name, count] in managerSummary" :key="name" class="summary-chip" :style="getMgrStyle(name, isLight)">{{ name }} · {{ count }}</span>
</div>
</div>
@@ -138,7 +136,7 @@ async function quickStatusChange(row: any, newStatus: string) {
</el-table-column>
<el-table-column label="客户经理" width="100">
<template #default="{ row }">
<span class="mgr-tag" :style="{ background: mgrColor(row.manager_name) }">{{ row.manager_name || '-' }}</span>
<span class="mgr-tag" :style="getMgrStyle(row.manager_name, isLight)">{{ row.manager_name || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="plan_content" label="工作计划" min-width="280" show-overflow-tooltip />
@@ -159,7 +157,7 @@ async function quickStatusChange(row: any, newStatus: string) {
</el-table-column>
<el-table-column label="操作" width="80" fixed="right">
<template #default="{ row }">
<el-button text size="small" type="danger" @click="handleDelete(row.id)">删除</el-button>
<el-button size="small" type="danger" @click="handleDelete(row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
@@ -201,12 +199,12 @@ async function quickStatusChange(row: any, newStatus: string) {
.edit-indicator { font-size: 12px; margin-left: 3px; opacity: 0.5; cursor: help; }
.page-head { margin-bottom: 20px; }
.page-head-row { display: flex; justify-content: space-between; align-items: flex-start; }
.page-title { margin: 0; font-family: 'ZCOOL XiaoWei', STSong, serif; font-size: 22px; font-weight: 400; color: var(--ink); letter-spacing: 0.06em; }
.page-desc { margin: 4px 0 0; font-size: 13px; color: var(--c-text-muted); font-family: 'Noto Serif SC', STSong, serif; }
.page-title { margin: 0; font-family: var(--font-heading); font-size: 22px; font-weight: 400; color: var(--ink); letter-spacing: 0.06em; }
.page-desc { margin: 4px 0 0; font-size: 13px; color: var(--c-text-muted); font-family: var(--font-body); }
.page-rule { width: 32px; height: 3px; background: var(--gold); margin-top: 12px; }
.summary-bar { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-top: 14px; padding: 10px 14px; background: var(--c-bg-light, #faf9f6); border-radius: 6px; border: 1px solid var(--c-border, #e8e5df); }
.summary-label { font-size: 12px; color: var(--c-text-muted); font-family: 'Noto Serif SC', STSong, serif; margin-right: 4px; }
.summary-chip { display: inline-block; padding: 2px 10px; border-radius: 12px; font-size: 12px; color: #fff; white-space: nowrap; }
.mgr-tag { display: inline-block; padding: 1px 9px; border-radius: 10px; font-size: 12px; color: #fff; white-space: nowrap; }
.empty { text-align: center; color: var(--c-text-muted); padding: 48px 0; font-family: 'Noto Serif SC', STSong, serif; }
.summary-label { font-size: 12px; color: var(--c-text-muted); font-family: var(--font-body); margin-right: 4px; }
.summary-chip { display: inline-block; padding: 2px 10px; border-radius: 12px; font-size: 12px; color: var(--mgr-chip-text); white-space: nowrap; }
.mgr-tag { display: inline-block; padding: 1px 9px; border-radius: 10px; font-size: 12px; color: var(--mgr-chip-text); white-space: nowrap; }
.empty { text-align: center; color: var(--c-text-muted); padding: 48px 0; font-family: var(--font-body); }
</style>
+7 -7
View File
@@ -189,13 +189,13 @@ async function handleDelete() {
}
.form-title {
margin: 0; font-family: 'ZCOOL XiaoWei', STSong, serif;
margin: 0; font-family: var(--font-heading);
font-size: 24px; font-weight: 400; color: var(--ink);
letter-spacing: 0.06em; line-height: 1.3;
}
.form-subtitle {
font-family: 'JetBrains Mono', 'SF Mono', monospace;
font-family: var(--font-mono);
font-size: 9px; color: var(--gold); letter-spacing: 0.2em;
}
@@ -210,12 +210,12 @@ async function handleDelete() {
/* ═══ Form Labels ═══ */
.form-label {
font-family: 'ZCOOL XiaoWei', STSong, serif !important;
font-family: var(--font-heading) !important;
font-size: 14px !important; color: var(--ink) !important;
letter-spacing: 0.04em; font-weight: 500 !important;
}
.form-label-hint {
font-family: 'Noto Serif SC', STSong, serif;
font-family: var(--font-body);
font-size: 11px; color: var(--warm-gray);
letter-spacing: 0.03em; font-weight: 400; margin-left: 6px;
}
@@ -231,7 +231,7 @@ async function handleDelete() {
background: var(--surface);
border: 1px solid var(--warm-border);
color: var(--ink);
font-family: 'Noto Serif SC', STSong, serif;
font-family: var(--font-body);
font-size: 13px;
letter-spacing: 0.03em;
cursor: pointer;
@@ -249,7 +249,7 @@ async function handleDelete() {
.submit-btn {
flex: 1; display: flex; align-items: center; justify-content: center; gap: 8px;
padding: 16px; background: var(--ink); color: #fff; border: none;
font-family: 'ZCOOL XiaoWei', STSong, serif;
font-family: var(--font-heading);
font-size: 16px; letter-spacing: 0.08em; cursor: pointer; transition: all 0.25s;
}
.submit-btn:hover { background: var(--ink-light); }
@@ -261,7 +261,7 @@ async function handleDelete() {
padding: 16px 20px;
background: var(--surface); color: var(--vermilion);
border: 1px solid var(--vermilion);
font-family: 'Noto Serif SC', STSong, serif;
font-family: var(--font-body);
font-size: 14px; letter-spacing: 0.04em; cursor: pointer; transition: all 0.25s;
}
.delete-btn:hover { background: var(--vermilion); color: #fff; }
+19 -19
View File
@@ -226,14 +226,14 @@ onMounted(loadToday)
}
.stat-label-sm {
font-family: 'JetBrains Mono', 'SF Mono', monospace;
font-family: var(--font-mono);
font-size: 10px;
letter-spacing: 0.2em;
color: var(--warm-gray);
}
.stat-date {
font-family: 'Noto Serif SC', STSong, serif;
font-family: var(--font-body);
font-size: 12px;
color: var(--warm-gray);
letter-spacing: 0.04em;
@@ -247,7 +247,7 @@ onMounted(loadToday)
}
.stat-number {
font-family: 'ZCOOL XiaoWei', STSong, serif;
font-family: var(--font-heading);
font-size: 56px;
color: var(--ink);
line-height: 1;
@@ -255,7 +255,7 @@ onMounted(loadToday)
}
.stat-unit {
font-family: 'Noto Serif SC', STSong, serif;
font-family: var(--font-body);
font-size: 16px;
color: var(--warm-gray);
letter-spacing: 0.06em;
@@ -270,14 +270,14 @@ onMounted(loadToday)
}
.stat-piece {
font-family: 'Noto Serif SC', STSong, serif;
font-family: var(--font-body);
font-size: 13px;
color: var(--warm-gray);
letter-spacing: 0.03em;
}
.stat-piece strong {
font-family: 'JetBrains Mono', 'SF Mono', monospace;
font-family: var(--font-mono);
color: var(--ink);
font-size: 15px;
}
@@ -314,7 +314,7 @@ onMounted(loadToday)
padding: 16px 12px;
border: 1px solid var(--warm-border);
cursor: pointer;
font-family: 'Noto Serif SC', STSong, serif;
font-family: var(--font-body);
font-size: 15px;
letter-spacing: 0.04em;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
@@ -365,7 +365,7 @@ onMounted(loadToday)
background: var(--surface);
border: 1px solid var(--warm-border);
color: var(--ink);
font-family: 'Noto Serif SC', STSong, serif;
font-family: var(--font-body);
font-size: 13px;
letter-spacing: 0.03em;
cursor: pointer;
@@ -390,7 +390,7 @@ onMounted(loadToday)
}
.section-title {
font-family: 'ZCOOL XiaoWei', STSong, serif;
font-family: var(--font-heading);
font-size: 18px;
color: var(--ink);
letter-spacing: 0.06em;
@@ -459,7 +459,7 @@ onMounted(loadToday)
width: 22px;
height: 22px;
color: #fff;
font-family: 'ZCOOL XiaoWei', STSong, serif;
font-family: var(--font-heading);
font-size: 13px;
flex-shrink: 0;
}
@@ -469,7 +469,7 @@ onMounted(loadToday)
}
.record-category {
font-family: 'ZCOOL XiaoWei', STSong, serif;
font-family: var(--font-heading);
font-size: 13px;
color: var(--ink);
letter-spacing: 0.04em;
@@ -477,7 +477,7 @@ onMounted(loadToday)
}
.record-badge {
font-family: 'Noto Serif SC', STSong, serif;
font-family: var(--font-body);
font-size: 11px;
padding: 1px 8px;
letter-spacing: 0.04em;
@@ -491,7 +491,7 @@ onMounted(loadToday)
.record-customer {
font-family: 'ZCOOL XiaoWei', STSong, serif;
font-family: var(--font-heading);
font-size: 15px;
color: var(--ink);
letter-spacing: 0.03em;
@@ -499,7 +499,7 @@ onMounted(loadToday)
}
.record-time {
font-family: 'JetBrains Mono', 'SF Mono', monospace;
font-family: var(--font-mono);
font-size: 11px;
color: var(--warm-gray);
letter-spacing: 0.03em;
@@ -507,7 +507,7 @@ onMounted(loadToday)
}
.record-content {
font-family: 'Noto Serif SC', STSong, serif;
font-family: var(--font-body);
font-size: 14px;
color: var(--c-text);
line-height: 1.7;
@@ -516,7 +516,7 @@ onMounted(loadToday)
}
.record-demand {
font-family: 'Noto Serif SC', STSong, serif;
font-family: var(--font-body);
font-size: 13px;
color: var(--vermilion);
margin: 0 0 8px;
@@ -559,7 +559,7 @@ onMounted(loadToday)
}
.empty-glyph {
font-family: 'ZCOOL XiaoWei', STSong, serif;
font-family: var(--font-heading);
font-size: 40px;
color: var(--gold);
opacity: 0.4;
@@ -567,14 +567,14 @@ onMounted(loadToday)
}
.empty-text {
font-family: 'Noto Serif SC', STSong, serif;
font-family: var(--font-body);
font-size: 15px;
color: var(--warm-gray);
margin: 0 0 4px;
}
.empty-hint {
font-family: 'Noto Serif SC', STSong, serif;
font-family: var(--font-body);
font-size: 12px;
color: var(--c-text-muted);
margin: 0;
+5 -5
View File
@@ -169,13 +169,13 @@ async function handleSubmit() {
.form-title-group { display: flex; flex-direction: column; gap: 0; flex: 1; }
.form-title {
margin: 0; font-family: 'ZCOOL XiaoWei', STSong, serif;
margin: 0; font-family: var(--font-heading);
font-size: 24px; font-weight: 400; color: var(--ink);
letter-spacing: 0.06em; line-height: 1.3;
}
.form-subtitle {
font-family: 'JetBrains Mono', 'SF Mono', monospace;
font-family: var(--font-mono);
font-size: 9px; color: var(--gold); letter-spacing: 0.2em;
}
@@ -189,7 +189,7 @@ async function handleSubmit() {
}
.form-label {
font-family: 'ZCOOL XiaoWei', STSong, serif !important;
font-family: var(--font-heading) !important;
font-size: 14px !important; color: var(--ink) !important;
letter-spacing: 0.04em; font-weight: 500 !important;
}
@@ -204,7 +204,7 @@ async function handleSubmit() {
padding: 10px 8px;
background: var(--surface);
border: 1px solid var(--warm-border);
font-family: 'Noto Serif SC', STSong, serif;
font-family: var(--font-body);
font-size: 13px; letter-spacing: 0.04em;
cursor: pointer; transition: all 0.25s;
text-align: center;
@@ -216,7 +216,7 @@ async function handleSubmit() {
.submit-btn {
width: 100%; display: flex; align-items: center; justify-content: center; gap: 8px;
padding: 16px; background: var(--ink); color: #fff; border: none;
font-family: 'ZCOOL XiaoWei', STSong, serif;
font-family: var(--font-heading);
font-size: 16px; letter-spacing: 0.08em; cursor: pointer; transition: all 0.25s;
margin-top: 24px;
}
@@ -122,13 +122,13 @@ async function handleSubmit() {
.form-title-group { display: flex; flex-direction: column; gap: 0; flex: 1; }
.form-title {
margin: 0; font-family: 'ZCOOL XiaoWei', STSong, serif;
margin: 0; font-family: var(--font-heading);
font-size: 24px; font-weight: 400; color: var(--ink);
letter-spacing: 0.06em; line-height: 1.3;
}
.form-subtitle {
font-family: 'JetBrains Mono', 'SF Mono', monospace;
font-family: var(--font-mono);
font-size: 9px; color: var(--gold); letter-spacing: 0.2em;
}
@@ -142,7 +142,7 @@ async function handleSubmit() {
}
.form-label {
font-family: 'ZCOOL XiaoWei', STSong, serif !important;
font-family: var(--font-heading) !important;
font-size: 14px !important; color: var(--ink) !important;
letter-spacing: 0.04em; font-weight: 500 !important;
}
@@ -152,7 +152,7 @@ async function handleSubmit() {
.submit-btn {
width: 100%; display: flex; align-items: center; justify-content: center; gap: 8px;
padding: 16px; background: var(--ink); color: #fff; border: none;
font-family: 'ZCOOL XiaoWei', STSong, serif;
font-family: var(--font-heading);
font-size: 16px; letter-spacing: 0.08em; cursor: pointer; transition: all 0.25s;
margin-top: 24px;
}
+10 -10
View File
@@ -413,7 +413,7 @@ async function handleDelete() {
.form-title {
margin: 0;
font-family: 'ZCOOL XiaoWei', STSong, serif;
font-family: var(--font-heading);
font-size: 24px;
font-weight: 400;
color: var(--ink);
@@ -422,7 +422,7 @@ async function handleDelete() {
}
.form-subtitle {
font-family: 'JetBrains Mono', 'SF Mono', monospace;
font-family: var(--font-mono);
font-size: 9px;
color: var(--gold);
letter-spacing: 0.2em;
@@ -448,7 +448,7 @@ async function handleDelete() {
/* ═══ Form Labels ═══ */
.form-label {
font-family: 'ZCOOL XiaoWei', STSong, serif !important;
font-family: var(--font-heading) !important;
font-size: 14px !important;
color: var(--ink) !important;
letter-spacing: 0.04em;
@@ -456,7 +456,7 @@ async function handleDelete() {
}
.form-label-hint {
font-family: 'Noto Serif SC', STSong, serif;
font-family: var(--font-body);
font-size: 11px;
color: var(--warm-gray);
letter-spacing: 0.03em;
@@ -478,7 +478,7 @@ async function handleDelete() {
border: 1px dashed var(--gold);
padding: 8px 14px;
color: var(--gold-dark);
font-family: 'Noto Serif SC', STSong, serif;
font-family: var(--font-body);
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
@@ -517,7 +517,7 @@ async function handleDelete() {
}
.photo-label {
font-family: 'JetBrains Mono', 'SF Mono', monospace;
font-family: var(--font-mono);
font-size: 11px;
color: var(--warm-gray);
letter-spacing: 0.03em;
@@ -545,7 +545,7 @@ async function handleDelete() {
padding: 12px 18px;
border: 1px dashed var(--warm-border);
color: var(--warm-gray);
font-family: 'Noto Serif SC', STSong, serif;
font-family: var(--font-body);
font-size: 13px;
transition: all 0.2s;
letter-spacing: 0.03em;
@@ -579,7 +579,7 @@ async function handleDelete() {
background: var(--ink);
color: #fff;
border: none;
font-family: 'ZCOOL XiaoWei', STSong, serif;
font-family: var(--font-heading);
font-size: 16px;
letter-spacing: 0.08em;
cursor: pointer;
@@ -598,7 +598,7 @@ async function handleDelete() {
background: var(--surface);
color: var(--vermilion);
border: 1px solid var(--vermilion);
font-family: 'Noto Serif SC', STSong, serif;
font-family: var(--font-body);
font-size: 14px;
letter-spacing: 0.04em;
cursor: pointer;
@@ -629,7 +629,7 @@ async function handleDelete() {
padding: 10px 8px;
background: var(--surface);
border: 1px solid var(--warm-border);
font-family: 'Noto Serif SC', STSong, serif;
font-family: var(--font-body);
font-size: 13px; letter-spacing: 0.04em;
cursor: pointer; transition: all 0.25s;
text-align: center;
+4 -4
View File
@@ -102,13 +102,13 @@ async function handleSubmit() {
.form-title-group { display: flex; flex-direction: column; gap: 0; flex: 1; }
.form-title {
margin: 0; font-family: 'ZCOOL XiaoWei', STSong, serif;
margin: 0; font-family: var(--font-heading);
font-size: 24px; font-weight: 400; color: var(--ink);
letter-spacing: 0.06em; line-height: 1.3;
}
.form-subtitle {
font-family: 'JetBrains Mono', 'SF Mono', monospace;
font-family: var(--font-mono);
font-size: 9px; color: var(--gold); letter-spacing: 0.2em;
}
@@ -122,7 +122,7 @@ async function handleSubmit() {
}
.form-label {
font-family: 'ZCOOL XiaoWei', STSong, serif !important;
font-family: var(--font-heading) !important;
font-size: 14px !important; color: var(--ink) !important;
letter-spacing: 0.04em; font-weight: 500 !important;
}
@@ -132,7 +132,7 @@ async function handleSubmit() {
.submit-btn {
width: 100%; display: flex; align-items: center; justify-content: center; gap: 8px;
padding: 16px; background: var(--ink); color: #fff; border: none;
font-family: 'ZCOOL XiaoWei', STSong, serif;
font-family: var(--font-heading);
font-size: 16px; letter-spacing: 0.08em; cursor: pointer; transition: all 0.25s;
margin-top: 24px;
}