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
+84 -52
View File
@@ -11,11 +11,9 @@ const remindLoading = ref(false)
const announceLoading = ref(false)
const dailyCheckLoading = ref(false)
// Import
const importDialogVisible = ref(false)
const importLoading = ref(false)
const importFile = ref<File | null>(null)
const importPreview = ref<any>(null)
const importLoading = ref(false)
const importResult = ref<any>(null)
onMounted(async () => {
@@ -29,32 +27,21 @@ async function handleRemind() {
if (!selectedUserIds.value.length) { ElMessage.warning('请选择要提醒的人员'); return }
remindLoading.value = true
try {
const res = await api.post('/wecom/remind', {
user_ids: selectedUserIds.value,
message: remindMessage.value || undefined,
})
const res = await api.post('/wecom/remind', { user_ids: selectedUserIds.value, message: remindMessage.value || undefined })
ElMessage.success(`已发送提醒给 ${res.data.sent_to}`)
} catch (e: any) {
ElMessage.error('发送失败')
} finally {
remindLoading.value = false
}
} catch (e: any) { ElMessage.error('发送失败') }
finally { remindLoading.value = false }
}
async function handleAnnouncement() {
if (!announcementContent.value) { ElMessage.warning('请输入公告内容'); return }
announceLoading.value = true
try {
const res = await api.post('/wecom/announcement', {
content: announcementContent.value,
})
const res = await api.post('/wecom/announcement', { content: announcementContent.value })
ElMessage.success(`公告已推送给 ${res.data.sent_to}`)
announcementContent.value = ''
} catch (e: any) {
ElMessage.error('推送失败')
} finally {
announceLoading.value = false
}
} catch (e: any) { ElMessage.error('推送失败') }
finally { announceLoading.value = false }
}
async function handleDailyCheck() {
@@ -62,18 +49,22 @@ async function handleDailyCheck() {
try {
const res = await api.post('/wecom/trigger-daily-check')
ElMessage.success(`已执行:${res.data.reported}/${res.data.total_managers} 人已填报`)
} catch (e: any) {
ElMessage.error('执行失败')
} finally {
dailyCheckLoading.value = false
}
} catch (e: any) { ElMessage.error('执行失败') }
finally { dailyCheckLoading.value = false }
}
function handleImportFile(e: Event) {
const target = e.target as HTMLInputElement
if (target.files?.[0]) {
importFile.value = target.files[0]
}
if (target.files?.[0]) importFile.value = target.files[0]
}
async function downloadWeeklyTemplate() {
try {
const res = await api.get('/import/template', { responseType: 'blob' })
const url = URL.createObjectURL(res.data)
const a = document.createElement('a'); a.href = url; a.download = 'weekly_report_template.xlsx'; a.click()
URL.revokeObjectURL(url)
} catch (e: any) { ElMessage.error('下载失败') }
}
async function handleImportPreview() {
@@ -82,26 +73,31 @@ async function handleImportPreview() {
const formData = new FormData()
formData.append('file', importFile.value)
try {
const res = await api.post('/import/weekly-report', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
})
const res = await api.post('/import/weekly-report', formData, { headers: { 'Content-Type': 'multipart/form-data' } })
importPreview.value = res.data.preview
importResult.value = res.data
} catch (e: any) {
ElMessage.error('解析失败')
} finally {
importLoading.value = false
}
} catch (e: any) { ElMessage.error('解析失败') }
finally { importLoading.value = false }
}
</script>
<template>
<div class="settings-page">
<h3>系统设置</h3>
<div class="page-head">
<h2 class="page-title">系统设置</h2>
<div class="page-rule"></div>
</div>
<!-- Manual Remind -->
<el-card class="setting-card">
<template #header>📢 手动催办</template>
<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)">
<path d="M22 17H2a3 3 0 0 0 3-3V9a7 7 0 0 1 14 0v5a3 3 0 0 0 3 3zm-8.27 4a2 2 0 0 1-3.46 0"></path>
</svg>
手动催办
</div>
</template>
<el-form>
<el-form-item label="选择人员">
<el-select v-model="selectedUserIds" multiple placeholder="选择客户经理" style="width:100%">
@@ -117,7 +113,15 @@ async function handleImportPreview() {
<!-- Announcement -->
<el-card class="setting-card">
<template #header>📣 全员公告</template>
<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)">
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path>
<path d="M13.73 21a2 2 0 0 1-3.46 0"></path>
</svg>
全员公告
</div>
</template>
<el-form>
<el-form-item label="公告内容">
<el-input v-model="announcementContent" type="textarea" :rows="3" placeholder="输入公告内容..." />
@@ -128,32 +132,47 @@ async function handleImportPreview() {
<!-- Daily Check -->
<el-card class="setting-card">
<template #header> 填报检查</template>
<p>手动触发今日填报检查通常每日 18:00 自动执行</p>
<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="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
</svg>
填报检查
</div>
</template>
<p style="color: var(--warm-gray); font-family: 'Noto Serif SC', STSong, serif;">手动触发今日填报检查通常每日 18:00 自动执行</p>
<el-button :loading="dailyCheckLoading" @click="handleDailyCheck">立即检查</el-button>
</el-card>
<!-- Import -->
<el-card class="setting-card">
<template #header>📥 Excel 数据导入</template>
<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)">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
Excel 数据导入
</div>
</template>
<el-form>
<el-form-item label="上传旧周报 Excel">
<input type="file" accept=".xlsx,.xls" @change="handleImportFile" />
<div style="display:flex;gap:8px;align-items:center">
<input type="file" accept=".xlsx,.xls" @change="handleImportFile" />
<el-button size="small" @click="downloadWeeklyTemplate">📋 下载模板</el-button>
</div>
</el-form-item>
<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>
<!-- Preview Results -->
<div v-if="importPreview" style="margin-top:16px">
<h4>文件预览</h4>
<h4 style="font-family: 'ZCOOL XiaoWei', STSong, serif; color: var(--ink)">文件预览</h4>
<el-table :data="Object.entries(importPreview)" stripe>
<el-table-column prop="0" label="Sheet" />
<el-table-column prop="1.row_count" label="数据行数" />
<el-table-column label="表头">
<template #default="{ row }">
{{ (row[1] as any).headers.join(', ') }}
</template>
<template #default="{ row }">{{ (row[1] as any).headers.join(', ') }}</template>
</el-table-column>
</el-table>
</div>
@@ -167,6 +186,19 @@ async function handleImportPreview() {
</template>
<style scoped>
.settings-page h3 { margin: 0 0 20px; }
.page-head { margin-bottom: 24px; }
.page-title {
margin: 0;
font-family: 'ZCOOL XiaoWei', STSong, serif;
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; }
.setting-card { margin-bottom: 16px; }
.card-header-title {
display: flex; align-items: center;
font-family: 'ZCOOL XiaoWei', STSong, serif;
font-size: 15px; color: var(--ink); letter-spacing: 0.05em;
}
</style>