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