Files
qiji/frontend/src/components/EditLogPanel.vue
T
v6ole deeafe239f feat: 双主题系统 — 编辑风 + 极简亮色
前端:
- 新增 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>
2026-07-06 16:03:13 +08:00

197 lines
4.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>