优化数据修订详情展示
缩小返回按钮和记录编号标签,统一显示记录编号名称,并简化编辑操作范围文案。
| | |
| | | export const revisionDataTypeLabels: Record<RevisionDataType, string> = { REALTIME: '实时', STEP: '工步', CYCLE: '循环' } |
| | | export const revisionOperationLabels: Record<RevisionOperationType, string> = { EDIT: '编辑', IMPORT: '导入', DELETE: '删除' } |
| | | export function formatRevisionTime(value: string | null | undefined) { return value ? new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'medium' }).format(new Date(value)) : '-' } |
| | | export function formatRevisionScope(value: string) { return value.replace(/^按格(?=修改)/, '') } |
| | | |
| | | export interface DataRevisionAction { |
| | | actionId: number |
| | |
| | | function openBatchEditor() { singleEditing.value = false; editing.value = true; draftRows.value = Object.fromEntries(sourceRows.value.map(row => [row.id, { ...row }])) } |
| | | function openRowEditor(row: DataRow) { singleEditing.value = true; editing.value = false; draftRows.value = { [row.id]: { ...row } }; selectedIds.value = new Set([row.id]); editForm.field = String(fieldOptions.value[0]?.value || 'voltage') as keyof DataRow; editForm.value = String(row[editForm.field] ?? ''); editForm.remark = ''; editorVisible.value = true } |
| | | async function saveBatchEdit() { if (!Object.keys(draftRows.value).length) return; try { const result = await ElMessageBox.prompt('请输入本次修改原因', '提交数据修改', { inputPlaceholder: '必填,将写入数据修订记录', inputValidator: value => value.trim() ? true : '修改原因不能为空' }); editForm.remark = result.value.trim(); await submitBatchEdit() } catch { /* 用户取消原因填写 */ } } |
| | | async function submitBatchEdit() { if (singleEditing.value && singleRow.value && fieldOptions.value.some(field => { const value = singleRow.value?.[field.value as keyof DataRow]; return !String(value ?? '').trim() || !Number.isFinite(Number(value)) })) { ElMessage.warning('请填写完整、有效的数据'); return } if (!editForm.remark.trim()) { ElMessage.warning('请填写修改原因'); return }; const originals = new Map(sourceRows.value.map(row => [row.id, row])); const changed = Object.values(draftRows.value).filter(row => JSON.stringify(originals.get(row.id)) !== JSON.stringify(row)); if (!changed.length) { ElMessage.warning('没有检测到修改'); return }; try { await submitConsoleDataAction(props.testId, { dataType: dataType.value, operationType: 'EDIT', scopeSummary: `按格修改${changed.length}条${activeTab.value}数据`, identitySummary: JSON.stringify(changed.map(row => row.id)), changesSummary: JSON.stringify(changed.map(row => ({ identity: row.id, before: originals.get(row.id), after: row }))), reason: editForm.remark.trim(), overrides: changed.map(row => ({ stableIdentity: row.id, fieldValues: JSON.stringify(Object.fromEntries(fieldOptions.value.map(item => [item.value, row[item.value as keyof DataRow]]))), deleted: false })) }); editorVisible.value = false; cancelBatchEdit(); await loadRecords(); ElMessage.success(`已提交 ${changed.length} 条数据修改`) } catch (error) { ElMessage.error(error instanceof Error ? error.message : '数据修改提交失败') } } |
| | | async function submitBatchEdit() { if (singleEditing.value && singleRow.value && fieldOptions.value.some(field => { const value = singleRow.value?.[field.value as keyof DataRow]; return !String(value ?? '').trim() || !Number.isFinite(Number(value)) })) { ElMessage.warning('请填写完整、有效的数据'); return } if (!editForm.remark.trim()) { ElMessage.warning('请填写修改原因'); return }; const originals = new Map(sourceRows.value.map(row => [row.id, row])); const changed = Object.values(draftRows.value).filter(row => JSON.stringify(originals.get(row.id)) !== JSON.stringify(row)); if (!changed.length) { ElMessage.warning('没有检测到修改'); return }; try { await submitConsoleDataAction(props.testId, { dataType: dataType.value, operationType: 'EDIT', scopeSummary: `修改${changed.length}条${activeTab.value}数据`, identitySummary: JSON.stringify(changed.map(row => row.id)), changesSummary: JSON.stringify(changed.map(row => ({ identity: row.id, before: originals.get(row.id), after: row }))), reason: editForm.remark.trim(), overrides: changed.map(row => ({ stableIdentity: row.id, fieldValues: JSON.stringify(Object.fromEntries(fieldOptions.value.map(item => [item.value, row[item.value as keyof DataRow]]))), deleted: false })) }); editorVisible.value = false; cancelBatchEdit(); await loadRecords(); ElMessage.success(`已提交 ${changed.length} 条数据修改`) } catch (error) { ElMessage.error(error instanceof Error ? error.message : '数据修改提交失败') } } |
| | | function cancelBatchEdit() { editing.value = false; singleEditing.value = false; draftRows.value = {}; selectedIds.value = new Set() } |
| | | async function deleteSelected(requestedIds?: string[] | Event) { const ids = Array.isArray(requestedIds) && requestedIds.length ? requestedIds : [...selectedIds.value]; if (!ids.length) { ElMessage.warning('请先勾选需要删除的数据'); return }; let result: { value: string }; try { result = await ElMessageBox.prompt(`确认物理删除 ${ids.length} 条${activeTab.value}数据?删除后不可恢复。`, '删除确认', { inputPlaceholder: '请输入删除原因', inputValidator: value => value.trim() ? true : '删除原因不能为空' }) } catch { return }; try { await submitConsoleDataAction(props.testId, { dataType: dataType.value, operationType: 'DELETE', scopeSummary: `物理删除${ids.length}条${activeTab.value}数据`, identitySummary: JSON.stringify(ids), changesSummary: null, reason: result.value.trim(), overrides: ids.map(stableIdentity => ({ stableIdentity, fieldValues: null, deleted: true })) }); const target = sourceRows.value; const kept = target.filter(row => !ids.includes(row.id)); if (activeTab.value === 'realtime') rows.value = kept; else if (activeTab.value === 'step') stepRows.value = kept; else cycleRows.value = kept; selectedIds.value = new Set(); ElMessage.success(`已物理删除 ${ids.length} 条数据`) } catch (error) { ElMessage.error(error instanceof Error ? error.message : '数据删除失败') } } |
| | | function changeTab(tab: DataTab) { |
| | |
| | | import { onMounted, reactive, ref } from 'vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import { useRoute, useRouter } from 'vue-router' |
| | | import { formatRevisionTime, getDataRevisionActions, revisionDataTypeLabels, revisionOperationLabels, type DataRevisionAction, type RevisionDataType, type RevisionOperationType } from '@/api/dataRevisions' |
| | | import { formatRevisionScope, formatRevisionTime, getDataRevisionActions, revisionDataTypeLabels, revisionOperationLabels, type DataRevisionAction, type RevisionDataType, type RevisionOperationType } from '@/api/dataRevisions' |
| | | interface FilterState { test: string; dataType: RevisionDataType | ''; operationType: RevisionOperationType | ''; dateRange: [string, string] | null } |
| | | const route = useRoute(); const router = useRouter(); const initialTest = String(route.query.testId || route.query.testName || '') |
| | | const initialDataType = ['REALTIME', 'STEP', 'CYCLE'].includes(String(route.query.dataType)) ? String(route.query.dataType) as RevisionDataType : '' |
| | | const filters = reactive<FilterState>({ test: initialTest, dataType: initialDataType, operationType: '', dateRange: null }); const applied = ref<FilterState>({ ...filters }) |
| | | const records = ref<DataRevisionAction[]>([]); const total = ref(0); const currentPage = ref(1); const pageSize = ref(10); const loading = ref(false); const loadError = ref('') |
| | | const dataTypeLabels = revisionDataTypeLabels; const operationLabels = revisionOperationLabels; const formatTime = formatRevisionTime |
| | | const dataTypeLabels = revisionDataTypeLabels; const operationLabels = revisionOperationLabels; const formatTime = formatRevisionTime; const formatScope = formatRevisionScope |
| | | function localDayBoundary(date: string, dayOffset = 0) { const value = new Date(`${date}T00:00:00`); value.setDate(value.getDate() + dayOffset); return value.toISOString() } |
| | | async function loadRecords(showResult = false) { loading.value = true; loadError.value = ''; const query = applied.value; const keyword = query.test.trim(); try { const result = await getDataRevisionActions({ pageNum: currentPage.value, pageSize: pageSize.value, testId: /^\d+$/.test(keyword) ? Number(keyword) : undefined, testName: keyword && !/^\d+$/.test(keyword) ? keyword : undefined, dataType: query.dataType || undefined, operationType: query.operationType || undefined, beginCreatedAt: query.dateRange ? localDayBoundary(query.dateRange[0]) : undefined, endCreatedAt: query.dateRange ? localDayBoundary(query.dateRange[1], 1) : undefined }); records.value = result.rows; total.value = result.total; if (showResult) ElMessage.success(`已找到 ${result.total} 条修订行为`) } catch (error) { records.value = []; total.value = 0; loadError.value = error instanceof Error ? error.message : '数据修订行为加载失败' } finally { loading.value = false } } |
| | | function applySearch() { applied.value = { test: filters.test, dataType: filters.dataType, operationType: filters.operationType, dateRange: filters.dateRange ? [...filters.dateRange] as [string, string] : null }; currentPage.value = 1; void loadRecords(true) } |
| | |
| | | function handlePageChange(page: number) { currentPage.value = page; void loadRecords() }; function handleSizeChange(size: number) { pageSize.value = size; currentPage.value = 1; void loadRecords() } |
| | | onMounted(loadRecords) |
| | | </script> |
| | | <template><div class="data-revision-page"><section class="filter-panel"><header class="section-heading"><div><i></i><strong>查询条件</strong><span>FILTER CONDITIONS</span></div><small>共 {{ total }} 条行为</small></header><div class="filter-grid"><label><span>测试</span><el-input v-model="filters.test" clearable placeholder="测试ID或名称" @keyup.enter="applySearch" /></label><label><span>数据类型</span><el-select v-model="filters.dataType" clearable placeholder="全部"><el-option label="实时" value="REALTIME" /><el-option label="工步" value="STEP" /><el-option label="循环" value="CYCLE" /></el-select></label><label><span>操作类型</span><el-select v-model="filters.operationType" clearable placeholder="全部"><el-option label="编辑" value="EDIT" /><el-option label="导入" value="IMPORT" /><el-option label="删除" value="DELETE" /></el-select></label><label><span>操作时间</span><el-date-picker v-model="filters.dateRange" type="daterange" value-format="YYYY-MM-DD" start-placeholder="开始日期" end-placeholder="结束日期" range-separator="至" unlink-panels /></label><div class="filter-actions"><button class="primary-button" type="button" :disabled="loading" @click="applySearch"><el-icon><Search /></el-icon>查询</button><button class="secondary-button" type="button" :disabled="loading" @click="resetSearch"><el-icon><RefreshLeft /></el-icon>重置</button></div></div></section><section class="result-panel"><div class="result-toolbar"><div><i></i><strong>数据修订行为</strong><span>{{ total }} RESULTS · 按操作时间倒序</span></div><span v-if="loadError" class="load-error">{{ loadError }}</span></div><div v-loading="loading" class="table-wrap"><table><thead><tr><th>行为ID</th><th>测试</th><th>数据类型</th><th>操作类型</th><th>操作范围</th><th>操作人</th><th>操作时间</th><th>原因</th><th class="action-column">操作</th></tr></thead><tbody><tr v-for="item in records" :key="item.actionId"><td class="id-cell">#{{ item.actionId }}</td><td><strong>{{ item.testName }}</strong><small>#{{ item.testId }}</small></td><td><span class="type-tag">{{ dataTypeLabels[item.dataType] }}</span></td><td><span class="operation-tag" :class="item.operationType.toLowerCase()">{{ operationLabels[item.operationType] }}</span></td><td :title="item.scopeSummary">{{ item.scopeSummary }}</td><td>{{ item.operatorName }}</td><td>{{ formatTime(item.createdAtUtc) }}</td><td :title="item.reason">{{ item.reason }}</td><td class="action-column"><div class="row-actions"><button type="button" @click="viewRecord(item)">查看</button><button type="button" @click="returnToTest(item)">回到测试</button></div></td></tr><tr v-if="!loading && !records.length"><td colspan="9" class="empty-cell">{{ loadError || '暂无符合条件的数据修订行为' }}</td></tr></tbody></table></div><footer class="pagination-bar"><span>显示 {{ records.length }} 条,共 {{ total }} 条</span><el-pagination :current-page="currentPage" :page-size="pageSize" :page-sizes="[10, 20, 50]" :total="total" :disabled="loading" layout="sizes, prev, pager, next" background @current-change="handlePageChange" @size-change="handleSizeChange" /></footer></section></div></template> |
| | | <template><div class="data-revision-page"><section class="filter-panel"><header class="section-heading"><div><i></i><strong>查询条件</strong><span>FILTER CONDITIONS</span></div><small>共 {{ total }} 条行为</small></header><div class="filter-grid"><label><span>测试</span><el-input v-model="filters.test" clearable placeholder="测试ID或名称" @keyup.enter="applySearch" /></label><label><span>数据类型</span><el-select v-model="filters.dataType" clearable placeholder="全部"><el-option label="实时" value="REALTIME" /><el-option label="工步" value="STEP" /><el-option label="循环" value="CYCLE" /></el-select></label><label><span>操作类型</span><el-select v-model="filters.operationType" clearable placeholder="全部"><el-option label="编辑" value="EDIT" /><el-option label="导入" value="IMPORT" /><el-option label="删除" value="DELETE" /></el-select></label><label><span>操作时间</span><el-date-picker v-model="filters.dateRange" type="daterange" value-format="YYYY-MM-DD" start-placeholder="开始日期" end-placeholder="结束日期" range-separator="至" unlink-panels /></label><div class="filter-actions"><button class="primary-button" type="button" :disabled="loading" @click="applySearch"><el-icon><Search /></el-icon>查询</button><button class="secondary-button" type="button" :disabled="loading" @click="resetSearch"><el-icon><RefreshLeft /></el-icon>重置</button></div></div></section><section class="result-panel"><div class="result-toolbar"><div><i></i><strong>数据修订行为</strong><span>{{ total }} RESULTS · 按操作时间倒序</span></div><span v-if="loadError" class="load-error">{{ loadError }}</span></div><div v-loading="loading" class="table-wrap"><table><thead><tr><th>行为ID</th><th>测试</th><th>数据类型</th><th>操作类型</th><th>操作范围</th><th>操作人</th><th>操作时间</th><th>原因</th><th class="action-column">操作</th></tr></thead><tbody><tr v-for="item in records" :key="item.actionId"><td class="id-cell">#{{ item.actionId }}</td><td><strong>{{ item.testName }}</strong><small>#{{ item.testId }}</small></td><td><span class="type-tag">{{ dataTypeLabels[item.dataType] }}</span></td><td><span class="operation-tag" :class="item.operationType.toLowerCase()">{{ operationLabels[item.operationType] }}</span></td><td :title="formatScope(item.scopeSummary)">{{ formatScope(item.scopeSummary) }}</td><td>{{ item.operatorName }}</td><td>{{ formatTime(item.createdAtUtc) }}</td><td :title="item.reason">{{ item.reason }}</td><td class="action-column"><div class="row-actions"><button type="button" @click="viewRecord(item)">查看</button><button type="button" @click="returnToTest(item)">回到测试</button></div></td></tr><tr v-if="!loading && !records.length"><td colspan="9" class="empty-cell">{{ loadError || '暂无符合条件的数据修订行为' }}</td></tr></tbody></table></div><footer class="pagination-bar"><span>显示 {{ records.length }} 条,共 {{ total }} 条</span><el-pagination :current-page="currentPage" :page-size="pageSize" :page-sizes="[10, 20, 50]" :total="total" :disabled="loading" layout="sizes, prev, pager, next" background @current-change="handlePageChange" @size-change="handleSizeChange" /></footer></section></div></template> |
| | | <style scoped>.data-revision-page{width:100%}.primary-button,.secondary-button{height:34px;display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:0 13px;border:1px solid transparent;border-radius:3px;font-size:13px;cursor:pointer}.primary-button{color:#fff;background:var(--blue)}.secondary-button{color:#52606d;background:#fff;border-color:var(--line-strong)}button:disabled{opacity:.55}.filter-panel,.result-panel{background:#fff;border:1px solid #d8e1e9}.filter-panel{margin-bottom:16px}.section-heading,.result-toolbar{display:flex;align-items:center;justify-content:space-between;padding:0 16px;background:var(--surface-soft);border-bottom:1px solid var(--line)}.section-heading{height:44px}.result-toolbar{height:50px}.section-heading>div,.result-toolbar>div{display:flex;align-items:center;gap:8px}.section-heading i,.result-toolbar i{width:3px;height:16px;background:#2b78a3}.section-heading strong,.result-toolbar strong{color:var(--ink);font-size:15px}.section-heading span,.result-toolbar span,.section-heading small{color:#81909d;font-size:10px}.filter-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px 18px;padding:16px}.filter-grid label{min-width:0;display:grid;grid-template-columns:70px minmax(0,1fr);align-items:center;gap:8px}.filter-grid label>span{color:#52606d;font-size:12px}.filter-grid :deep(.el-select),.filter-grid :deep(.el-date-editor){width:100%}.filter-actions{grid-column:4;display:flex;justify-content:flex-end;gap:8px}.load-error{color:#b34f4f!important}.table-wrap{overflow-x:auto}table{width:100%;min-width:1380px;table-layout:fixed;border-collapse:collapse;color:#52606d;font-size:12px}th,td{height:50px;padding:0 12px;border-right:1px solid #dde7f0;border-bottom:1px solid #dde7f0;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}th{height:40px;background:#eaf4ff;border-color:#c7e0ff;font-weight:600}th:nth-child(1){width:85px}th:nth-child(2){width:190px}th:nth-child(3),th:nth-child(4){width:90px}th:nth-child(5){width:250px}th:nth-child(6){width:100px}th:nth-child(7){width:165px}th:nth-child(8){width:250px}th:nth-child(9){width:145px}td strong,td small{display:block}td strong{color:#26333f}td small{margin-top:3px;color:#8795a3}.id-cell{font-family:'Cascadia Mono',Consolas,monospace}.type-tag,.operation-tag{display:inline-flex;padding:3px 8px;border:1px solid #c9d9e7;background:#f5f8fb}.operation-tag.edit{color:#176fcb;background:#edf6ff;border-color:#b8d9fb}.operation-tag.import{color:#23865e;background:#edf9f3;border-color:#b8dfcd}.operation-tag.delete{color:#b34f4f;background:#fff1f1;border-color:#e9bcbc}.action-column{position:sticky;right:0;z-index:2;background:#fff;box-shadow:inset 1px 0 0 #b4c7d9,-8px 0 14px rgba(23,49,74,.1)}th.action-column{z-index:3;background:#eaf4ff}.row-actions{display:flex;justify-content:center;gap:12px}.row-actions button{border:0;color:var(--blue);background:transparent;cursor:pointer}.empty-cell{height:160px;color:#8795a3;text-align:center}.pagination-bar{min-height:58px;display:flex;align-items:center;justify-content:space-between;gap:16px;padding:8px 14px 8px 16px}.pagination-bar>span{color:#7b8794;font-size:11px}@media(max-width:1100px){.filter-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.filter-actions{grid-column:2}}@media(max-width:680px){.filter-grid{grid-template-columns:1fr}.filter-actions{grid-column:auto}.filter-actions button{flex:1}.pagination-bar{align-items:flex-start;flex-direction:column;overflow-x:auto}}</style> |
| | |
| | | <script setup lang="ts"> |
| | | import { computed, onMounted, ref } from 'vue' |
| | | import { useRoute, useRouter } from 'vue-router' |
| | | import { formatRevisionTime, getDataRevisionAction, revisionDataTypeLabels, revisionOperationLabels, type DataRevisionAction } from '@/api/dataRevisions' |
| | | import { formatRevisionScope, formatRevisionTime, getDataRevisionAction, revisionDataTypeLabels, revisionOperationLabels, type DataRevisionAction } from '@/api/dataRevisions' |
| | | const route = useRoute(); const router = useRouter(); const record = ref<DataRevisionAction | null>(null); const loading = ref(true); const loadError = ref('') |
| | | const dataTypeLabels = revisionDataTypeLabels; const operationLabels = revisionOperationLabels |
| | | const fieldLabels: Record<string, string> = { voltage: '电压', current: '电流', capacity: '容量', energy: '能量', stepDuration: '工步时间', startVoltage: '起始电压', endVoltage: '终止电压', chargePeakVoltage: '充电峰值电压', dischargeValleyVoltage: '放电谷值电压', chargeCapacity: '充电容量', dischargeCapacity: '放电容量', efficiency: '效率', chargeEnergy: '充电能量', dischargeEnergy: '放电能量' } |
| | | function parseArray(value: string | null) { if (!value) return [] as Array<Record<string, unknown> | string>; try { const parsed = JSON.parse(value); return Array.isArray(parsed) ? parsed : [] } catch { return [] as Array<Record<string, unknown> | string> } } |
| | | const identities = computed(() => parseArray(record.value?.identitySummary || null).map(item => String(item))); const changeRows = computed(() => parseArray(record.value?.changesSummary || null).map((item, index) => { if (!item || typeof item !== 'object') return { identity: identities.value[index] || '-', changes: [] as Array<{ field: string; before: unknown; after: unknown }> }; const row = item as Record<string, unknown>; const before = row.before && typeof row.before === 'object' ? row.before as Record<string, unknown> : {}; const after = row.after && typeof row.after === 'object' ? row.after as Record<string, unknown> : row; const changes = Object.keys(after).filter(key => before[key] !== after[key] && fieldLabels[key]).map(key => ({ field: fieldLabels[key], before: before[key], after: after[key] })); return { identity: String(row.identity || row.recordNo || identities.value[index] || '-'), changes } })) |
| | | const formatTime = formatRevisionTime; function goBack() { router.push('/data-revision') }; function returnToTest() { if (record.value) router.push({ path: `/test-list/${record.value.testId}/console`, query: { tab: 'data-list', dataType: record.value.dataType, ...(record.value.testReceptionStatus === 'ENDED' ? { state: 'ended' } : {}) } }) } |
| | | const formatTime = formatRevisionTime; const formatScope = formatRevisionScope; function goBack() { router.push('/data-revision') }; function returnToTest() { if (record.value) router.push({ path: `/test-list/${record.value.testId}/console`, query: { tab: 'data-list', dataType: record.value.dataType, ...(record.value.testReceptionStatus === 'ENDED' ? { state: 'ended' } : {}) } }) } |
| | | onMounted(async () => { try { record.value = await getDataRevisionAction(Number(route.params.id)) } catch (error) { loadError.value = error instanceof Error ? error.message : '数据修订行为加载失败' } finally { loading.value = false } }) |
| | | </script> |
| | | <template><div class="revision-detail-page"><div class="page-tools"><button class="back-button" type="button" @click="goBack"><el-icon><ArrowLeft /></el-icon>返回数据修订记录</button><span>DATA REVISION ACTION</span></div><section v-loading="loading" class="detail-panel"><template v-if="record"><header class="detail-toolbar"><div class="heading-group"><i></i><div><strong>修订行为 #{{ record.actionId }}</strong><span>一次用户提交 · 只读审计记录</span></div></div><span class="operation-tag" :class="record.operationType.toLowerCase()">{{ operationLabels[record.operationType] }}</span></header><section class="summary-section"><div class="summary-grid"><div><span>测试</span><strong>{{ record.testName }} (#{{ record.testId }})</strong></div><div><span>数据类型</span><strong>{{ dataTypeLabels[record.dataType] }}</strong></div><div><span>操作范围</span><strong>{{ record.scopeSummary }}</strong></div><div><span>涉及记录</span><strong>{{ identities.length }} 条</strong></div><div><span>操作人</span><strong>{{ record.operatorName }}</strong></div><div><span>操作时间</span><strong>{{ formatTime(record.createdAtUtc) }}</strong></div></div><div class="reason-line"><span>操作原因</span><p>{{ record.reason }}</p></div><button class="primary-button" type="button" @click="returnToTest">回到对应测试数据列表</button></section><section class="data-section"><div class="section-title"><i></i><strong>记录身份摘要</strong><span>IDENTITY SUMMARY</span></div><div class="identity-list"><code v-for="identity in identities" :key="identity">{{ identity }}</code><span v-if="!identities.length">无身份摘要</span></div></section><section class="data-section"><div class="section-title"><i></i><strong>字段变化摘要</strong><span>CHANGE SUMMARY</span></div><div v-if="record.operationType === 'DELETE'" class="deleted-notice"><el-icon><Delete /></el-icon><div><strong>数据已物理删除</strong><p>本页仅保留本次删除行为的范围、身份和原因摘要,不恢复或展示已删除数据。</p></div></div><div v-else-if="changeRows.length" class="change-list"><article v-for="row in changeRows" :key="row.identity"><header>{{ row.identity }}</header><dl v-if="row.changes.length"><div v-for="change in row.changes" :key="change.field"><dt>{{ change.field }}</dt><dd><span>{{ change.before ?? '-' }}</span><el-icon><Right /></el-icon><strong>{{ change.after ?? '-' }}</strong></dd></div></dl><p v-else>本次导入包含该记录,字段值已按提交内容保存。</p></article></div><div v-else class="empty-summary">无字段变化摘要</div></section></template><div v-else-if="!loading" class="not-found"><el-empty :description="loadError || '未找到该数据修订行为'" /><button class="secondary-button" type="button" @click="goBack">返回记录列表</button></div></section></div></template> |
| | | <style scoped>.revision-detail-page{width:100%}.page-tools{height:34px;display:flex;align-items:center;justify-content:space-between}.page-tools>span{color:#7f8d99;font-size:10px}.back-button{display:inline-flex;align-items:center;gap:6px;border:0;color:#52606d;background:transparent;cursor:pointer}.detail-panel{min-height:460px;background:#fff;border:1px solid #d8e1e9}.detail-toolbar{height:54px;display:flex;align-items:center;justify-content:space-between;padding:0 16px;background:var(--surface-soft);border-bottom:1px solid var(--line)}.heading-group{display:flex;align-items:center;gap:9px}.heading-group>i,.section-title i{width:3px;height:18px;background:#2b78a3}.heading-group strong,.heading-group span{display:block}.heading-group strong{color:var(--ink);font-size:15px}.heading-group span{margin-top:2px;color:#81909d;font-size:10px}.operation-tag{padding:4px 9px;border:1px solid}.operation-tag.edit{color:#176fcb;background:#edf6ff;border-color:#b8d9fb}.operation-tag.import{color:#23865e;background:#edf9f3;border-color:#b8dfcd}.operation-tag.delete{color:#b34f4f;background:#fff1f1;border-color:#e9bcbc}.summary-section,.data-section{padding:20px 22px}.data-section{border-top:1px solid var(--line)}.summary-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:20px 28px}.summary-grid>div{display:grid;grid-template-columns:72px minmax(0,1fr);gap:8px}.summary-grid span,.reason-line>span{color:#7b8794;font-size:12px}.summary-grid strong{min-width:0;color:#34414d;font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.reason-line{display:grid;grid-template-columns:72px minmax(0,1fr);gap:8px;margin:18px 0;padding-top:16px;border-top:1px dashed #dce5ed}.reason-line p{color:#34414d;font-size:13px}.primary-button,.secondary-button{height:34px;padding:0 13px;border:1px solid transparent;border-radius:3px;cursor:pointer}.primary-button{color:#fff;background:var(--blue)}.secondary-button{color:#52606d;background:#fff;border-color:var(--line-strong)}.section-title{display:flex;align-items:center;gap:8px;margin-bottom:16px}.section-title i{height:15px}.section-title strong{color:#26333f;font-size:14px}.section-title span{color:#8a98a5;font-size:10px}.identity-list{display:flex;flex-wrap:wrap;gap:8px}.identity-list code{padding:6px 8px;border:1px solid #d8e3ec;background:#f6f8fa;color:#405261}.identity-list>span,.empty-summary{color:#8795a3;font-size:12px}.change-list{display:grid;gap:12px}.change-list article{border:1px solid #d8e3ec}.change-list article>header{padding:9px 12px;background:#f6f9fc;color:#405261;font-family:'Cascadia Mono',Consolas,monospace;font-size:12px}.change-list dl{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:1px;background:#e2e9ef}.change-list dl>div{padding:10px 12px;background:#fff}.change-list dt{margin-bottom:6px;color:#71808d;font-size:11px}.change-list dd{display:flex;align-items:center;gap:8px;font-size:12px}.change-list dd span{color:#8795a3;text-decoration:line-through}.change-list dd strong{color:#176fcb}.change-list article>p{padding:12px;color:#52606d;font-size:12px}.deleted-notice{display:flex;align-items:center;gap:14px;padding:18px;border:1px solid #efd0d0;background:#fff7f7;color:#b34f4f}.deleted-notice>.el-icon{font-size:24px}.deleted-notice p{margin-top:5px;color:#785c5c;font-size:12px}.not-found{min-height:420px;display:flex;align-items:center;justify-content:center;flex-direction:column}@media(max-width:900px){.summary-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.change-list dl{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:620px){.summary-section,.data-section{padding:16px 12px}.summary-grid,.change-list dl{grid-template-columns:1fr}.reason-line{grid-template-columns:1fr}}</style> |
| | | <template><div class="revision-detail-page"><div class="page-tools"><button class="back-button" type="button" @click="goBack"><el-icon><ArrowLeft /></el-icon>返回数据修订记录</button><span>DATA REVISION ACTION</span></div><section v-loading="loading" class="detail-panel"><template v-if="record"><header class="detail-toolbar"><div class="heading-group"><i></i><div><strong>修订行为 #{{ record.actionId }}</strong><span>一次用户提交 · 只读审计记录</span></div></div><span class="operation-tag" :class="record.operationType.toLowerCase()">{{ operationLabels[record.operationType] }}</span></header><section class="summary-section"><div class="summary-grid"><div><span>测试</span><strong>{{ record.testName }} (#{{ record.testId }})</strong></div><div><span>数据类型</span><strong>{{ dataTypeLabels[record.dataType] }}</strong></div><div><span>操作范围</span><strong>{{ formatScope(record.scopeSummary) }}</strong></div><div><span>涉及记录</span><strong>{{ identities.length }} 条</strong></div><div><span>操作人</span><strong>{{ record.operatorName }}</strong></div><div><span>操作时间</span><strong>{{ formatTime(record.createdAtUtc) }}</strong></div></div><div class="reason-line"><span>操作原因</span><p>{{ record.reason }}</p></div><button class="primary-button return-button" type="button" @click="returnToTest">回到对应测试数据列表</button></section><section class="data-section"><div class="section-title"><i></i><strong>记录编号</strong><span>RECORD NUMBERS</span></div><div class="identity-list"><code v-for="identity in identities" :key="identity">{{ identity }}</code><span v-if="!identities.length">暂无记录编号</span></div></section><section class="data-section"><div class="section-title"><i></i><strong>字段变化摘要</strong><span>CHANGE SUMMARY</span></div><div v-if="record.operationType === 'DELETE'" class="deleted-notice"><el-icon><Delete /></el-icon><div><strong>数据已物理删除</strong><p>本页仅保留本次删除行为的范围、身份和原因摘要,不恢复或展示已删除数据。</p></div></div><div v-else-if="changeRows.length" class="change-list"><article v-for="row in changeRows" :key="row.identity"><header>{{ row.identity }}</header><dl v-if="row.changes.length"><div v-for="change in row.changes" :key="change.field"><dt>{{ change.field }}</dt><dd><span>{{ change.before ?? '-' }}</span><el-icon><Right /></el-icon><strong>{{ change.after ?? '-' }}</strong></dd></div></dl><p v-else>本次导入包含该记录,字段值已按提交内容保存。</p></article></div><div v-else class="empty-summary">无字段变化摘要</div></section></template><div v-else-if="!loading" class="not-found"><el-empty :description="loadError || '未找到该数据修订行为'" /><button class="secondary-button" type="button" @click="goBack">返回记录列表</button></div></section></div></template> |
| | | <style scoped>.revision-detail-page{width:100%}.page-tools{height:34px;display:flex;align-items:center;justify-content:space-between}.page-tools>span{color:#7f8d99;font-size:10px}.back-button{display:inline-flex;align-items:center;gap:6px;border:0;color:#52606d;background:transparent;cursor:pointer}.detail-panel{min-height:460px;background:#fff;border:1px solid #d8e1e9}.detail-toolbar{height:54px;display:flex;align-items:center;justify-content:space-between;padding:0 16px;background:var(--surface-soft);border-bottom:1px solid var(--line)}.heading-group{display:flex;align-items:center;gap:9px}.heading-group>i,.section-title i{width:3px;height:18px;background:#2b78a3}.heading-group strong,.heading-group span{display:block}.heading-group strong{color:var(--ink);font-size:15px}.heading-group span{margin-top:2px;color:#81909d;font-size:10px}.operation-tag{padding:4px 9px;border:1px solid}.operation-tag.edit{color:#176fcb;background:#edf6ff;border-color:#b8d9fb}.operation-tag.import{color:#23865e;background:#edf9f3;border-color:#b8dfcd}.operation-tag.delete{color:#b34f4f;background:#fff1f1;border-color:#e9bcbc}.summary-section,.data-section{padding:20px 22px}.data-section{border-top:1px solid var(--line)}.summary-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:20px 28px}.summary-grid>div{display:grid;grid-template-columns:72px minmax(0,1fr);gap:8px}.summary-grid span,.reason-line>span{color:#7b8794;font-size:12px}.summary-grid strong{min-width:0;color:#34414d;font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.reason-line{display:grid;grid-template-columns:72px minmax(0,1fr);gap:8px;margin:18px 0;padding-top:16px;border-top:1px dashed #dce5ed}.reason-line p{color:#34414d;font-size:13px}.primary-button,.secondary-button{height:34px;padding:0 13px;border:1px solid transparent;border-radius:3px;cursor:pointer}.primary-button{color:#fff;background:var(--blue)}.return-button{height:30px;padding:0 10px;font-size:12px}.secondary-button{color:#52606d;background:#fff;border-color:var(--line-strong)}.section-title{display:flex;align-items:center;gap:8px;margin-bottom:14px}.section-title i{height:15px}.section-title strong{color:#26333f;font-size:14px}.section-title span{color:#8a98a5;font-size:10px}.identity-list{display:flex;flex-wrap:wrap;gap:6px}.identity-list code{padding:3px 7px;border:1px solid #d8e3ec;background:#f6f8fa;color:#405261;font-size:11px;line-height:18px}.identity-list>span,.empty-summary{color:#8795a3;font-size:12px}.change-list{display:grid;gap:12px}.change-list article{border:1px solid #d8e3ec}.change-list article>header{padding:9px 12px;background:#f6f9fc;color:#405261;font-family:'Cascadia Mono',Consolas,monospace;font-size:12px}.change-list dl{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:1px;background:#e2e9ef}.change-list dl>div{padding:10px 12px;background:#fff}.change-list dt{margin-bottom:6px;color:#71808d;font-size:11px}.change-list dd{display:flex;align-items:center;gap:8px;font-size:12px}.change-list dd span{color:#8795a3;text-decoration:line-through}.change-list dd strong{color:#176fcb}.change-list article>p{padding:12px;color:#52606d;font-size:12px}.deleted-notice{display:flex;align-items:center;gap:14px;padding:18px;border:1px solid #efd0d0;background:#fff7f7;color:#b34f4f}.deleted-notice>.el-icon{font-size:24px}.deleted-notice p{margin-top:5px;color:#785c5c;font-size:12px}.not-found{min-height:420px;display:flex;align-items:center;justify-content:center;flex-direction:column}@media(max-width:900px){.summary-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.change-list dl{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:620px){.summary-section,.data-section{padding:16px 12px}.summary-grid,.change-list dl{grid-template-columns:1fr}.reason-line{grid-template-columns:1fr}}</style> |