feat(M08-B): 补联调快照导入导出

This commit is contained in:
Codex
2026-07-05 16:10:54 +08:00
parent 1f758cc29f
commit 06f905e792
7 changed files with 103 additions and 10 deletions
@@ -46,8 +46,17 @@
<el-button type="primary" :icon="Check" @click="applyBulkMark">应用到当前筛选</el-button>
</el-form>
</el-popover>
<el-button :icon="FileDown" @click="exportSnapshot">快照</el-button>
<el-button :icon="FileUp" @click="openSnapshotImport">导入</el-button>
<el-button :icon="Download" @click="exportChecklist">导出</el-button>
<el-button :icon="RotateCcw" @click="resetDraft">重置</el-button>
<input
ref="snapshotInput"
class="hidden-file-input"
type="file"
accept="application/json,.json"
@change="importSnapshot"
/>
</div>
</div>
@@ -158,7 +167,7 @@
<script setup lang="ts">
import { computed, reactive, ref } from 'vue';
import { Check, Copy, Download, ListChecks, RotateCcw } from '@lucide/vue';
import { Check, Copy, Download, FileDown, FileUp, ListChecks, RotateCcw } from '@lucide/vue';
import { ElMessage } from 'element-plus';
type FieldStatus = 'PENDING' | 'PASS' | 'BLOCKED' | 'SKIP';
@@ -188,6 +197,7 @@ const storageKey = 'qipai.cleaning.field-handoff';
const sessionStorageKey = 'qipai.cleaning.field-handoff.session';
const category = ref<'全部' | HandoffItem['category']>('全部');
const statusFilter = ref<'ALL' | FieldStatus>('ALL');
const snapshotInput = ref<HTMLInputElement | null>(null);
const bulkForm = reactive<{ status: FieldStatus; owner: string; note: string }>({
status: 'PASS',
owner: '',
@@ -264,6 +274,7 @@ const passedCount = computed(() => checklist.filter((item) => draft[item.id].sta
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 blockedItems = computed(() => checklist.filter((item) => draft[item.id].status === 'BLOCKED'));
const checklistIds = new Set(checklist.map((item) => item.id));
function createInitialDraft() {
const saved = readSavedDraft();
@@ -359,9 +370,72 @@ function exportChecklist() {
]);
}
function exportSnapshot() {
const payload = {
schema: 'qipai.cleaning.field-handoff.snapshot.v1',
exportedAt: new Date().toISOString(),
sessionMeta,
draft
};
downloadText(
`cleaning-field-handoff-snapshot-${Date.now()}.json`,
JSON.stringify(payload, null, 2),
'application/json;charset=utf-8'
);
}
function openSnapshotImport() {
snapshotInput.value?.click();
}
async function importSnapshot(event: Event) {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
input.value = '';
if (!file) return;
try {
const payload = JSON.parse(await file.text()) as {
schema?: string;
sessionMeta?: Partial<SessionMeta>;
draft?: Record<string, Partial<HandoffDraft>>;
};
if (payload.schema !== 'qipai.cleaning.field-handoff.snapshot.v1' || !payload.draft) {
throw new Error('SNAPSHOT_SCHEMA_INVALID');
}
Object.assign(sessionMeta, {
batchNo: payload.sessionMeta?.batchNo || '',
site: payload.sessionMeta?.site || '',
coordinator: payload.sessionMeta?.coordinator || '',
executedAt: payload.sessionMeta?.executedAt || ''
});
for (const [id, value] of Object.entries(payload.draft)) {
if (!checklistIds.has(id)) continue;
const status = isFieldStatus(value.status) ? value.status : 'PENDING';
draft[id] = {
status,
owner: value.owner || '',
note: value.note || ''
};
}
persistSessionMeta();
persistDraft();
ElMessage.success('联调快照已导入');
} catch {
ElMessage.error('联调快照格式无效');
}
}
function isFieldStatus(value: unknown): value is FieldStatus {
return value === 'PENDING' || value === 'PASS' || value === 'BLOCKED' || value === 'SKIP';
}
function downloadCsv(filename: string, rows: string[][]) {
const content = rows.map((row) => row.map(csvCell).join(',')).join('\r\n');
const blob = new Blob([`\uFEFF${content}`], { type: 'text/csv;charset=utf-8' });
downloadText(filename, `\uFEFF${content}`, 'text/csv;charset=utf-8');
}
function downloadText(filename: string, content: string, type: string) {
const blob = new Blob([content], { type });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
+4
View File
@@ -634,6 +634,10 @@ textarea {
background: #d8e0ea;
}
.hidden-file-input {
display: none;
}
@media (max-width: 980px) {
.app-shell {
grid-template-columns: 1fr;