<script setup lang="ts">
|
import { onMounted, reactive, ref } from 'vue'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { useRouter } from 'vue-router'
|
import { deleteTests, getTestList } from '@/api/tests'
|
import { getTestTypeList } from '@/api/testTypes'
|
import type { TestRecord, TestStatus } from '@/stores/tests'
|
|
interface FilterState {
|
name: string
|
typeId: number | null
|
status: string
|
batteryNo: string
|
dateRange: [string, string] | null
|
}
|
|
interface TestTypeOption {
|
id: number
|
name: string
|
}
|
|
const statuses: Array<{ label: TestStatus; value: string }> = [
|
{ label: '待测试', value: 'WAITING' },
|
{ label: '检测中', value: 'RUNNING' },
|
{ label: '暂停', value: 'PAUSED' },
|
{ label: '检测完成', value: 'COMPLETED' },
|
]
|
const router = useRouter()
|
const testTypes = ref<TestTypeOption[]>([])
|
const records = ref<TestRecord[]>([])
|
const total = ref(0)
|
const loading = ref(false)
|
const filters = reactive<FilterState>({ name: '', typeId: null, status: '', batteryNo: '', dateRange: null })
|
const appliedFilters = ref<FilterState>({ name: '', typeId: null, status: '', batteryNo: '', dateRange: null })
|
const currentPage = ref(1)
|
const pageSize = ref(10)
|
const deletingTestId = ref<number | null>(null)
|
|
async function loadTestTypes() {
|
try {
|
const result = await getTestTypeList({ pageNum: 1, pageSize: 1000, status: '0' })
|
testTypes.value = result.rows.map(item => ({ id: item.id, name: item.name }))
|
} catch (error) {
|
ElMessage.error(error instanceof Error ? error.message : '测试类型加载失败')
|
}
|
}
|
|
async function loadTests() {
|
loading.value = true
|
const query = appliedFilters.value
|
try {
|
const result = await getTestList({
|
pageNum: currentPage.value,
|
pageSize: pageSize.value,
|
testName: query.name.trim() || undefined,
|
testTypeId: query.typeId || undefined,
|
batteryNo: query.batteryNo.trim() || undefined,
|
status: query.status || undefined,
|
beginCreateTime: query.dateRange?.[0] ? `${query.dateRange[0]} 00:00:00` : undefined,
|
endCreateTime: query.dateRange?.[1] ? `${query.dateRange[1]} 23:59:59` : undefined,
|
})
|
records.value = result.rows
|
total.value = result.total
|
} catch (error) {
|
records.value = []
|
total.value = 0
|
ElMessage.error(error instanceof Error ? error.message : '测试列表加载失败')
|
} finally {
|
loading.value = false
|
}
|
}
|
|
async function applySearch() {
|
appliedFilters.value = {
|
name: filters.name,
|
typeId: filters.typeId,
|
status: filters.status,
|
batteryNo: filters.batteryNo,
|
dateRange: filters.dateRange ? [...filters.dateRange] as [string, string] : null,
|
}
|
currentPage.value = 1
|
await loadTests()
|
}
|
|
async function resetSearch() {
|
filters.name = ''
|
filters.typeId = null
|
filters.status = ''
|
filters.batteryNo = ''
|
filters.dateRange = null
|
appliedFilters.value = { name: '', typeId: null, status: '', batteryNo: '', dateRange: null }
|
currentPage.value = 1
|
await loadTests()
|
}
|
|
function statusClass(status: TestStatus) {
|
return {
|
'待测试': 'pending',
|
'检测中': 'running',
|
'检测完成': 'completed',
|
'暂停': 'paused',
|
'未知': 'paused',
|
}[status]
|
}
|
|
function createTest() {
|
router.push('/test-list/new')
|
}
|
|
function openConsole(item: TestRecord) {
|
router.push(`/test-list/${item.id}/console`)
|
}
|
|
function editTest(item: TestRecord) {
|
router.push(`/test-list/${item.id}/edit`)
|
}
|
|
async function removeTest(item: TestRecord) {
|
try {
|
await ElMessageBox.confirm(`删除测试“${item.name}”将一并删除全部测试数据、绑定、阈值、修订和异常记录,且无法恢复,是否继续?`, '确认删除测试', {
|
confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning', confirmButtonClass: 'danger-confirm',
|
})
|
const deletionPage = currentPage.value
|
deletingTestId.value = item.id
|
await deleteTests([item.id])
|
ElMessage.success('测试已删除')
|
await loadTests()
|
if (currentPage.value === deletionPage && records.value.length === 0 && deletionPage > 1 && total.value > 0) {
|
currentPage.value = deletionPage - 1
|
await loadTests()
|
}
|
} catch (error) {
|
if (error === 'cancel' || error === 'close') return
|
ElMessage.error(error instanceof Error ? error.message : '测试删除失败')
|
} finally {
|
deletingTestId.value = null
|
}
|
}
|
|
function downloadTests() {
|
ElMessage.info('下载接口尚未对接')
|
}
|
|
function handlePageChange(page: number) {
|
currentPage.value = page
|
loadTests()
|
}
|
|
function handleSizeChange(size: number) {
|
pageSize.value = size
|
currentPage.value = 1
|
loadTests()
|
}
|
|
onMounted(() => {
|
loadTests()
|
loadTestTypes()
|
})
|
</script>
|
|
<template>
|
<div class="test-list-page">
|
<section class="filter-panel" aria-label="测试查询条件">
|
<div class="section-heading">
|
<div><i></i><strong>查询条件</strong><span>FILTER CONDITIONS</span></div>
|
<small>共 {{ total }} 条记录</small>
|
</div>
|
<div class="filter-grid">
|
<label>
|
<span>测试名称</span>
|
<el-input v-model="filters.name" clearable placeholder="输入测试名称" @keyup.enter="applySearch" />
|
</label>
|
<label>
|
<span>电池号</span>
|
<el-input v-model="filters.batteryNo" clearable placeholder="输入电池号" @keyup.enter="applySearch" />
|
</label>
|
<label>
|
<span>测试类型</span>
|
<el-select v-model="filters.typeId" clearable placeholder="全部类型">
|
<el-option v-for="item in testTypes" :key="item.id" :label="item.name" :value="item.id" />
|
</el-select>
|
</label>
|
<label>
|
<span>状态</span>
|
<el-select v-model="filters.status" clearable placeholder="全部状态">
|
<el-option v-for="item in statuses" :key="item.value" :label="item.label" :value="item.value" />
|
</el-select>
|
</label>
|
<label class="date-filter">
|
<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>{{ total }} RESULTS · 按创建时间倒序</span>
|
</div>
|
<div class="toolbar-actions">
|
<button class="primary-button" type="button" @click="createTest"><el-icon><Plus /></el-icon>新建测试</button>
|
<button class="secondary-button" type="button" @click="downloadTests"><el-icon><Download /></el-icon>下载结果</button>
|
</div>
|
</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>测试备注</th><th>状态</th><th class="action-column">操作</th>
|
</tr>
|
</thead>
|
<tbody>
|
<tr v-for="item in records" :key="item.id">
|
<td class="id-cell">#{{ item.id }}</td>
|
<td><strong class="test-name">{{ item.name }}</strong></td>
|
<td>{{ item.type }}</td>
|
<td class="code-cell">{{ item.batteryNo }}</td>
|
<td>{{ item.createdAt }}</td>
|
<td><span class="time-stack"><b>{{ item.startedAt }}</b><small>{{ item.endedAt }}</small></span></td>
|
<td :title="item.purpose">{{ item.purpose }}</td>
|
<td>{{ item.owner }}</td>
|
<td :title="item.remark">{{ item.remark }}</td>
|
<td><span class="status-tag" :class="statusClass(item.status)"><i></i>{{ item.status }}</span></td>
|
<td class="action-column">
|
<div class="row-actions">
|
<button type="button" @click="openConsole(item)">控制台</button>
|
<button v-if="item.editable" type="button" @click="editTest(item)">编辑</button>
|
<button class="delete-action" type="button" :disabled="deletingTestId !== null" @click="removeTest(item)">{{ deletingTestId === item.id ? '删除中...' : '删除' }}</button>
|
</div>
|
</td>
|
</tr>
|
<tr v-if="!loading && !records.length"><td colspan="11" class="empty-cell">暂无符合条件的测试记录</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"
|
layout="sizes, prev, pager, next"
|
background
|
@current-change="handlePageChange"
|
@size-change="handleSizeChange"
|
/>
|
</footer>
|
</section>
|
</div>
|
</template>
|
|
<style scoped>
|
.test-list-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-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 { 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; }
|
.toolbar-actions { display: flex; align-items: center; gap: 8px; }
|
.table-wrap { overflow-x: auto; }
|
table { width: 100%; min-width: 1540px; 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: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; }
|
th:nth-child(1) { width: 78px; } th:nth-child(2) { width: 176px; } th:nth-child(3) { width: 105px; }
|
th:nth-child(4) { width: 125px; } th:nth-child(5) { width: 142px; } th:nth-child(6) { width: 155px; }
|
th:nth-child(7) { width: 185px; } th:nth-child(8) { width: 80px; } th:nth-child(9) { width: 135px; }
|
th:nth-child(10) { width: 125px; } th:nth-child(11) { width: 124px; }
|
.id-cell, .code-cell { color: #647380; font-family: 'Cascadia Mono', Consolas, monospace; font-size: 12px; }
|
.test-name { color: #26333f; font-weight: 600; }
|
.time-stack b, .time-stack small { display: block; font-size: 11px; font-weight: 400; }
|
.time-stack small { margin-top: 3px; color: #8b98a4; }
|
.status-tag { display: inline-flex; align-items: center; gap: 6px; padding: 4px 7px; border: 1px solid; font-size: 11px; line-height: 1; }
|
.status-tag i { width: 5px; height: 5px; border-radius: 50%; background: currentColor; }
|
.status-tag.pending { color: #a96b16; background: #fff8e9; border-color: #efd59e; }
|
.status-tag.running { color: #176fcb; background: #edf6ff; border-color: #b8d9fb; }
|
.status-tag.completed { color: #23865e; background: #edf9f3; border-color: #b8dfcd; }
|
.status-tag.paused { color: #697682; background: #f2f4f6; border-color: #ccd4da; }
|
.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 .delete-action { color: #ad4a4a; }
|
.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-grid label { grid-template-columns: 72px minmax(0, 1fr); }
|
.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; }
|
.toolbar-actions { gap: 6px; }
|
.toolbar-actions button { padding: 0 9px; }
|
.pagination-bar { align-items: flex-start; flex-direction: column; overflow-x: auto; }
|
}
|
</style>
|