feat(M08-B): 补联调执行阶段看板
This commit is contained in:
@@ -159,6 +159,30 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="handoff-stage-board">
|
||||
<header>
|
||||
<div>
|
||||
<h3>执行阶段看板</h3>
|
||||
<span>{{ stageProgress.length }} 个现场阶段</span>
|
||||
</div>
|
||||
<el-button :icon="Copy" @click="copyStageSummary">复制阶段摘要</el-button>
|
||||
</header>
|
||||
<div class="handoff-stage-grid">
|
||||
<button
|
||||
v-for="item in stageProgress"
|
||||
:key="item.category"
|
||||
type="button"
|
||||
class="handoff-stage-card"
|
||||
:class="{ blocked: item.blocked > 0, ready: item.ready }"
|
||||
@click="category = item.category"
|
||||
>
|
||||
<span>{{ item.category }}</span>
|
||||
<strong>{{ item.verdict }}</strong>
|
||||
<em>{{ item.passed }}/{{ item.total }} 通过 · 证据 {{ item.evidenceRecorded }}/{{ item.total }}</em>
|
||||
<small>{{ item.nextFocus }}</small>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="handoff-owner-board">
|
||||
<header>
|
||||
<div>
|
||||
@@ -487,6 +511,31 @@ const categoryProgress = computed(() => categoryOptions
|
||||
passPercent: items.length ? Math.round((passed / items.length) * 100) : 0
|
||||
};
|
||||
}));
|
||||
const stageProgress = computed(() => categoryProgress.value.map((item) => {
|
||||
const items = checklist.filter((check) => check.category === item.category);
|
||||
const evidenceRecorded = items.filter((check) => draft[check.id].evidenceRef.trim()).length;
|
||||
const evidenceMissing = items.filter((check) => (
|
||||
draft[check.id].status !== 'SKIP' && !draft[check.id].evidenceRef.trim()
|
||||
)).length;
|
||||
const overdue = items.filter((check) => isOverdueDraft(draft[check.id])).length;
|
||||
const ready = item.pending === 0 && item.blocked === 0 && evidenceMissing === 0 && overdue === 0;
|
||||
const nextItem = items.find((check) => {
|
||||
const itemDraft = draft[check.id];
|
||||
return itemDraft.status === 'BLOCKED'
|
||||
|| itemDraft.status === 'PENDING'
|
||||
|| isOverdueDraft(itemDraft)
|
||||
|| (itemDraft.status !== 'SKIP' && !itemDraft.evidenceRef.trim());
|
||||
});
|
||||
return {
|
||||
...item,
|
||||
evidenceRecorded,
|
||||
evidenceMissing,
|
||||
overdue,
|
||||
ready,
|
||||
verdict: ready ? '可收口' : item.blocked > 0 ? '存在阻断' : '待补齐',
|
||||
nextFocus: nextItem ? `${nextItem.title} · ${draft[nextItem.id].owner || '未分派'}` : '无待处理缺口'
|
||||
};
|
||||
}));
|
||||
const ownerProgress = computed(() => {
|
||||
const groups = new Map<string, {
|
||||
ownerKey: string;
|
||||
@@ -721,6 +770,25 @@ async function copyOwnerSummary() {
|
||||
}
|
||||
}
|
||||
|
||||
async function copyStageSummary() {
|
||||
const header = [
|
||||
`联调批次:${sessionMeta.batchNo || '未填写'}`,
|
||||
`现场/门店:${sessionMeta.site || '未填写'}`,
|
||||
`负责人:${sessionMeta.coordinator || '未填写'}`,
|
||||
`执行时间:${sessionMeta.executedAt || '未填写'}`,
|
||||
`最后更新:${sessionMeta.updatedAt || '未记录'}`
|
||||
].join('\n');
|
||||
const content = stageProgress.value.map((item) => (
|
||||
`${item.category}:${item.verdict},通过 ${item.passed}/${item.total},阻断 ${item.blocked},待处理 ${item.pending},缺证据 ${item.evidenceMissing},逾期 ${item.overdue},下一关注 ${item.nextFocus}`
|
||||
)).join('\n');
|
||||
try {
|
||||
await navigator.clipboard.writeText(`${header}\n\n${content}`);
|
||||
ElMessage.success('阶段摘要已复制');
|
||||
} catch {
|
||||
ElMessage.warning('当前浏览器不允许自动复制,请使用归档包留存');
|
||||
}
|
||||
}
|
||||
|
||||
async function copyVerdict() {
|
||||
const verdict = [
|
||||
`联调批次:${sessionMeta.batchNo || '未填写'}`,
|
||||
@@ -807,6 +875,7 @@ function buildCloseoutArchive() {
|
||||
overdue: overdueItems.value.length
|
||||
},
|
||||
categoryProgress: categoryProgress.value,
|
||||
stageProgress: stageProgress.value,
|
||||
ownerProgress: ownerProgress.value,
|
||||
riskItems: riskItems.value.map((item) => serializeHandoffItem(item)),
|
||||
evidenceMissingItems: evidenceMissingItems.value.map((item) => serializeHandoffItem(item)),
|
||||
@@ -851,6 +920,9 @@ function buildHandoffReport() {
|
||||
const progress = categoryProgress.value.map((item) => (
|
||||
`- ${item.category}:${item.passed}/${item.total} 通过,阻断 ${item.blocked},待处理 ${item.pending},不适用 ${item.skipped}`
|
||||
));
|
||||
const stages = stageProgress.value.map((item) => (
|
||||
`- ${item.category}:${item.verdict},通过 ${item.passed}/${item.total},证据 ${item.evidenceRecorded}/${item.total},缺证据 ${item.evidenceMissing},逾期 ${item.overdue},下一关注 ${item.nextFocus}`
|
||||
));
|
||||
const owners = ownerProgress.value.map((item) => (
|
||||
`- ${item.ownerLabel}:总项 ${item.total},通过 ${item.passed},待处理 ${item.pending},阻断 ${item.blocked},缺证据 ${item.evidenceMissing},逾期 ${item.overdue}`
|
||||
));
|
||||
@@ -870,6 +942,9 @@ function buildHandoffReport() {
|
||||
'## 分类进度',
|
||||
...progress,
|
||||
'',
|
||||
'## 执行阶段看板',
|
||||
...stages,
|
||||
'',
|
||||
'## 负责人看板',
|
||||
...owners,
|
||||
'',
|
||||
|
||||
@@ -569,6 +569,73 @@ textarea {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.handoff-stage-board {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
border: 1px solid #d6e4dc;
|
||||
border-radius: 8px;
|
||||
background: #f8fcfa;
|
||||
}
|
||||
|
||||
.handoff-stage-board header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.handoff-stage-board h3 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.handoff-stage-board header span {
|
||||
color: #60708a;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.handoff-stage-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.handoff-stage-card {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
min-width: 0;
|
||||
padding: 10px;
|
||||
border: 1px solid #d6e4dc;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
color: #172033;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.handoff-stage-card.blocked {
|
||||
border-color: #f0c7c7;
|
||||
background: #fff7f7;
|
||||
}
|
||||
|
||||
.handoff-stage-card.ready {
|
||||
border-color: #b8dec4;
|
||||
background: #f3fbf5;
|
||||
}
|
||||
|
||||
.handoff-stage-card span,
|
||||
.handoff-stage-card em,
|
||||
.handoff-stage-card small {
|
||||
color: #60708a;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.handoff-stage-card strong {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.handoff-owner-board {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
@@ -803,6 +870,10 @@ textarea {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.handoff-stage-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.trend-row {
|
||||
grid-template-columns: 92px minmax(80px, 1fr);
|
||||
}
|
||||
@@ -826,6 +897,10 @@ textarea {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.handoff-stage-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.action-board {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user