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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user