From ce135477fd1703b5001264687656e5a363d29988 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期二, 08 九月 2026 14:49:55 +0800
Subject: [PATCH] Merge commit '3f30e0c' into codex/ticket-03
---
src/views/TestConsole.vue | 10 +++
src/components/ConsoleChartPanel.vue | 9 ++
tests/e2e/support/ticket04-stack.ts | 2
tests/e2e/console-detail-readback.spec.ts | 114 ++++++++++++++++++++++++++++++++++++++
4 files changed, 132 insertions(+), 3 deletions(-)
diff --git a/src/components/ConsoleChartPanel.vue b/src/components/ConsoleChartPanel.vue
index dca28c8..11b3ba1 100644
--- a/src/components/ConsoleChartPanel.vue
+++ b/src/components/ConsoleChartPanel.vue
@@ -45,6 +45,8 @@
consoleDetail: null,
})
+const emit = defineEmits<{ detailSaved: [testId: number, detail: TestConsoleDetail] }>()
+
use([CanvasRenderer, LineChart, GridComponent, LegendComponent, TooltipComponent])
const palette = ['#3978f6', '#7d42d8', '#f08a3e', '#23a983', '#e15268', '#1b9ac8', '#b5892f', '#6957c8', '#4f8d44', '#d264a3', '#46748e', '#a95d37']
@@ -241,10 +243,14 @@
experimentName: experimentName.value.trim() || null,
batteryFields: [...batteryFields.value],
specialFields: [...specialFields.value],
- fieldValues: JSON.parse(JSON.stringify(detailFieldValues)) as Record<string, Record<string, string>>,
+ // 模板移除的字段不再提交,仍选中的字段保留全部电池填写值和空字符串。
+ fieldValues: Object.fromEntries(detailFields.value.map(field => [field, { ...detailFieldValues[field] }])),
}
const saved = await saveTestConsoleDetail(props.testId, detail)
+ if (!saved.consoleDetail) throw new Error('保存后未能读取测试详情')
applyConsoleDetail(saved.consoleDetail)
+ // 父页持有正式详情缓存;切换页签重新挂载时必须使用这次保存结果。
+ emit('detailSaved', props.testId, saved.consoleDetail)
ElMessage.success('测试详情已保存')
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '测试详情保存失败')
@@ -476,6 +482,7 @@
<div class="report-chart-page">
<section class="detail-panel">
<header class="panel-heading"><div><strong>测试详情</strong><el-icon><ArrowUp /></el-icon></div></header>
+ <p v-if="!consoleDetail" class="detail-meta" role="status">暂无已保存的测试详情</p>
<div class="detail-meta"><label><span>实验</span><el-input v-model="experimentName" /></label></div>
<div class="field-table-wrap">
<table class="field-table" :style="{ minWidth: `${Math.max(900, detailCells.length * 220 + 214)}px` }">
diff --git a/src/views/TestConsole.vue b/src/views/TestConsole.vue
index f90d0d9..1d62180 100644
--- a/src/views/TestConsole.vue
+++ b/src/views/TestConsole.vue
@@ -17,6 +17,7 @@
const router = useRouter()
const testId = computed(() => Number(route.params.id))
const backendDetail = ref<TestConfigurationDetail | null>(null)
+const consoleLoadError = ref('')
const liveState = ref<TestLiveState | null>(null)
const liveConnection = ref<'连接中' | '已连接' | '已断开' | '权限不足' | '空数据'>('连接中')
const liveError = ref('')
@@ -159,6 +160,7 @@
const detail = await getTestDetail(testId.value)
if (disposed) return
backendDetail.value = detail
+ consoleLoadError.value = ''
stepSettings.value = detail.logicStepMappings || []
runtimeAttach.value = detail.runtimeAttach === true
if (detail.receptionStatus === 'NOT_STARTED') {
@@ -186,10 +188,15 @@
if (activeTab.value === 'step-chart') await loadRealtimeRecords()
if (activeTab.value === 'project-chart') await loadProjectChartData()
} catch (error) {
+ consoleLoadError.value = error instanceof Error ? error.message : '测试详情读取失败'
handleLiveError(error instanceof Error ? error : new Error('测试状态加载失败'))
}
}
onMounted(loadConsole)
+
+function applySavedConsoleDetail(savedTestId: number, detail: NonNullable<TestConfigurationDetail['consoleDetail']>) {
+ if (backendDetail.value?.testId === savedTestId) backendDetail.value.consoleDetail = detail
+}
onBeforeUnmount(() => { disposed = true; closeLiveStream?.(); if (reconnectTimer !== null) window.clearTimeout(reconnectTimer) })
async function loadRealtimeRecords() {
@@ -534,7 +541,8 @@
</div>
</div>
- <ConsoleChartPanel v-if="activeTab === 'step-chart'" :test-id="testId" :cells="cells" :records="realtimeRecords" :step-plan-rows="detectorPlanRows" :logic-step-mappings="stepSettings" :console-detail="backendDetail?.consoleDetail" :test-name="profile.name" :battery-group="profile.batteryGroup" />
+ <p v-if="activeTab === 'step-chart' && !backendDetail && consoleLoadError" class="live-error" role="alert">{{ consoleLoadError }}</p>
+ <ConsoleChartPanel v-if="activeTab === 'step-chart' && backendDetail" :test-id="testId" :cells="cells" :records="realtimeRecords" :step-plan-rows="detectorPlanRows" :logic-step-mappings="stepSettings" :console-detail="backendDetail.consoleDetail" :test-name="profile.name" :battery-group="profile.batteryGroup" @detail-saved="applySavedConsoleDetail" />
<ConsoleProjectChartPanel v-if="activeTab === 'project-chart'" :cells="cells" :steps="projectSteps" :cycles="projectCycles" :capacity-factor="projectCapacityFactor" />
<ConsoleDataListPanel v-if="activeTab === 'data-list' && backendDetail" :test-id="testId" :test-name="profile.name" :cells="cells" :can-import="state === '检测完成'" :reception-status="backendDetail.receptionStatus" :bindings="backendDetail.batteries.map(item => ({ batteryNo: item.batteryNo, bindingId: item.binding?.bindingId, channelId: item.binding?.channelId, devid: item.binding?.devid, chlid: item.binding?.chlid }))" />
diff --git a/tests/e2e/console-detail-readback.spec.ts b/tests/e2e/console-detail-readback.spec.ts
new file mode 100644
index 0000000..9b60394
--- /dev/null
+++ b/tests/e2e/console-detail-readback.spec.ts
@@ -0,0 +1,114 @@
+import { expect, test, type Page } from '@playwright/test'
+import { Ticket04Stack } from './support/ticket04-stack'
+
+const stack = new Ticket04Stack()
+test.use({ actionTimeout: 20_000 })
+test.beforeAll(async () => {
+ await stack.start()
+ stack.executeIsolatedSql(`
+ INSERT INTO t_battery_group(group_id,group_name,model) VALUES(9201,'详情验收组','验收型号');
+ INSERT INTO t_battery_group_field(field_id,field_name,field_type) VALUES(9201,'额定容量','text'),(9202,'电流','text');
+ INSERT INTO t_test_type(test_type_id,test_type_name) VALUES(9201,'详情验收');
+ INSERT INTO t_test(test_id,test_name,creator_id,principal_id,test_type_id,test_purpose,test_status,reception_status)
+ VALUES(9201,'详情测试甲',1,1,9201,'隔离验收','COMPLETED','ENDED'),(9202,'详情测试乙',1,1,9201,'隔离验收','COMPLETED','ENDED'),(9203,'无详情测试',1,1,9201,'隔离验收','COMPLETED','ENDED');
+ INSERT INTO t_test_battery(test_battery_id,test_id,group_id,battery_no,collector_id)
+ VALUES(9201,9201,9201,'BAT-A',9201),(9202,9201,9201,'BAT-B',9202),(9203,9202,9201,'BAT-A',9203),(9204,9202,9201,'BAT-B',9204);
+ `)
+})
+test.afterAll(async () => stack.stop())
+
+async function login(page: Page) {
+ await page.goto('/login')
+ await page.locator('input[autocomplete="username"]').fill('admin')
+ await page.locator('input[autocomplete="current-password"]').fill('admin123')
+ await page.getByRole('button', { name: '登录', exact: true }).click()
+ await expect(page).toHaveURL(/\/dashboard$/)
+}
+
+async function api(page: Page, id: number, data?: unknown) {
+ const token = await page.evaluate(() => localStorage.getItem('battery_detection_auth_token'))
+ const response = await page.request.fetch(`${stack.backendUrl}/system/test/${id}${data ? '/console-detail' : ''}`, {
+ method: data ? 'PUT' : 'GET', data, headers: { Authorization: `Bearer ${token}` },
+ })
+ const body = await response.json()
+ expect(body.code, body.msg).toBe(200)
+ return body.data
+}
+
+const initial = {
+ experimentName: '原实验', batteryFields: ['额定容量', '电流'], specialFields: ['人工说明'],
+ fieldValues: { 额定容量: { 'BAT-A': '甲容量', 'BAT-B': '' }, 电流: { 'BAT-A': '甲电流', 'BAT-B': '乙电流' }, 人工说明: { 'BAT-A': '甲说明', 'BAT-B': '乙说明' } },
+}
+const row = (page: Page, field: string) => page.locator('.field-table tbody tr').filter({ has: page.locator('td').filter({ hasText: new RegExp(`^${field}$`) }) })
+async function save(page: Page) {
+ const response = page.waitForResponse(r => r.url().endsWith('/console-detail') && r.request().method() === 'PUT')
+ await page.getByRole('button', { name: '保存详情', exact: true }).click()
+ return (await response).json()
+}
+
+test('保存后页签返回、刷新及正式重读保持逐电池详情', async ({ page }) => {
+ await login(page)
+ await api(page, 9201, initial)
+ await page.goto('/test-list/9201/console?tab=step-chart')
+ await expect(page.locator('.detail-meta input')).toHaveValue('原实验')
+ await page.locator('.detail-meta input').fill('新实验甲')
+ await row(page, '人工说明').locator('input').nth(0).fill('新甲说明')
+ expect((await save(page)).code).toBe(200)
+ const saved = (await api(page, 9201)).consoleDetail
+ expect(saved.experimentName).toBe('新实验甲')
+ expect(saved.fieldValues['额定容量']['BAT-B']).toBe('')
+ expect(saved.fieldValues['人工说明']).toEqual({ 'BAT-A': '新甲说明', 'BAT-B': '乙说明' })
+ await page.getByRole('button', { name: '实时监控', exact: true }).click()
+ await page.getByRole('button', { name: '工步图表', exact: true }).click()
+ await expect(page.locator('.detail-meta input')).toHaveValue('新实验甲')
+ await expect(row(page, '人工说明').locator('input').nth(0)).toHaveValue('新甲说明')
+ await page.reload()
+ await expect(page.locator('.detail-meta input')).toHaveValue('新实验甲')
+ await expect(row(page, '额定容量').locator('input').nth(1)).toHaveValue('')
+})
+
+test('取消模板字段再次保存只提交当前字段且保留其他电池值', async ({ page }) => {
+ await login(page)
+ await api(page, 9202, { ...initial, experimentName: '实验乙' })
+ await page.goto('/test-list/9202/console?tab=step-chart')
+ await expect(page.locator('.detail-meta input')).toHaveValue('实验乙')
+ await page.getByRole('button', { name: '电池组字段模板', exact: true }).click()
+ await page.getByRole('dialog').getByText('电流', { exact: true }).click()
+ await page.getByRole('dialog').getByRole('button', { name: '应用模板', exact: true }).click()
+ expect((await save(page)).code).toBe(200)
+ const saved = (await api(page, 9202)).consoleDetail
+ expect(saved.batteryFields).toEqual(['额定容量'])
+ expect(saved.fieldValues).toEqual({ 额定容量: initial.fieldValues['额定容量'], 人工说明: initial.fieldValues['人工说明'] })
+ await page.reload()
+ await expect(page.locator('.detail-meta input')).toHaveValue('实验乙')
+ await expect(row(page, '人工说明').locator('input').nth(1)).toHaveValue('乙说明')
+ await page.setViewportSize({ width: 1440, height: 1000 })
+ await page.screenshot({ path: 'tests/e2e/.artifacts/console-detail-desktop.png', fullPage: true, animations: 'disabled' })
+ await page.setViewportSize({ width: 390, height: 844 })
+ await expect(page.getByRole('button', { name: '保存详情', exact: true })).toBeVisible()
+ await page.screenshot({ path: 'tests/e2e/.artifacts/console-detail-narrow.png', fullPage: true, animations: 'disabled' })
+})
+
+test('无详情、读取失败与保存失败分别反馈且失败保留填写', async ({ page }) => {
+ await login(page)
+ expect((await api(page, 9203)).consoleDetail).toBeNull()
+ await page.goto('/test-list/9203/console?tab=step-chart')
+ await expect(page.getByText('暂无已保存的测试详情', { exact: true })).toBeVisible()
+ await page.route('**/dev-api/system/test/9203', route => route.fulfill({ json: { code: 500, msg: '详情读取失败验收' } }))
+ await page.reload()
+ await expect(page.getByText('详情读取失败验收', { exact: true }).first()).toBeVisible()
+ await expect(page.getByRole('button', { name: '保存详情', exact: true })).toHaveCount(0)
+ await page.unroute('**/dev-api/system/test/9203')
+ await api(page, 9202, initial)
+ await page.goto('/test-list/9202/console?tab=step-chart')
+ await expect(page.locator('.detail-meta input')).toHaveValue('原实验')
+ await page.locator('.detail-meta input').fill('失败时保留实验')
+ await row(page, '人工说明').locator('input').nth(1).fill('失败时保留乙说明')
+ await page.route('**/dev-api/system/test/9202/console-detail', route => route.fulfill({ json: { code: 500, msg: '详情保存失败验收' } }), { times: 1 })
+ expect((await save(page)).code).toBe(500)
+ await expect(page.getByText('详情保存失败验收', { exact: true })).toBeVisible()
+ await expect(page.getByText('测试详情已保存', { exact: true })).toHaveCount(0)
+ await expect(page.locator('.detail-meta input')).toHaveValue('失败时保留实验')
+ await expect(row(page, '人工说明').locator('input').nth(1)).toHaveValue('失败时保留乙说明')
+ expect((await api(page, 9202)).consoleDetail).toEqual(initial)
+})
diff --git a/tests/e2e/support/ticket04-stack.ts b/tests/e2e/support/ticket04-stack.ts
index d5ec5bf..a0b8d29 100644
--- a/tests/e2e/support/ticket04-stack.ts
+++ b/tests/e2e/support/ticket04-stack.ts
@@ -9,7 +9,7 @@
}
const webRepo = resolve(import.meta.dirname, '../../..')
-const backendRepo = resolve(webRepo, '../BatteryDetection')
+const backendRepo = resolve(process.env.TICKET04_BACKEND_REPO || resolve(webRepo, '../BatteryDetection'))
const artifactDir = resolve(webRepo, 'tests/e2e/.artifacts')
const backendJar = resolve(backendRepo, 'ruoyi-admin/target/ruoyi-admin.jar')
const apiPort = Number(process.env.TICKET04_API_PORT || 18001)
--
Gitblit v1.7.1