feat: 完整功能迭代 — 移动端+PC端全面优化

新增:
- 今日纪要 (6分类+彩色标签+时间选择器)
- 客户经理PC端工作台 (我的数据,5模块CRUD)
- 用户管理页 (支局长修改角色)
- 客户备注/收支费用(金额+单位)/联系人管理
- 拜访人姓名+电话字段
- 客户导入导出/旧周报导入模板下载
- 序号列+分页(25/50/100)

修复/优化:
- Hash路由→HTML5 History, Casdoor回调正常
- 时区统一为Asia/Shanghai (today_cst)
- 数据库懒加载→selectinload预加载
- 日期解析兼容ISO datetime字符串
- 文件上传定位修复 (position:relative)
- 表单button type='button'防止误提交
- 分管领导权限(只读客户档案,不可编辑)
- 侧边栏折叠+SVG图标
- 拜访方式/紧急度统一chip风格
- 时间范围统一为el-time-picker(is-range)
- 全局CSS设计变量+box-sizing修复滚动条

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-23 11:40:03 +08:00
parent a1886074dd
commit e7af0e15ab
37 changed files with 4561 additions and 643 deletions
+241 -44
View File
@@ -1,55 +1,89 @@
<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router'
import { computed } from 'vue'
import { computed, ref } from 'vue'
import { useAuthStore } from '@/stores/auth'
const route = useRoute()
const router = useRouter()
const auth = useAuthStore()
const collapsed = ref(false)
const menuItems = computed(() => {
const items = [
{ path: '/', label: '仪表盘', icon: 'Odometer' },
{ path: '/weekly-report', label: '周报详情', icon: 'Document' },
const items: { path: string; label: string; icon: string }[] = [
{ path: '/', label: '仪表盘', icon: '<polyline points="4 7 12 3 20 7"></polyline><polyline points="20 7 20 21 4 21 4 7"></polyline><line x1="8" y1="21" x2="8" y2="12"></line><line x1="16" y1="21" x2="16" y2="12"></line>' },
{ path: '/weekly-report', label: '周报详情', icon: '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line>' },
]
if (auth.isManager || auth.isDirector) {
items.push({ path: '/workspace', label: '我的数据', icon: 'List' })
items.push({ path: '/workspace', label: '我的数据', icon: '<line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line>' })
}
items.push({ path: '/customers', label: '客户管理', icon: 'UserFilled' })
items.push({ path: '/customers', label: '客户管理', icon: '<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle>' })
if (auth.isDirector) {
items.push({ path: '/users', label: '用户管理', icon: 'Avatar' })
items.push({ path: '/settings', label: '系统设置', icon: 'Setting' })
items.push({ path: '/users', label: '用户管理', icon: '<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path>' })
items.push({ path: '/settings', label: '系统设置', icon: '<circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>' })
}
return items
})
const roleLabel = computed(() => {
if (auth.isDirector) return '支局长'
if (auth.isLeader) return '分管领导'
return '客户经理'
})
</script>
<template>
<div class="desktop-layout">
<aside class="sidebar">
<div class="logo">
<h2>企迹</h2>
<p>政企周报管理</p>
<!-- Sidebar Ink & Gold Editorial -->
<aside class="sidebar" :class="{ 'sidebar--collapsed': collapsed }">
<div class="sidebar-brand">
<h1 class="brand-title">企迹</h1>
<p class="brand-sub">政企周报管理</p>
<div class="brand-rule"></div>
</div>
<nav class="sidebar-nav">
<router-link
v-for="item in menuItems"
:key="item.path"
:to="item.path"
class="nav-item"
:class="{ 'nav-item--active': route.path === item.path }"
>
<svg class="nav-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" v-html="item.icon"></svg>
<span class="nav-label">{{ item.label }}</span>
</router-link>
</nav>
<div class="sidebar-footer">
<span class="footer-role">{{ roleLabel }}</span>
<span class="footer-name">{{ auth.name }}</span>
</div>
<el-menu
:default-active="route.path"
router
background-color="#304156"
text-color="#bfcbd9"
active-text-color="#409EFF"
>
<el-menu-item v-for="item in menuItems" :key="item.path" :index="item.path">
<el-icon><component :is="item.icon" /></el-icon>
<span>{{ item.label }}</span>
</el-menu-item>
</el-menu>
</aside>
<!-- Main Area -->
<div class="main-area">
<header class="topbar">
<span class="greeting">{{ auth.isDirector ? '支局长' : auth.isLeader ? '分管领导' : '客户经理' }} {{ auth.name }}</span>
<div style="display:flex; align-items:center; gap:8px">
<el-button text @click="router.push('/m')">📱 移动端</el-button>
<el-button text @click="auth.logout(); router.push('/login')">退出</el-button>
<button class="collapse-btn" @click="collapsed = !collapsed" :title="collapsed ? '展开菜单' : '折叠菜单'">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline :points="collapsed ? '13 17 18 12 13 7' : '11 7 6 12 11 17'"></polyline>
</svg>
</button>
<div class="topbar-actions">
<button class="topbar-btn" @click="router.push('/m')" title="移动端">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="5" y="2" width="14" height="20" rx="2" ry="2"></rect>
<line x1="12" y1="18" x2="12.01" y2="18"></line>
</svg>
移动端
</button>
<button class="topbar-btn topbar-btn--logout" @click="auth.logout(); router.push('/login')">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
<polyline points="16 17 21 12 16 7"></polyline>
<line x1="21" y1="12" x2="9" y2="12"></line>
</svg>
退出
</button>
</div>
</header>
<main class="content">
@@ -60,42 +94,205 @@ const menuItems = computed(() => {
</template>
<style scoped>
/* ═══ Layout ═══ */
.desktop-layout {
display: flex;
height: 100vh;
overflow: hidden;
}
/* ═══ Sidebar ═══ */
.sidebar {
width: 220px;
background: #304156;
color: white;
width: 240px;
background: var(--ink);
color: #fff;
display: flex;
flex-direction: column;
flex-shrink: 0;
overflow: hidden;
transition: width 0.25s ease;
}
.logo {
padding: 20px 16px;
text-align: center;
border-bottom: 1px solid #4a5a6a;
.sidebar--collapsed {
width: 64px;
}
.logo h2 { margin: 0; font-size: 18px; color: #fff; }
.logo p { margin: 4px 0 0; font-size: 11px; color: #bfcbd9; }
.sidebar--collapsed .brand-sub,
.sidebar--collapsed .brand-rule,
.sidebar--collapsed .nav-label,
.sidebar--collapsed .footer-role,
.sidebar--collapsed .footer-name { display: none; }
.sidebar--collapsed .brand-title { font-size: 20px; text-align: center; }
.sidebar--collapsed .nav-item { justify-content: center; padding: 11px 0; }
.sidebar--collapsed .nav-icon { font-size: 20px; margin: 0; }
.sidebar--collapsed .sidebar-footer { text-align: center; padding: 10px; }
.sidebar-brand {
padding: 26px 22px 18px;
border-bottom: 1px solid rgba(255,255,255,0.06);
}
.brand-title {
margin: 0;
font-family: 'ZCOOL XiaoWei', STSong, Songti SC, serif;
font-size: 24px;
font-weight: 400;
letter-spacing: 0.12em;
color: #fff;
line-height: 1.2;
}
.brand-sub {
margin: 2px 0 0;
font-family: 'Noto Serif SC', STSong, serif;
font-size: 10px;
color: rgba(255,255,255,0.4);
letter-spacing: 0.1em;
}
.brand-rule {
width: 28px;
height: 3px;
background: var(--gold);
margin-top: 14px;
}
/* ═══ Navigation ═══ */
.sidebar-nav {
flex: 1;
padding: 12px 10px;
display: flex;
flex-direction: column;
gap: 2px;
}
.nav-item {
display: flex;
align-items: center;
gap: 10px;
padding: 11px 14px;
color: rgba(255,255,255,0.55);
text-decoration: none;
font-family: 'Noto Serif SC', STSong, serif;
font-size: 14px;
letter-spacing: 0.04em;
transition: all 0.22s;
border-left: 2px solid transparent;
}
.nav-item:hover {
color: rgba(255,255,255,0.85);
background: rgba(255,255,255,0.04);
}
.nav-item--active {
color: #fff;
background: rgba(255,255,255,0.06);
border-left-color: var(--gold);
}
.nav-icon {
font-size: 18px;
flex-shrink: 0;
}
.nav-label {
font-weight: 500;
}
/* ═══ Sidebar Footer ═══ */
.sidebar-footer {
padding: 14px 22px;
border-top: 1px solid rgba(255,255,255,0.06);
display: flex;
flex-direction: column;
gap: 2px;
}
.footer-role {
font-family: 'Noto Serif SC', STSong, serif;
font-size: 10px;
color: var(--gold);
letter-spacing: 0.1em;
}
.footer-name {
font-family: 'ZCOOL XiaoWei', STSong, serif;
font-size: 13px;
color: rgba(255,255,255,0.7);
letter-spacing: 0.04em;
}
/* ═══ Main Area ═══ */
.main-area {
flex: 1;
display: flex;
flex-direction: column;
background: #f0f2f5;
background: var(--paper);
min-width: 0;
}
/* ═══ Topbar ═══ */
.topbar {
background: white;
padding: 12px 24px;
background: var(--surface);
padding: 10px 24px;
display: flex;
justify-content: flex-end;
justify-content: space-between;
align-items: center;
gap: 16px;
box-shadow: 0 1px 4px rgba(0,0,0,0.05);
border-bottom: 1px solid var(--warm-border);
flex-shrink: 0;
}
.topbar-greeting {
font-family: 'ZCOOL XiaoWei', STSong, serif;
font-size: 14px;
color: var(--ink);
letter-spacing: 0.04em;
}
.collapse-btn {
display: flex; align-items: center; justify-content: center;
width: 28px; height: 28px;
background: none; border: none;
color: var(--warm-gray); cursor: pointer;
transition: all 0.2s; flex-shrink: 0; margin-right: 8px;
opacity: 0.5;
}
.collapse-btn:hover { color: var(--ink); opacity: 1; }
.topbar-actions {
display: flex;
align-items: center;
gap: 8px;
}
.topbar-btn {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 6px 12px;
background: none;
border: 1px solid var(--warm-border);
color: var(--warm-gray);
font-family: 'Noto Serif SC', STSong, serif;
font-size: 12px;
letter-spacing: 0.03em;
cursor: pointer;
transition: all 0.2s;
}
.topbar-btn:hover {
color: var(--ink);
border-color: var(--ink);
}
.topbar-btn--logout:hover {
color: var(--vermilion);
border-color: var(--vermilion);
}
/* ═══ Content ═══ */
.content {
flex: 1;
overflow-y: auto;
padding: 20px;
padding: 24px;
}
</style>