feat(M08-B): 补现场执行记录追溯

This commit is contained in:
Codex
2026-08-10 09:46:11 +08:00
parent 01e86e6856
commit 90e230c153
3 changed files with 306 additions and 11 deletions
@@ -350,14 +350,86 @@
/> />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="执行记录" width="130" fixed="right">
<template #default="{ row }">
<el-button :icon="History" size="small" @click="openExecutionHistory(row)">
记录 {{ executionHistory[row.id].length }}
</el-button>
</template>
</el-table-column>
</el-table> </el-table>
</div> </div>
<el-dialog
v-model="executionDialogVisible"
:title="selectedExecutionItem ? `${selectedExecutionItem.title} · 现场执行记录` : '现场执行记录'"
width="min(760px, 94vw)"
>
<template v-if="selectedExecutionItem">
<el-alert
:title="selectedExecutionItem.expected"
type="info"
show-icon
:closable="false"
/>
<div v-if="selectedExecutionRecords.length" class="handoff-execution-list">
<article v-for="record in selectedExecutionRecords" :key="record.id">
<header>
<el-tag :type="executionStatusTag(record.status)">{{ statusText(record.status) }}</el-tag>
<strong>{{ record.executedAt }}</strong>
<span>{{ record.operator }}</span>
</header>
<p>{{ record.result }}</p>
<small>证据{{ record.evidenceRef || '未记录' }}</small>
</article>
</div>
<el-empty v-else description="还没有现场执行记录" :image-size="72" />
<el-form class="handoff-execution-form" label-position="top">
<el-form-item label="执行时间">
<el-input v-model="executionForm.executedAt" placeholder="YYYY-MM-DD HH:mm" />
</el-form-item>
<el-form-item label="执行人">
<el-input v-model="executionForm.operator" placeholder="现场执行人" />
</el-form-item>
<el-form-item label="结果状态">
<el-select v-model="executionForm.status">
<el-option label="待处理" value="PENDING" />
<el-option label="通过" value="PASS" />
<el-option label="阻断" value="BLOCKED" />
<el-option label="不适用" value="SKIP" />
</el-select>
</el-form-item>
<el-form-item label="执行结果" class="handoff-execution-result">
<el-input
v-model="executionForm.result"
type="textarea"
:rows="3"
placeholder="记录请求、流水、设备回包、失败原因或重试结果"
/>
</el-form-item>
<el-form-item label="证据记录" class="handoff-execution-result">
<el-input v-model="executionForm.evidenceRef" placeholder="截图编号、流水号、回包或文件链接" />
</el-form-item>
</el-form>
</template>
<template #footer>
<el-button @click="executionDialogVisible = false">关闭</el-button>
<el-button
type="primary"
:icon="Plus"
:disabled="!canAppendExecution"
@click="appendExecutionRecord"
>
追加记录
</el-button>
</template>
</el-dialog>
</section> </section>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, reactive, ref } from 'vue'; import { computed, reactive, ref } from 'vue';
import { Check, Copy, Download, FileDown, FileUp, FilterX, ListChecks, RotateCcw, Search } from '@lucide/vue'; import { Check, Copy, Download, FileDown, FileUp, FilterX, History, ListChecks, Plus, RotateCcw, Search } from '@lucide/vue';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
type FieldStatus = 'PENDING' | 'PASS' | 'BLOCKED' | 'SKIP'; type FieldStatus = 'PENDING' | 'PASS' | 'BLOCKED' | 'SKIP';
@@ -389,13 +461,32 @@ interface SessionMeta {
updatedAt: string; updatedAt: string;
} }
interface HandoffExecution {
id: string;
executedAt: string;
operator: string;
status: FieldStatus;
result: string;
evidenceRef: string;
}
const storageKey = 'qipai.cleaning.field-handoff'; const storageKey = 'qipai.cleaning.field-handoff';
const sessionStorageKey = 'qipai.cleaning.field-handoff.session'; const sessionStorageKey = 'qipai.cleaning.field-handoff.session';
const executionStorageKey = 'qipai.cleaning.field-handoff.executions';
const category = ref<'全部' | HandoffItem['category']>('全部'); const category = ref<'全部' | HandoffItem['category']>('全部');
const statusFilter = ref<'ALL' | FieldStatus>('ALL'); const statusFilter = ref<'ALL' | FieldStatus>('ALL');
const ownerFilter = ref('ALL'); const ownerFilter = ref('ALL');
const keywordFilter = ref(''); const keywordFilter = ref('');
const snapshotInput = ref<HTMLInputElement | null>(null); const snapshotInput = ref<HTMLInputElement | null>(null);
const executionDialogVisible = ref(false);
const selectedExecutionItem = ref<HandoffItem | null>(null);
const executionForm = reactive<Omit<HandoffExecution, 'id'>>({
executedAt: '',
operator: '',
status: 'PASS',
result: '',
evidenceRef: ''
});
const bulkForm = reactive<{ status: FieldStatus; owner: string; deadline: string; note: string }>({ const bulkForm = reactive<{ status: FieldStatus; owner: string; deadline: string; note: string }>({
status: 'PASS', status: 'PASS',
owner: '', owner: '',
@@ -463,6 +554,7 @@ const checklist: HandoffItem[] = [
const categoryOptions = ['全部', '商户', '微信', '转账', '硬件']; const categoryOptions = ['全部', '商户', '微信', '转账', '硬件'];
const draft = reactive<Record<string, HandoffDraft>>(createInitialDraft()); const draft = reactive<Record<string, HandoffDraft>>(createInitialDraft());
const sessionMeta = reactive<SessionMeta>(readSessionMeta()); const sessionMeta = reactive<SessionMeta>(readSessionMeta());
const executionHistory = reactive<Record<string, HandoffExecution[]>>(createExecutionHistory());
const filteredChecklist = computed(() => checklist.filter((item) => { const filteredChecklist = computed(() => checklist.filter((item) => {
const categoryMatched = category.value === '全部' || item.category === category.value; const categoryMatched = category.value === '全部' || item.category === category.value;
@@ -495,7 +587,14 @@ function matchesKeyword(item: HandoffItem) {
itemDraft.owner, itemDraft.owner,
itemDraft.deadline, itemDraft.deadline,
itemDraft.note, itemDraft.note,
itemDraft.evidenceRef itemDraft.evidenceRef,
...executionHistory[item.id].flatMap((record) => [
record.executedAt,
record.operator,
record.status,
record.result,
record.evidenceRef
])
].some((value) => String(value).toLowerCase().includes(keyword)); ].some((value) => String(value).toLowerCase().includes(keyword));
} }
@@ -506,6 +605,8 @@ const passedCount = computed(() => checklist.filter((item) => draft[item.id].sta
const blockedCount = computed(() => checklist.filter((item) => draft[item.id].status === 'BLOCKED').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 pendingCount = computed(() => checklist.filter((item) => draft[item.id].status === 'PENDING').length);
const evidenceRecordedCount = computed(() => checklist.filter((item) => draft[item.id].evidenceRef.trim()).length); const evidenceRecordedCount = computed(() => checklist.filter((item) => draft[item.id].evidenceRef.trim()).length);
const executedItemCount = computed(() => checklist.filter((item) => executionHistory[item.id].length > 0).length);
const totalExecutionCount = computed(() => checklist.reduce((total, item) => total + executionHistory[item.id].length, 0));
const evidenceMissingItems = computed(() => checklist.filter((item) => ( const evidenceMissingItems = computed(() => checklist.filter((item) => (
draft[item.id].status !== 'SKIP' && !draft[item.id].evidenceRef.trim() draft[item.id].status !== 'SKIP' && !draft[item.id].evidenceRef.trim()
))); )));
@@ -524,6 +625,9 @@ const missingCloseoutFields = computed(() => {
const fields: string[] = []; const fields: string[] = [];
if (!handoffReady.value) fields.push('检查项状态'); if (!handoffReady.value) fields.push('检查项状态');
if (evidenceMissingCount.value > 0) fields.push('现场证据'); if (evidenceMissingCount.value > 0) fields.push('现场证据');
if (checklist.some((item) => draft[item.id].status !== 'SKIP' && executionHistory[item.id].length === 0)) {
fields.push('现场执行记录');
}
if (overdueItems.value.length > 0) fields.push('逾期检查项'); if (overdueItems.value.length > 0) fields.push('逾期检查项');
if (!sessionMeta.acceptor.trim()) fields.push('验收人'); if (!sessionMeta.acceptor.trim()) fields.push('验收人');
if (!sessionMeta.closeoutVerdict.trim()) fields.push('收口结论'); if (!sessionMeta.closeoutVerdict.trim()) fields.push('收口结论');
@@ -611,6 +715,15 @@ const ownerProgress = computed(() => {
- (a.blocked + a.pending + a.evidenceMissing + a.overdue) - (a.blocked + a.pending + a.evidenceMissing + a.overdue)
) || a.ownerLabel.localeCompare(b.ownerLabel, 'zh-Hans-CN')); ) || a.ownerLabel.localeCompare(b.ownerLabel, 'zh-Hans-CN'));
}); });
const selectedExecutionRecords = computed(() => (
selectedExecutionItem.value ? executionHistory[selectedExecutionItem.value.id] : []
));
const canAppendExecution = computed(() => Boolean(
selectedExecutionItem.value
&& executionForm.executedAt.trim()
&& executionForm.operator.trim()
&& executionForm.result.trim()
));
function createInitialDraft() { function createInitialDraft() {
const saved = readSavedDraft(); const saved = readSavedDraft();
@@ -620,6 +733,22 @@ function createInitialDraft() {
])) as Record<string, HandoffDraft>; ])) as Record<string, HandoffDraft>;
} }
function createExecutionHistory() {
const saved = readExecutionHistory();
return Object.fromEntries(checklist.map((item) => [
item.id,
normalizeExecutionRecords(saved[item.id])
])) as Record<string, HandoffExecution[]>;
}
function readExecutionHistory() {
try {
return JSON.parse(localStorage.getItem(executionStorageKey) || '{}') as Record<string, unknown>;
} catch {
return {};
}
}
function readSavedDraft() { function readSavedDraft() {
try { try {
return JSON.parse(localStorage.getItem(storageKey) || '{}') as Record<string, HandoffDraft>; return JSON.parse(localStorage.getItem(storageKey) || '{}') as Record<string, HandoffDraft>;
@@ -634,6 +763,12 @@ function persistDraft() {
localStorage.setItem(storageKey, JSON.stringify(draft)); localStorage.setItem(storageKey, JSON.stringify(draft));
} }
function persistExecutionHistory() {
touchSessionMeta();
localStorage.setItem(sessionStorageKey, JSON.stringify(sessionMeta));
localStorage.setItem(executionStorageKey, JSON.stringify(executionHistory));
}
function readSessionMeta(): SessionMeta { function readSessionMeta(): SessionMeta {
try { try {
const saved = JSON.parse(localStorage.getItem(sessionStorageKey) || '{}') as Partial<SessionMeta>; const saved = JSON.parse(localStorage.getItem(sessionStorageKey) || '{}') as Partial<SessionMeta>;
@@ -701,10 +836,12 @@ function parseDeadline(value: string) {
function resetDraft() { function resetDraft() {
for (const item of checklist) { for (const item of checklist) {
draft[item.id] = normalizeDraft(); draft[item.id] = normalizeDraft();
executionHistory[item.id] = [];
} }
ownerFilter.value = 'ALL'; ownerFilter.value = 'ALL';
touchSessionMeta(); touchSessionMeta();
persistDraft(); persistDraft();
persistExecutionHistory();
} }
function clearHandoffFilters() { function clearHandoffFilters() {
@@ -724,6 +861,60 @@ function applyBulkMark() {
persistDraft(); persistDraft();
} }
function openExecutionHistory(item: HandoffItem) {
selectedExecutionItem.value = item;
Object.assign(executionForm, {
executedAt: formatLocalMinute(new Date()),
operator: draft[item.id].owner.trim() || sessionMeta.coordinator.trim(),
status: draft[item.id].status === 'PENDING' ? 'PASS' : draft[item.id].status,
result: '',
evidenceRef: ''
});
executionDialogVisible.value = true;
}
function appendExecutionRecord() {
const item = selectedExecutionItem.value;
if (!item || !canAppendExecution.value) return;
const record: HandoffExecution = {
id: createExecutionId(),
executedAt: executionForm.executedAt.trim(),
operator: executionForm.operator.trim(),
status: executionForm.status,
result: executionForm.result.trim(),
evidenceRef: executionForm.evidenceRef.trim()
};
executionHistory[item.id].unshift(record);
draft[item.id].status = record.status;
draft[item.id].owner = record.operator;
draft[item.id].note = record.result;
if (record.evidenceRef) draft[item.id].evidenceRef = record.evidenceRef;
persistDraft();
persistExecutionHistory();
Object.assign(executionForm, {
executedAt: formatLocalMinute(new Date()),
operator: record.operator,
status: record.status,
result: '',
evidenceRef: ''
});
ElMessage.success('现场执行记录已追加');
}
function createExecutionId() {
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
return crypto.randomUUID();
}
return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
}
function executionStatusTag(status: FieldStatus) {
if (status === 'PASS') return 'success';
if (status === 'BLOCKED') return 'danger';
if (status === 'SKIP') return 'info';
return 'warning';
}
async function copyBlockedSummary() { async function copyBlockedSummary() {
const header = [ const header = [
`联调批次:${sessionMeta.batchNo || '未填写'}`, `联调批次:${sessionMeta.batchNo || '未填写'}`,
@@ -916,7 +1107,9 @@ function buildCloseoutArchive() {
pending: pendingCount.value, pending: pendingCount.value,
evidenceRecorded: evidenceRecordedCount.value, evidenceRecorded: evidenceRecordedCount.value,
evidenceMissing: evidenceMissingCount.value, evidenceMissing: evidenceMissingCount.value,
overdue: overdueItems.value.length overdue: overdueItems.value.length,
executedItems: executedItemCount.value,
executionRecords: totalExecutionCount.value
}, },
categoryProgress: categoryProgress.value, categoryProgress: categoryProgress.value,
stageProgress: stageProgress.value, stageProgress: stageProgress.value,
@@ -942,7 +1135,8 @@ function serializeHandoffItem(item: HandoffItem) {
deadline: itemDraft.deadline, deadline: itemDraft.deadline,
note: itemDraft.note, note: itemDraft.note,
evidence: item.evidence, evidence: item.evidence,
evidenceRef: itemDraft.evidenceRef evidenceRef: itemDraft.evidenceRef,
executionHistory: executionHistory[item.id].map((record) => ({ ...record }))
}; };
} }
@@ -959,7 +1153,7 @@ function buildHandoffReport() {
`最后更新:${sessionMeta.updatedAt || '未记录'}`, `最后更新:${sessionMeta.updatedAt || '未记录'}`,
`结论:${handoffReady.value ? '可进入现场验收收口' : '仍不可收口'}`, `结论:${handoffReady.value ? '可进入现场验收收口' : '仍不可收口'}`,
`归档门禁:${closeoutReady.value ? '可归档' : `待补齐 ${missingCloseoutFields.value.join('、')}`}`, `归档门禁:${closeoutReady.value ? '可归档' : `待补齐 ${missingCloseoutFields.value.join('、')}`}`,
`汇总:总项 ${checklist.length},通过 ${passedCount.value},阻断 ${blockedCount.value},待处理 ${pendingCount.value},证据 ${evidenceRecordedCount.value}/${checklist.length},待补证据 ${evidenceMissingCount.value},逾期 ${overdueItems.value.length}` `汇总:总项 ${checklist.length},通过 ${passedCount.value},阻断 ${blockedCount.value},待处理 ${pendingCount.value},证据 ${evidenceRecordedCount.value}/${checklist.length},待补证据 ${evidenceMissingCount.value},逾期 ${overdueItems.value.length},已执行 ${executedItemCount.value}/${checklist.length},执行记录 ${totalExecutionCount.value}`
]; ];
const progress = categoryProgress.value.map((item) => ( const progress = categoryProgress.value.map((item) => (
`- ${item.category}${item.passed}/${item.total} 通过,阻断 ${item.blocked},待处理 ${item.pending},不适用 ${item.skipped}` `- ${item.category}${item.passed}/${item.total} 通过,阻断 ${item.blocked},待处理 ${item.pending},不适用 ${item.skipped}`
@@ -1015,7 +1209,11 @@ function formatReportItem(item: HandoffItem) {
` 期限:${itemDraft.deadline || '未填写'}`, ` 期限:${itemDraft.deadline || '未填写'}`,
` 结果:${itemDraft.note || '未记录'}`, ` 结果:${itemDraft.note || '未记录'}`,
` 应留证据:${item.evidence}`, ` 应留证据:${item.evidence}`,
` 证据记录:${itemDraft.evidenceRef || '未记录'}` ` 证据记录:${itemDraft.evidenceRef || '未记录'}`,
` 执行记录:${executionHistory[item.id].length || '无'}`,
...executionHistory[item.id].map((record) => (
` - ${record.executedAt} / ${record.operator} / ${statusText(record.status)} / ${record.result} / 证据:${record.evidenceRef || '未记录'}`
))
].join('\n'); ].join('\n');
} }
@@ -1047,7 +1245,9 @@ function exportChecklist() {
['session', '证据记录数', `${evidenceRecordedCount.value}/${checklist.length}`, '', '', '', '', '', ''], ['session', '证据记录数', `${evidenceRecordedCount.value}/${checklist.length}`, '', '', '', '', '', ''],
['session', '待补证据数', String(evidenceMissingCount.value), '', '', '', '', '', ''], ['session', '待补证据数', String(evidenceMissingCount.value), '', '', '', '', '', ''],
['session', '逾期检查项', String(overdueItems.value.length), '', '', '', '', '', ''], ['session', '逾期检查项', String(overdueItems.value.length), '', '', '', '', '', ''],
['ID', '类别', '检查项', '期望', '状态', '负责人', '期限', '结果记录', '应留证据', '证据记录'], ['session', '已执行检查项', `${executedItemCount.value}/${checklist.length}`, '', '', '', '', '', ''],
['session', '执行记录数', String(totalExecutionCount.value), '', '', '', '', '', ''],
['ID', '类别', '检查项', '期望', '状态', '负责人', '期限', '结果记录', '应留证据', '证据记录', '执行记录数', '最近执行时间'],
...exportItems.map((item) => [ ...exportItems.map((item) => [
item.id, item.id,
item.category, item.category,
@@ -1058,7 +1258,9 @@ function exportChecklist() {
draft[item.id].deadline, draft[item.id].deadline,
draft[item.id].note, draft[item.id].note,
item.evidence, item.evidence,
draft[item.id].evidenceRef draft[item.id].evidenceRef,
String(executionHistory[item.id].length),
executionHistory[item.id][0]?.executedAt || ''
]) ])
]); ]);
} }
@@ -1107,10 +1309,11 @@ function exportRiskRegister() {
function exportSnapshot() { function exportSnapshot() {
const payload = { const payload = {
schema: 'qipai.cleaning.field-handoff.snapshot.v1', schema: 'qipai.cleaning.field-handoff.snapshot.v2',
exportedAt: new Date().toISOString(), exportedAt: new Date().toISOString(),
sessionMeta, sessionMeta,
draft draft,
executionHistory
}; };
downloadText( downloadText(
`cleaning-field-handoff-snapshot-${Date.now()}.json`, `cleaning-field-handoff-snapshot-${Date.now()}.json`,
@@ -1133,8 +1336,12 @@ async function importSnapshot(event: Event) {
schema?: string; schema?: string;
sessionMeta?: Partial<SessionMeta>; sessionMeta?: Partial<SessionMeta>;
draft?: Record<string, Partial<HandoffDraft>>; draft?: Record<string, Partial<HandoffDraft>>;
executionHistory?: Record<string, unknown>;
}; };
if (payload.schema !== 'qipai.cleaning.field-handoff.snapshot.v1' || !payload.draft) { if (
!['qipai.cleaning.field-handoff.snapshot.v1', 'qipai.cleaning.field-handoff.snapshot.v2'].includes(payload.schema || '')
|| !payload.draft
) {
throw new Error('SNAPSHOT_SCHEMA_INVALID'); throw new Error('SNAPSHOT_SCHEMA_INVALID');
} }
Object.assign(sessionMeta, { Object.assign(sessionMeta, {
@@ -1158,9 +1365,13 @@ async function importSnapshot(event: Event) {
evidenceRef: value.evidenceRef || '' evidenceRef: value.evidenceRef || ''
}; };
} }
for (const item of checklist) {
executionHistory[item.id] = normalizeExecutionRecords(payload.executionHistory?.[item.id]);
}
touchSessionMeta(); touchSessionMeta();
persistSessionMeta(); persistSessionMeta();
persistDraft(); persistDraft();
persistExecutionHistory();
ElMessage.success('联调快照已导入'); ElMessage.success('联调快照已导入');
} catch { } catch {
ElMessage.error('联调快照格式无效'); ElMessage.error('联调快照格式无效');
@@ -1181,6 +1392,29 @@ function normalizeDraft(value?: Partial<HandoffDraft>): HandoffDraft {
}; };
} }
function normalizeExecutionRecords(value: unknown): HandoffExecution[] {
if (!Array.isArray(value)) return [];
return value.flatMap((entry) => {
if (!entry || typeof entry !== 'object') return [];
const record = entry as Partial<HandoffExecution>;
if (
typeof record.id !== 'string'
|| typeof record.executedAt !== 'string'
|| typeof record.operator !== 'string'
|| !isFieldStatus(record.status)
|| typeof record.result !== 'string'
) return [];
return [{
id: record.id,
executedAt: record.executedAt,
operator: record.operator,
status: record.status,
result: record.result,
evidenceRef: typeof record.evidenceRef === 'string' ? record.evidenceRef : ''
}];
}).slice(0, 200);
}
function downloadCsv(filename: string, rows: string[][]) { function downloadCsv(filename: string, rows: string[][]) {
const content = rows.map((row) => row.map(csvCell).join(',')).join('\r\n'); const content = rows.map((row) => row.map(csvCell).join(',')).join('\r\n');
downloadText(filename, `\uFEFF${content}`, 'text/csv;charset=utf-8'); downloadText(filename, `\uFEFF${content}`, 'text/csv;charset=utf-8');
+48
View File
@@ -779,6 +779,53 @@ textarea {
gap: 3px; gap: 3px;
} }
.handoff-execution-list {
display: grid;
gap: 10px;
margin: 16px 0;
max-height: 300px;
overflow-y: auto;
}
.handoff-execution-list article {
background: var(--el-fill-color-light);
border: 1px solid var(--el-border-color-lighter);
border-radius: 10px;
display: grid;
gap: 8px;
padding: 12px;
}
.handoff-execution-list header {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.handoff-execution-list header span:last-child,
.handoff-execution-list small {
color: var(--el-text-color-secondary);
}
.handoff-execution-list p {
margin: 0;
white-space: pre-wrap;
}
.handoff-execution-form {
border-top: 1px solid var(--el-border-color-lighter);
display: grid;
gap: 0 12px;
grid-template-columns: repeat(3, minmax(0, 1fr));
margin-top: 16px;
padding-top: 16px;
}
.handoff-execution-result {
grid-column: 1 / -1;
}
.stats-grid { .stats-grid {
display: grid; display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -983,6 +1030,7 @@ textarea {
.photo-grid, .photo-grid,
.trend-row, .trend-row,
.handoff-session .el-form, .handoff-session .el-form,
.handoff-execution-form,
.handoff-category-progress, .handoff-category-progress,
.stats-filter, .stats-filter,
.stats-grid { .stats-grid {
+13
View File
@@ -307,7 +307,18 @@ for (const pattern of [
'exportSnapshot', 'exportSnapshot',
'importSnapshot', 'importSnapshot',
'qipai.cleaning.field-handoff.snapshot.v1', 'qipai.cleaning.field-handoff.snapshot.v1',
'qipai.cleaning.field-handoff.snapshot.v2',
'cleaning-field-handoff-snapshot', 'cleaning-field-handoff-snapshot',
'qipai.cleaning.field-handoff.executions',
'executionHistory',
'openExecutionHistory',
'appendExecutionRecord',
'normalizeExecutionRecords',
'selectedExecutionRecords',
'canAppendExecution',
'现场执行记录',
'追加记录',
'执行记录数',
'categoryProgress', 'categoryProgress',
'passPercent', 'passPercent',
'handoff-category-progress', 'handoff-category-progress',
@@ -405,6 +416,8 @@ assert.match(styles, /\.handoff-verdict/);
assert.match(styles, /\.handoff-status-select/); assert.match(styles, /\.handoff-status-select/);
assert.match(styles, /\.handoff-bulk-form/); assert.match(styles, /\.handoff-bulk-form/);
assert.match(styles, /\.handoff-blockers/); assert.match(styles, /\.handoff-blockers/);
assert.match(styles, /\.handoff-execution-list/);
assert.match(styles, /\.handoff-execution-form/);
assert.match(styles, /\.cleaner-performance-board/); assert.match(styles, /\.cleaner-performance-board/);
assert.match(styles, /\.cleaner-performance-grid/); assert.match(styles, /\.cleaner-performance-grid/);
assert.match(styles, /\.cleaner-performance-card/); assert.match(styles, /\.cleaner-performance-card/);