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