接入数据修订行为页面
新增服务端分页查询和行为详情,支持三类数据筛选、删除摘要和返回测试控制台上下文。
| New file |
| | |
| | | import { request, type ApiResponse } from './request' |
| | | |
| | | export type RevisionDataType = 'REALTIME' | 'STEP' | 'CYCLE' |
| | | export type RevisionOperationType = 'EDIT' | 'IMPORT' | 'DELETE' |
| | | |
| | | 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 interface DataRevisionAction { |
| | | actionId: number |
| | | testId: number |
| | | testName: string |
| | | testReceptionStatus?: string |
| | | dataType: RevisionDataType |
| | | operationType: RevisionOperationType |
| | | scopeSummary: string |
| | | identitySummary: string |
| | | changesSummary: string | null |
| | | reason: string |
| | | operatorId: number | null |
| | | operatorName: string |
| | | createdAtUtc: string |
| | | } |
| | | |
| | | interface RevisionListResponse extends ApiResponse { total: number; rows: DataRevisionAction[] } |
| | | interface RevisionDetailResponse extends ApiResponse { data: DataRevisionAction } |
| | | |
| | | export interface DataRevisionQuery { |
| | | pageNum: number |
| | | pageSize: number |
| | | testId?: number |
| | | testName?: string |
| | | dataType?: RevisionDataType |
| | | operationType?: RevisionOperationType |
| | | beginCreatedAt?: string |
| | | endCreatedAt?: string |
| | | } |
| | | |
| | | export async function getDataRevisionActions(query: DataRevisionQuery) { |
| | | const params = new URLSearchParams() |
| | | Object.entries(query).forEach(([key, value]) => { |
| | | if (value !== undefined && value !== null && value !== '') params.set(key, String(value)) |
| | | }) |
| | | const result = await request<RevisionListResponse>(`/system/data-revision/list?${params.toString()}`) |
| | | return { total: Number(result.total) || 0, rows: result.rows || [] } |
| | | } |
| | | |
| | | export async function getDataRevisionAction(actionId: number) { |
| | | return (await request<RevisionDetailResponse>(`/system/data-revision/${actionId}`)).data |
| | | } |
| | |
| | | <script setup lang="ts"> |
| | | import { computed, onMounted, reactive, ref } from 'vue' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import { useRouter } from 'vue-router' |
| | | import { useRoute, useRouter } from 'vue-router' |
| | | import * as XLSX from 'xlsx' |
| | | import { Download, EditPen, Refresh, Search, UploadFilled, Clock, Check } from '@element-plus/icons-vue' |
| | | import { getBtsCycles, getBtsRecords, getBtsSteps, type BtsCycleRecord, type BtsRecord, type BtsStepRecord } from '@/api/tests' |
| | |
| | | interface FilterState { batteryNo: string; dateRange: [string, string] | null; cycleNo: string; stepNo: string } |
| | | interface DataRow { id: string; recordNo: string; batteryNo: string; cycleNo: number; stepNo: number; stepName: string; sequenceId: number; recordedAt: string; absoluteTime: string; stepDuration: string; startVoltage: string; endVoltage: string; chargePeakVoltage: string; dischargeValleyVoltage: string; voltage: string; current: string; capacity: string; energy: string; chargeCapacity: string; dischargeCapacity: string; efficiency: string; chargeEnergy: string; dischargeEnergy: string } |
| | | const props = defineProps<{ testId: number; testName: string; cells: ConsoleCell[]; canImport?: boolean; bindings?: Array<{ batteryNo: string; devid?: number | null; chlid?: number | null }> }>() |
| | | const router = useRouter() |
| | | const activeTab = ref<DataTab>('realtime'); const loading = ref(false); const loadError = ref(''); const rows = ref<DataRow[]>([]); const stepRows = ref<DataRow[]>([]); const cycleRows = ref<DataRow[]>([]) |
| | | const route = useRoute(); const router = useRouter() |
| | | const requestedDataType = String(route.query.dataType || 'REALTIME') |
| | | const activeTab = ref<DataTab>(requestedDataType === 'STEP' ? 'step' : requestedDataType === 'CYCLE' ? 'cycle' : 'realtime'); const loading = ref(false); const loadError = ref(''); const rows = ref<DataRow[]>([]); const stepRows = ref<DataRow[]>([]); const cycleRows = ref<DataRow[]>([]) |
| | | const dataTabs = [{ key: 'realtime', label: '实时' }, { key: 'step', label: '工步' }, { key: 'cycle', label: '循环' }] as const |
| | | const filters = reactive<FilterState>({ batteryNo: '', dateRange: null, cycleNo: '', stepNo: '' }); const appliedFilters = ref<FilterState>({ batteryNo: '', dateRange: null, cycleNo: '', stepNo: '' }); const currentPage = ref(1); const pageSize = ref(20) |
| | | const selectedIds = ref<Set<string>>(new Set()); const editorVisible = ref(false); const editing = ref(false); const singleEditing = ref(false); const draftRows = ref<Record<string, DataRow>>({}); const editForm = reactive({ field: 'voltage' as keyof DataRow, value: '', remark: '' }) |
| | |
| | | const allPageSelected = computed(() => pagedRows.value.length > 0 && pagedRows.value.every(row => selectedIds.value.has(row.id))) |
| | | const somePageSelected = computed(() => pagedRows.value.some(row => selectedIds.value.has(row.id)) && !allPageSelected.value) |
| | | function goToImport() { router.push({ path: `/test-list/${props.testId}/console/data/import`, query: { testName: props.testName, completed: props.canImport ? '1' : '0', dataType: dataType.value } }) } |
| | | function goToRevisions() { router.push({ path: '/data-revision', query: { testId: String(props.testId), testName: props.testName } }) } |
| | | function goToRevisions() { router.push({ path: '/data-revision', query: { testId: String(props.testId), dataType: dataType.value } }) } |
| | | function templateHeaders() { return activeTab.value === 'cycle' ? ['记录编号', '电池号', '循环序号', '充放电容量(Ah)', '效率(%)', '能量(Wh)'] : activeTab.value === 'step' ? ['工步记录编号', '电池号', '循环序号', '工步序号', '工步名称', '工步时间', '容量(Ah)', '能量(Wh)', '起止电压(V)', '充电峰值电压(V)', '放电谷值电压(V)'] : ['记录编号', '电池号', '记录时间', '循环序号', '工步序号', '工步名称', '记录序号', '电压(V)', '电流(A)', '容量(Ah)', '能量(Wh)'] } |
| | | function downloadTemplate() { const worksheet = XLSX.utils.aoa_to_sheet([templateHeaders()]); const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, `${activeTab.value}模板`); XLSX.writeFile(workbook, `测试${activeTab.value}数据导入模板.xlsx`); ElMessage.success('导入模板已下载') } |
| | | function openBatchEditor() { singleEditing.value = false; editing.value = true; draftRows.value = Object.fromEntries(sourceRows.value.map(row => [row.id, { ...row }])) } |
| | |
| | | <script setup lang="ts"> |
| | | import { computed, reactive, ref } from 'vue' |
| | | import { onMounted, reactive, ref } from 'vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import { useRoute, useRouter } from 'vue-router' |
| | | import { dataRevisionRecords, type DataRevisionRecord } from '@/stores/dataRevisions' |
| | | |
| | | interface FilterState { |
| | | keyword: string |
| | | dateRange: [string, string] | null |
| | | } |
| | | |
| | | const router = useRouter() |
| | | const route = useRoute() |
| | | const initialKeyword = String(route.query.testId || route.query.testName || '') |
| | | const filters = reactive<FilterState>({ keyword: initialKeyword, dateRange: null }) |
| | | const appliedFilters = ref<FilterState>({ keyword: initialKeyword, dateRange: null }) |
| | | const currentPage = ref(1) |
| | | const pageSize = ref(10) |
| | | |
| | | const filteredRecords = computed(() => { |
| | | const keyword = appliedFilters.value.keyword.trim().toLowerCase() |
| | | const dateRange = appliedFilters.value.dateRange |
| | | |
| | | return dataRevisionRecords |
| | | .filter(item => !keyword |
| | | || String(item.testId).includes(keyword) |
| | | || item.testName.toLowerCase().includes(keyword) |
| | | || item.id.toLowerCase().includes(keyword)) |
| | | .filter(item => { |
| | | if (!dateRange) return true |
| | | const generatedDate = item.generatedAt.slice(0, 10) |
| | | return generatedDate >= dateRange[0] && generatedDate <= dateRange[1] |
| | | }) |
| | | .sort((a, b) => b.modifiedAt.localeCompare(a.modifiedAt)) |
| | | }) |
| | | |
| | | const pagedRecords = computed(() => { |
| | | const start = (currentPage.value - 1) * pageSize.value |
| | | return filteredRecords.value.slice(start, start + pageSize.value) |
| | | }) |
| | | |
| | | function applySearch() { |
| | | appliedFilters.value = { |
| | | keyword: filters.keyword, |
| | | dateRange: filters.dateRange ? [...filters.dateRange] as [string, string] : null, |
| | | } |
| | | currentPage.value = 1 |
| | | ElMessage.success(`已找到 ${filteredRecords.value.length} 条修订记录`) |
| | | } |
| | | |
| | | function resetSearch() { |
| | | filters.keyword = '' |
| | | filters.dateRange = null |
| | | appliedFilters.value = { keyword: '', dateRange: null } |
| | | currentPage.value = 1 |
| | | } |
| | | |
| | | function viewRecord(item: DataRevisionRecord) { |
| | | router.push(`/data-revision/${item.id}`) |
| | | } |
| | | |
| | | function escapeCsv(value: string | number) { |
| | | return `"${String(value).replaceAll('"', '""')}"` |
| | | } |
| | | |
| | | function downloadRecords() { |
| | | if (!filteredRecords.value.length) { |
| | | ElMessage.warning('当前筛选条件下没有可下载的数据') |
| | | return |
| | | } |
| | | |
| | | const headers = ['修改记录ID', '测试编号', '测试名称', '测试时间', '记录生成时间', '记录修改时间', '修改人', '修改类型', '修改备注'] |
| | | const rows = filteredRecords.value.map(item => [ |
| | | item.id, item.testId, item.testName, item.testedAt, item.generatedAt, |
| | | item.modifiedAt, item.modifier, item.operation, item.remark, |
| | | ]) |
| | | const csv = `\uFEFF${[headers, ...rows].map(row => row.map(escapeCsv).join(',')).join('\n')}` |
| | | const url = URL.createObjectURL(new Blob([csv], { type: 'text/csv;charset=utf-8' })) |
| | | const anchor = document.createElement('a') |
| | | anchor.href = url |
| | | anchor.download = `数据修订记录_${new Date().toISOString().slice(0, 10)}.csv` |
| | | anchor.style.display = 'none' |
| | | document.body.appendChild(anchor) |
| | | anchor.click() |
| | | anchor.remove() |
| | | window.setTimeout(() => URL.revokeObjectURL(url), 1000) |
| | | ElMessage.success(`已导出 ${filteredRecords.value.length} 条修订记录`) |
| | | } |
| | | import { 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 |
| | | 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 resetSearch() { Object.assign(filters, { test: '', dataType: '', operationType: '', dateRange: null }); applied.value = { test: '', dataType: '', operationType: '', dateRange: null }; currentPage.value = 1; void loadRecords() } |
| | | function viewRecord(item: DataRevisionAction) { router.push(`/data-revision/${item.actionId}`) } |
| | | function returnToTest(item: DataRevisionAction) { router.push({ path: `/test-list/${item.testId}/console`, query: { tab: 'data-list', dataType: item.dataType, ...(item.testReceptionStatus === 'ENDED' ? { state: 'ended' } : {}) } }) } |
| | | 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" aria-label="数据修订记录查询条件"> |
| | | <header class="section-heading"> |
| | | <div><i></i><strong>查询条件</strong><span>FILTER CONDITIONS</span></div> |
| | | <small>共 {{ dataRevisionRecords.length }} 条记录</small> |
| | | </header> |
| | | <div class="filter-grid"> |
| | | <label> |
| | | <span>测试搜索</span> |
| | | <el-input v-model="filters.keyword" clearable placeholder="输入测试ID、测试名称" @keyup.enter="applySearch" /> |
| | | </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" @click="applySearch"><el-icon><Search /></el-icon>查询</button> |
| | | <button class="secondary-button" type="button" @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>{{ filteredRecords.length }} RESULTS · 按记录修改时间倒序</span></div> |
| | | <button class="secondary-button" type="button" @click="downloadRecords"><el-icon><Download /></el-icon>下载结果</button> |
| | | </div> |
| | | |
| | | <div 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 pagedRecords" :key="item.id"> |
| | | <td class="id-cell">{{ item.id }}</td> |
| | | <td class="code-cell">#{{ item.testId }}</td> |
| | | <td><strong>{{ item.testName }}</strong></td> |
| | | <td>{{ item.testedAt }}</td> |
| | | <td>{{ item.generatedAt }}</td> |
| | | <td>{{ item.modifiedAt }}</td> |
| | | <td>{{ item.modifier }}</td> |
| | | <td :title="item.remark"><span class="operation-dot" :class="item.operation === '删除' ? 'deleted' : 'modified'"></span>{{ item.remark }}</td> |
| | | <td class="action-column"><button class="view-action" type="button" @click="viewRecord(item)">查看</button></td> |
| | | </tr> |
| | | <tr v-if="!pagedRecords.length"><td colspan="9" class="empty-cell">暂无符合条件的数据修订记录</td></tr> |
| | | </tbody> |
| | | </table> |
| | | </div> |
| | | |
| | | <footer class="pagination-bar"> |
| | | <span>显示 {{ pagedRecords.length }} 条,共 {{ filteredRecords.length }} 条</span> |
| | | <el-pagination |
| | | v-model:current-page="currentPage" |
| | | v-model:page-size="pageSize" |
| | | :page-sizes="[10, 20, 50]" |
| | | :total="filteredRecords.length" |
| | | layout="sizes, prev, pager, next" |
| | | background |
| | | /> |
| | | </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; font-weight: 500; cursor: pointer; white-space: nowrap; } |
| | | .primary-button { color: #fff; background: var(--blue); border-color: var(--blue); } |
| | | .primary-button:hover { background: #176fcb; border-color: #176fcb; } |
| | | .secondary-button { color: #52606d; background: #fff; border-color: var(--line-strong); } |
| | | .secondary-button:hover { color: var(--blue); border-color: #9dc8f5; } |
| | | .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; background: var(--surface-soft); border-bottom: 1px solid var(--line); } |
| | | .section-heading { height: 44px; padding: 0 16px; } |
| | | .section-heading > div, .result-toolbar > div { display: flex; align-items: center; gap: 8px; } |
| | | .section-heading > div > i, .result-toolbar > div:first-child > i { width: 3px; height: 16px; background: #2b78a3; } |
| | | .section-heading strong, .result-toolbar strong { color: var(--ink); font-size: 15px; font-weight: 600; } |
| | | .section-heading span, .result-toolbar span { color: #81909d; font-size: 10px; } |
| | | .section-heading small { color: #647380; font-size: 11px; } |
| | | .filter-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px 18px; padding: 16px; } |
| | | .filter-grid label { width: 100%; min-width: 0; display: grid; grid-template-columns: 72px minmax(0, 1fr); align-items: center; gap: 8px; } |
| | | .filter-grid label > span { color: #52606d; font-size: 12px; font-weight: 500; } |
| | | .filter-grid :deep(.el-date-editor) { width: 100%; } |
| | | .filter-grid :deep(.el-input__wrapper) { min-height: 34px; border-radius: 3px; box-shadow: 0 0 0 1px #d8e1e9 inset; background: #f8fafc; } |
| | | .filter-actions { grid-column: -2 / -1; min-width: 0; display: flex; align-items: center; justify-content: flex-end; gap: 8px; } |
| | | .result-toolbar { height: 50px; padding: 0 16px; } |
| | | .table-wrap { overflow-x: auto; } |
| | | table { width: 100%; min-width: 1460px; table-layout: fixed; border-collapse: collapse; color: #52606d; font-size: 13px; } |
| | | 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; color: #52606d; background: #eaf4ff; border-color: #c7e0ff; font-size: 12px; font-weight: 600; } |
| | | th:nth-child(1) { width: 145px; } th:nth-child(2) { width: 90px; } th:nth-child(3) { width: 190px; } |
| | | th:nth-child(4), th:nth-child(5), th:nth-child(6) { width: 160px; } th:nth-child(7) { width: 85px; } |
| | | th:nth-child(8) { width: 300px; } th:nth-child(9) { width: 85px; } |
| | | th:last-child, td:last-child { border-right: 0; text-align: center; } |
| | | .action-column { position: sticky; right: 0; z-index: 2; background: #fff; overflow: visible; border-left: 0; box-shadow: inset 1px 0 0 #b4c7d9, -8px 0 14px rgba(23, 49, 74, .12); } |
| | | th.action-column { z-index: 3; background: #eaf4ff; } |
| | | tbody tr:hover td.action-column { background: #f7fbff; } |
| | | tbody tr:hover { background: #f7fbff; } |
| | | tbody tr:last-child td { border-bottom: 0; } |
| | | td strong { color: #26333f; font-weight: 600; } |
| | | .id-cell, .code-cell { color: #647380; font-family: 'Cascadia Mono', Consolas, monospace; font-size: 12px; } |
| | | .operation-dot { display: inline-block; width: 6px; height: 6px; margin-right: 7px; border-radius: 50%; vertical-align: 1px; } |
| | | .operation-dot.modified { background: #2188f5; } |
| | | .operation-dot.deleted { background: #d15b5b; } |
| | | .view-action { border: 0; color: var(--blue); background: transparent; font-size: 12px; font-weight: 500; cursor: pointer; } |
| | | .view-action:hover { color: #176fcb; text-decoration: underline; } |
| | | .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; border-top: 1px solid var(--line); } |
| | | .pagination-bar > span { color: #7b8794; font-size: 11px; } |
| | | .pagination-bar :deep(.el-pagination) { --el-pagination-button-bg-color: #edf4fa; --el-pagination-hover-color: var(--blue); } |
| | | |
| | | @media (max-width: 1320px) { .filter-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } } |
| | | @media (max-width: 980px) { .filter-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } } |
| | | @media (max-width: 680px) { |
| | | .filter-grid { grid-template-columns: minmax(0, 1fr); gap: 12px; padding: 14px 12px; } |
| | | .filter-actions { justify-content: stretch; } |
| | | .filter-actions button { flex: 1; } |
| | | .section-heading, .result-toolbar { padding-left: 12px; } |
| | | .section-heading span, .result-toolbar span { display: none; } |
| | | .pagination-bar { align-items: flex-start; flex-direction: column; overflow-x: auto; } |
| | | } |
| | | </style> |
| | | <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> |
| | | <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 } from 'vue' |
| | | import { computed, onMounted, ref } from 'vue' |
| | | import { useRoute, useRouter } from 'vue-router' |
| | | import { getDataRevision, type RevisionDataValues } from '@/stores/dataRevisions' |
| | | |
| | | const route = useRoute() |
| | | const router = useRouter() |
| | | const record = computed(() => getDataRevision(String(route.params.id))) |
| | | |
| | | const dataFields: Array<{ key: keyof RevisionDataValues; label: string }> = [ |
| | | { key: 'voltage', label: '电压 (V)' }, |
| | | { key: 'current', label: '电流 (mA)' }, |
| | | { key: 'capacity', label: '容量 (mAh)' }, |
| | | { key: 'energy', label: '能量 (mWh)' }, |
| | | { key: 'temperature', label: '温度(采集器)' }, |
| | | { key: 'positivePotential', label: '正电位(采集器)' }, |
| | | { key: 'negativePotential', label: '负电位(采集器)' }, |
| | | ] |
| | | |
| | | function isChanged(key: keyof RevisionDataValues) { |
| | | const item = record.value |
| | | return Boolean(item?.revisedData && item.originalData[key] !== item.revisedData[key]) |
| | | } |
| | | |
| | | function goBack() { |
| | | router.push('/data-revision') |
| | | } |
| | | import { 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' } : {}) } }) } |
| | | 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 DETAILS</span> |
| | | </div> |
| | | <section v-if="record" class="detail-panel"> |
| | | <header class="detail-toolbar"> |
| | | <div class="heading-group"> |
| | | <i></i> |
| | | <div><strong>数据详情</strong><span>{{ record.id }} · 只读审计记录</span></div> |
| | | </div> |
| | | <span class="operation-tag" :class="record.operation === '删除' ? 'deleted' : 'modified'"> |
| | | <el-icon><Delete v-if="record.operation === '删除'" /><EditPen v-else /></el-icon>{{ record.operation }} |
| | | </span> |
| | | </header> |
| | | |
| | | <section class="summary-section" aria-label="修订记录概要"> |
| | | <div class="summary-grid"> |
| | | <div><span>测试编号</span><strong>#{{ record.testId }}</strong></div> |
| | | <div><span>测试名称</span><strong>{{ record.testName }}</strong></div> |
| | | <div><span>工步名称</span><strong>{{ record.stepName }}</strong></div> |
| | | <div><span>循环编号</span><strong>{{ record.cycleNo }}</strong></div> |
| | | <div><span>工步序号</span><strong>{{ record.stepNo }}</strong></div> |
| | | <div><span>工步时间</span><strong class="mono-value">{{ record.stepDuration }}</strong></div> |
| | | <div><span>绝对时间</span><strong>{{ record.absoluteTime }}</strong></div> |
| | | <div><span>记录生成时间</span><strong>{{ record.generatedAt }}</strong></div> |
| | | <div><span>修改时间</span><strong>{{ record.modifiedAt }}</strong></div> |
| | | <div><span>修改人</span><strong>{{ record.modifier }}</strong></div> |
| | | </div> |
| | | <div class="remark-line"><span>修改备注</span><p>{{ record.remark }}</p></div> |
| | | </section> |
| | | |
| | | <section class="data-section" aria-label="原始数据"> |
| | | <div class="section-title"><i></i><strong>原始数据</strong><span>ORIGINAL DATA</span></div> |
| | | <div class="data-grid"> |
| | | <div v-for="field in dataFields" :key="field.key" class="data-field"> |
| | | <span>{{ field.label }}</span> |
| | | <output :class="{ changed: isChanged(field.key) }">{{ record.originalData[field.key] }}</output> |
| | | </div> |
| | | </div> |
| | | </section> |
| | | |
| | | <section class="data-section revised-section" aria-label="修改结果"> |
| | | <div class="section-title"><i></i><strong>修改后数据</strong><span>REVISION RESULT</span></div> |
| | | <div v-if="record.revisedData" class="data-grid"> |
| | | <div v-for="field in dataFields" :key="field.key" class="data-field"> |
| | | <span>{{ field.label }}</span> |
| | | <output :class="{ changed: isChanged(field.key) }"> |
| | | {{ record.revisedData[field.key] }} |
| | | <small v-if="isChanged(field.key)">已修订</small> |
| | | </output> |
| | | </div> |
| | | </div> |
| | | <div v-else class="deleted-result"> |
| | | <span class="deleted-icon"><el-icon><Delete /></el-icon></span> |
| | | <div><strong>原始数据已删除</strong><p>该采样记录已从测试结果中移除,原始值仅在本审计页面保留展示。</p></div> |
| | | </div> |
| | | </section> |
| | | </section> |
| | | |
| | | <section v-else class="not-found-panel"> |
| | | <el-empty description="未找到该数据修订记录" /> |
| | | <button class="secondary-button" type="button" @click="goBack"><el-icon><ArrowLeft /></el-icon>返回记录列表</button> |
| | | </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; font-weight: 600; } |
| | | .back-button { display: inline-flex; align-items: center; gap: 6px; border: 0; color: #52606d; background: transparent; font-size: 13px; cursor: pointer; } |
| | | .back-button:hover { color: var(--blue); } |
| | | .detail-panel, .not-found-panel { 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; min-width: 0; } |
| | | .heading-group > i { width: 3px; height: 18px; background: #2b78a3; } |
| | | .heading-group > div { min-width: 0; } |
| | | .heading-group strong, .heading-group span { display: block; } |
| | | .heading-group strong { color: var(--ink); font-size: 15px; font-weight: 600; } |
| | | .heading-group span { margin-top: 2px; color: #81909d; font-size: 10px; } |
| | | .operation-tag { height: 26px; display: inline-flex; align-items: center; gap: 5px; padding: 0 9px; border: 1px solid; font-size: 12px; font-weight: 600; } |
| | | .operation-tag.modified { color: #176fcb; background: #edf6ff; border-color: #b8d9fb; } |
| | | .operation-tag.deleted { color: #b34f4f; background: #fff1f1; border-color: #e9bcbc; } |
| | | .summary-section { padding: 20px 22px; } |
| | | .summary-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 20px 28px; } |
| | | .summary-grid > div { min-width: 0; display: grid; grid-template-columns: 92px minmax(0, 1fr); align-items: baseline; gap: 8px; } |
| | | .summary-grid span, .remark-line > span { color: #7b8794; font-size: 12px; } |
| | | .summary-grid strong { min-width: 0; color: #34414d; font-size: 13px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } |
| | | .mono-value { font-family: 'Cascadia Mono', Consolas, monospace; } |
| | | .remark-line { display: grid; grid-template-columns: 92px minmax(0, 1fr); gap: 8px; margin-top: 20px; padding-top: 16px; border-top: 1px dashed #dce5ed; } |
| | | .remark-line p { color: #34414d; font-size: 13px; line-height: 1.6; } |
| | | .data-section { padding: 20px 22px 24px; border-top: 1px solid var(--line); } |
| | | .section-title { display: flex; align-items: center; gap: 8px; margin-bottom: 16px; } |
| | | .section-title i { width: 3px; height: 15px; background: #2b78a3; } |
| | | .section-title strong { color: #26333f; font-size: 14px; font-weight: 600; } |
| | | .section-title span { color: #8a98a5; font-size: 10px; } |
| | | .data-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 18px 28px; } |
| | | .data-field { min-width: 0; } |
| | | .data-field > span { display: block; margin-bottom: 7px; color: #52606d; font-size: 12px; font-weight: 500; } |
| | | .data-field output { min-height: 36px; display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 0 10px; border: 1px solid #e0e8ef; color: #34414d; background: #f6f8fa; font-family: 'Cascadia Mono', Consolas, monospace; font-size: 13px; } |
| | | .data-field output.changed { color: #176fcb; background: #eef7ff; border-color: #b8d9fb; } |
| | | .data-field output small { color: #176fcb; font-family: 'Noto Sans SC', 'Microsoft YaHei', sans-serif; font-size: 10px; font-weight: 500; } |
| | | .revised-section { min-height: 210px; } |
| | | .deleted-result { min-height: 118px; display: flex; align-items: center; gap: 16px; padding: 18px; border: 1px solid #efd0d0; background: #fff7f7; } |
| | | .deleted-icon { width: 42px; height: 42px; flex: 0 0 42px; display: grid; place-items: center; color: #b34f4f; background: #ffe9e9; font-size: 20px; } |
| | | .deleted-result strong { color: #9e4141; font-size: 14px; font-weight: 600; } |
| | | .deleted-result p { margin-top: 6px; color: #785c5c; font-size: 12px; line-height: 1.6; } |
| | | .not-found-panel { min-height: 420px; display: flex; align-items: center; justify-content: center; flex-direction: column; } |
| | | .secondary-button { height: 34px; display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 0 13px; border: 1px solid var(--line-strong); border-radius: 3px; color: #52606d; background: #fff; font-size: 13px; font-weight: 500; cursor: pointer; } |
| | | .secondary-button:hover { color: var(--blue); border-color: #9dc8f5; } |
| | | |
| | | @media (max-width: 1280px) { |
| | | .summary-grid, .data-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } |
| | | } |
| | | |
| | | @media (max-width: 900px) { |
| | | .summary-grid, .data-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } |
| | | } |
| | | |
| | | @media (max-width: 620px) { |
| | | .detail-toolbar { padding: 0 12px; } |
| | | .heading-group span { display: none; } |
| | | .summary-section, .data-section { padding: 16px 12px 20px; } |
| | | .summary-grid, .data-grid { grid-template-columns: minmax(0, 1fr); gap: 16px; } |
| | | .summary-grid > div, .remark-line { grid-template-columns: 86px minmax(0, 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>{{ 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> |