<script setup lang="ts">
|
import { computed, onMounted, reactive, ref } from 'vue'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { useRouter } from 'vue-router'
|
import { deleteCompareReport, listCompareReports } from '@/api/compareReports'
|
import type { CompareReportRecord } from '@/stores/compareReports'
|
import { getUserInfo } from '@/api/auth'
|
import CompareReportHistoryDialog from '@/components/CompareReportHistoryDialog.vue'
|
|
interface FilterState {
|
reportName: string
|
creator: string
|
dateRange: [string, string] | null
|
}
|
|
const router = useRouter()
|
const records = ref<CompareReportRecord[]>([])
|
const loading = ref(false)
|
const loadError = ref('')
|
const canCreate = ref(false)
|
const canEdit = ref(false)
|
const canExport = ref(false)
|
const canDelete = ref(false)
|
const deletingId = ref<number | null>(null)
|
const historyVisible = ref(false)
|
const historyReportId = ref<number | null>(null)
|
onMounted(async () => {
|
loading.value = true
|
try {
|
const info = await getUserInfo()
|
canCreate.value = Boolean(info.permissions?.some(item => item === '*:*:*' || item === 'system:compare-report:add'))
|
canEdit.value = Boolean(info.permissions?.some(item => item === '*:*:*' || item === 'system:compare-report:edit'))
|
canExport.value = Boolean(info.permissions?.some(item => item === '*:*:*' || item === 'system:compare-report:export'))
|
canDelete.value = Boolean(info.permissions?.some(item => item === '*:*:*' || item === 'system:compare-report:remove'))
|
await loadReports()
|
} catch (error) {
|
loadError.value = error instanceof Error ? error.message : '报表列表加载失败'
|
} finally { loading.value = false }
|
})
|
const filters = reactive<FilterState>({ reportName: '', creator: '', dateRange: null })
|
const appliedFilters = ref<FilterState>({ reportName: '', creator: '', dateRange: null })
|
const currentPage = ref(1)
|
const pageSize = ref(10)
|
|
const creatorOptions = computed(() => [...new Set(records.value.map(item => item.creator))].sort((a, b) => a.localeCompare(b, 'zh-CN')))
|
|
const filteredRecords = computed(() => {
|
const query = appliedFilters.value
|
const keyword = query.reportName.trim().toLowerCase()
|
return records.value
|
.filter(item => !keyword || item.name.toLowerCase().includes(keyword) || String(item.id).includes(keyword))
|
.filter(item => !query.creator || query.creator === '全部' || item.creator === query.creator)
|
.filter(item => {
|
if (!query.dateRange) return true
|
const date = item.createdAt.slice(0, 10)
|
return date >= query.dateRange[0] && date <= query.dateRange[1]
|
})
|
.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
|
})
|
|
const pagedRecords = computed(() => {
|
const start = (currentPage.value - 1) * pageSize.value
|
return filteredRecords.value.slice(start, start + pageSize.value)
|
})
|
|
function applySearch() {
|
appliedFilters.value = {
|
reportName: filters.reportName,
|
creator: filters.creator,
|
dateRange: filters.dateRange ? [...filters.dateRange] as [string, string] : null,
|
}
|
currentPage.value = 1
|
ElMessage.success(`已找到 ${filteredRecords.value.length} 份对比报表`)
|
}
|
|
function resetSearch() {
|
Object.assign(filters, { reportName: '', creator: '', dateRange: null })
|
appliedFilters.value = { reportName: '', creator: '', dateRange: null }
|
currentPage.value = 1
|
}
|
|
function openCreate() {
|
router.push('/report-compare/new')
|
}
|
|
function openView(item: CompareReportRecord) {
|
router.push({ path: `/report-compare/${item.id}/edit`, query: { mode: 'view' } })
|
}
|
|
async function loadReports() {
|
records.value = (await listCompareReports()).map(item => ({ ...item,
|
createdAt: new Date(item.createdAt).toLocaleString('sv-SE'),
|
baselineTest: '', comparisonTest: '', description: '', revisions: [],
|
}))
|
currentPage.value = Math.min(currentPage.value, Math.max(1, Math.ceil(filteredRecords.value.length / pageSize.value)))
|
loadError.value = ''
|
}
|
|
async function deleteReport(item: CompareReportRecord) {
|
if (deletingId.value !== null) return
|
deletingId.value = item.id
|
try {
|
await ElMessageBox.confirm(`将删除对比报表“${item.name}”(#${item.id})及全部版本,删除后无法恢复,不影响来源测试及其数据。是否继续?`, '确认删除对比报表', {
|
confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning',
|
})
|
} catch {
|
deletingId.value = null
|
return
|
}
|
try {
|
await deleteCompareReport(item.id)
|
// 正式删除成功后才移除展示缓存;刷新失败不能误报删除失败。
|
records.value = records.value.filter(record => record.id !== item.id)
|
ElMessage.success('对比报表及全部版本已删除')
|
try { await loadReports() } catch (error) {
|
loadError.value = `报表已删除,列表刷新失败:${error instanceof Error ? error.message : '请刷新页面重试'}`
|
}
|
} catch (error) {
|
ElMessage.error(`删除失败:${error instanceof Error ? error.message : '请稍后重试'}`)
|
} finally { deletingId.value = null }
|
}
|
|
</script>
|
|
<template>
|
<div class="compare-report-page">
|
<el-alert v-if="loadError" :title="loadError" type="error" :closable="false" show-icon />
|
<section class="filter-panel" aria-label="对比报表查询条件">
|
<header class="section-heading"><div><i></i><strong>查询报表</strong><span>REPORT QUERY</span></div><small>共 {{ records.length }} 份报表</small></header>
|
<div class="filter-grid">
|
<label><span>报告名称</span><el-input v-model="filters.reportName" clearable placeholder="输入报告名称或ID" @keyup.enter="applySearch" /></label>
|
<label><span>创建人</span><el-select v-model="filters.creator" clearable placeholder="全部"><el-option label="全部" value="全部" /><el-option v-for="item in creatorOptions" :key="item" :label="item" :value="item" /></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" @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 v-if="canCreate" class="primary-button" type="button" @click="openCreate"><el-icon><Plus /></el-icon>新建报表</button></div>
|
<div class="table-wrap">
|
<table>
|
<thead><tr><th>报告ID</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 :title="item.name"><strong>{{ item.name }}</strong></td><td>{{ item.createdAt }}</td><td>{{ item.creator }}</td>
|
<td class="action-column"><div class="row-actions"><button type="button" @click="openView(item)">查看报表</button><button v-if="canEdit" type="button" @click="router.push({ path: `/report-compare/${item.id}/edit`, query: { mode: 'edit' } })">编辑报表</button><button type="button" @click="historyReportId = item.id; historyVisible = true">修订记录</button><button v-if="canDelete" class="delete-action" type="button" :disabled="deletingId !== null" @click="deleteReport(item)">删除</button></div></td>
|
</tr>
|
<tr v-if="!pagedRecords.length"><td colspan="5" class="empty-cell">{{ loading ? '正在加载报表' : loadError ? '报表列表加载失败' : '暂无符合条件的对比报表' }}</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>
|
|
<CompareReportHistoryDialog v-model="historyVisible" :report-id="historyReportId" :can-export="canExport" />
|
</div>
|
</template>
|
|
<style scoped>
|
.compare-report-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; }
|
.primary-button:disabled { opacity: .55; cursor: wait; }
|
.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-select), .filter-grid :deep(.el-date-editor) { width: 100%; }
|
.filter-grid :deep(.el-input__wrapper), .filter-grid :deep(.el-select__wrapper) { min-height: 34px; border-radius: 3px; box-shadow: 0 0 0 1px #d8e1e9 inset; background: #f8fafc; }
|
.filter-actions { 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: 1100px; 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: 120px; } th:nth-child(2) { width: 310px; } th:nth-child(3) { width: 180px; } th:nth-child(4) { width: 110px; } th:last-child { width: 380px; }
|
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 { color: #647380; font-family: 'Cascadia Mono', Consolas, monospace; font-size: 12px; }
|
.row-actions { display: flex; align-items: center; justify-content: center; gap: 12px; }
|
.row-actions button { border: 0; color: var(--blue); background: transparent; font-size: 12px; font-weight: 500; cursor: pointer; }
|
.row-actions button:hover { color: #176fcb; text-decoration: underline; }
|
.row-actions .delete-action { color: #ad4a4a; }
|
.row-actions button:disabled { opacity: .55; cursor: wait; }
|
.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); }
|
.compare-report-page :deep(.revision-dialog) { max-width: calc(100vw - 28px); border-radius: 4px; }
|
.compare-report-page :deep(.el-dialog__header) { margin: 0; padding: 17px 20px; border-bottom: 1px solid var(--line); }
|
.compare-report-page :deep(.el-dialog__body) { padding: 20px; }
|
.compare-report-page :deep(.el-dialog__footer) { padding: 14px 20px; border-top: 1px solid var(--line); }
|
.revision-summary { min-height: 42px; display: flex; align-items: center; gap: 22px; margin-bottom: 14px; padding: 0 12px; background: #f6f9fc; border: 1px solid #d8e1e9; }
|
.revision-summary span { min-width: 0; color: #7b8794; font-size: 12px; }
|
.revision-summary strong { margin-left: 5px; color: #44515d; font-weight: 600; }
|
.revision-summary button { margin-left: auto; }
|
.revision-table-wrap { overflow-x: auto; }
|
.revision-table { min-width: 680px; }
|
.revision-table th:nth-child(1) { width: 135px; } .revision-table th:nth-child(2) { width: 155px; } .revision-table th:nth-child(3) { width: 95px; } .revision-table th:nth-child(4) { width: auto; }
|
.revision-table th:last-child, .revision-table td:last-child { text-align: left; }
|
@media (max-width: 1100px) { .filter-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
@media (max-width: 680px) {
|
.filter-grid { grid-template-columns: minmax(0, 1fr); gap: 12px; }
|
.filter-grid { 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, .revision-summary { align-items: flex-start; flex-direction: column; }
|
.revision-summary { padding: 12px; }
|
.revision-summary button { width: 100%; margin-left: 0; }
|
}
|
</style>
|