feat: 信息架构重组 + 变更追踪 + 图片预览增强

- 信息架构重组: 周报精简为拜访+纪要两个Tab,工作计划/商机/要客独立为侧边栏「工作」分组下的独立页面
- 侧边栏分组: 汇总/工作/管理三层分组,仪表盘四卡可点击跳转
- 变更追踪(edit_log): 5张表新增JSONB edit_log列,POST创建/PUT diff自动记录,编辑弹窗变更时间轴,表格🕐编辑标记
- 图片预览增强: ImagePreview统一组件,支持适应页面/缩放/拖拽平移/滚轮缩放/键盘快捷键
- 修复客户导入500错误(errors变量未初始化)
- 移除工作计划/商机/要客页面冗余编辑按钮

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-24 08:28:46 +08:00
parent aa3fbca710
commit bf67e0575f
27 changed files with 1332 additions and 117 deletions
+196
View File
@@ -0,0 +1,196 @@
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps<{
editLog: Array<{ editor: string; time: string; changes: Record<string, [string, string]>; reason?: string }>
}>()
const fieldLabels: Record<string, string> = {
customer_id: '客户单位',
customer_name: '客户名称',
visit_date: '拜访日期',
note_date: '日期',
visit_method: '拜访方式',
time_range: '时间范围',
visitor_name: '拜访人',
visitor_phone: '电话',
communication_content: '沟通内容',
customer_demand: '客户需求',
category: '分类',
content: '工作内容',
plan_content: '计划内容',
plan_date: '计划时间',
status: '状态',
product_type: '产品类型',
amount: '金额',
follow_up_detail: '跟进内容',
expected_revenue_date: '预计列收',
urgency_level: '紧急度',
description: '描述',
progress_status: '进展',
planned_date: '计划日期',
planned_visitor: '拜访人',
visit_target: '拜访对象',
}
const hasHistory = computed(() => (props.editLog || []).length > 0)
function formatTime(t: string) {
try {
const d = new Date(t)
return d.toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })
} catch { return t }
}
function labelFor(field: string) {
return fieldLabels[field] || field
}
</script>
<template>
<el-collapse v-if="hasHistory" style="margin-top:16px">
<el-collapse-item title="变更记录" name="1">
<div class="log-timeline">
<div v-for="(entry, idx) in editLog" :key="idx" class="log-entry"
:class="{ 'log-entry--create': !entry.changes || Object.keys(entry.changes).length === 0 }">
<div class="log-dot"></div>
<div class="log-body">
<div class="log-meta">
<span class="log-editor">{{ entry.editor }}</span>
<span class="log-time">{{ formatTime(entry.time) }}</span>
<el-tag v-if="!entry.changes || Object.keys(entry.changes).length === 0" size="small" type="info" effect="plain">创建</el-tag>
</div>
<div v-if="entry.changes && Object.keys(entry.changes).length > 0" class="log-changes">
<div v-for="(vals, field) in entry.changes" :key="field" class="log-change">
<span class="change-label">{{ labelFor(field) }}</span>
<span class="change-old">{{ vals[0] || '(空)' }}</span>
<span class="change-arrow"></span>
<span class="change-new">{{ vals[1] || '(空)' }}</span>
</div>
</div>
<div v-if="entry.reason" class="log-reason">
<span class="reason-label">原因</span>{{ entry.reason }}
</div>
</div>
</div>
</div>
</el-collapse-item>
</el-collapse>
</template>
<style scoped>
.log-timeline {
padding: 4px 0;
}
.log-entry {
display: flex;
gap: 10px;
padding: 10px 0;
border-left: 2px solid var(--warm-border);
margin-left: 6px;
padding-left: 16px;
}
.log-entry:first-child {
border-left-color: var(--gold);
}
.log-entry--create {
opacity: 0.6;
}
.log-dot {
width: 8px; height: 8px;
background: var(--warm-border);
margin-left: -21px;
margin-top: 5px;
flex-shrink: 0;
}
.log-entry:first-child .log-dot {
background: var(--gold);
}
.log-body {
flex: 1;
min-width: 0;
}
.log-meta {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.log-editor {
font-family: 'ZCOOL XiaoWei', STSong, serif;
font-size: 13px;
color: var(--ink);
font-weight: 600;
}
.log-time {
font-family: 'JetBrains Mono', 'SF Mono', monospace;
font-size: 11px;
color: var(--c-text-muted);
}
.log-changes {
display: flex;
flex-direction: column;
gap: 2px;
margin-top: 4px;
}
.log-change {
font-size: 12px;
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.change-label {
font-weight: 600;
color: var(--ink);
min-width: 56px;
}
.change-old {
color: var(--vermilion);
text-decoration: line-through;
text-decoration-color: var(--vermilion);
text-decoration-thickness: 1px;
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.change-arrow {
color: var(--gold);
font-weight: 600;
}
.change-new {
color: var(--sage);
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.log-reason {
margin-top: 4px;
font-size: 12px;
color: var(--c-text-secondary);
font-style: italic;
}
.reason-label {
color: var(--warm-gray);
font-weight: 500;
}
</style>