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:
@@ -16,6 +16,7 @@ class UpdateUserRoleRequest(BaseModel):
|
|||||||
role: str # manager / director / leader
|
role: str # manager / director / leader
|
||||||
department: str = ""
|
department: str = ""
|
||||||
require_report: bool = True
|
require_report: bool = True
|
||||||
|
color: Optional[str] = None # hex color for manager tags
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=list[UserOut])
|
@router.get("/", response_model=list[UserOut])
|
||||||
@@ -73,6 +74,8 @@ async def update_user_role(
|
|||||||
if data.department:
|
if data.department:
|
||||||
user.department = data.department
|
user.department = data.department
|
||||||
user.require_report = data.require_report
|
user.require_report = data.require_report
|
||||||
|
if data.color is not None:
|
||||||
|
user.color = data.color
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(user)
|
await db.refresh(user)
|
||||||
|
|
||||||
@@ -82,6 +85,7 @@ async def update_user_role(
|
|||||||
"role": user.role,
|
"role": user.role,
|
||||||
"department": user.department,
|
"department": user.department,
|
||||||
"require_report": user.require_report,
|
"require_report": user.require_report,
|
||||||
|
"color": user.color,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ async def lifespan(app: FastAPI):
|
|||||||
await conn.run_sync(lambda c: c.exec_driver_sql(
|
await conn.run_sync(lambda c: c.exec_driver_sql(
|
||||||
"ALTER TABLE visits ADD COLUMN IF NOT EXISTS companion_names TEXT[] DEFAULT '{}'"
|
"ALTER TABLE visits ADD COLUMN IF NOT EXISTS companion_names TEXT[] DEFAULT '{}'"
|
||||||
))
|
))
|
||||||
|
# v0.6 — manager color tag
|
||||||
|
await conn.run_sync(lambda c: c.exec_driver_sql(
|
||||||
|
"ALTER TABLE users ADD COLUMN IF NOT EXISTS color VARCHAR(7)"
|
||||||
|
))
|
||||||
# system_config table for v0.5
|
# system_config table for v0.5
|
||||||
await conn.run_sync(lambda c: c.exec_driver_sql(
|
await conn.run_sync(lambda c: c.exec_driver_sql(
|
||||||
"CREATE TABLE IF NOT EXISTS system_config (key VARCHAR(64) PRIMARY KEY, value TEXT DEFAULT '')"
|
"CREATE TABLE IF NOT EXISTS system_config (key VARCHAR(64) PRIMARY KEY, value TEXT DEFAULT '')"
|
||||||
|
|||||||
@@ -16,4 +16,5 @@ class User(Base):
|
|||||||
department: Mapped[str] = mapped_column(String(100), default="")
|
department: Mapped[str] = mapped_column(String(100), default="")
|
||||||
wecom_userid: Mapped[str | None] = mapped_column(String(100), unique=True, nullable=True)
|
wecom_userid: Mapped[str | None] = mapped_column(String(100), unique=True, nullable=True)
|
||||||
require_report: Mapped[bool] = mapped_column(default=True, server_default="true")
|
require_report: Mapped[bool] = mapped_column(default=True, server_default="true")
|
||||||
|
color: Mapped[str | None] = mapped_column(String(7), nullable=True) # e.g. "#C62828"
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class UserOut(BaseModel):
|
|||||||
department: str
|
department: str
|
||||||
wecom_userid: Optional[str] = None
|
wecom_userid: Optional[str] = None
|
||||||
require_report: bool = True
|
require_report: bool = True
|
||||||
|
color: Optional[str] = None
|
||||||
|
|
||||||
model_config = {"from_attributes": True}
|
model_config = {"from_attributes": True}
|
||||||
|
|
||||||
|
|||||||
+118
-10
@@ -67,6 +67,114 @@ const isMobile = computed(() => {
|
|||||||
--radius-sm: 2px;
|
--radius-sm: 2px;
|
||||||
--radius-xs: 2px;
|
--radius-xs: 2px;
|
||||||
--transition: 0.22s cubic-bezier(0.4, 0, 0.2, 1);
|
--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 ── */
|
/* ── Reset & Base ── */
|
||||||
@@ -76,7 +184,7 @@ html, body, #app {
|
|||||||
margin: 0; padding: 0; height: 100%;
|
margin: 0; padding: 0; height: 100%;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
font-family: 'Noto Serif SC', STSong, Songti SC, '宋体', serif;
|
font-family: var(--font-body);
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
color: var(--c-text);
|
color: var(--c-text);
|
||||||
@@ -87,7 +195,7 @@ html, body, #app {
|
|||||||
|
|
||||||
/* ── Editorial heading style ── */
|
/* ── Editorial heading style ── */
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
font-family: 'ZCOOL XiaoWei', STSong, Songti SC, serif;
|
font-family: var(--font-heading);
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,7 +215,7 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
.el-card__header {
|
.el-card__header {
|
||||||
border-bottom: 2px solid var(--gold) !important;
|
border-bottom: 2px solid var(--gold) !important;
|
||||||
padding: 16px 20px !important;
|
padding: 16px 20px !important;
|
||||||
font-family: 'ZCOOL XiaoWei', STSong, serif;
|
font-family: var(--font-heading);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
@@ -121,7 +229,7 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
transition: all var(--transition);
|
transition: all var(--transition);
|
||||||
font-family: 'Noto Serif SC', STSong, Songti SC, serif;
|
font-family: var(--font-body);
|
||||||
}
|
}
|
||||||
.el-button--primary {
|
.el-button--primary {
|
||||||
background: var(--ink) !important;
|
background: var(--ink) !important;
|
||||||
@@ -149,14 +257,14 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
.el-tag {
|
.el-tag {
|
||||||
border-radius: 2px !important;
|
border-radius: 2px !important;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-family: 'Noto Serif SC', STSong, serif;
|
font-family: var(--font-body);
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Tabs ── */
|
/* ── Tabs ── */
|
||||||
.el-tabs__item {
|
.el-tabs__item {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-family: 'ZCOOL XiaoWei', STSong, serif;
|
font-family: var(--font-heading);
|
||||||
}
|
}
|
||||||
.el-tabs__active-bar { background: var(--gold) !important; }
|
.el-tabs__active-bar { background: var(--gold) !important; }
|
||||||
.el-tabs__item.is-active { color: var(--ink) !important; }
|
.el-tabs__item.is-active { color: var(--ink) !important; }
|
||||||
@@ -167,7 +275,7 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-family: 'ZCOOL XiaoWei', STSong, serif;
|
font-family: var(--font-heading);
|
||||||
letter-spacing: 0.03em;
|
letter-spacing: 0.03em;
|
||||||
}
|
}
|
||||||
.el-table--striped .el-table__body tr.el-table__row--striped td.el-table__cell {
|
.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 {
|
.el-dialog__header {
|
||||||
padding: 20px 24px 0 !important;
|
padding: 20px 24px 0 !important;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-family: 'ZCOOL XiaoWei', STSong, serif;
|
font-family: var(--font-heading);
|
||||||
}
|
}
|
||||||
.el-dialog__body { padding: 20px 24px !important; }
|
.el-dialog__body { padding: 20px 24px !important; }
|
||||||
|
|
||||||
@@ -225,7 +333,7 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
/* ── Textarea ── */
|
/* ── Textarea ── */
|
||||||
.el-textarea__inner {
|
.el-textarea__inner {
|
||||||
border-radius: 2px !important;
|
border-radius: 2px !important;
|
||||||
font-family: 'Noto Serif SC', STSong, serif;
|
font-family: var(--font-body);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Radio ── */
|
/* ── Radio ── */
|
||||||
@@ -258,7 +366,7 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
|
|
||||||
/* ── Divider ── */
|
/* ── Divider ── */
|
||||||
.el-divider__text {
|
.el-divider__text {
|
||||||
font-family: 'ZCOOL XiaoWei', STSong, serif;
|
font-family: var(--font-heading);
|
||||||
color: var(--warm-gray);
|
color: var(--warm-gray);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,12 @@
|
|||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import { useThemeStore, themeLabels } from '@/stores/theme'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
|
const themeStore = useThemeStore()
|
||||||
const collapsed = ref(false)
|
const collapsed = ref(false)
|
||||||
|
|
||||||
interface MenuGroup { label?: string; items: { path: string; label: string; icon: string }[] }
|
interface MenuGroup { label?: string; items: { path: string; label: string; icon: string }[] }
|
||||||
@@ -57,6 +59,11 @@ const roleLabel = computed(() => {
|
|||||||
if (auth.isLeader) return '分管领导'
|
if (auth.isLeader) return '分管领导'
|
||||||
return '客户经理'
|
return '客户经理'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function cycleTheme() {
|
||||||
|
const next = themeStore.currentTheme === 'editorial' ? 'light' : 'editorial'
|
||||||
|
themeStore.setTheme(next)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -100,6 +107,13 @@ const roleLabel = computed(() => {
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="topbar-actions">
|
<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="移动端">
|
<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">
|
<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>
|
<rect x="5" y="2" width="14" height="20" rx="2" ry="2"></rect>
|
||||||
@@ -135,8 +149,8 @@ const roleLabel = computed(() => {
|
|||||||
/* ═══ Sidebar ═══ */
|
/* ═══ Sidebar ═══ */
|
||||||
.sidebar {
|
.sidebar {
|
||||||
width: 240px;
|
width: 240px;
|
||||||
background: var(--ink);
|
background: var(--sidebar-bg);
|
||||||
color: #fff;
|
color: var(--sidebar-color);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
@@ -158,31 +172,31 @@ const roleLabel = computed(() => {
|
|||||||
|
|
||||||
.sidebar-brand {
|
.sidebar-brand {
|
||||||
padding: 26px 22px 18px;
|
padding: 26px 22px 18px;
|
||||||
border-bottom: 1px solid rgba(255,255,255,0.06);
|
border-bottom: 1px solid var(--sidebar-divider);
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-title {
|
.brand-title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: 'ZCOOL XiaoWei', STSong, Songti SC, serif;
|
font-family: var(--font-heading);
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
letter-spacing: 0.12em;
|
letter-spacing: 0.12em;
|
||||||
color: #fff;
|
color: var(--sidebar-brand-color);
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-sub {
|
.brand-sub {
|
||||||
margin: 2px 0 0;
|
margin: 2px 0 0;
|
||||||
font-family: 'Noto Serif SC', STSong, serif;
|
font-family: var(--font-body);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: rgba(255,255,255,0.4);
|
color: var(--sidebar-brand-sub-color);
|
||||||
letter-spacing: 0.1em;
|
letter-spacing: 0.1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-rule {
|
.brand-rule {
|
||||||
width: 28px;
|
width: 28px;
|
||||||
height: 3px;
|
height: 3px;
|
||||||
background: var(--gold);
|
background: var(--sidebar-accent);
|
||||||
margin-top: 14px;
|
margin-top: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,10 +211,10 @@ const roleLabel = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.nav-group-label {
|
.nav-group-label {
|
||||||
font-family: 'JetBrains Mono', 'SF Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
letter-spacing: 0.15em;
|
letter-spacing: 0.15em;
|
||||||
color: rgba(255,255,255,0.25);
|
color: var(--sidebar-group-label-color);
|
||||||
padding: 12px 14px 4px;
|
padding: 12px 14px 4px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
@@ -212,9 +226,9 @@ const roleLabel = computed(() => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 11px 14px;
|
padding: 11px 14px;
|
||||||
color: rgba(255,255,255,0.55);
|
color: var(--sidebar-nav-text);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-family: 'Noto Serif SC', STSong, serif;
|
font-family: var(--font-body);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
transition: all 0.22s;
|
transition: all 0.22s;
|
||||||
@@ -222,14 +236,14 @@ const roleLabel = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.nav-item:hover {
|
.nav-item:hover {
|
||||||
color: rgba(255,255,255,0.85);
|
color: var(--sidebar-nav-text-hover);
|
||||||
background: rgba(255,255,255,0.04);
|
background: var(--sidebar-nav-bg-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item--active {
|
.nav-item--active {
|
||||||
color: #fff;
|
color: var(--sidebar-nav-text-active);
|
||||||
background: rgba(255,255,255,0.06);
|
background: var(--sidebar-nav-bg-active);
|
||||||
border-left-color: var(--gold);
|
border-left-color: var(--sidebar-nav-border-active);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-icon {
|
.nav-icon {
|
||||||
@@ -244,23 +258,23 @@ const roleLabel = computed(() => {
|
|||||||
/* ═══ Sidebar Footer ═══ */
|
/* ═══ Sidebar Footer ═══ */
|
||||||
.sidebar-footer {
|
.sidebar-footer {
|
||||||
padding: 14px 22px;
|
padding: 14px 22px;
|
||||||
border-top: 1px solid rgba(255,255,255,0.06);
|
border-top: 1px solid var(--sidebar-divider);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-role {
|
.footer-role {
|
||||||
font-family: 'Noto Serif SC', STSong, serif;
|
font-family: var(--font-body);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: var(--gold);
|
color: var(--sidebar-footer-role-color);
|
||||||
letter-spacing: 0.1em;
|
letter-spacing: 0.1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-name {
|
.footer-name {
|
||||||
font-family: 'ZCOOL XiaoWei', STSong, serif;
|
font-family: var(--font-heading);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: rgba(255,255,255,0.7);
|
color: var(--sidebar-footer-name-color);
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,7 +299,7 @@ const roleLabel = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.topbar-greeting {
|
.topbar-greeting {
|
||||||
font-family: 'ZCOOL XiaoWei', STSong, serif;
|
font-family: var(--font-heading);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
@@ -315,7 +329,7 @@ const roleLabel = computed(() => {
|
|||||||
background: none;
|
background: none;
|
||||||
border: 1px solid var(--warm-border);
|
border: 1px solid var(--warm-border);
|
||||||
color: var(--warm-gray);
|
color: var(--warm-gray);
|
||||||
font-family: 'Noto Serif SC', STSong, serif;
|
font-family: var(--font-body);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
letter-spacing: 0.03em;
|
letter-spacing: 0.03em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -125,14 +125,14 @@ function labelFor(field: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.log-editor {
|
.log-editor {
|
||||||
font-family: 'ZCOOL XiaoWei', STSong, serif;
|
font-family: var(--font-heading);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-time {
|
.log-time {
|
||||||
font-family: 'JetBrains Mono', 'SF Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--c-text-muted);
|
color: var(--c-text-muted);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -266,7 +266,7 @@ const imgStyle = computed(() => {
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
color: rgba(255, 255, 255, 0.75);
|
color: rgba(255, 255, 255, 0.75);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-family: 'Noto Serif SC', STSong, serif;
|
font-family: var(--font-body);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
transition: all 0.15s;
|
transition: all 0.15s;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -287,7 +287,7 @@ const imgStyle = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.fit-label {
|
.fit-label {
|
||||||
font-family: 'JetBrains Mono', 'SF Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
letter-spacing: 0.03em;
|
letter-spacing: 0.03em;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ const activeTab = computed(() => {
|
|||||||
|
|
||||||
.header-title {
|
.header-title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: 'ZCOOL XiaoWei', STSong, Songti SC, serif;
|
font-family: var(--font-heading);
|
||||||
font-size: 26px;
|
font-size: 26px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
@@ -138,7 +138,7 @@ const activeTab = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.header-subtitle {
|
.header-subtitle {
|
||||||
font-family: 'Noto Serif SC', STSong, Songti SC, serif;
|
font-family: var(--font-body);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--warm-gray);
|
color: var(--warm-gray);
|
||||||
letter-spacing: 0.12em;
|
letter-spacing: 0.12em;
|
||||||
@@ -178,7 +178,7 @@ const activeTab = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.header-user {
|
.header-user {
|
||||||
font-family: 'Noto Serif SC', STSong, serif;
|
font-family: var(--font-body);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--warm-gray);
|
color: var(--warm-gray);
|
||||||
letter-spacing: 0.03em;
|
letter-spacing: 0.03em;
|
||||||
@@ -201,7 +201,7 @@ const activeTab = computed(() => {
|
|||||||
right: 0;
|
right: 0;
|
||||||
width: 60px;
|
width: 60px;
|
||||||
height: 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;
|
pointer-events: none;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
}
|
}
|
||||||
@@ -238,7 +238,7 @@ const activeTab = computed(() => {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
font-family: 'Noto Serif SC', STSong, serif;
|
font-family: var(--font-body);
|
||||||
letter-spacing: 0.03em;
|
letter-spacing: 0.03em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
})
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/* ── Google Fonts import for Editorial Chinese fonts ── */
|
/* ── Google Fonts import ── */
|
||||||
@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');
|
@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 base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
|
|||||||
@@ -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) }
|
||||||
|
}
|
||||||
@@ -25,6 +25,6 @@ onMounted(() => {
|
|||||||
height: 100vh;
|
height: 100vh;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
font-family: 'Noto Serif SC', STSong, serif;
|
font-family: var(--font-body);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ async function doBind() {
|
|||||||
.wecom-bind-page { max-width: 400px; margin: 80px auto; padding: 32px 24px; text-align: center; }
|
.wecom-bind-page { max-width: 400px; margin: 80px auto; padding: 32px 24px; text-align: center; }
|
||||||
.bind-error { color: var(--vermilion); font-size: 15px; }
|
.bind-error { color: var(--vermilion); font-size: 15px; }
|
||||||
.bind-icon { font-size: 56px; margin-bottom: 16px; }
|
.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-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; }
|
.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; }
|
.info-row { display: flex; justify-content: space-between; align-items: center; padding: 8px 0; }
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import { useThemeStore } from '@/stores/theme'
|
||||||
import { customersApi } from '@/api/customers'
|
import { customersApi } from '@/api/customers'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { getManagerColor as mgrColorFn, getMgrTextColor, loadManagerColors } from '@/utils/managerColor'
|
||||||
import api from '@/api/index'
|
import api from '@/api/index'
|
||||||
|
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
|
const themeStore = useThemeStore()
|
||||||
|
const isLight = computed(() => themeStore.currentTheme === 'light')
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
const filterIndustry = ref('')
|
const filterIndustry = ref('')
|
||||||
@@ -43,7 +47,7 @@ const importLoading = ref(false)
|
|||||||
const importResult = ref<any>(null)
|
const importResult = ref<any>(null)
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadCustomers()
|
await Promise.all([loadCustomers(), loadManagerColors()])
|
||||||
try {
|
try {
|
||||||
const res = await api.get('/users/')
|
const res = await api.get('/users/')
|
||||||
managers.value = (res.data || []).filter((u: any) => u.role !== 'leader')
|
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('删除失败') }
|
} catch (e: any) { if (e !== 'cancel') ElMessage.error('删除失败') }
|
||||||
}
|
}
|
||||||
|
|
||||||
const mgrColors = ['#1C3738','#4A6741','#5B7FA5','#7B7568','#8B6F47','#6B5B4F','#3D5A5C','#5C4A3D']
|
|
||||||
function mgrColor(name: string) {
|
function mgrColor(name: string) {
|
||||||
if (!name) return '#909399'
|
return mgrColorFn(name, isLight.value)
|
||||||
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]
|
function mgrTextColor(name: string) {
|
||||||
|
return getMgrTextColor(mgrColor(name))
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openDetail(customer: any) {
|
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 prop="in_use_services" label="在用业务" min-width="150" />
|
||||||
<el-table-column label="客户经理" width="90">
|
<el-table-column label="客户经理" width="90">
|
||||||
<template #default="{ row }">
|
<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>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -410,7 +414,7 @@ async function handleImport() {
|
|||||||
<el-descriptions-item label="备注">{{ detailCustomer.remarks || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="备注">{{ detailCustomer.remarks || '-' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="客户经理">{{ detailCustomer.primary_manager_name || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="客户经理">{{ detailCustomer.primary_manager_name || '-' }}</el-descriptions-item>
|
||||||
</el-descriptions>
|
</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">
|
<div v-if="detailCustomer.contacts?.length">
|
||||||
<el-tag v-for="c in detailCustomer.contacts" :key="c.id" style="margin:4px">
|
<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+')' : '' }}
|
{{ 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-head-row { display: flex; justify-content: space-between; align-items: center; }
|
||||||
.page-title {
|
.page-title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: 'ZCOOL XiaoWei', STSong, serif;
|
font-family: var(--font-heading);
|
||||||
font-size: 22px; font-weight: 400;
|
font-size: 22px; font-weight: 400;
|
||||||
color: var(--ink); letter-spacing: 0.06em;
|
color: var(--ink); letter-spacing: 0.06em;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ function rowState(p: any): 'full' | 'catching' | 'missing' {
|
|||||||
.page-head { margin-bottom: 24px; }
|
.page-head { margin-bottom: 24px; }
|
||||||
.page-title {
|
.page-title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: 'ZCOOL XiaoWei', STSong, serif;
|
font-family: var(--font-heading);
|
||||||
font-size: 22px; font-weight: 400;
|
font-size: 22px; font-weight: 400;
|
||||||
color: var(--ink); letter-spacing: 0.06em;
|
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 { 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:hover:not(:disabled) { color: var(--ink); border-color: var(--ink); }
|
||||||
.week-nav-btn:disabled { opacity: 0.3; cursor: not-allowed; }
|
.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 { 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; }
|
.week-nav-reset:hover { background: var(--gold); color: #fff; }
|
||||||
.page-rule { width: 32px; height: 3px; background: var(--gold); margin-top: 12px; }
|
.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-body { display: flex; flex-direction: column; }
|
||||||
.stat-num {
|
.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;
|
font-size: 28px; color: var(--ink); line-height: 1.1; letter-spacing: 0.04em;
|
||||||
}
|
}
|
||||||
.stat-label {
|
.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;
|
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; }
|
.progress-card { margin-top: 4px; }
|
||||||
.card-header-title {
|
.card-header-title {
|
||||||
display: flex; align-items: center;
|
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;
|
font-size: 15px; color: var(--ink); letter-spacing: 0.05em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,7 +339,7 @@ function rowState(p: any): 'full' | 'catching' | 'missing' {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.seal-char {
|
.seal-char {
|
||||||
font-family: 'ZCOOL XiaoWei', STSong, serif;
|
font-family: var(--font-heading);
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
@@ -356,7 +356,7 @@ function rowState(p: any): 'full' | 'catching' | 'missing' {
|
|||||||
|
|
||||||
/* Inline status text (replaces el-tag) */
|
/* Inline status text (replaces el-tag) */
|
||||||
.progress-status {
|
.progress-status {
|
||||||
font-family: 'Noto Serif SC', STSong, serif;
|
font-family: var(--font-body);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
}
|
}
|
||||||
@@ -365,10 +365,10 @@ function rowState(p: any): 'full' | 'catching' | 'missing' {
|
|||||||
.progress-status--full { color: var(--sage); }
|
.progress-status--full { color: var(--sage); }
|
||||||
|
|
||||||
.progress-count {
|
.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;
|
font-size: 13px; color: var(--warm-gray); letter-spacing: 0.03em;
|
||||||
position: relative; z-index: 1;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import { useThemeStore } from '@/stores/theme'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { todayStr } from '@/utils'
|
import { todayStr } from '@/utils'
|
||||||
|
import { getMgrStyle, loadManagerColors } from '@/utils/managerColor'
|
||||||
import api from '@/api/index'
|
import api from '@/api/index'
|
||||||
import EditLogPanel from '@/components/EditLogPanel.vue'
|
import EditLogPanel from '@/components/EditLogPanel.vue'
|
||||||
|
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
|
const themeStore = useThemeStore()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
const isLight = computed(() => themeStore.currentTheme === 'light')
|
||||||
const keyVisits = ref<any[]>([])
|
const keyVisits = ref<any[]>([])
|
||||||
const customers = ref<any[]>([])
|
const customers = ref<any[]>([])
|
||||||
const allUsers = ref<any[]>([])
|
const allUsers = ref<any[]>([])
|
||||||
@@ -19,13 +23,6 @@ const form = ref<any>({})
|
|||||||
const statusPick = ref<Record<string, string>>({})
|
const statusPick = ref<Record<string, string>>({})
|
||||||
const keyStatuses = ['未开始', '进行中', '已完成']
|
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 managerSummary = computed(() => {
|
||||||
const map: Record<string, number> = {}
|
const map: Record<string, number> = {}
|
||||||
keyVisits.value.forEach((k: any) => {
|
keyVisits.value.forEach((k: any) => {
|
||||||
@@ -36,7 +33,7 @@ const managerSummary = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await Promise.all([loadItems(), loadCustomers(), loadUsers()])
|
await Promise.all([loadItems(), loadCustomers(), loadUsers(), loadManagerColors()])
|
||||||
})
|
})
|
||||||
|
|
||||||
async function loadUsers() {
|
async function loadUsers() {
|
||||||
@@ -135,7 +132,7 @@ async function quickStatusChange(row: any, newStatus: string) {
|
|||||||
<div class="page-rule"></div>
|
<div class="page-rule"></div>
|
||||||
<div v-if="managerSummary.length" class="summary-bar">
|
<div v-if="managerSummary.length" class="summary-bar">
|
||||||
<span class="summary-label">客户经理汇总</span>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -153,7 +150,7 @@ async function quickStatusChange(row: any, newStatus: string) {
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="客户经理" width="100">
|
<el-table-column label="客户经理" width="100">
|
||||||
<template #default="{ row }">
|
<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>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="urgency_level" label="重要度" width="90">
|
<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 prop="visit_target" label="拜访对象" width="110" />
|
||||||
<el-table-column label="操作" width="80" fixed="right">
|
<el-table-column label="操作" width="80" fixed="right">
|
||||||
<template #default="{ row }">
|
<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>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</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; }
|
.edit-indicator { font-size: 12px; margin-left: 3px; opacity: 0.5; cursor: help; }
|
||||||
.page-head { margin-bottom: 20px; }
|
.page-head { margin-bottom: 20px; }
|
||||||
.page-head-row { display: flex; justify-content: space-between; align-items: flex-start; }
|
.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; }
|
||||||
.page-desc { margin: 4px 0 0; font-size: 13px; color: var(--c-text-muted); font-family: 'Noto Serif SC', STSong, serif; }
|
.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; }
|
.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-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-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: #fff; white-space: nowrap; }
|
.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: #fff; 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; }
|
.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>
|
</style>
|
||||||
|
|||||||
@@ -111,12 +111,7 @@ const statusLabel: Record<string, string> = { green: '近30天已拜访', yellow
|
|||||||
<div class="page-head">
|
<div class="page-head">
|
||||||
<div class="page-head-row">
|
<div class="page-head-row">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="page-title">
|
<h2 class="page-title">客户拜访亮灯表</h2>
|
||||||
<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>
|
|
||||||
<div class="month-nav">
|
<div class="month-nav">
|
||||||
<button class="month-nav-btn" @click="changeMonth(-1)">◀</button>
|
<button class="month-nav-btn" @click="changeMonth(-1)">◀</button>
|
||||||
<span class="month-label">{{ referenceMonth }}</span>
|
<span class="month-label">{{ referenceMonth }}</span>
|
||||||
@@ -277,12 +272,12 @@ const statusLabel: Record<string, string> = { green: '近30天已拜访', yellow
|
|||||||
/* ═══ Page Header ═══ */
|
/* ═══ Page Header ═══ */
|
||||||
.page-head { margin-bottom: 20px; }
|
.page-head { margin-bottom: 20px; }
|
||||||
.page-head-row { display: flex; justify-content: space-between; align-items: flex-start; }
|
.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 { 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 { 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:hover:not(:disabled) { color: var(--ink); border-color: var(--ink); }
|
||||||
.month-nav-btn:disabled { opacity: 0.3; cursor: not-allowed; }
|
.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 { 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; }
|
.month-nav-reset:hover { background: var(--gold); color: #fff; }
|
||||||
.page-rule { width: 32px; height: 3px; background: var(--gold); margin-top: 12px; }
|
.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-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 { 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: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-num { font-family: var(--font-heading); 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-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--green .team-stat-num { color: var(--sage); }
|
||||||
.team-stat--yellow .team-stat-num { color: var(--gold); }
|
.team-stat--yellow .team-stat-num { color: var(--gold); }
|
||||||
.team-stat--red .team-stat-num { color: var(--vermilion); }
|
.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-summary:hover { background: rgba(196,147,74,0.03); }
|
||||||
.manager-info { display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0; }
|
.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-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-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; }
|
.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--green { background: #EDF2EC; color: var(--sage); }
|
||||||
.light-dot--yellow { background: #FBF6EE; color: var(--gold); }
|
.light-dot--yellow { background: #FBF6EE; color: var(--gold); }
|
||||||
.light-dot--red { background: #FBF1EE; color: var(--vermilion); }
|
.light-dot--red { background: #FBF1EE; color: var(--vermilion); }
|
||||||
.manager-coverage { display: flex; align-items: center; gap: 10px; width: 180px; flex-shrink: 0; }
|
.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-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-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 ═══ */
|
||||||
.customer-grid { display: flex; flex-wrap: wrap; gap: 10px; padding: 0 18px 16px; border-top: 1px solid var(--warm-border); padding-top: 14px; }
|
.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--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--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); }
|
.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-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-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-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; }
|
.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; }
|
.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-section { border-style: dashed; border-color: var(--warm-gray); }
|
||||||
.unassigned-summary { cursor: pointer; }
|
.unassigned-summary { cursor: pointer; }
|
||||||
.empty-state { text-align: center; padding: 48px 0; }
|
.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 { 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: 'Noto Serif SC', STSong, serif; }
|
.empty { text-align: center; padding: 20px; color: var(--c-text-muted); font-family: var(--font-body); }
|
||||||
|
|
||||||
/* ═══ Dialog ═══ */
|
/* ═══ Dialog ═══ */
|
||||||
.dlg-status { padding: 8px 12px; border-radius: 6px; font-size: 13px; font-weight: 600; margin-bottom: 12px; }
|
.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 { 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-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-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-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-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; }
|
.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 prop="communication_content" label="沟通内容" min-width="200" show-overflow-tooltip />
|
||||||
<el-table-column label="操作" width="70">
|
<el-table-column label="操作" width="70">
|
||||||
<template #default="{ row }">
|
<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>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -328,7 +328,7 @@ const notesByDate = computed(() => {
|
|||||||
<el-table-column prop="time_range" label="时间" width="100" />
|
<el-table-column prop="time_range" label="时间" width="100" />
|
||||||
<el-table-column label="操作" width="70">
|
<el-table-column label="操作" width="70">
|
||||||
<template #default="{ row }">
|
<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>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -361,7 +361,7 @@ const notesByDate = computed(() => {
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="70">
|
<el-table-column label="操作" width="70">
|
||||||
<template #default="{ row }">
|
<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>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -395,7 +395,7 @@ const notesByDate = computed(() => {
|
|||||||
<el-table-column prop="expected_revenue_date" label="预计列收" width="110" />
|
<el-table-column prop="expected_revenue_date" label="预计列收" width="110" />
|
||||||
<el-table-column label="操作" width="70">
|
<el-table-column label="操作" width="70">
|
||||||
<template #default="{ row }">
|
<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>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -430,7 +430,7 @@ const notesByDate = computed(() => {
|
|||||||
<el-table-column prop="visit_target" label="拜访对象" width="100" />
|
<el-table-column prop="visit_target" label="拜访对象" width="100" />
|
||||||
<el-table-column label="操作" width="70">
|
<el-table-column label="操作" width="70">
|
||||||
<template #default="{ row }">
|
<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>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -534,9 +534,9 @@ const notesByDate = computed(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page-head { margin-bottom: 20px; }
|
.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; }
|
.page-rule { width: 32px; height: 3px; background: var(--gold); margin-top: 12px; }
|
||||||
.date-group { margin-bottom: 20px; }
|
.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; }
|
.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: 'Noto Serif SC', STSong, serif; }
|
.empty { text-align: center; color: var(--c-text-muted); padding: 40px 0; font-family: var(--font-body); }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import { useThemeStore } from '@/stores/theme'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { todayStr } from '@/utils'
|
import { todayStr } from '@/utils'
|
||||||
|
import { getMgrStyle, loadManagerColors } from '@/utils/managerColor'
|
||||||
import api from '@/api/index'
|
import api from '@/api/index'
|
||||||
import EditLogPanel from '@/components/EditLogPanel.vue'
|
import EditLogPanel from '@/components/EditLogPanel.vue'
|
||||||
|
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
|
const themeStore = useThemeStore()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
const isLight = computed(() => themeStore.currentTheme === 'light')
|
||||||
const miniBusiness = ref<any[]>([])
|
const miniBusiness = ref<any[]>([])
|
||||||
const customers = ref<any[]>([])
|
const customers = ref<any[]>([])
|
||||||
|
|
||||||
@@ -17,13 +21,6 @@ const form = ref<any>({})
|
|||||||
const statusPick = ref<Record<string, string>>({})
|
const statusPick = ref<Record<string, string>>({})
|
||||||
const miniStatuses = ['跟进中', '已签约', '已流失']
|
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 managerSummary = computed(() => {
|
||||||
const map: Record<string, number> = {}
|
const map: Record<string, number> = {}
|
||||||
miniBusiness.value.forEach((m: any) => {
|
miniBusiness.value.forEach((m: any) => {
|
||||||
@@ -34,7 +31,7 @@ const managerSummary = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await Promise.all([loadItems(), loadCustomers()])
|
await Promise.all([loadItems(), loadCustomers(), loadManagerColors()])
|
||||||
})
|
})
|
||||||
|
|
||||||
async function loadCustomers(q?: string) {
|
async function loadCustomers(q?: string) {
|
||||||
@@ -120,7 +117,7 @@ async function quickStatusChange(row: any, newStatus: string) {
|
|||||||
<div class="page-rule"></div>
|
<div class="page-rule"></div>
|
||||||
<div v-if="managerSummary.length" class="summary-bar">
|
<div v-if="managerSummary.length" class="summary-bar">
|
||||||
<span class="summary-label">客户经理汇总</span>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -138,7 +135,7 @@ async function quickStatusChange(row: any, newStatus: string) {
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="客户经理" width="100">
|
<el-table-column label="客户经理" width="100">
|
||||||
<template #default="{ row }">
|
<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>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="product_type" label="产品类型" width="130" />
|
<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 prop="expected_revenue_date" label="预计列收" width="110" />
|
||||||
<el-table-column label="操作" width="80" fixed="right">
|
<el-table-column label="操作" width="80" fixed="right">
|
||||||
<template #default="{ row }">
|
<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>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</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; }
|
.edit-indicator { font-size: 12px; margin-left: 3px; opacity: 0.5; cursor: help; }
|
||||||
.page-head { margin-bottom: 20px; }
|
.page-head { margin-bottom: 20px; }
|
||||||
.page-head-row { display: flex; justify-content: space-between; align-items: flex-start; }
|
.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; }
|
||||||
.page-desc { margin: 4px 0 0; font-size: 13px; color: var(--c-text-muted); font-family: 'Noto Serif SC', STSong, serif; }
|
.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; }
|
.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-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-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: #fff; white-space: nowrap; }
|
.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: #fff; 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: 'Noto Serif SC', STSong, serif; }
|
.empty { text-align: center; color: var(--c-text-muted); padding: 48px 0; font-family: var(--font-body); }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import api from '@/api/index'
|
import api from '@/api/index'
|
||||||
|
import { useThemeStore, themeLabels, type Theme } from '@/stores/theme'
|
||||||
|
|
||||||
|
const themeStore = useThemeStore()
|
||||||
|
|
||||||
const managers = ref<any[]>([])
|
const managers = ref<any[]>([])
|
||||||
const selectedUserIds = ref<string[]>([])
|
const selectedUserIds = ref<string[]>([])
|
||||||
@@ -107,6 +110,24 @@ async function handleImportPreview() {
|
|||||||
<div class="page-rule"></div>
|
<div class="page-rule"></div>
|
||||||
</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 -->
|
<!-- Manual Remind -->
|
||||||
<el-card class="setting-card">
|
<el-card class="setting-card">
|
||||||
<template #header>
|
<template #header>
|
||||||
@@ -206,7 +227,7 @@ async function handleImportPreview() {
|
|||||||
<el-button type="success" :loading="importLoading" @click="handleImportPreview" :disabled="!importFile">预览并导入</el-button>
|
<el-button type="success" :loading="importLoading" @click="handleImportPreview" :disabled="!importFile">预览并导入</el-button>
|
||||||
</el-form>
|
</el-form>
|
||||||
<div v-if="importPreview" style="margin-top:16px">
|
<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 :data="Object.entries(importPreview)" stripe>
|
||||||
<el-table-column prop="0" label="Sheet" />
|
<el-table-column prop="0" label="Sheet" />
|
||||||
<el-table-column prop="1.row_count" label="数据行数" />
|
<el-table-column prop="1.row_count" label="数据行数" />
|
||||||
@@ -249,7 +270,7 @@ async function handleImportPreview() {
|
|||||||
.page-head { margin-bottom: 24px; }
|
.page-head { margin-bottom: 24px; }
|
||||||
.page-title {
|
.page-title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: 'ZCOOL XiaoWei', STSong, serif;
|
font-family: var(--font-heading);
|
||||||
font-size: 22px; font-weight: 400;
|
font-size: 22px; font-weight: 400;
|
||||||
color: var(--ink); letter-spacing: 0.06em;
|
color: var(--ink); letter-spacing: 0.06em;
|
||||||
}
|
}
|
||||||
@@ -258,10 +279,18 @@ async function handleImportPreview() {
|
|||||||
.setting-card { margin-bottom: 16px; }
|
.setting-card { margin-bottom: 16px; }
|
||||||
.card-header-title {
|
.card-header-title {
|
||||||
display: flex; align-items: center;
|
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;
|
font-size: 15px; color: var(--ink); letter-spacing: 0.05em;
|
||||||
}
|
}
|
||||||
.setting-row { display: flex; gap: 32px; align-items: flex-start; flex-wrap: wrap; }
|
.setting-row { display: flex; gap: 32px; align-items: flex-start; flex-wrap: wrap; }
|
||||||
.setting-col { display: flex; flex-direction: column; gap: 8px; }
|
.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>
|
</style>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const editUser = ref<any>(null)
|
|||||||
const editRole = ref('')
|
const editRole = ref('')
|
||||||
const editDepartment = ref('')
|
const editDepartment = ref('')
|
||||||
const editWecomId = ref('')
|
const editWecomId = ref('')
|
||||||
|
const editColor = ref('')
|
||||||
const editRequireReport = ref(true)
|
const editRequireReport = ref(true)
|
||||||
|
|
||||||
const roleOptions = [
|
const roleOptions = [
|
||||||
@@ -44,13 +45,14 @@ function openEdit(user: any) {
|
|||||||
editDepartment.value = user.department || ''
|
editDepartment.value = user.department || ''
|
||||||
editWecomId.value = user.wecom_userid || ''
|
editWecomId.value = user.wecom_userid || ''
|
||||||
editRequireReport.value = user.require_report !== false // default true
|
editRequireReport.value = user.require_report !== false // default true
|
||||||
|
editColor.value = user.color || ''
|
||||||
editDialogVisible.value = true
|
editDialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
if (!editUser.value) return
|
if (!editUser.value) return
|
||||||
try {
|
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)
|
await api.put(`/users/${editUser.value.id}/role`, payload)
|
||||||
const newWecomId = editWecomId.value.trim()
|
const newWecomId = editWecomId.value.trim()
|
||||||
if (newWecomId !== (editUser.value.wecom_userid || '')) {
|
if (newWecomId !== (editUser.value.wecom_userid || '')) {
|
||||||
@@ -127,6 +129,14 @@ async function handleDelete(row: any) {
|
|||||||
<el-switch v-model="editRequireReport" active-text="是" inactive-text="否" />
|
<el-switch v-model="editRequireReport" active-text="是" inactive-text="否" />
|
||||||
<div class="form-hint">关闭后该用户不计入填报统计,不接收催办提醒</div>
|
<div class="form-hint">关闭后该用户不计入填报统计,不接收催办提醒</div>
|
||||||
</el-form-item>
|
</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>
|
</el-form>
|
||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -144,12 +154,12 @@ async function handleDelete(row: any) {
|
|||||||
.page-head { margin-bottom: 20px; }
|
.page-head { margin-bottom: 20px; }
|
||||||
.page-title {
|
.page-title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: 'ZCOOL XiaoWei', STSong, serif;
|
font-family: var(--font-heading);
|
||||||
font-size: 22px; font-weight: 400;
|
font-size: 22px; font-weight: 400;
|
||||||
color: var(--ink); letter-spacing: 0.06em;
|
color: var(--ink); letter-spacing: 0.06em;
|
||||||
}
|
}
|
||||||
.page-rule { width: 32px; height: 3px; background: var(--gold); margin-top: 12px; }
|
.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; }
|
.form-hint { font-size: 12px; color: var(--c-text-muted); margin-top: 4px; }
|
||||||
.tag-yes { font-size: 12px; color: var(--sage); }
|
.tag-yes { font-size: 12px; color: var(--sage); }
|
||||||
.tag-no { font-size: 12px; color: var(--c-text-muted); }
|
.tag-no { font-size: 12px; color: var(--c-text-muted); }
|
||||||
|
|||||||
@@ -392,7 +392,7 @@ const notesByDate = computed(() => {
|
|||||||
.page-head-row { display: flex; justify-content: space-between; align-items: flex-start; }
|
.page-head-row { display: flex; justify-content: space-between; align-items: flex-start; }
|
||||||
.page-title {
|
.page-title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: 'ZCOOL XiaoWei', STSong, serif;
|
font-family: var(--font-heading);
|
||||||
font-size: 22px; font-weight: 400;
|
font-size: 22px; font-weight: 400;
|
||||||
color: var(--ink); letter-spacing: 0.06em;
|
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 { 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:hover:not(:disabled) { color: var(--ink); border-color: var(--ink); }
|
||||||
.week-nav-btn:disabled { opacity: 0.3; cursor: not-allowed; }
|
.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 { 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; }
|
.week-nav-reset:hover { background: var(--gold); color: #fff; }
|
||||||
|
|
||||||
@@ -411,13 +411,13 @@ const notesByDate = computed(() => {
|
|||||||
.date-title {
|
.date-title {
|
||||||
display: flex; align-items: center;
|
display: flex; align-items: center;
|
||||||
margin: 12px 0 8px;
|
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;
|
font-size: 14px; color: var(--ink); letter-spacing: 0.04em;
|
||||||
}
|
}
|
||||||
.photo-cell { display: flex; gap: 4px; }
|
.photo-cell { display: flex; gap: 4px; }
|
||||||
.mini-thumb { width: 36px; height: 36px; object-fit: cover; cursor: pointer; }
|
.mini-thumb { width: 36px; height: 36px; object-fit: cover; cursor: pointer; }
|
||||||
.edit-indicator { font-size: 12px; margin-left: 3px; opacity: 0.5; cursor: help; }
|
.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 Panel ═══ */
|
||||||
.ai-summary-card {
|
.ai-summary-card {
|
||||||
@@ -441,11 +441,11 @@ const notesByDate = computed(() => {
|
|||||||
display: flex; align-items: center;
|
display: flex; align-items: center;
|
||||||
}
|
}
|
||||||
.ai-header-title {
|
.ai-header-title {
|
||||||
font-family: 'ZCOOL XiaoWei', STSong, serif;
|
font-family: var(--font-heading);
|
||||||
font-size: 15px; color: var(--ink); letter-spacing: 0.05em;
|
font-size: 15px; color: var(--ink); letter-spacing: 0.05em;
|
||||||
}
|
}
|
||||||
.ai-header-hint {
|
.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;
|
font-size: 11px; color: var(--warm-gray); margin-left: 10px;
|
||||||
}
|
}
|
||||||
.ai-header-actions {
|
.ai-header-actions {
|
||||||
@@ -455,7 +455,7 @@ const notesByDate = computed(() => {
|
|||||||
text-align: center; padding: 32px 0;
|
text-align: center; padding: 32px 0;
|
||||||
}
|
}
|
||||||
.ai-loading-text {
|
.ai-loading-text {
|
||||||
font-family: 'ZCOOL XiaoWei', STSong, serif;
|
font-family: var(--font-heading);
|
||||||
font-size: 15px; color: var(--warm-gray);
|
font-size: 15px; color: var(--warm-gray);
|
||||||
animation: pulse-text 1.8s ease-in-out infinite;
|
animation: pulse-text 1.8s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
@@ -465,22 +465,22 @@ const notesByDate = computed(() => {
|
|||||||
}
|
}
|
||||||
.ai-error {
|
.ai-error {
|
||||||
color: var(--vermilion);
|
color: var(--vermilion);
|
||||||
font-family: 'Noto Serif SC', STSong, serif;
|
font-family: var(--font-body);
|
||||||
padding: 12px 0;
|
padding: 12px 0;
|
||||||
}
|
}
|
||||||
.ai-content {
|
.ai-content {
|
||||||
font-family: 'Noto Serif SC', STSong, serif;
|
font-family: var(--font-body);
|
||||||
line-height: 1.85;
|
line-height: 1.85;
|
||||||
color: var(--c-text);
|
color: var(--c-text);
|
||||||
}
|
}
|
||||||
.ai-content :deep(.ai-h3) {
|
.ai-content :deep(.ai-h3) {
|
||||||
font-family: 'ZCOOL XiaoWei', STSong, serif;
|
font-family: var(--font-heading);
|
||||||
font-size: 16px; color: var(--ink);
|
font-size: 16px; color: var(--ink);
|
||||||
margin: 16px 0 8px; letter-spacing: 0.04em;
|
margin: 16px 0 8px; letter-spacing: 0.04em;
|
||||||
border-left: 3px solid var(--gold); padding-left: 10px;
|
border-left: 3px solid var(--gold); padding-left: 10px;
|
||||||
}
|
}
|
||||||
.ai-content :deep(.ai-h4) {
|
.ai-content :deep(.ai-h4) {
|
||||||
font-family: 'ZCOOL XiaoWei', STSong, serif;
|
font-family: var(--font-heading);
|
||||||
font-size: 14px; color: var(--ink);
|
font-size: 14px; color: var(--ink);
|
||||||
margin: 12px 0 6px; letter-spacing: 0.03em;
|
margin: 12px 0 6px; letter-spacing: 0.03em;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import { useThemeStore } from '@/stores/theme'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { todayStr } from '@/utils'
|
import { todayStr } from '@/utils'
|
||||||
|
import { getMgrStyle, loadManagerColors } from '@/utils/managerColor'
|
||||||
import api from '@/api/index'
|
import api from '@/api/index'
|
||||||
import EditLogPanel from '@/components/EditLogPanel.vue'
|
import EditLogPanel from '@/components/EditLogPanel.vue'
|
||||||
|
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
|
const themeStore = useThemeStore()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const workPlans = ref<any[]>([])
|
const workPlans = ref<any[]>([])
|
||||||
const customers = ref<any[]>([])
|
const customers = ref<any[]>([])
|
||||||
@@ -17,12 +20,7 @@ const form = ref<any>({})
|
|||||||
const statusPick = ref<Record<string, string>>({})
|
const statusPick = ref<Record<string, string>>({})
|
||||||
const planStatuses = ['计划中', '已完成', '已取消']
|
const planStatuses = ['计划中', '已完成', '已取消']
|
||||||
|
|
||||||
const mgrColors = ['#1C3738','#4A6741','#5B7FA5','#7B7568','#8B6F47','#6B5B4F','#3D5A5C','#5C4A3D']
|
const isLight = computed(() => themeStore.currentTheme === 'light')
|
||||||
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 managerSummary = computed(() => {
|
||||||
const map: Record<string, number> = {}
|
const map: Record<string, number> = {}
|
||||||
@@ -34,7 +32,7 @@ const managerSummary = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await Promise.all([loadPlans(), loadCustomers()])
|
await Promise.all([loadPlans(), loadCustomers(), loadManagerColors()])
|
||||||
})
|
})
|
||||||
|
|
||||||
async function loadCustomers(q?: string) {
|
async function loadCustomers(q?: string) {
|
||||||
@@ -120,7 +118,7 @@ async function quickStatusChange(row: any, newStatus: string) {
|
|||||||
<div class="page-rule"></div>
|
<div class="page-rule"></div>
|
||||||
<div v-if="managerSummary.length" class="summary-bar">
|
<div v-if="managerSummary.length" class="summary-bar">
|
||||||
<span class="summary-label">客户经理汇总</span>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -138,7 +136,7 @@ async function quickStatusChange(row: any, newStatus: string) {
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="客户经理" width="100">
|
<el-table-column label="客户经理" width="100">
|
||||||
<template #default="{ row }">
|
<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>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="plan_content" label="工作计划" min-width="280" show-overflow-tooltip />
|
<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>
|
||||||
<el-table-column label="操作" width="80" fixed="right">
|
<el-table-column label="操作" width="80" fixed="right">
|
||||||
<template #default="{ row }">
|
<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>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</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; }
|
.edit-indicator { font-size: 12px; margin-left: 3px; opacity: 0.5; cursor: help; }
|
||||||
.page-head { margin-bottom: 20px; }
|
.page-head { margin-bottom: 20px; }
|
||||||
.page-head-row { display: flex; justify-content: space-between; align-items: flex-start; }
|
.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; }
|
||||||
.page-desc { margin: 4px 0 0; font-size: 13px; color: var(--c-text-muted); font-family: 'Noto Serif SC', STSong, serif; }
|
.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; }
|
.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-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-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: #fff; white-space: nowrap; }
|
.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: #fff; 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: 'Noto Serif SC', STSong, serif; }
|
.empty { text-align: center; color: var(--c-text-muted); padding: 48px 0; font-family: var(--font-body); }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -189,13 +189,13 @@ async function handleDelete() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.form-title {
|
.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);
|
font-size: 24px; font-weight: 400; color: var(--ink);
|
||||||
letter-spacing: 0.06em; line-height: 1.3;
|
letter-spacing: 0.06em; line-height: 1.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-subtitle {
|
.form-subtitle {
|
||||||
font-family: 'JetBrains Mono', 'SF Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 9px; color: var(--gold); letter-spacing: 0.2em;
|
font-size: 9px; color: var(--gold); letter-spacing: 0.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,12 +210,12 @@ async function handleDelete() {
|
|||||||
|
|
||||||
/* ═══ Form Labels ═══ */
|
/* ═══ Form Labels ═══ */
|
||||||
.form-label {
|
.form-label {
|
||||||
font-family: 'ZCOOL XiaoWei', STSong, serif !important;
|
font-family: var(--font-heading) !important;
|
||||||
font-size: 14px !important; color: var(--ink) !important;
|
font-size: 14px !important; color: var(--ink) !important;
|
||||||
letter-spacing: 0.04em; font-weight: 500 !important;
|
letter-spacing: 0.04em; font-weight: 500 !important;
|
||||||
}
|
}
|
||||||
.form-label-hint {
|
.form-label-hint {
|
||||||
font-family: 'Noto Serif SC', STSong, serif;
|
font-family: var(--font-body);
|
||||||
font-size: 11px; color: var(--warm-gray);
|
font-size: 11px; color: var(--warm-gray);
|
||||||
letter-spacing: 0.03em; font-weight: 400; margin-left: 6px;
|
letter-spacing: 0.03em; font-weight: 400; margin-left: 6px;
|
||||||
}
|
}
|
||||||
@@ -231,7 +231,7 @@ async function handleDelete() {
|
|||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
border: 1px solid var(--warm-border);
|
border: 1px solid var(--warm-border);
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
font-family: 'Noto Serif SC', STSong, serif;
|
font-family: var(--font-body);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
letter-spacing: 0.03em;
|
letter-spacing: 0.03em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -249,7 +249,7 @@ async function handleDelete() {
|
|||||||
.submit-btn {
|
.submit-btn {
|
||||||
flex: 1; display: flex; align-items: center; justify-content: center; gap: 8px;
|
flex: 1; display: flex; align-items: center; justify-content: center; gap: 8px;
|
||||||
padding: 16px; background: var(--ink); color: #fff; border: none;
|
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;
|
font-size: 16px; letter-spacing: 0.08em; cursor: pointer; transition: all 0.25s;
|
||||||
}
|
}
|
||||||
.submit-btn:hover { background: var(--ink-light); }
|
.submit-btn:hover { background: var(--ink-light); }
|
||||||
@@ -261,7 +261,7 @@ async function handleDelete() {
|
|||||||
padding: 16px 20px;
|
padding: 16px 20px;
|
||||||
background: var(--surface); color: var(--vermilion);
|
background: var(--surface); color: var(--vermilion);
|
||||||
border: 1px solid 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;
|
font-size: 14px; letter-spacing: 0.04em; cursor: pointer; transition: all 0.25s;
|
||||||
}
|
}
|
||||||
.delete-btn:hover { background: var(--vermilion); color: #fff; }
|
.delete-btn:hover { background: var(--vermilion); color: #fff; }
|
||||||
|
|||||||
@@ -226,14 +226,14 @@ onMounted(loadToday)
|
|||||||
}
|
}
|
||||||
|
|
||||||
.stat-label-sm {
|
.stat-label-sm {
|
||||||
font-family: 'JetBrains Mono', 'SF Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
letter-spacing: 0.2em;
|
letter-spacing: 0.2em;
|
||||||
color: var(--warm-gray);
|
color: var(--warm-gray);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-date {
|
.stat-date {
|
||||||
font-family: 'Noto Serif SC', STSong, serif;
|
font-family: var(--font-body);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--warm-gray);
|
color: var(--warm-gray);
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
@@ -247,7 +247,7 @@ onMounted(loadToday)
|
|||||||
}
|
}
|
||||||
|
|
||||||
.stat-number {
|
.stat-number {
|
||||||
font-family: 'ZCOOL XiaoWei', STSong, serif;
|
font-family: var(--font-heading);
|
||||||
font-size: 56px;
|
font-size: 56px;
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
@@ -255,7 +255,7 @@ onMounted(loadToday)
|
|||||||
}
|
}
|
||||||
|
|
||||||
.stat-unit {
|
.stat-unit {
|
||||||
font-family: 'Noto Serif SC', STSong, serif;
|
font-family: var(--font-body);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: var(--warm-gray);
|
color: var(--warm-gray);
|
||||||
letter-spacing: 0.06em;
|
letter-spacing: 0.06em;
|
||||||
@@ -270,14 +270,14 @@ onMounted(loadToday)
|
|||||||
}
|
}
|
||||||
|
|
||||||
.stat-piece {
|
.stat-piece {
|
||||||
font-family: 'Noto Serif SC', STSong, serif;
|
font-family: var(--font-body);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--warm-gray);
|
color: var(--warm-gray);
|
||||||
letter-spacing: 0.03em;
|
letter-spacing: 0.03em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-piece strong {
|
.stat-piece strong {
|
||||||
font-family: 'JetBrains Mono', 'SF Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
@@ -314,7 +314,7 @@ onMounted(loadToday)
|
|||||||
padding: 16px 12px;
|
padding: 16px 12px;
|
||||||
border: 1px solid var(--warm-border);
|
border: 1px solid var(--warm-border);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-family: 'Noto Serif SC', STSong, serif;
|
font-family: var(--font-body);
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
@@ -365,7 +365,7 @@ onMounted(loadToday)
|
|||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
border: 1px solid var(--warm-border);
|
border: 1px solid var(--warm-border);
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
font-family: 'Noto Serif SC', STSong, serif;
|
font-family: var(--font-body);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
letter-spacing: 0.03em;
|
letter-spacing: 0.03em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -390,7 +390,7 @@ onMounted(loadToday)
|
|||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
font-family: 'ZCOOL XiaoWei', STSong, serif;
|
font-family: var(--font-heading);
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
letter-spacing: 0.06em;
|
letter-spacing: 0.06em;
|
||||||
@@ -459,7 +459,7 @@ onMounted(loadToday)
|
|||||||
width: 22px;
|
width: 22px;
|
||||||
height: 22px;
|
height: 22px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-family: 'ZCOOL XiaoWei', STSong, serif;
|
font-family: var(--font-heading);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
@@ -469,7 +469,7 @@ onMounted(loadToday)
|
|||||||
}
|
}
|
||||||
|
|
||||||
.record-category {
|
.record-category {
|
||||||
font-family: 'ZCOOL XiaoWei', STSong, serif;
|
font-family: var(--font-heading);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
@@ -477,7 +477,7 @@ onMounted(loadToday)
|
|||||||
}
|
}
|
||||||
|
|
||||||
.record-badge {
|
.record-badge {
|
||||||
font-family: 'Noto Serif SC', STSong, serif;
|
font-family: var(--font-body);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
padding: 1px 8px;
|
padding: 1px 8px;
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
@@ -491,7 +491,7 @@ onMounted(loadToday)
|
|||||||
|
|
||||||
|
|
||||||
.record-customer {
|
.record-customer {
|
||||||
font-family: 'ZCOOL XiaoWei', STSong, serif;
|
font-family: var(--font-heading);
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
letter-spacing: 0.03em;
|
letter-spacing: 0.03em;
|
||||||
@@ -499,7 +499,7 @@ onMounted(loadToday)
|
|||||||
}
|
}
|
||||||
|
|
||||||
.record-time {
|
.record-time {
|
||||||
font-family: 'JetBrains Mono', 'SF Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--warm-gray);
|
color: var(--warm-gray);
|
||||||
letter-spacing: 0.03em;
|
letter-spacing: 0.03em;
|
||||||
@@ -507,7 +507,7 @@ onMounted(loadToday)
|
|||||||
}
|
}
|
||||||
|
|
||||||
.record-content {
|
.record-content {
|
||||||
font-family: 'Noto Serif SC', STSong, serif;
|
font-family: var(--font-body);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: var(--c-text);
|
color: var(--c-text);
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
@@ -516,7 +516,7 @@ onMounted(loadToday)
|
|||||||
}
|
}
|
||||||
|
|
||||||
.record-demand {
|
.record-demand {
|
||||||
font-family: 'Noto Serif SC', STSong, serif;
|
font-family: var(--font-body);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--vermilion);
|
color: var(--vermilion);
|
||||||
margin: 0 0 8px;
|
margin: 0 0 8px;
|
||||||
@@ -559,7 +559,7 @@ onMounted(loadToday)
|
|||||||
}
|
}
|
||||||
|
|
||||||
.empty-glyph {
|
.empty-glyph {
|
||||||
font-family: 'ZCOOL XiaoWei', STSong, serif;
|
font-family: var(--font-heading);
|
||||||
font-size: 40px;
|
font-size: 40px;
|
||||||
color: var(--gold);
|
color: var(--gold);
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
@@ -567,14 +567,14 @@ onMounted(loadToday)
|
|||||||
}
|
}
|
||||||
|
|
||||||
.empty-text {
|
.empty-text {
|
||||||
font-family: 'Noto Serif SC', STSong, serif;
|
font-family: var(--font-body);
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
color: var(--warm-gray);
|
color: var(--warm-gray);
|
||||||
margin: 0 0 4px;
|
margin: 0 0 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-hint {
|
.empty-hint {
|
||||||
font-family: 'Noto Serif SC', STSong, serif;
|
font-family: var(--font-body);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--c-text-muted);
|
color: var(--c-text-muted);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
@@ -169,13 +169,13 @@ async function handleSubmit() {
|
|||||||
.form-title-group { display: flex; flex-direction: column; gap: 0; flex: 1; }
|
.form-title-group { display: flex; flex-direction: column; gap: 0; flex: 1; }
|
||||||
|
|
||||||
.form-title {
|
.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);
|
font-size: 24px; font-weight: 400; color: var(--ink);
|
||||||
letter-spacing: 0.06em; line-height: 1.3;
|
letter-spacing: 0.06em; line-height: 1.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-subtitle {
|
.form-subtitle {
|
||||||
font-family: 'JetBrains Mono', 'SF Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 9px; color: var(--gold); letter-spacing: 0.2em;
|
font-size: 9px; color: var(--gold); letter-spacing: 0.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,7 +189,7 @@ async function handleSubmit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.form-label {
|
.form-label {
|
||||||
font-family: 'ZCOOL XiaoWei', STSong, serif !important;
|
font-family: var(--font-heading) !important;
|
||||||
font-size: 14px !important; color: var(--ink) !important;
|
font-size: 14px !important; color: var(--ink) !important;
|
||||||
letter-spacing: 0.04em; font-weight: 500 !important;
|
letter-spacing: 0.04em; font-weight: 500 !important;
|
||||||
}
|
}
|
||||||
@@ -204,7 +204,7 @@ async function handleSubmit() {
|
|||||||
padding: 10px 8px;
|
padding: 10px 8px;
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
border: 1px solid var(--warm-border);
|
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;
|
font-size: 13px; letter-spacing: 0.04em;
|
||||||
cursor: pointer; transition: all 0.25s;
|
cursor: pointer; transition: all 0.25s;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -216,7 +216,7 @@ async function handleSubmit() {
|
|||||||
.submit-btn {
|
.submit-btn {
|
||||||
width: 100%; display: flex; align-items: center; justify-content: center; gap: 8px;
|
width: 100%; display: flex; align-items: center; justify-content: center; gap: 8px;
|
||||||
padding: 16px; background: var(--ink); color: #fff; border: none;
|
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;
|
font-size: 16px; letter-spacing: 0.08em; cursor: pointer; transition: all 0.25s;
|
||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,13 +122,13 @@ async function handleSubmit() {
|
|||||||
.form-title-group { display: flex; flex-direction: column; gap: 0; flex: 1; }
|
.form-title-group { display: flex; flex-direction: column; gap: 0; flex: 1; }
|
||||||
|
|
||||||
.form-title {
|
.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);
|
font-size: 24px; font-weight: 400; color: var(--ink);
|
||||||
letter-spacing: 0.06em; line-height: 1.3;
|
letter-spacing: 0.06em; line-height: 1.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-subtitle {
|
.form-subtitle {
|
||||||
font-family: 'JetBrains Mono', 'SF Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 9px; color: var(--gold); letter-spacing: 0.2em;
|
font-size: 9px; color: var(--gold); letter-spacing: 0.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +142,7 @@ async function handleSubmit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.form-label {
|
.form-label {
|
||||||
font-family: 'ZCOOL XiaoWei', STSong, serif !important;
|
font-family: var(--font-heading) !important;
|
||||||
font-size: 14px !important; color: var(--ink) !important;
|
font-size: 14px !important; color: var(--ink) !important;
|
||||||
letter-spacing: 0.04em; font-weight: 500 !important;
|
letter-spacing: 0.04em; font-weight: 500 !important;
|
||||||
}
|
}
|
||||||
@@ -152,7 +152,7 @@ async function handleSubmit() {
|
|||||||
.submit-btn {
|
.submit-btn {
|
||||||
width: 100%; display: flex; align-items: center; justify-content: center; gap: 8px;
|
width: 100%; display: flex; align-items: center; justify-content: center; gap: 8px;
|
||||||
padding: 16px; background: var(--ink); color: #fff; border: none;
|
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;
|
font-size: 16px; letter-spacing: 0.08em; cursor: pointer; transition: all 0.25s;
|
||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -413,7 +413,7 @@ async function handleDelete() {
|
|||||||
|
|
||||||
.form-title {
|
.form-title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: 'ZCOOL XiaoWei', STSong, serif;
|
font-family: var(--font-heading);
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
@@ -422,7 +422,7 @@ async function handleDelete() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.form-subtitle {
|
.form-subtitle {
|
||||||
font-family: 'JetBrains Mono', 'SF Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
color: var(--gold);
|
color: var(--gold);
|
||||||
letter-spacing: 0.2em;
|
letter-spacing: 0.2em;
|
||||||
@@ -448,7 +448,7 @@ async function handleDelete() {
|
|||||||
|
|
||||||
/* ═══ Form Labels ═══ */
|
/* ═══ Form Labels ═══ */
|
||||||
.form-label {
|
.form-label {
|
||||||
font-family: 'ZCOOL XiaoWei', STSong, serif !important;
|
font-family: var(--font-heading) !important;
|
||||||
font-size: 14px !important;
|
font-size: 14px !important;
|
||||||
color: var(--ink) !important;
|
color: var(--ink) !important;
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
@@ -456,7 +456,7 @@ async function handleDelete() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.form-label-hint {
|
.form-label-hint {
|
||||||
font-family: 'Noto Serif SC', STSong, serif;
|
font-family: var(--font-body);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--warm-gray);
|
color: var(--warm-gray);
|
||||||
letter-spacing: 0.03em;
|
letter-spacing: 0.03em;
|
||||||
@@ -478,7 +478,7 @@ async function handleDelete() {
|
|||||||
border: 1px dashed var(--gold);
|
border: 1px dashed var(--gold);
|
||||||
padding: 8px 14px;
|
padding: 8px 14px;
|
||||||
color: var(--gold-dark);
|
color: var(--gold-dark);
|
||||||
font-family: 'Noto Serif SC', STSong, serif;
|
font-family: var(--font-body);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
@@ -517,7 +517,7 @@ async function handleDelete() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.photo-label {
|
.photo-label {
|
||||||
font-family: 'JetBrains Mono', 'SF Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--warm-gray);
|
color: var(--warm-gray);
|
||||||
letter-spacing: 0.03em;
|
letter-spacing: 0.03em;
|
||||||
@@ -545,7 +545,7 @@ async function handleDelete() {
|
|||||||
padding: 12px 18px;
|
padding: 12px 18px;
|
||||||
border: 1px dashed var(--warm-border);
|
border: 1px dashed var(--warm-border);
|
||||||
color: var(--warm-gray);
|
color: var(--warm-gray);
|
||||||
font-family: 'Noto Serif SC', STSong, serif;
|
font-family: var(--font-body);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
letter-spacing: 0.03em;
|
letter-spacing: 0.03em;
|
||||||
@@ -579,7 +579,7 @@ async function handleDelete() {
|
|||||||
background: var(--ink);
|
background: var(--ink);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border: none;
|
border: none;
|
||||||
font-family: 'ZCOOL XiaoWei', STSong, serif;
|
font-family: var(--font-heading);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.08em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -598,7 +598,7 @@ async function handleDelete() {
|
|||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
color: var(--vermilion);
|
color: var(--vermilion);
|
||||||
border: 1px solid var(--vermilion);
|
border: 1px solid var(--vermilion);
|
||||||
font-family: 'Noto Serif SC', STSong, serif;
|
font-family: var(--font-body);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -629,7 +629,7 @@ async function handleDelete() {
|
|||||||
padding: 10px 8px;
|
padding: 10px 8px;
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
border: 1px solid var(--warm-border);
|
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;
|
font-size: 13px; letter-spacing: 0.04em;
|
||||||
cursor: pointer; transition: all 0.25s;
|
cursor: pointer; transition: all 0.25s;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -102,13 +102,13 @@ async function handleSubmit() {
|
|||||||
.form-title-group { display: flex; flex-direction: column; gap: 0; flex: 1; }
|
.form-title-group { display: flex; flex-direction: column; gap: 0; flex: 1; }
|
||||||
|
|
||||||
.form-title {
|
.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);
|
font-size: 24px; font-weight: 400; color: var(--ink);
|
||||||
letter-spacing: 0.06em; line-height: 1.3;
|
letter-spacing: 0.06em; line-height: 1.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-subtitle {
|
.form-subtitle {
|
||||||
font-family: 'JetBrains Mono', 'SF Mono', monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 9px; color: var(--gold); letter-spacing: 0.2em;
|
font-size: 9px; color: var(--gold); letter-spacing: 0.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@ async function handleSubmit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.form-label {
|
.form-label {
|
||||||
font-family: 'ZCOOL XiaoWei', STSong, serif !important;
|
font-family: var(--font-heading) !important;
|
||||||
font-size: 14px !important; color: var(--ink) !important;
|
font-size: 14px !important; color: var(--ink) !important;
|
||||||
letter-spacing: 0.04em; font-weight: 500 !important;
|
letter-spacing: 0.04em; font-weight: 500 !important;
|
||||||
}
|
}
|
||||||
@@ -132,7 +132,7 @@ async function handleSubmit() {
|
|||||||
.submit-btn {
|
.submit-btn {
|
||||||
width: 100%; display: flex; align-items: center; justify-content: center; gap: 8px;
|
width: 100%; display: flex; align-items: center; justify-content: center; gap: 8px;
|
||||||
padding: 16px; background: var(--ink); color: #fff; border: none;
|
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;
|
font-size: 16px; letter-spacing: 0.08em; cursor: pointer; transition: all 0.25s;
|
||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,15 @@
|
|||||||
"name": "ui-design-guide",
|
"name": "ui-design-guide",
|
||||||
"zip_url": "https://api.skillhub.cn/api/v1/download?slug=ui-design-guide",
|
"zip_url": "https://api.skillhub.cn/api/v1/download?slug=ui-design-guide",
|
||||||
"source": "community",
|
"source": "community",
|
||||||
"version": "2.24.0",
|
"version": "2.24.2-beta.1",
|
||||||
"installedAt": "2026-06-23T00:18:23Z"
|
"installedAt": "2026-07-06T04:51:11Z"
|
||||||
|
},
|
||||||
|
"ui-new": {
|
||||||
|
"name": "UI设计Skill",
|
||||||
|
"zip_url": "https://api.skillhub.cn/api/v1/download?slug=ui-new",
|
||||||
|
"source": "community",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"installedAt": "2026-07-06T06:39:42Z"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: ui-design-guide
|
name: ui-design-guide
|
||||||
description: Use when users need visual direction, interface hierarchy, layout decisions, design specifications, or prototypes before implementing a Web or mini program UI.
|
description: Use when users need visual direction, interface hierarchy, layout decisions, design specifications, or prototypes before implementing a Web or mini program UI.
|
||||||
version: 2.23.0
|
version: 2.23.1
|
||||||
alwaysApply: false
|
alwaysApply: false
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"ownerId": "544116",
|
"ownerId": "544116",
|
||||||
"slug": "ui-design-guide",
|
"slug": "ui-design-guide",
|
||||||
"version": "2.24.0",
|
"version": "2.24.2-beta.1",
|
||||||
"publishedAt": 1781845782468
|
"publishedAt": 1782199074089
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,386 @@
|
|||||||
|
---
|
||||||
|
name: ui-design-system
|
||||||
|
description: 专业UI生成与现有界面优化技能。当用户请求生成UI界面、设计页面、优化现有界面、创建组件库、设计系统、App界面、Web界面、Dashboard、Landing Page、或任何涉及视觉交互设计时必须触发此技能。参考Google Material Design、Apple HIG、以及Pinterest级别审美标准,输出大气简约、高级美学的UI代码。即使用户只是说"帮我做个界面"、"优化一下UI"、"设计一个页面"也必须触发。
|
||||||
|
---
|
||||||
|
|
||||||
|
# UI Design System Skill
|
||||||
|
|
||||||
|
## 📋 背景与设计哲学
|
||||||
|
|
||||||
|
### 核心定位
|
||||||
|
本 Skill 专注于生成**大厂级别、Pinterest 审美、高级简约**的 UI 界面。参考标准:
|
||||||
|
- **Google Material Design 3** — 色彩系统、动效原则、组件规范
|
||||||
|
- **Apple Human Interface Guidelines** — 空间设计、排版节奏、细腻质感
|
||||||
|
- **Fluent Design (Microsoft)** — Acrylic 材质、深度层次
|
||||||
|
- **Pinterest / Dribbble 高赞设计** — 视觉张力、留白美学、色彩大胆
|
||||||
|
|
||||||
|
### 设计价值观
|
||||||
|
> "少即是多,但每一处都要有意图"
|
||||||
|
- **克制的奢华**:不是堆砌,而是精准取舍
|
||||||
|
- **系统性思维**:每个组件都来自同一设计语言
|
||||||
|
- **情绪传达**:界面有温度,有性格,不是功能机器
|
||||||
|
- **细节决定品质**:阴影、圆角、间距都是设计语言
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 触发后的工作流程
|
||||||
|
|
||||||
|
### Step 1: 需求分析(必须先做)
|
||||||
|
|
||||||
|
分析以下维度,在回复前先梳理清楚:
|
||||||
|
|
||||||
|
```
|
||||||
|
用途分类:
|
||||||
|
├── 生成新UI → 执行「UI Generation Protocol」
|
||||||
|
├── 优化现有UI → 执行「UI Audit & Optimization Protocol」
|
||||||
|
└── 组件设计 → 执行「Component Design Protocol」
|
||||||
|
|
||||||
|
产品类型:
|
||||||
|
├── Mobile App (iOS/Android 风格)
|
||||||
|
├── Web App / Dashboard
|
||||||
|
├── Landing Page / Marketing
|
||||||
|
├── Admin / B端系统
|
||||||
|
└── 创意/作品集页面
|
||||||
|
```
|
||||||
|
|
||||||
|
**必须明确的信息(缺失时主动询问或合理假设并说明):**
|
||||||
|
- 产品定位(面向谁?解决什么问题?)
|
||||||
|
- 品牌调性(科技感/温暖/奢华/年轻/专业?)
|
||||||
|
- 主色倾向(有偏好色系,或给我自由发挥)
|
||||||
|
- 交付格式(HTML/CSS、React/JSX、SVG 原型图)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 色彩系统参考体系
|
||||||
|
|
||||||
|
### 参考「谷歌 Material You」调色板生成规则
|
||||||
|
|
||||||
|
**Tonal Palette 结构:**
|
||||||
|
```
|
||||||
|
Primary → 品牌主色,用于关键操作和焦点元素
|
||||||
|
Secondary → 辅助色,用于标签、图标、次要按钮
|
||||||
|
Tertiary → 对比色,用于强调差异化内容
|
||||||
|
Neutral → 灰阶体系,背景/表面/文字层级
|
||||||
|
Error → 错误/警告状态色
|
||||||
|
```
|
||||||
|
|
||||||
|
**推荐高级色板(可直接使用):**
|
||||||
|
|
||||||
|
| 风格 | Primary | Secondary | Accent | Surface |
|
||||||
|
|------|---------|-----------|--------|---------|
|
||||||
|
| 极简白 | #1A1A2E | #16213E | #E94560 | #F8F9FA |
|
||||||
|
| 深空科技 | #6C63FF | #3ECFCF | #FF6584 | #0D0D1A |
|
||||||
|
| 自然有机 | #2D6A4F | #95D5B2 | #F4A261 | #FEFDF8 |
|
||||||
|
| 奢华金融 | #1C1C28 | #8B7355 | #C9A84C | #F5F3EF |
|
||||||
|
| 现代橙红 | #FF4B2B | #FF416C | #FFA07A | #1A1A1A |
|
||||||
|
| 苹果式灰 | #007AFF | #34C759 | #FF9500 | #F2F2F7 |
|
||||||
|
| 薰衣草紫 | #7B61FF | #B693FD | #4DC9D9 | #FAFAFF |
|
||||||
|
| 珊瑚暖调 | #FF6B6B | #FFE66D | #4ECDC4 | #FFFAF0 |
|
||||||
|
|
||||||
|
### Apple HIG 间距系统
|
||||||
|
```css
|
||||||
|
/* 参考 Apple 8pt 网格系统 */
|
||||||
|
--space-1: 4px;
|
||||||
|
--space-2: 8px;
|
||||||
|
--space-3: 12px;
|
||||||
|
--space-4: 16px; /* 标准间距 */
|
||||||
|
--space-5: 20px;
|
||||||
|
--space-6: 24px; /* 组件内间距 */
|
||||||
|
--space-8: 32px; /* 区块间距 */
|
||||||
|
--space-10: 40px;
|
||||||
|
--space-12: 48px; /* 大区块间距 */
|
||||||
|
--space-16: 64px; /* 页面级间距 */
|
||||||
|
--space-24: 96px;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📐 排版系统
|
||||||
|
|
||||||
|
### 字体层级(参考 Apple SF / Google 字体)
|
||||||
|
```css
|
||||||
|
/* 优先使用有个性的字体组合 */
|
||||||
|
|
||||||
|
/* 选项 A: 科技感 */
|
||||||
|
--font-display: 'Space Grotesk', 'DM Sans';
|
||||||
|
--font-body: 'Inter', system-ui;
|
||||||
|
|
||||||
|
/* 选项 B: 人文优雅 */
|
||||||
|
--font-display: 'Playfair Display', 'Cormorant';
|
||||||
|
--font-body: 'Source Serif 4', Georgia;
|
||||||
|
|
||||||
|
/* 选项 C: 现代极简 */
|
||||||
|
--font-display: 'Syne', 'Outfit';
|
||||||
|
--font-body: 'Manrope', sans-serif;
|
||||||
|
|
||||||
|
/* 选项 D: 中文友好 */
|
||||||
|
--font-display: 'Noto Serif SC', serif;
|
||||||
|
--font-body: 'Noto Sans SC', sans-serif;
|
||||||
|
|
||||||
|
/* Type Scale (参考 Material 3) */
|
||||||
|
--type-display-large: clamp(40px, 5vw, 57px) / 1.12;
|
||||||
|
--type-display-medium: clamp(32px, 4vw, 45px) / 1.16;
|
||||||
|
--type-headline-large: clamp(24px, 3vw, 32px) / 1.25;
|
||||||
|
--type-headline-medium: clamp(20px, 2.5vw, 28px) / 1.29;
|
||||||
|
--type-title-large: 22px / 1.27;
|
||||||
|
--type-title-medium: 16px / 1.5;
|
||||||
|
--type-body-large: 16px / 1.5;
|
||||||
|
--type-body-medium: 14px / 1.43;
|
||||||
|
--type-label-large: 14px / 1.43;
|
||||||
|
--type-label-small: 11px / 1.45;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧩 组件设计规范
|
||||||
|
|
||||||
|
### 卡片组件(Card)
|
||||||
|
```css
|
||||||
|
/* 参考 Material 3 Card Variants */
|
||||||
|
.card-elevated {
|
||||||
|
background: var(--surface);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0px 1px 2px rgba(0,0,0,.06),
|
||||||
|
0px 4px 16px rgba(0,0,0,.08);
|
||||||
|
transition: box-shadow 200ms ease, transform 200ms ease;
|
||||||
|
}
|
||||||
|
.card-elevated:hover {
|
||||||
|
box-shadow: 0px 4px 8px rgba(0,0,0,.08),
|
||||||
|
0px 12px 32px rgba(0,0,0,.12);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-filled {
|
||||||
|
background: var(--surface-variant);
|
||||||
|
border-radius: 12px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-outlined {
|
||||||
|
background: var(--surface);
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--outline-variant);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 按钮系统(Button)
|
||||||
|
```css
|
||||||
|
/* Apple + Material 融合规范 */
|
||||||
|
.btn-primary {
|
||||||
|
height: 44px; /* Apple HIG 最小触控尺寸 */
|
||||||
|
padding: 0 24px;
|
||||||
|
border-radius: 10px; /* Apple 风格圆角 */
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 15px;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
transition: all 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮层级: Filled > Filled Tonal > Outlined > Text > Icon */
|
||||||
|
|
||||||
|
.btn-primary:hover { filter: brightness(1.08); transform: translateY(-1px); }
|
||||||
|
.btn-primary:active { transform: translateY(0); filter: brightness(0.95); }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 输入框(Input)
|
||||||
|
```css
|
||||||
|
/* Material 3 风格输入框 */
|
||||||
|
.input-outlined {
|
||||||
|
height: 56px;
|
||||||
|
border: 1.5px solid var(--outline);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 16px;
|
||||||
|
transition: border-color 200ms;
|
||||||
|
}
|
||||||
|
.input-outlined:focus {
|
||||||
|
border-color: var(--primary);
|
||||||
|
border-width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Apple 风格输入框 */
|
||||||
|
.input-apple {
|
||||||
|
height: 44px;
|
||||||
|
background: rgba(120,120,128,0.12);
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ 动效与交互规范
|
||||||
|
|
||||||
|
### 动效曲线(参考 Apple Spring 和 Material Motion)
|
||||||
|
```css
|
||||||
|
/* Apple 弹性曲线 */
|
||||||
|
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
/* Material 标准曲线 */
|
||||||
|
--ease-standard: cubic-bezier(0.2, 0, 0, 1);
|
||||||
|
/* 强调曲线 */
|
||||||
|
--ease-emphasized: cubic-bezier(0.2, 0, 0, 1);
|
||||||
|
/* 快入慢出 */
|
||||||
|
--ease-decelerate: cubic-bezier(0, 0, 0.2, 1);
|
||||||
|
/* 慢入快出 */
|
||||||
|
--ease-accelerate: cubic-bezier(0.3, 0, 1, 1);
|
||||||
|
|
||||||
|
/* 时长规范 */
|
||||||
|
--duration-short1: 50ms;
|
||||||
|
--duration-short2: 100ms;
|
||||||
|
--duration-short3: 150ms;
|
||||||
|
--duration-short4: 200ms; /* 微交互 */
|
||||||
|
--duration-medium1: 250ms;
|
||||||
|
--duration-medium2: 300ms; /* 标准转场 */
|
||||||
|
--duration-long1: 350ms;
|
||||||
|
--duration-long2: 400ms; /* 复杂动画 */
|
||||||
|
--duration-extra-long: 600ms+; /* 页面级切换 */
|
||||||
|
```
|
||||||
|
|
||||||
|
### 必须实现的微交互
|
||||||
|
- **Hover 状态**:背景色变化 + 轻微位移
|
||||||
|
- **Active 状态**:轻压效果 (scale 0.97)
|
||||||
|
- **Focus 状态**:清晰的焦点环(无障碍必须)
|
||||||
|
- **Loading 状态**:骨架屏或脉冲动画
|
||||||
|
- **页面加载**:元素交错淡入 (stagger delay)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ UI Generation Protocol(生成新 UI)
|
||||||
|
|
||||||
|
### 执行顺序
|
||||||
|
1. **定义设计 DNA** — 确定调性、主色、字体组合
|
||||||
|
2. **建立 CSS 变量体系** — 完整的 token 系统
|
||||||
|
3. **搭建布局骨架** — Grid/Flex 响应式结构
|
||||||
|
4. **实现核心组件** — 从最重要的交互元素开始
|
||||||
|
5. **添加视觉质感** — 渐变、阴影、纹理、背景
|
||||||
|
6. **注入微交互** — 动效、过渡、Hover 状态
|
||||||
|
7. **精调细节** — 间距、对齐、字重、行高
|
||||||
|
8. **响应式适配** — Mobile First
|
||||||
|
|
||||||
|
### 代码规范
|
||||||
|
```html
|
||||||
|
<!-- 必须包含的 meta -->
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
|
||||||
|
<!-- 结构原则 -->
|
||||||
|
<!-- 1. CSS Variables 在 :root 统一定义 -->
|
||||||
|
<!-- 2. 组件用 BEM 或语义化命名 -->
|
||||||
|
<!-- 3. 响应式用 clamp() 而非 media query 断点堆叠 -->
|
||||||
|
<!-- 4. 动画性能优先用 transform/opacity -->
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 UI Audit & Optimization Protocol(优化现有 UI)
|
||||||
|
|
||||||
|
### 分析维度(拿到现有 UI 后,必须从这 6 个维度审查)
|
||||||
|
|
||||||
|
**1. 视觉层级**
|
||||||
|
- [ ] 是否有清晰的信息优先级?
|
||||||
|
- [ ] 用户视线路径是否顺畅?
|
||||||
|
- [ ] 重要信息是否足够突出?
|
||||||
|
|
||||||
|
**2. 色彩系统**
|
||||||
|
- [ ] 色彩对比度是否达标(WCAG AA: 4.5:1 文字)?
|
||||||
|
- [ ] 颜色使用是否一致、有系统性?
|
||||||
|
- [ ] 是否存在色彩混乱或过多颜色?
|
||||||
|
|
||||||
|
**3. 排版质量**
|
||||||
|
- [ ] 字体层级是否清晰(最多 3 个层级)?
|
||||||
|
- [ ] 行高/字间距是否舒适?
|
||||||
|
- [ ] 字体选择是否与品牌调性匹配?
|
||||||
|
|
||||||
|
**4. 间距与对齐**
|
||||||
|
- [ ] 是否遵循基准网格(4pt 或 8pt)?
|
||||||
|
- [ ] 组件内外间距是否统一?
|
||||||
|
- [ ] 元素对齐是否精准?
|
||||||
|
|
||||||
|
**5. 组件一致性**
|
||||||
|
- [ ] 相同功能的组件样式是否统一?
|
||||||
|
- [ ] 交互状态是否完整(默认/Hover/Active/Disabled)?
|
||||||
|
- [ ] 圆角、阴影是否系统化?
|
||||||
|
|
||||||
|
**6. 现代感与审美**
|
||||||
|
- [ ] 是否有过时的 UI 模式(阴影过重、渐变廉价)?
|
||||||
|
- [ ] 是否有 Pinterest 级别的视觉吸引力?
|
||||||
|
- [ ] 留白是否足够、有设计感?
|
||||||
|
|
||||||
|
### 优化输出格式
|
||||||
|
```
|
||||||
|
## UI 审查报告
|
||||||
|
|
||||||
|
### 🎯 核心问题(3-5个最重要的)
|
||||||
|
1. [问题描述] → [具体改进建议]
|
||||||
|
|
||||||
|
### 🎨 设计升级方案
|
||||||
|
[输出改进后的完整代码]
|
||||||
|
|
||||||
|
### 📊 改进对比
|
||||||
|
Before: [原方案问题]
|
||||||
|
After: [改进后效果]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎪 Component Design Protocol(单组件设计)
|
||||||
|
|
||||||
|
适用于:按钮、卡片、导航栏、表单、Modal、Toast、Tab、Chip 等单一组件。
|
||||||
|
|
||||||
|
**输出标准:**
|
||||||
|
- 包含所有状态(Default / Hover / Active / Focus / Disabled / Loading)
|
||||||
|
- 提供 Dark Mode 变体
|
||||||
|
- 包含使用示例
|
||||||
|
- 代码直接可用,不需要额外依赖(除非用户指定框架)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌟 高质量 UI 的标志性特征
|
||||||
|
|
||||||
|
参考 Pinterest 高赞、Dribbble 精选的共同特征:
|
||||||
|
|
||||||
|
### 必须具备
|
||||||
|
1. **清晰的焦点** — 每个页面只有一个最重要的视觉中心
|
||||||
|
2. **呼吸感** — 足够的留白,不拥挤
|
||||||
|
3. **色彩自信** — 不是"安全"的配色,而是有个性的
|
||||||
|
4. **字体有品位** — 不用默认字体,字重搭配有对比
|
||||||
|
5. **圆角有度** — 统一的圆角半径,不乱用
|
||||||
|
6. **阴影克制** — 一两层精准的阴影,不堆叠
|
||||||
|
7. **动效有意义** — 每个动画都在传达信息
|
||||||
|
|
||||||
|
### 绝对避免
|
||||||
|
- 彩虹色渐变按钮
|
||||||
|
- 廉价 box-shadow(`0 4px 8px rgba(0,0,0,0.5)` 这种)
|
||||||
|
- 全大写 + 粗体 + 红色 的"重要提示"
|
||||||
|
- 16 种以上颜色同时出现
|
||||||
|
- 响应式断点堆叠(用 clamp 代替)
|
||||||
|
- 图标和文字对不齐
|
||||||
|
- 移动端点击区域小于 44px
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 参考文件
|
||||||
|
|
||||||
|
需要深度参考时,读取以下文件:
|
||||||
|
- `references/color-systems.md` — 完整色彩系统参考(Material 3, Apple, 自定义)
|
||||||
|
- `references/component-patterns.md` — 高质量组件代码片段库
|
||||||
|
- `references/layout-templates.md` — 常见页面布局模板(Dashboard, Landing, App)
|
||||||
|
- `references/animation-library.md` — 精选动效代码库
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚡ 快速决策树
|
||||||
|
|
||||||
|
```
|
||||||
|
用户给了设计稿/截图要优化?
|
||||||
|
→ 执行 UI Audit Protocol,先分析问题,再输出改进版本
|
||||||
|
|
||||||
|
用户要从零生成界面?
|
||||||
|
→ 问清楚用途和调性,选择色板,执行 Generation Protocol
|
||||||
|
|
||||||
|
用户只需要某个组件?
|
||||||
|
→ 执行 Component Protocol,输出所有状态变体
|
||||||
|
|
||||||
|
用户没说清楚要什么?
|
||||||
|
→ 询问:① 是什么产品 ② 大概什么风格 ③ 要 HTML 还是 React
|
||||||
|
```
|
||||||
@@ -0,0 +1,317 @@
|
|||||||
|
# Color Systems Reference
|
||||||
|
|
||||||
|
## Google Material Design 3 — Dynamic Color
|
||||||
|
|
||||||
|
### Tonal Palette 完整结构
|
||||||
|
Material 3 的色彩系统基于 HCT 色彩空间(Hue, Chroma, Tone)。
|
||||||
|
|
||||||
|
**Primary Tonal Palette(主色调)**
|
||||||
|
```
|
||||||
|
Tone 100: #FFFFFF (最亮)
|
||||||
|
Tone 99: 极浅主色背景
|
||||||
|
Tone 95: 浅色主色容器
|
||||||
|
Tone 90: Primary Container (Light Mode)
|
||||||
|
Tone 80: Primary (Dark Mode)
|
||||||
|
Tone 70:
|
||||||
|
Tone 60:
|
||||||
|
Tone 50:
|
||||||
|
Tone 40: Primary (Light Mode)
|
||||||
|
Tone 30: On Primary Container (Light)
|
||||||
|
Tone 20: Primary Container (Dark)
|
||||||
|
Tone 10: On Primary (Light / On Primary Container Dark)
|
||||||
|
Tone 0: #000000 (最暗)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 完整 Material 3 语义色 Token
|
||||||
|
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
/* === Light Mode === */
|
||||||
|
|
||||||
|
/* Primary */
|
||||||
|
--md-sys-color-primary: #6750A4;
|
||||||
|
--md-sys-color-on-primary: #FFFFFF;
|
||||||
|
--md-sys-color-primary-container: #EADDFF;
|
||||||
|
--md-sys-color-on-primary-container: #21005D;
|
||||||
|
|
||||||
|
/* Secondary */
|
||||||
|
--md-sys-color-secondary: #625B71;
|
||||||
|
--md-sys-color-on-secondary: #FFFFFF;
|
||||||
|
--md-sys-color-secondary-container: #E8DEF8;
|
||||||
|
--md-sys-color-on-secondary-container: #1D192B;
|
||||||
|
|
||||||
|
/* Tertiary */
|
||||||
|
--md-sys-color-tertiary: #7D5260;
|
||||||
|
--md-sys-color-on-tertiary: #FFFFFF;
|
||||||
|
--md-sys-color-tertiary-container: #FFD8E4;
|
||||||
|
--md-sys-color-on-tertiary-container: #31111D;
|
||||||
|
|
||||||
|
/* Error */
|
||||||
|
--md-sys-color-error: #B3261E;
|
||||||
|
--md-sys-color-on-error: #FFFFFF;
|
||||||
|
--md-sys-color-error-container: #F9DEDC;
|
||||||
|
--md-sys-color-on-error-container: #410E0B;
|
||||||
|
|
||||||
|
/* Surface */
|
||||||
|
--md-sys-color-surface: #FFFBFE;
|
||||||
|
--md-sys-color-on-surface: #1C1B1F;
|
||||||
|
--md-sys-color-surface-variant: #E7E0EC;
|
||||||
|
--md-sys-color-on-surface-variant: #49454F;
|
||||||
|
--md-sys-color-surface-container-lowest: #FFFFFF;
|
||||||
|
--md-sys-color-surface-container-low: #F7F2FA;
|
||||||
|
--md-sys-color-surface-container: #F3EDF7;
|
||||||
|
--md-sys-color-surface-container-high: #ECE6F0;
|
||||||
|
--md-sys-color-surface-container-highest: #E6E0E9;
|
||||||
|
|
||||||
|
/* Outline */
|
||||||
|
--md-sys-color-outline: #79747E;
|
||||||
|
--md-sys-color-outline-variant: #CAC4D0;
|
||||||
|
|
||||||
|
/* Inverse */
|
||||||
|
--md-sys-color-inverse-surface: #313033;
|
||||||
|
--md-sys-color-inverse-on-surface: #F4EFF4;
|
||||||
|
--md-sys-color-inverse-primary: #D0BCFF;
|
||||||
|
|
||||||
|
/* Background */
|
||||||
|
--md-sys-color-background: #FFFBFE;
|
||||||
|
--md-sys-color-on-background: #1C1B1F;
|
||||||
|
|
||||||
|
/* Shadow & Scrim */
|
||||||
|
--md-sys-color-shadow: #000000;
|
||||||
|
--md-sys-color-scrim: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark Mode */
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--md-sys-color-primary: #D0BCFF;
|
||||||
|
--md-sys-color-on-primary: #381E72;
|
||||||
|
--md-sys-color-primary-container: #4F378B;
|
||||||
|
--md-sys-color-on-primary-container: #EADDFF;
|
||||||
|
--md-sys-color-secondary: #CCC2DC;
|
||||||
|
--md-sys-color-on-secondary: #332D41;
|
||||||
|
--md-sys-color-secondary-container: #4A4458;
|
||||||
|
--md-sys-color-on-secondary-container: #E8DEF8;
|
||||||
|
--md-sys-color-surface: #1C1B1F;
|
||||||
|
--md-sys-color-on-surface: #E6E1E5;
|
||||||
|
--md-sys-color-surface-container: #211F26;
|
||||||
|
--md-sys-color-surface-container-high: #2B2930;
|
||||||
|
--md-sys-color-surface-container-highest: #36343B;
|
||||||
|
--md-sys-color-outline: #938F99;
|
||||||
|
--md-sys-color-background: #1C1B1F;
|
||||||
|
--md-sys-color-on-background: #E6E1E5;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Apple HIG — System Colors
|
||||||
|
|
||||||
|
### iOS/macOS 系统色(完整集)
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
/* iOS System Colors (Light) */
|
||||||
|
--apple-blue: #007AFF;
|
||||||
|
--apple-green: #34C759;
|
||||||
|
--apple-indigo: #5856D6;
|
||||||
|
--apple-orange: #FF9500;
|
||||||
|
--apple-pink: #FF2D55;
|
||||||
|
--apple-purple: #AF52DE;
|
||||||
|
--apple-red: #FF3B30;
|
||||||
|
--apple-teal: #30B0C7;
|
||||||
|
--apple-yellow: #FFCC00;
|
||||||
|
--apple-cyan: #32ADE6;
|
||||||
|
--apple-mint: #00C7BE;
|
||||||
|
--apple-brown: #A2845E;
|
||||||
|
|
||||||
|
/* iOS System Grays */
|
||||||
|
--apple-gray1: #8E8E93;
|
||||||
|
--apple-gray2: #AEAEB2;
|
||||||
|
--apple-gray3: #C7C7CC;
|
||||||
|
--apple-gray4: #D1D1D6;
|
||||||
|
--apple-gray5: #E5E5EA;
|
||||||
|
--apple-gray6: #F2F2F7;
|
||||||
|
|
||||||
|
/* iOS System Backgrounds */
|
||||||
|
--apple-bg-primary: #FFFFFF;
|
||||||
|
--apple-bg-secondary: #F2F2F7;
|
||||||
|
--apple-bg-tertiary: #FFFFFF;
|
||||||
|
--apple-grouped-bg: #F2F2F7;
|
||||||
|
--apple-grouped-bg-secondary: #FFFFFF;
|
||||||
|
|
||||||
|
/* iOS Separators */
|
||||||
|
--apple-separator: rgba(60, 60, 67, 0.29);
|
||||||
|
--apple-separator-opaque: #C6C6C8;
|
||||||
|
|
||||||
|
/* iOS Label Colors */
|
||||||
|
--apple-label: #000000;
|
||||||
|
--apple-label-secondary: rgba(60, 60, 67, 0.6);
|
||||||
|
--apple-label-tertiary: rgba(60, 60, 67, 0.3);
|
||||||
|
--apple-label-quaternary: rgba(60, 60, 67, 0.18);
|
||||||
|
|
||||||
|
/* iOS Fill Colors */
|
||||||
|
--apple-fill: rgba(120, 120, 128, 0.2);
|
||||||
|
--apple-fill-secondary: rgba(120, 120, 128, 0.16);
|
||||||
|
--apple-fill-tertiary: rgba(118, 118, 128, 0.12);
|
||||||
|
--apple-fill-quaternary: rgba(116, 116, 128, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark Mode */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--apple-blue: #0A84FF;
|
||||||
|
--apple-green: #30D158;
|
||||||
|
--apple-indigo: #5E5CE6;
|
||||||
|
--apple-orange: #FF9F0A;
|
||||||
|
--apple-pink: #FF375F;
|
||||||
|
--apple-purple: #BF5AF2;
|
||||||
|
--apple-red: #FF453A;
|
||||||
|
--apple-teal: #40CBE0;
|
||||||
|
--apple-yellow: #FFD60A;
|
||||||
|
--apple-cyan: #64D2FF;
|
||||||
|
--apple-mint: #63E6E2;
|
||||||
|
|
||||||
|
--apple-gray1: #8E8E93;
|
||||||
|
--apple-gray2: #636366;
|
||||||
|
--apple-gray3: #48484A;
|
||||||
|
--apple-gray4: #3A3A3C;
|
||||||
|
--apple-gray5: #2C2C2E;
|
||||||
|
--apple-gray6: #1C1C1E;
|
||||||
|
|
||||||
|
--apple-bg-primary: #000000;
|
||||||
|
--apple-bg-secondary: #1C1C1E;
|
||||||
|
--apple-bg-tertiary: #2C2C2E;
|
||||||
|
|
||||||
|
--apple-label: #FFFFFF;
|
||||||
|
--apple-label-secondary: rgba(235, 235, 245, 0.6);
|
||||||
|
--apple-label-tertiary: rgba(235, 235, 245, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 精选高级自定义色板
|
||||||
|
|
||||||
|
### 1. Midnight Studio(深色科技)
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
--primary: #7C3AED; /* 紫罗兰主色 */
|
||||||
|
--primary-light: #A78BFA;
|
||||||
|
--primary-dark: #5B21B6;
|
||||||
|
--accent: #10B981; /* 翡翠绿强调 */
|
||||||
|
--accent-warm: #F59E0B; /* 琥珀暖色 */
|
||||||
|
|
||||||
|
--bg: #09090B; /* 近黑背景 */
|
||||||
|
--bg-2: #18181B; /* 卡片表面 */
|
||||||
|
--bg-3: #27272A; /* 悬浮层 */
|
||||||
|
--border: rgba(255,255,255,0.08);
|
||||||
|
--border-strong: rgba(255,255,255,0.15);
|
||||||
|
|
||||||
|
--text-1: #FAFAFA;
|
||||||
|
--text-2: #A1A1AA;
|
||||||
|
--text-3: #71717A;
|
||||||
|
|
||||||
|
--shadow-glow: 0 0 40px rgba(124, 58, 237, 0.3);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Linen & Oak(人文暖调)
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
--primary: #92400E; /* 深琥珀 */
|
||||||
|
--primary-light: #D97706;
|
||||||
|
--accent: #047857; /* 墨绿 */
|
||||||
|
--accent-warm: #9D174D; /* 酒红 */
|
||||||
|
|
||||||
|
--bg: #FEFCE8; /* 米黄背景 */
|
||||||
|
--bg-2: #FFFBEB;
|
||||||
|
--bg-3: #FEF3C7;
|
||||||
|
--surface: #FFFFFF;
|
||||||
|
--border: rgba(120, 80, 20, 0.12);
|
||||||
|
|
||||||
|
--text-1: #1C1917;
|
||||||
|
--text-2: #57534E;
|
||||||
|
--text-3: #A8A29E;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Arctic Glass(冷感极简)
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
--primary: #0EA5E9; /* 冰蓝 */
|
||||||
|
--primary-light: #38BDF8;
|
||||||
|
--primary-dark: #0284C7;
|
||||||
|
--accent: #8B5CF6; /* 淡紫强调 */
|
||||||
|
|
||||||
|
--bg: #F8FAFC; /* 极浅灰蓝背景 */
|
||||||
|
--bg-2: #F1F5F9;
|
||||||
|
--bg-3: #E2E8F0;
|
||||||
|
--surface: #FFFFFF;
|
||||||
|
--glass: rgba(255, 255, 255, 0.7);
|
||||||
|
--glass-border: rgba(148, 163, 184, 0.3);
|
||||||
|
|
||||||
|
--text-1: #0F172A;
|
||||||
|
--text-2: #475569;
|
||||||
|
--text-3: #94A3B8;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Tokyo Night(暗色电影感)
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
--primary: #7AA2F7; /* 东京蓝 */
|
||||||
|
--secondary: #BB9AF7; /* 东京紫 */
|
||||||
|
--accent: #7DCFFF; /* 青蓝 */
|
||||||
|
--warn: #FF9E64; /* 橙色警告 */
|
||||||
|
--error: #F7768E; /* 红色错误 */
|
||||||
|
--success: #9ECE6A; /* 绿色成功 */
|
||||||
|
|
||||||
|
--bg: #1A1B26; /* 东京暗背景 */
|
||||||
|
--bg-2: #16161E;
|
||||||
|
--bg-3: #24283B;
|
||||||
|
--bg-highlight: #292E42;
|
||||||
|
--border: #292E42;
|
||||||
|
|
||||||
|
--text-1: #C0CAF5;
|
||||||
|
--text-2: #9AA5CE;
|
||||||
|
--text-3: #565F89;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Coral Sunrise(活力橙红)
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
--primary: #F97316; /* 活力橙 */
|
||||||
|
--primary-dark: #EA580C;
|
||||||
|
--secondary: #EC4899; /* 玫瑰粉 */
|
||||||
|
--accent: #14B8A6; /* 蒂芙尼绿 */
|
||||||
|
|
||||||
|
--bg: #FFFAF0; /* 象牙白背景 */
|
||||||
|
--bg-2: #FFF7ED;
|
||||||
|
--surface: #FFFFFF;
|
||||||
|
--border: rgba(249, 115, 22, 0.15);
|
||||||
|
|
||||||
|
--text-1: #1C1917;
|
||||||
|
--text-2: #78716C;
|
||||||
|
--text-3: #A8A29E;
|
||||||
|
|
||||||
|
--gradient: linear-gradient(135deg, #F97316, #EC4899);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## WCAG 对比度检查速查
|
||||||
|
|
||||||
|
| 文字/背景组合 | 对比度 | 是否达标 AA |
|
||||||
|
|-------------|--------|------------|
|
||||||
|
| #000 / #FFF | 21:1 | ✅ AAA |
|
||||||
|
| #1C1B1F / #FFFBFE | 18.1:1 | ✅ AAA |
|
||||||
|
| #6750A4 / #FFF | 4.6:1 | ✅ AA |
|
||||||
|
| #8E8E93 / #FFF | 3.9:1 | ⚠️ 仅大文字 |
|
||||||
|
| #007AFF / #FFF | 4.5:1 | ✅ AA |
|
||||||
|
|
||||||
|
**规则提醒:**
|
||||||
|
- 正文 (≤18pt 非粗体): 最低 4.5:1
|
||||||
|
- 大文字 (≥18pt 或 ≥14pt 粗体): 最低 3:1
|
||||||
|
- UI 组件和图形: 最低 3:1
|
||||||
@@ -0,0 +1,601 @@
|
|||||||
|
# Component Patterns Library
|
||||||
|
|
||||||
|
## Navigation
|
||||||
|
|
||||||
|
### Top App Bar (Material 3 + Apple 混合风格)
|
||||||
|
```html
|
||||||
|
<header class="top-bar">
|
||||||
|
<div class="top-bar__inner">
|
||||||
|
<button class="icon-btn" aria-label="Menu">
|
||||||
|
<svg><!-- hamburger --></svg>
|
||||||
|
</button>
|
||||||
|
<h1 class="top-bar__title">Page Title</h1>
|
||||||
|
<div class="top-bar__actions">
|
||||||
|
<button class="icon-btn" aria-label="Search">
|
||||||
|
<svg><!-- search --></svg>
|
||||||
|
</button>
|
||||||
|
<button class="icon-btn" aria-label="More">
|
||||||
|
<svg><!-- more --></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.top-bar {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
background: var(--surface);
|
||||||
|
border-bottom: 1px solid var(--outline-variant);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
background: rgba(255, 251, 254, 0.8);
|
||||||
|
}
|
||||||
|
.top-bar__inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
height: 64px;
|
||||||
|
padding: 0 8px;
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.top-bar__title {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
color: var(--on-surface);
|
||||||
|
}
|
||||||
|
.icon-btn {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 150ms;
|
||||||
|
}
|
||||||
|
.icon-btn:hover { background: var(--on-surface-variant-8, rgba(73,69,79,0.08)); }
|
||||||
|
.icon-btn:active { background: var(--on-surface-variant-12, rgba(73,69,79,0.12)); }
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Side Navigation Rail (Material 3)
|
||||||
|
```html
|
||||||
|
<nav class="nav-rail">
|
||||||
|
<div class="nav-rail__fab">
|
||||||
|
<button class="fab-btn">+</button>
|
||||||
|
</div>
|
||||||
|
<ul class="nav-rail__list">
|
||||||
|
<li class="nav-item active">
|
||||||
|
<div class="nav-item__indicator">
|
||||||
|
<svg class="nav-item__icon"><!-- icon --></svg>
|
||||||
|
</div>
|
||||||
|
<span class="nav-item__label">Home</span>
|
||||||
|
</li>
|
||||||
|
<!-- more items -->
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.nav-rail {
|
||||||
|
width: 80px;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 0;
|
||||||
|
background: var(--surface);
|
||||||
|
}
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px 0;
|
||||||
|
cursor: pointer;
|
||||||
|
min-width: 72px;
|
||||||
|
}
|
||||||
|
.nav-item__indicator {
|
||||||
|
width: 56px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 16px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
transition: background 200ms;
|
||||||
|
}
|
||||||
|
.nav-item.active .nav-item__indicator {
|
||||||
|
background: var(--secondary-container);
|
||||||
|
}
|
||||||
|
.nav-item:hover:not(.active) .nav-item__indicator {
|
||||||
|
background: var(--on-surface-8);
|
||||||
|
}
|
||||||
|
.nav-item__label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
color: var(--on-surface-variant);
|
||||||
|
}
|
||||||
|
.nav-item.active .nav-item__label {
|
||||||
|
color: var(--on-surface);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cards
|
||||||
|
|
||||||
|
### Product Card (Pinterest 风格)
|
||||||
|
```html
|
||||||
|
<article class="product-card">
|
||||||
|
<div class="product-card__img-wrap">
|
||||||
|
<img class="product-card__img" src="..." alt="...">
|
||||||
|
<button class="product-card__save">♡</button>
|
||||||
|
</div>
|
||||||
|
<div class="product-card__body">
|
||||||
|
<p class="product-card__category">Category</p>
|
||||||
|
<h3 class="product-card__title">Product Name</h3>
|
||||||
|
<div class="product-card__footer">
|
||||||
|
<span class="product-card__price">$99</span>
|
||||||
|
<button class="product-card__cta">Add to Cart</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.product-card {
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--surface);
|
||||||
|
transition: transform 300ms var(--ease-spring), box-shadow 300ms;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.product-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 12px 40px rgba(0,0,0,0.12);
|
||||||
|
}
|
||||||
|
.product-card__img-wrap {
|
||||||
|
position: relative;
|
||||||
|
aspect-ratio: 4/3;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.product-card__img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform 600ms var(--ease-decelerate);
|
||||||
|
}
|
||||||
|
.product-card:hover .product-card__img {
|
||||||
|
transform: scale(1.04);
|
||||||
|
}
|
||||||
|
.product-card__save {
|
||||||
|
position: absolute;
|
||||||
|
top: 12px; right: 12px;
|
||||||
|
width: 36px; height: 36px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255,255,255,0.9);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
border: none;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.8);
|
||||||
|
transition: opacity 200ms, transform 200ms var(--ease-spring);
|
||||||
|
}
|
||||||
|
.product-card:hover .product-card__save {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
.product-card__body {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.product-card__category {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: var(--primary);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.product-card__title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--on-surface);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
.product-card__footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.product-card__price {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--on-surface);
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
.product-card__cta {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: var(--primary);
|
||||||
|
color: var(--on-primary);
|
||||||
|
border: none;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: filter 150ms, transform 150ms;
|
||||||
|
}
|
||||||
|
.product-card__cta:hover { filter: brightness(1.1); }
|
||||||
|
.product-card__cta:active { transform: scale(0.96); }
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stats Card (Dashboard)
|
||||||
|
```html
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-card__header">
|
||||||
|
<span class="stat-card__label">Total Revenue</span>
|
||||||
|
<div class="stat-card__icon-wrap">
|
||||||
|
<svg><!-- icon --></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="stat-card__value">$48,295</p>
|
||||||
|
<div class="stat-card__change positive">
|
||||||
|
<svg><!-- arrow up --></svg>
|
||||||
|
<span>+12.5% from last month</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.stat-card {
|
||||||
|
background: var(--surface);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 24px;
|
||||||
|
border: 1px solid var(--outline-variant);
|
||||||
|
transition: box-shadow 200ms;
|
||||||
|
}
|
||||||
|
.stat-card:hover {
|
||||||
|
box-shadow: 0 4px 24px rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
.stat-card__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.stat-card__label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--on-surface-variant);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.stat-card__icon-wrap {
|
||||||
|
width: 40px; height: 40px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--primary-container);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
.stat-card__value {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
color: var(--on-surface);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-feature-settings: 'tnum';
|
||||||
|
}
|
||||||
|
.stat-card__change {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.stat-card__change.positive { color: #16A34A; }
|
||||||
|
.stat-card__change.negative { color: #DC2626; }
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Forms
|
||||||
|
|
||||||
|
### Floating Label Input (Material 3)
|
||||||
|
```html
|
||||||
|
<div class="text-field">
|
||||||
|
<div class="text-field__container">
|
||||||
|
<input type="text" id="email" class="text-field__input" placeholder=" ">
|
||||||
|
<label for="email" class="text-field__label">Email address</label>
|
||||||
|
<fieldset class="text-field__border"><legend><span>Email address</span></legend></fieldset>
|
||||||
|
</div>
|
||||||
|
<span class="text-field__helper">We'll never share your email</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.text-field { display: flex; flex-direction: column; gap: 4px; }
|
||||||
|
.text-field__container {
|
||||||
|
position: relative;
|
||||||
|
height: 56px;
|
||||||
|
}
|
||||||
|
.text-field__input {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 16px 16px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
color: var(--on-surface);
|
||||||
|
z-index: 1;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.text-field__label {
|
||||||
|
position: absolute;
|
||||||
|
left: 16px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--on-surface-variant);
|
||||||
|
pointer-events: none;
|
||||||
|
transition: all 150ms var(--ease-standard);
|
||||||
|
z-index: 2;
|
||||||
|
background: var(--surface);
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
.text-field__input:focus + .text-field__label,
|
||||||
|
.text-field__input:not(:placeholder-shown) + .text-field__label {
|
||||||
|
top: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.text-field__input:focus + .text-field__label { color: var(--primary); }
|
||||||
|
.text-field__border {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
margin: 0;
|
||||||
|
border: 1.5px solid var(--outline);
|
||||||
|
border-radius: 4px;
|
||||||
|
pointer-events: none;
|
||||||
|
padding: 0 12px;
|
||||||
|
transition: border-color 150ms, border-width 150ms;
|
||||||
|
}
|
||||||
|
.text-field__input:focus ~ .text-field__border {
|
||||||
|
border-color: var(--primary);
|
||||||
|
border-width: 2px;
|
||||||
|
}
|
||||||
|
.text-field__border legend { height: 0; font-size: 12px; font-weight: 500; padding: 0; }
|
||||||
|
.text-field__input:focus ~ .text-field__border legend,
|
||||||
|
.text-field__input:not(:placeholder-shown) ~ .text-field__border legend {
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
.text-field__helper {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--on-surface-variant);
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Buttons — Complete System
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Primary (Filled) -->
|
||||||
|
<button class="btn btn--filled">Get Started</button>
|
||||||
|
|
||||||
|
<!-- Secondary (Filled Tonal) -->
|
||||||
|
<button class="btn btn--tonal">Learn More</button>
|
||||||
|
|
||||||
|
<!-- Outlined -->
|
||||||
|
<button class="btn btn--outlined">Cancel</button>
|
||||||
|
|
||||||
|
<!-- Text -->
|
||||||
|
<button class="btn btn--text">Skip</button>
|
||||||
|
|
||||||
|
<!-- Elevated -->
|
||||||
|
<button class="btn btn--elevated">Save Draft</button>
|
||||||
|
|
||||||
|
<!-- Icon + Label -->
|
||||||
|
<button class="btn btn--filled btn--icon">
|
||||||
|
<svg><!-- icon --></svg>
|
||||||
|
Add Item
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Loading State -->
|
||||||
|
<button class="btn btn--filled btn--loading" disabled>
|
||||||
|
<span class="btn__spinner"></span>
|
||||||
|
Loading...
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
height: 40px;
|
||||||
|
padding: 0 24px;
|
||||||
|
border-radius: 20px; /* Material 3 全圆角 */
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: all 150ms var(--ease-standard);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.btn:disabled { opacity: 0.38; cursor: not-allowed; }
|
||||||
|
.btn:active:not(:disabled) { transform: scale(0.97); }
|
||||||
|
|
||||||
|
/* State layer */
|
||||||
|
.btn::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: currentColor;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 150ms;
|
||||||
|
}
|
||||||
|
.btn:hover::after { opacity: 0.08; }
|
||||||
|
.btn:active::after { opacity: 0.12; }
|
||||||
|
|
||||||
|
/* Variants */
|
||||||
|
.btn--filled { background: var(--primary); color: var(--on-primary); }
|
||||||
|
.btn--filled:hover { box-shadow: 0 1px 2px rgba(0,0,0,.1), 0 2px 6px rgba(0,0,0,.08); }
|
||||||
|
|
||||||
|
.btn--tonal { background: var(--secondary-container); color: var(--on-secondary-container); }
|
||||||
|
.btn--tonal:hover { box-shadow: 0 1px 2px rgba(0,0,0,.06), 0 2px 6px rgba(0,0,0,.06); }
|
||||||
|
|
||||||
|
.btn--outlined {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--primary);
|
||||||
|
border: 1px solid var(--outline);
|
||||||
|
}
|
||||||
|
.btn--outlined:hover { background: rgba(103, 80, 164, 0.08); }
|
||||||
|
|
||||||
|
.btn--text { background: transparent; color: var(--primary); padding: 0 12px; }
|
||||||
|
|
||||||
|
.btn--elevated {
|
||||||
|
background: var(--surface-container-low);
|
||||||
|
color: var(--primary);
|
||||||
|
box-shadow: 0 1px 2px rgba(0,0,0,.06), 0 2px 8px rgba(0,0,0,.08);
|
||||||
|
}
|
||||||
|
.btn--elevated:hover { box-shadow: 0 2px 4px rgba(0,0,0,.08), 0 4px 12px rgba(0,0,0,.1); }
|
||||||
|
|
||||||
|
/* Spinner */
|
||||||
|
.btn__spinner {
|
||||||
|
width: 16px; height: 16px;
|
||||||
|
border: 2px solid rgba(255,255,255,0.3);
|
||||||
|
border-top-color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.6s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes spin { to { transform: rotate(360deg); } }
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feedback Components
|
||||||
|
|
||||||
|
### Toast / Snackbar (Material 3)
|
||||||
|
```html
|
||||||
|
<div class="snackbar" role="status">
|
||||||
|
<span class="snackbar__message">Changes saved successfully</span>
|
||||||
|
<button class="snackbar__action">Undo</button>
|
||||||
|
<button class="snackbar__close" aria-label="Close">✕</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.snackbar {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
min-width: 288px;
|
||||||
|
max-width: 568px;
|
||||||
|
background: var(--inverse-surface);
|
||||||
|
color: var(--inverse-on-surface);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
box-shadow: 0 3px 5px rgba(0,0,0,.1), 0 8px 24px rgba(0,0,0,.14);
|
||||||
|
font-size: 14px;
|
||||||
|
animation: snackbar-in 300ms var(--ease-decelerate) forwards;
|
||||||
|
}
|
||||||
|
@keyframes snackbar-in {
|
||||||
|
from { transform: translateY(100%); opacity: 0; }
|
||||||
|
to { transform: translateY(0); opacity: 1; }
|
||||||
|
}
|
||||||
|
.snackbar__message { flex: 1; }
|
||||||
|
.snackbar__action {
|
||||||
|
background: none; border: none;
|
||||||
|
color: var(--inverse-primary);
|
||||||
|
font-size: 14px; font-weight: 500;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background 150ms;
|
||||||
|
}
|
||||||
|
.snackbar__action:hover { background: rgba(255,255,255,0.1); }
|
||||||
|
.snackbar__close {
|
||||||
|
background: none; border: none;
|
||||||
|
color: var(--inverse-on-surface);
|
||||||
|
cursor: pointer; font-size: 18px; line-height: 1;
|
||||||
|
opacity: 0.7; transition: opacity 150ms;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Badge
|
||||||
|
```css
|
||||||
|
.badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
padding: 0 4px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--error);
|
||||||
|
color: var(--on-error);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.badge--dot {
|
||||||
|
width: 8px; height: 8px;
|
||||||
|
padding: 0; min-width: unset;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.badge--large {
|
||||||
|
min-width: 24px; height: 24px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Page Skeleton / Loading
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="skeleton-card">
|
||||||
|
<div class="skeleton skeleton--image"></div>
|
||||||
|
<div class="skeleton-card__body">
|
||||||
|
<div class="skeleton skeleton--line" style="width: 60%"></div>
|
||||||
|
<div class="skeleton skeleton--line" style="width: 90%"></div>
|
||||||
|
<div class="skeleton skeleton--line" style="width: 75%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% { background-position: -200% 0; }
|
||||||
|
100% { background-position: 200% 0; }
|
||||||
|
}
|
||||||
|
.skeleton {
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
var(--surface-variant) 25%,
|
||||||
|
var(--surface-container) 50%,
|
||||||
|
var(--surface-variant) 75%
|
||||||
|
);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 1.5s infinite;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.skeleton--image { width: 100%; aspect-ratio: 16/9; border-radius: 12px 12px 0 0; }
|
||||||
|
.skeleton--line { height: 14px; margin-bottom: 8px; border-radius: 7px; }
|
||||||
|
</style>
|
||||||
|
```
|
||||||
@@ -0,0 +1,499 @@
|
|||||||
|
# Layout Templates Reference
|
||||||
|
|
||||||
|
## Dashboard Layout (Material 3 Canonical)
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Dashboard</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="app-shell">
|
||||||
|
<!-- Side Navigation -->
|
||||||
|
<aside class="nav-drawer">
|
||||||
|
<div class="nav-drawer__header">
|
||||||
|
<div class="brand">
|
||||||
|
<div class="brand__icon">◈</div>
|
||||||
|
<span class="brand__name">AppName</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<nav class="nav-list">
|
||||||
|
<a href="#" class="nav-link active">
|
||||||
|
<svg class="nav-link__icon" viewBox="0 0 24 24">
|
||||||
|
<path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z"/>
|
||||||
|
</svg>
|
||||||
|
<span>Dashboard</span>
|
||||||
|
</a>
|
||||||
|
<!-- more links -->
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div class="main-content">
|
||||||
|
<!-- Top Bar -->
|
||||||
|
<header class="top-bar">
|
||||||
|
<h1 class="page-title">Overview</h1>
|
||||||
|
<div class="top-bar__actions">
|
||||||
|
<button class="icon-btn">🔔</button>
|
||||||
|
<div class="avatar">JD</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Content Area -->
|
||||||
|
<main class="content-area">
|
||||||
|
<!-- Stats Row -->
|
||||||
|
<div class="stats-grid">
|
||||||
|
<!-- stat cards here -->
|
||||||
|
</div>
|
||||||
|
<!-- Charts -->
|
||||||
|
<div class="charts-grid">
|
||||||
|
<!-- chart cards here -->
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--primary: #6750A4;
|
||||||
|
--primary-container: #EADDFF;
|
||||||
|
--on-primary-container: #21005D;
|
||||||
|
--secondary-container: #E8DEF8;
|
||||||
|
--surface: #FFFBFE;
|
||||||
|
--surface-container: #F3EDF7;
|
||||||
|
--surface-container-low: #F7F2FA;
|
||||||
|
--on-surface: #1C1B1F;
|
||||||
|
--on-surface-variant: #49454F;
|
||||||
|
--outline: #79747E;
|
||||||
|
--outline-variant: #CAC4D0;
|
||||||
|
--nav-width: 256px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Manrope', system-ui, sans-serif;
|
||||||
|
background: var(--surface-container-low);
|
||||||
|
color: var(--on-surface);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell {
|
||||||
|
display: flex;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nav Drawer */
|
||||||
|
.nav-drawer {
|
||||||
|
width: var(--nav-width);
|
||||||
|
height: 100vh;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
background: var(--surface-container-low);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border-right: 1px solid var(--outline-variant);
|
||||||
|
padding: 16px 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-drawer__header { padding: 8px 12px 24px; }
|
||||||
|
.brand { display: flex; align-items: center; gap: 12px; }
|
||||||
|
.brand__icon {
|
||||||
|
width: 40px; height: 40px;
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
.brand__name { font-size: 18px; font-weight: 700; color: var(--on-surface); }
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 28px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--on-surface-variant);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: background 150ms, color 150ms;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.nav-link:hover { background: rgba(103,80,164,0.08); color: var(--on-surface); }
|
||||||
|
.nav-link.active {
|
||||||
|
background: var(--secondary-container);
|
||||||
|
color: var(--on-surface);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.nav-link__icon { width: 20px; height: 20px; flex-shrink: 0; }
|
||||||
|
|
||||||
|
/* Main Content */
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-bar {
|
||||||
|
height: 72px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 32px;
|
||||||
|
background: var(--surface-container-low);
|
||||||
|
border-bottom: 1px solid var(--outline-variant);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
.page-title { font-size: 28px; font-weight: 700; letter-spacing: -0.02em; }
|
||||||
|
.top-bar__actions { display: flex; align-items: center; gap: 8px; }
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 36px; height: 36px;
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-area { padding: 32px; overflow-y: auto; flex: 1; }
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.charts-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 2fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.nav-drawer { display: none; }
|
||||||
|
.charts-grid { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Landing Page Layout
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Hero Section -->
|
||||||
|
<section class="hero">
|
||||||
|
<div class="hero__container">
|
||||||
|
<div class="hero__eyebrow">
|
||||||
|
<span class="badge-pill">New →</span>
|
||||||
|
<span>Introducing v2.0</span>
|
||||||
|
</div>
|
||||||
|
<h1 class="hero__headline">
|
||||||
|
Build beautiful apps<br>
|
||||||
|
<span class="hero__gradient-text">10x faster</span>
|
||||||
|
</h1>
|
||||||
|
<p class="hero__description">
|
||||||
|
The design system that scales with your team.
|
||||||
|
Beautiful by default, customizable by design.
|
||||||
|
</p>
|
||||||
|
<div class="hero__ctas">
|
||||||
|
<a href="#" class="btn btn--primary btn--lg">Get Started Free</a>
|
||||||
|
<a href="#" class="btn btn--ghost btn--lg">View Demo ↗</a>
|
||||||
|
</div>
|
||||||
|
<div class="hero__social-proof">
|
||||||
|
<div class="avatar-stack">
|
||||||
|
<img src="..." alt="">
|
||||||
|
<img src="..." alt="">
|
||||||
|
<img src="..." alt="">
|
||||||
|
<img src="..." alt="">
|
||||||
|
</div>
|
||||||
|
<p><strong>2,000+</strong> teams already building</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hero__visual">
|
||||||
|
<!-- Product Screenshot / Illustration -->
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.hero {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
align-items: center;
|
||||||
|
gap: 64px;
|
||||||
|
padding: 80px clamp(24px, 5vw, 80px);
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.hero__eyebrow {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--on-surface-variant);
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.badge-pill {
|
||||||
|
background: var(--primary-container);
|
||||||
|
color: var(--on-primary-container);
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.hero__headline {
|
||||||
|
font-size: clamp(36px, 5vw, 64px);
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: -0.04em;
|
||||||
|
line-height: 1.05;
|
||||||
|
color: var(--on-surface);
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.hero__gradient-text {
|
||||||
|
background: linear-gradient(135deg, var(--primary), var(--tertiary));
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
.hero__description {
|
||||||
|
font-size: clamp(16px, 2vw, 20px);
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--on-surface-variant);
|
||||||
|
max-width: 48ch;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
.hero__ctas {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
.btn--lg { height: 52px; padding: 0 32px; font-size: 16px; }
|
||||||
|
.btn--ghost {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--on-surface);
|
||||||
|
border: 1.5px solid var(--outline-variant);
|
||||||
|
border-radius: 26px;
|
||||||
|
}
|
||||||
|
.btn--ghost:hover { background: var(--surface-variant); }
|
||||||
|
.hero__social-proof {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--on-surface-variant);
|
||||||
|
}
|
||||||
|
.avatar-stack {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.avatar-stack img {
|
||||||
|
width: 28px; height: 28px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid var(--surface);
|
||||||
|
margin-right: -8px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.hero {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
text-align: center;
|
||||||
|
min-height: auto;
|
||||||
|
padding-top: 100px;
|
||||||
|
}
|
||||||
|
.hero__visual { display: none; }
|
||||||
|
.hero__ctas { justify-content: center; }
|
||||||
|
.hero__social-proof { justify-content: center; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Mobile App Layout (iOS-like)
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="app">
|
||||||
|
<!-- Status Bar placeholder -->
|
||||||
|
<div class="status-bar"></div>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<main class="app-content">
|
||||||
|
<!-- Large Title (iOS style) -->
|
||||||
|
<div class="large-title-area">
|
||||||
|
<h1 class="large-title">My Library</h1>
|
||||||
|
<button class="circle-btn">+</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search Bar (iOS style) -->
|
||||||
|
<div class="search-bar">
|
||||||
|
<span class="search-icon">🔍</span>
|
||||||
|
<input type="search" placeholder="Search" class="search-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Horizontal Scroll Section -->
|
||||||
|
<section class="section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2 class="section-title">Featured</h2>
|
||||||
|
<a href="#" class="see-all">See All</a>
|
||||||
|
</div>
|
||||||
|
<div class="h-scroll">
|
||||||
|
<!-- cards -->
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Tab Bar (iOS style) -->
|
||||||
|
<nav class="tab-bar">
|
||||||
|
<button class="tab-item active">
|
||||||
|
<svg class="tab-icon"><!-- icon --></svg>
|
||||||
|
<span class="tab-label">Home</span>
|
||||||
|
</button>
|
||||||
|
<button class="tab-item">
|
||||||
|
<svg class="tab-icon"><!-- icon --></svg>
|
||||||
|
<span class="tab-label">Explore</span>
|
||||||
|
</button>
|
||||||
|
<button class="tab-item">
|
||||||
|
<svg class="tab-icon"><!-- icon --></svg>
|
||||||
|
<span class="tab-label">Library</span>
|
||||||
|
</button>
|
||||||
|
<button class="tab-item">
|
||||||
|
<svg class="tab-icon"><!-- icon --></svg>
|
||||||
|
<span class="tab-label">Profile</span>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.app {
|
||||||
|
width: 390px; /* iPhone 14 width */
|
||||||
|
height: 844px;
|
||||||
|
background: var(--bg-primary, #F2F2F7);
|
||||||
|
border-radius: 44px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: 0 32px 80px rgba(0,0,0,0.3);
|
||||||
|
font-family: -apple-system, 'SF Pro Display', system-ui;
|
||||||
|
}
|
||||||
|
.status-bar {
|
||||||
|
height: 47px;
|
||||||
|
background: transparent;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.app-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
.app-content::-webkit-scrollbar { display: none; }
|
||||||
|
.large-title-area {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 0 20px;
|
||||||
|
}
|
||||||
|
.large-title {
|
||||||
|
font-size: 34px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
color: var(--apple-label, #000);
|
||||||
|
}
|
||||||
|
.circle-btn {
|
||||||
|
width: 32px; height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--apple-fill, rgba(120,120,128,0.2));
|
||||||
|
border: none;
|
||||||
|
font-size: 22px;
|
||||||
|
line-height: 1;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--apple-blue, #007AFF);
|
||||||
|
}
|
||||||
|
.search-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
background: rgba(118, 118, 128, 0.12);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 0 12px;
|
||||||
|
height: 36px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.search-input {
|
||||||
|
flex: 1;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
font-size: 17px;
|
||||||
|
color: var(--apple-label, #000);
|
||||||
|
}
|
||||||
|
.section { margin-bottom: 32px; }
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.section-title { font-size: 22px; font-weight: 700; letter-spacing: -0.01em; }
|
||||||
|
.see-all { font-size: 15px; color: var(--apple-blue, #007AFF); text-decoration: none; }
|
||||||
|
.h-scroll {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 0 -20px;
|
||||||
|
padding: 0 20px 4px;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
.h-scroll::-webkit-scrollbar { display: none; }
|
||||||
|
|
||||||
|
/* Tab Bar */
|
||||||
|
.tab-bar {
|
||||||
|
height: 83px;
|
||||||
|
background: rgba(249, 249, 249, 0.94);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
border-top: 0.5px solid rgba(0,0,0,0.12);
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 8px 0 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.tab-item {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 0;
|
||||||
|
color: var(--apple-gray1, #8E8E93);
|
||||||
|
transition: color 150ms;
|
||||||
|
}
|
||||||
|
.tab-item.active { color: var(--apple-blue, #007AFF); }
|
||||||
|
.tab-icon { width: 26px; height: 26px; }
|
||||||
|
.tab-label { font-size: 10px; font-weight: 500; letter-spacing: -0.01em; }
|
||||||
|
</style>
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user