feat(M08-B): 补联调证据记录

This commit is contained in:
Codex
2026-07-06 08:32:33 +08:00
parent a8c3fde95a
commit a9bb06f732
6 changed files with 60 additions and 22 deletions
@@ -6,6 +6,7 @@
<span>通过 <strong>{{ passedCount }}</strong></span>
<span>阻断 <strong>{{ blockedCount }}</strong></span>
<span>待处理 <strong>{{ pendingCount }}</strong></span>
<span>证据 <strong>{{ evidenceRecordedCount }}</strong></span>
<span>更新 <strong>{{ sessionMeta.updatedAt || '未记录' }}</strong></span>
</div>
<div class="toolbar-actions">
@@ -188,7 +189,17 @@
/>
</template>
</el-table-column>
<el-table-column prop="evidence" label="证据" min-width="210" />
<el-table-column prop="evidence" label="应留证据" min-width="210" />
<el-table-column label="证据记录" min-width="230">
<template #default="{ row }">
<el-input
v-model="draft[row.id].evidenceRef"
size="small"
placeholder="截图编号、流水号、回包或文件链接"
@change="persistDraft"
/>
</template>
</el-table-column>
</el-table>
</div>
</section>
@@ -213,6 +224,7 @@ interface HandoffDraft {
status: FieldStatus;
owner: string;
note: string;
evidenceRef: string;
}
interface SessionMeta {
@@ -303,6 +315,7 @@ const filteredChecklist = computed(() => checklist.filter((item) => {
const passedCount = computed(() => checklist.filter((item) => draft[item.id].status === 'PASS').length);
const blockedCount = computed(() => checklist.filter((item) => draft[item.id].status === 'BLOCKED').length);
const pendingCount = computed(() => checklist.filter((item) => draft[item.id].status === 'PENDING').length);
const evidenceRecordedCount = computed(() => checklist.filter((item) => draft[item.id].evidenceRef.trim()).length);
const blockedItems = computed(() => checklist.filter((item) => draft[item.id].status === 'BLOCKED'));
const checklistIds = new Set(checklist.map((item) => item.id));
const handoffReady = computed(() => pendingCount.value === 0 && blockedCount.value === 0);
@@ -329,7 +342,7 @@ function createInitialDraft() {
const saved = readSavedDraft();
return Object.fromEntries(checklist.map((item) => [
item.id,
saved[item.id] || { status: 'PENDING', owner: '', note: '' }
normalizeDraft(saved[item.id])
])) as Record<string, HandoffDraft>;
}
@@ -373,7 +386,7 @@ function touchSessionMeta() {
function resetDraft() {
for (const item of checklist) {
draft[item.id] = { status: 'PENDING', owner: '', note: '' };
draft[item.id] = normalizeDraft();
}
touchSessionMeta();
persistDraft();
@@ -398,7 +411,8 @@ async function copyBlockedSummary() {
].join('\n');
const content = blockedItems.value.map((item) => {
const itemDraft = draft[item.id];
return `[${item.category}] ${item.title} - ${itemDraft.owner || '未分派'} - ${itemDraft.note || item.expected}`;
const evidence = itemDraft.evidenceRef ? ` - 证据:${itemDraft.evidenceRef}` : '';
return `[${item.category}] ${item.title} - ${itemDraft.owner || '未分派'} - ${itemDraft.note || item.expected}${evidence}`;
}).join('\n');
try {
await navigator.clipboard.writeText(`${header}\n\n${content}`);
@@ -416,7 +430,7 @@ async function copyVerdict() {
`执行时间:${sessionMeta.executedAt || '未填写'}`,
`最后更新:${sessionMeta.updatedAt || '未记录'}`,
`结论:${handoffReady.value ? '可进入现场验收收口' : '仍不可收口'}`,
`统计:通过 ${passedCount.value},阻断 ${blockedCount.value},待处理 ${pendingCount.value}`
`统计:通过 ${passedCount.value},阻断 ${blockedCount.value},待处理 ${pendingCount.value},证据 ${evidenceRecordedCount.value}/${checklist.length}`
].join('\n');
try {
await navigator.clipboard.writeText(verdict);
@@ -428,13 +442,14 @@ async function copyVerdict() {
function exportChecklist() {
downloadCsv(`cleaning-field-handoff-${Date.now()}.csv`, [
['类别', '字段', '值', '', '', '', '', ''],
['session', '联调批次', sessionMeta.batchNo, '', '', '', '', ''],
['session', '现场/门店', sessionMeta.site, '', '', '', '', ''],
['session', '负责人', sessionMeta.coordinator, '', '', '', '', ''],
['session', '执行时间', sessionMeta.executedAt, '', '', '', '', ''],
['session', '最后更新', sessionMeta.updatedAt, '', '', '', '', ''],
['ID', '类别', '检查项', '期望', '状态', '负责人', '结果记录', '证据'],
['类别', '字段', '值', '', '', '', '', '', ''],
['session', '联调批次', sessionMeta.batchNo, '', '', '', '', '', ''],
['session', '现场/门店', sessionMeta.site, '', '', '', '', '', ''],
['session', '负责人', sessionMeta.coordinator, '', '', '', '', '', ''],
['session', '执行时间', sessionMeta.executedAt, '', '', '', '', '', ''],
['session', '最后更新', sessionMeta.updatedAt, '', '', '', '', '', ''],
['session', '证据记录数', `${evidenceRecordedCount.value}/${checklist.length}`, '', '', '', '', '', ''],
['ID', '类别', '检查项', '期望', '状态', '负责人', '结果记录', '应留证据', '证据记录'],
...checklist.map((item) => [
item.id,
item.category,
@@ -443,7 +458,8 @@ function exportChecklist() {
draft[item.id].status,
draft[item.id].owner,
draft[item.id].note,
item.evidence
item.evidence,
draft[item.id].evidenceRef
])
]);
}
@@ -493,7 +509,8 @@ async function importSnapshot(event: Event) {
draft[id] = {
status,
owner: value.owner || '',
note: value.note || ''
note: value.note || '',
evidenceRef: value.evidenceRef || ''
};
}
touchSessionMeta();
@@ -509,6 +526,15 @@ function isFieldStatus(value: unknown): value is FieldStatus {
return value === 'PENDING' || value === 'PASS' || value === 'BLOCKED' || value === 'SKIP';
}
function normalizeDraft(value?: Partial<HandoffDraft>): HandoffDraft {
return {
status: isFieldStatus(value?.status) ? value.status : 'PENDING',
owner: value?.owner || '',
note: value?.note || '',
evidenceRef: value?.evidenceRef || ''
};
}
function downloadCsv(filename: string, rows: string[][]) {
const content = rows.map((row) => row.map(csvCell).join(',')).join('\r\n');
downloadText(filename, `\uFEFF${content}`, 'text/csv;charset=utf-8');