feat(M08-B): 补联调负责人看板
This commit is contained in:
@@ -159,6 +159,30 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="handoff-owner-board">
|
||||
<header>
|
||||
<div>
|
||||
<h3>负责人看板</h3>
|
||||
<span>{{ ownerProgress.length }} 个责任分组</span>
|
||||
</div>
|
||||
<el-button :icon="Copy" @click="copyOwnerSummary">复制负责人摘要</el-button>
|
||||
</header>
|
||||
<div class="handoff-owner-grid">
|
||||
<button
|
||||
v-for="item in ownerProgress"
|
||||
:key="item.ownerKey"
|
||||
type="button"
|
||||
class="handoff-owner-card"
|
||||
:class="{ active: ownerFilter === item.ownerKey }"
|
||||
@click="ownerFilter = item.ownerKey"
|
||||
>
|
||||
<strong>{{ item.ownerLabel }}</strong>
|
||||
<span>总项 {{ item.total }} / 通过 {{ item.passed }}</span>
|
||||
<span>待处理 {{ item.pending }} · 阻断 {{ item.blocked }}</span>
|
||||
<span>缺证据 {{ item.evidenceMissing }} · 逾期 {{ item.overdue }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="handoff-verdict" :class="{ ready: handoffReady }">
|
||||
<div>
|
||||
<h3>{{ handoffReady ? '可进入现场验收收口' : '仍不可收口' }}</h3>
|
||||
@@ -463,6 +487,44 @@ const categoryProgress = computed(() => categoryOptions
|
||||
passPercent: items.length ? Math.round((passed / items.length) * 100) : 0
|
||||
};
|
||||
}));
|
||||
const ownerProgress = computed(() => {
|
||||
const groups = new Map<string, {
|
||||
ownerKey: string;
|
||||
ownerLabel: string;
|
||||
total: number;
|
||||
passed: number;
|
||||
pending: number;
|
||||
blocked: number;
|
||||
evidenceMissing: number;
|
||||
overdue: number;
|
||||
}>();
|
||||
for (const item of checklist) {
|
||||
const itemDraft = draft[item.id];
|
||||
const owner = itemDraft.owner.trim();
|
||||
const ownerKey = owner || '__UNASSIGNED__';
|
||||
const current = groups.get(ownerKey) || {
|
||||
ownerKey,
|
||||
ownerLabel: owner || '未分派',
|
||||
total: 0,
|
||||
passed: 0,
|
||||
pending: 0,
|
||||
blocked: 0,
|
||||
evidenceMissing: 0,
|
||||
overdue: 0
|
||||
};
|
||||
current.total += 1;
|
||||
if (itemDraft.status === 'PASS') current.passed += 1;
|
||||
if (itemDraft.status === 'PENDING') current.pending += 1;
|
||||
if (itemDraft.status === 'BLOCKED') current.blocked += 1;
|
||||
if (itemDraft.status !== 'SKIP' && !itemDraft.evidenceRef.trim()) current.evidenceMissing += 1;
|
||||
if (isOverdueDraft(itemDraft)) current.overdue += 1;
|
||||
groups.set(ownerKey, current);
|
||||
}
|
||||
return Array.from(groups.values()).sort((a, b) => (
|
||||
(b.blocked + b.pending + b.evidenceMissing + b.overdue)
|
||||
- (a.blocked + a.pending + a.evidenceMissing + a.overdue)
|
||||
) || a.ownerLabel.localeCompare(b.ownerLabel, 'zh-Hans-CN'));
|
||||
});
|
||||
|
||||
function createInitialDraft() {
|
||||
const saved = readSavedDraft();
|
||||
@@ -640,6 +702,25 @@ async function copyOverdueSummary() {
|
||||
}
|
||||
}
|
||||
|
||||
async function copyOwnerSummary() {
|
||||
const header = [
|
||||
`联调批次:${sessionMeta.batchNo || '未填写'}`,
|
||||
`现场/门店:${sessionMeta.site || '未填写'}`,
|
||||
`负责人:${sessionMeta.coordinator || '未填写'}`,
|
||||
`执行时间:${sessionMeta.executedAt || '未填写'}`,
|
||||
`最后更新:${sessionMeta.updatedAt || '未记录'}`
|
||||
].join('\n');
|
||||
const content = ownerProgress.value.map((item) => (
|
||||
`${item.ownerLabel}:总项 ${item.total},通过 ${item.passed},待处理 ${item.pending},阻断 ${item.blocked},缺证据 ${item.evidenceMissing},逾期 ${item.overdue}`
|
||||
)).join('\n');
|
||||
try {
|
||||
await navigator.clipboard.writeText(`${header}\n\n${content}`);
|
||||
ElMessage.success('负责人摘要已复制');
|
||||
} catch {
|
||||
ElMessage.warning('当前浏览器不允许自动复制,请使用归档包留存');
|
||||
}
|
||||
}
|
||||
|
||||
async function copyVerdict() {
|
||||
const verdict = [
|
||||
`联调批次:${sessionMeta.batchNo || '未填写'}`,
|
||||
@@ -726,6 +807,7 @@ function buildCloseoutArchive() {
|
||||
overdue: overdueItems.value.length
|
||||
},
|
||||
categoryProgress: categoryProgress.value,
|
||||
ownerProgress: ownerProgress.value,
|
||||
riskItems: riskItems.value.map((item) => serializeHandoffItem(item)),
|
||||
evidenceMissingItems: evidenceMissingItems.value.map((item) => serializeHandoffItem(item)),
|
||||
overdueItems: overdueItems.value.map((item) => serializeHandoffItem(item)),
|
||||
@@ -769,6 +851,9 @@ function buildHandoffReport() {
|
||||
const progress = categoryProgress.value.map((item) => (
|
||||
`- ${item.category}:${item.passed}/${item.total} 通过,阻断 ${item.blocked},待处理 ${item.pending},不适用 ${item.skipped}`
|
||||
));
|
||||
const owners = ownerProgress.value.map((item) => (
|
||||
`- ${item.ownerLabel}:总项 ${item.total},通过 ${item.passed},待处理 ${item.pending},阻断 ${item.blocked},缺证据 ${item.evidenceMissing},逾期 ${item.overdue}`
|
||||
));
|
||||
const blockers = blockedItems.value.length
|
||||
? blockedItems.value.map((item) => formatReportItem(item))
|
||||
: ['- 无'];
|
||||
@@ -785,6 +870,9 @@ function buildHandoffReport() {
|
||||
'## 分类进度',
|
||||
...progress,
|
||||
'',
|
||||
'## 负责人看板',
|
||||
...owners,
|
||||
'',
|
||||
'## 阻断项',
|
||||
...blockers,
|
||||
'',
|
||||
|
||||
@@ -569,6 +569,61 @@ textarea {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.handoff-owner-board {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
border: 1px solid #d8e2ef;
|
||||
border-radius: 8px;
|
||||
background: #f8fbff;
|
||||
}
|
||||
|
||||
.handoff-owner-board header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.handoff-owner-board h3 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.handoff-owner-board header span {
|
||||
color: #60708a;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.handoff-owner-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.handoff-owner-card {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
min-width: 0;
|
||||
padding: 10px;
|
||||
border: 1px solid #d8e2ef;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
color: #172033;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.handoff-owner-card.active {
|
||||
border-color: #2f6fed;
|
||||
background: #f2f6ff;
|
||||
}
|
||||
|
||||
.handoff-owner-card span {
|
||||
color: #60708a;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.handoff-verdict {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -744,6 +799,10 @@ textarea {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.handoff-owner-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.trend-row {
|
||||
grid-template-columns: 92px minmax(80px, 1fr);
|
||||
}
|
||||
@@ -763,6 +822,10 @@ textarea {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.handoff-owner-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.action-board {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user