<script setup lang="ts">
|
import { computed, 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} 条修订记录`)
|
}
|
</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>
|