feat(M08-B): 补联调快照导入导出
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user