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:
@@ -8,10 +8,23 @@ const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
|
||||
const tabs = [
|
||||
{ path: '/m', label: '首页', icon: 'HomeFilled' },
|
||||
{ path: '/m/visit/new', label: '今日拜访', icon: 'Edit' },
|
||||
{ path: '/m/note/new', label: '今日纪要', icon: 'Notebook' },
|
||||
{
|
||||
path: '/m',
|
||||
label: '首页',
|
||||
icon: 'home',
|
||||
},
|
||||
{
|
||||
path: '/m/visit/new',
|
||||
label: '拜访',
|
||||
icon: 'visit',
|
||||
},
|
||||
{
|
||||
path: '/m/note/new',
|
||||
label: '纪要',
|
||||
icon: 'note',
|
||||
},
|
||||
]
|
||||
|
||||
const activeTab = computed(() => {
|
||||
if (route.path === '/m') return '/m'
|
||||
if (route.path.includes('/visit')) return '/m/visit/new'
|
||||
@@ -22,71 +35,247 @@ const activeTab = computed(() => {
|
||||
|
||||
<template>
|
||||
<div class="mobile-layout">
|
||||
<!-- ═══ Magazine Header ═══ -->
|
||||
<header class="mobile-header">
|
||||
<span class="header-title">企迹</span>
|
||||
<div style="display:flex; align-items:center; gap:8px">
|
||||
<el-button text size="small" style="color:white" @click="router.push('/')">💻 PC</el-button>
|
||||
<div class="header-brand">
|
||||
<h1 class="header-title">企迹</h1>
|
||||
<span class="header-subtitle">政企周报</span>
|
||||
<div class="header-rule"></div>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button class="header-pc-btn" @click="router.push('/')" title="PC版">
|
||||
<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="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
|
||||
<line x1="8" y1="21" x2="16" y2="21"></line>
|
||||
<line x1="12" y1="17" x2="12" y2="21"></line>
|
||||
</svg>
|
||||
</button>
|
||||
<span class="header-user">{{ auth.name }}</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- ═══ Main Content ═══ -->
|
||||
<main class="mobile-main">
|
||||
<!-- Gold corner accent (decorative) -->
|
||||
<div class="page-accent"></div>
|
||||
<router-view />
|
||||
</main>
|
||||
|
||||
<!-- ═══ Floating Pill TabBar ═══ -->
|
||||
<nav class="mobile-tabbar">
|
||||
<div
|
||||
v-for="tab in tabs" :key="tab.path"
|
||||
class="tab-item" :class="{ active: activeTab === tab.path }"
|
||||
@click="router.push(tab.path)"
|
||||
>
|
||||
<el-icon><component :is="tab.icon" /></el-icon>
|
||||
<span>{{ tab.label }}</span>
|
||||
<div class="tabbar-pill">
|
||||
<div
|
||||
v-for="tab in tabs"
|
||||
:key="tab.path"
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === tab.path }"
|
||||
@click="router.push(tab.path)"
|
||||
>
|
||||
<!-- Home icon -->
|
||||
<svg v-if="tab.icon === 'home'" class="tab-icon" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
|
||||
<polyline points="9 22 9 12 15 12 15 22"></polyline>
|
||||
</svg>
|
||||
<!-- Visit icon -->
|
||||
<svg v-else-if="tab.icon === 'visit'" class="tab-icon" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path>
|
||||
<rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect>
|
||||
<line x1="12" y1="11" x2="12" y2="17"></line>
|
||||
<line x1="9" y1="14" x2="15" y2="14"></line>
|
||||
</svg>
|
||||
<!-- Note icon -->
|
||||
<svg v-else-if="tab.icon === 'note'" class="tab-icon" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||
<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>
|
||||
<polyline points="10 9 9 9 8 9"></polyline>
|
||||
</svg>
|
||||
<span class="tab-label">{{ tab.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* ═══ Layout ═══ */
|
||||
.mobile-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background: #f5f7fa;
|
||||
height: 100dvh;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
background: var(--paper);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* ═══ Header — Editorial Style ═══ */
|
||||
.mobile-header {
|
||||
background: linear-gradient(135deg, #1a73e8, #4080ff);
|
||||
color: white;
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
align-items: flex-start;
|
||||
padding: 18px 20px 10px;
|
||||
background: var(--paper);
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header-brand {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
margin: 0;
|
||||
font-family: 'ZCOOL XiaoWei', STSong, Songti SC, serif;
|
||||
font-size: 26px;
|
||||
font-weight: 400;
|
||||
color: var(--ink);
|
||||
letter-spacing: 0.08em;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
font-family: 'Noto Serif SC', STSong, Songti SC, serif;
|
||||
font-size: 11px;
|
||||
color: var(--warm-gray);
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
margin-left: 1px;
|
||||
}
|
||||
|
||||
.header-rule {
|
||||
width: 28px;
|
||||
height: 3px;
|
||||
background: var(--gold);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.header-pc-btn {
|
||||
background: none;
|
||||
border: 1px solid var(--warm-border);
|
||||
border-radius: 2px;
|
||||
padding: 4px 6px;
|
||||
cursor: pointer;
|
||||
color: var(--warm-gray);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: all var(--transition);
|
||||
}
|
||||
|
||||
.header-pc-btn:hover {
|
||||
color: var(--ink);
|
||||
border-color: var(--ink);
|
||||
}
|
||||
|
||||
.header-user {
|
||||
font-family: 'Noto Serif SC', STSong, serif;
|
||||
font-size: 13px;
|
||||
color: var(--warm-gray);
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
/* ═══ Main Scroll Area ═══ */
|
||||
.mobile-main {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 12px;
|
||||
overflow-x: hidden;
|
||||
padding: 0 16px 20px;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Decorative accent — subtle diagonal line in corner */
|
||||
.page-accent {
|
||||
position: fixed;
|
||||
top: 60px;
|
||||
right: 0;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background: linear-gradient(135deg, transparent 60%, rgba(196,147,74,0.06) 60%);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* ═══ Floating Pill TabBar ═══ */
|
||||
.mobile-tabbar {
|
||||
display: flex;
|
||||
background: white;
|
||||
border-top: 1px solid #e4e7ed;
|
||||
padding: 6px 0;
|
||||
justify-content: center;
|
||||
padding: 0 16px 12px;
|
||||
padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px));
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
|
||||
.tabbar-pill {
|
||||
display: flex;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--warm-border);
|
||||
border-radius: 40px;
|
||||
padding: 6px 8px;
|
||||
box-shadow: 0 4px 18px rgba(28,55,56,0.08);
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 6px 0;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
gap: 6px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 32px;
|
||||
color: var(--warm-gray);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
white-space: nowrap;
|
||||
font-family: 'Noto Serif SC', STSong, serif;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.tab-item:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
color: #1a73e8;
|
||||
background: var(--ink);
|
||||
color: #fff;
|
||||
box-shadow: 0 2px 8px rgba(28,55,56,0.25);
|
||||
}
|
||||
.tab-item .el-icon {
|
||||
font-size: 22px;
|
||||
margin-bottom: 2px;
|
||||
|
||||
.tab-item.active .tab-icon {
|
||||
stroke: #fff;
|
||||
}
|
||||
|
||||
.tab-icon {
|
||||
flex-shrink: 0;
|
||||
transition: stroke 0.25s;
|
||||
}
|
||||
|
||||
.tab-label {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Active tab has gold accent dot */
|
||||
.tab-item.active .tab-label::after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
background: var(--gold);
|
||||
border-radius: 50%;
|
||||
margin-left: 6px;
|
||||
vertical-align: middle;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user