feat: 历史周报+图片预览+照片管理+导入优化+用户合并

新增:
- 历史周报: Dashboard/周报详情支持周选择器翻看往周,归档只读
- 图片预览: 全屏大图(移动端+PC端),点击遮罩关闭
- PC端照片管理: 编辑时可上传/删除照片
- 客户导入改为更新模式: 重名自动更新信息,显示操作明细
- 导入跳过原因: 旧周报导入也显示每条跳过原因

优化:
- 填报进度权限: 经理只看到自己,支局长/领导看全员
- 客户经理暖灰色hash标签列
- 用户去重合并(4组),数据完整迁移
- CLAUDE.md 更新到最新状态

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-23 16:59:31 +08:00
parent 8926475b20
commit aa3fbca710
15 changed files with 323 additions and 74 deletions
+45 -11
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { dashboardApi } from '@/api/dashboard'
@@ -10,13 +10,37 @@ const auth = useAuthStore()
const loading = ref(false)
const stats = ref({ week_visits: 0, work_plans: 0, mini_business: 0, key_visits: 0, week_start: '', week_end: '' })
const progress = ref<any[]>([])
const weekOffset = ref(0) // 0 = current week, -1 = last week, etc.
onMounted(async () => {
const isHistoricalWeek = computed(() => weekOffset.value < 0)
const weekPickerDate = computed({
get: () => {
const d = new Date()
d.setDate(d.getDate() + weekOffset.value * 7)
return d.toISOString().slice(0, 10)
},
set: (_val: string) => {} // placeholder, actual change via buttons
})
function changeWeek(delta: number) {
weekOffset.value += delta
loadData()
}
function goCurrentWeek() {
weekOffset.value = 0
loadData()
}
async function loadData() {
loading.value = true
try {
const refDate = new Date()
refDate.setDate(refDate.getDate() + weekOffset.value * 7)
const refStr = refDate.toISOString().slice(0, 10)
const [sRes, pRes] = await Promise.all([
dashboardApi.getStats(),
dashboardApi.getProgress(),
dashboardApi.getStats({ reference_date: refStr }),
dashboardApi.getProgress({ reference_date: refStr }),
])
stats.value = sRes.data
progress.value = pRes.data
@@ -25,7 +49,9 @@ onMounted(async () => {
} finally {
loading.value = false
}
})
}
onMounted(loadData)
function goWeeklyReport(managerId?: string) {
if (managerId) router.push({ path: '/weekly-report', query: { manager_id: managerId } })
@@ -44,7 +70,13 @@ function rowState(p: any): 'full' | 'catching' | 'missing' {
<!-- Editorial Page Header -->
<div class="page-head">
<h2 class="page-title">仪表盘</h2>
<p class="page-sub">{{ stats.week_start }} {{ stats.week_end }}</p>
<div class="week-nav">
<button class="week-nav-btn" @click="changeWeek(-1)" title="上一周"></button>
<span class="week-label">{{ stats.week_start }} {{ stats.week_end }}</span>
<button class="week-nav-btn" @click="changeWeek(1)" :disabled="weekOffset >= 0" title="下一周"></button>
<button v-if="isHistoricalWeek" class="week-nav-reset" @click="goCurrentWeek">回到本周</button>
<el-tag v-if="isHistoricalWeek" type="info" size="small" style="margin-left:8px">📦 历史归档 · 只读</el-tag>
</div>
<div class="page-rule"></div>
</div>
@@ -182,11 +214,13 @@ function rowState(p: any): 'full' | 'catching' | 'missing' {
font-size: 22px; font-weight: 400;
color: var(--ink); letter-spacing: 0.06em;
}
.page-sub {
margin: 2px 0 0;
font-family: 'JetBrains Mono', 'SF Mono', monospace;
font-size: 11px; color: var(--gold); letter-spacing: 0.08em;
}
.week-nav { display: flex; align-items: center; gap: 10px; margin: 6px 0 4px; }
.week-nav-btn { background: var(--surface); border: 1px solid var(--warm-border); padding: 4px 10px; cursor: pointer; color: var(--warm-gray); font-size: 12px; }
.week-nav-btn:hover:not(:disabled) { color: var(--ink); border-color: var(--ink); }
.week-nav-btn:disabled { opacity: 0.3; cursor: not-allowed; }
.week-label { font-family: 'JetBrains Mono', 'SF Mono', monospace; font-size: 12px; color: var(--ink); letter-spacing: 0.04em; }
.week-nav-reset { background: none; border: 1px solid var(--gold); color: var(--gold); padding: 4px 10px; cursor: pointer; font-size: 12px; }
.week-nav-reset:hover { background: var(--gold); color: #fff; }
.page-rule { width: 32px; height: 3px; background: var(--gold); margin-top: 12px; }
/* ═══ Stat Grid ═══ */