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