deeafe239f
前端: - 新增 Pinia theme store,localStorage 持久化,data-theme 切换 - App.vue: :root 新增 --font-*/--sidebar-* CSS 变量;新增 :root[data-theme=light] 完整配色(极简黑白金 + Noto Sans SC) - DesktopLayout: 侧边栏 15 个变量化 + 顶栏主题切换按钮 - MobileLayout: 装饰色变量化 - Settings: 新增「界面主题」卡片 - 21 个组件 font-family 统一替换为 var(--font-*) - 新增 utils/managerColor.ts — 12 色调色板 + 自适应文字色 + 后端自定义色优先 - WorkPlans/MiniBusiness/KeyVisits/CustomerManage: 客户经理标签色统一走共享工具 - UserManage: 编辑弹窗新增颜色选择器(仅 manager) - LightBoard: 去标题星标图标 - ManagerWorkspace/WorkPlans/MiniBusiness/KeyVisits: 删除按钮改为实心红底白字 后端: - User 模型新增 color 列 + lifespan 自动迁移 - users API 支持 color 字段读写 - UserOut schema 新增 color 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: var(--font-heading);
|
||
font-size: 13px;
|
||
color: var(--ink);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.log-time {
|
||
font-family: var(--font-mono);
|
||
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>
|