feat: add event waveform export selection

This commit is contained in:
2026-05-16 07:07:50 +08:00
parent 8b19e4a779
commit 609fdd5379
7 changed files with 675 additions and 17 deletions

View File

@@ -0,0 +1,473 @@
# Event List Export Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Rename the existing event list export action to “事件导出”, add independent waveform export selection and download behavior, and show the visible event columns in the requested order with “暂降/暂升幅值(%)” before “持续时间(s)”.
**Architecture:** Keep all UI orchestration in `frontend/src/views/event/eventList/index.vue`, because the change is narrow and the page already owns table columns, export handlers, and row actions. Add one event-list API method and one request type for waveform export. Extend the existing eventList contract scripts so selection semantics and export payloads are checked without introducing a new test framework.
**Tech Stack:** Vue 3 `<script setup>`, Element Plus, TypeScript, existing `ProTable`, existing download hook, Node-based contract scripts.
---
## File Structure
- Modify `frontend/src/views/event/eventList/check-search-layout-contract.mjs`: assert the event export button text and event export parameter source.
- Modify `frontend/src/views/event/eventList/check-visible-contract.mjs`: assert the waveform selection column and selection rules.
- Create `frontend/src/views/event/eventList/check-export-contract.mjs`: assert the new waveform export API, payload shape, button state, and cleanup behavior.
- Modify `frontend/src/api/event/eventList/interface/index.ts`: add `TransientWaveformExportParams`.
- Modify `frontend/src/api/event/eventList/index.ts`: add `exportTransientWaveforms`.
- Modify `frontend/src/views/event/eventList/index.vue`: add independent waveform selection UI and handlers, rename event export, and wire waveform export.
## Task 1: Contract Checks For Export Behavior
**Files:**
- Modify: `frontend/src/views/event/eventList/check-search-layout-contract.mjs`
- Modify: `frontend/src/views/event/eventList/check-visible-contract.mjs`
- Create: `frontend/src/views/event/eventList/check-export-contract.mjs`
- [ ] **Step 1: Update search layout contract for event export naming**
In `frontend/src/views/event/eventList/check-search-layout-contract.mjs`, keep the existing current-search assertion and add a check that the button is now labeled “事件导出”:
```js
[
'event export button is named explicitly',
/<el-button[\s\S]*@click="handleEventExport"[\s\S]*>事件导出<\/el-button>/
],
[
'event export uses the same current search params as the table request',
/exportTransientEvents,[\s\S]*buildEventQueryParams\(resolveCurrentSearchParams\(searchParam\)\)/
]
```
- [ ] **Step 2: Update visible contract for waveform selection column**
In `frontend/src/views/event/eventList/check-visible-contract.mjs`, add expectations that identify the manual waveform selection column and confirm the old business columns remain:
```js
[
'waveform selection column is rendered near the left side',
/prop:\s*'waveformSelection'[\s\S]*label:\s*'波形选择'[\s\S]*headerRender:\s*renderWaveformSelectionHeader[\s\S]*render:\s*renderWaveformSelectionCell/
],
[
'waveform selection requires event id, waveform flag and waveform path',
/const isWaveformExportable[\s\S]*Boolean\(row\.eventId\)[\s\S]*Number\(row\.fileFlag\)\s*===\s*1[\s\S]*Boolean\(row\.wavePath\)/
]
```
- [ ] **Step 3: Create export contract script**
Create `frontend/src/views/event/eventList/check-export-contract.mjs` with:
```js
/* eslint-env node */
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const currentDir = path.dirname(fileURLToPath(import.meta.url))
const pageFile = path.join(currentDir, 'index.vue')
const apiFile = path.resolve(currentDir, '../../../api/event/eventList/index.ts')
const interfaceFile = path.resolve(currentDir, '../../../api/event/eventList/interface/index.ts')
const source = fs.readFileSync(pageFile, 'utf8')
const apiSource = fs.readFileSync(apiFile, 'utf8')
const interfaceSource = fs.readFileSync(interfaceFile, 'utf8')
const expectations = [
['waveform export api is imported by the page', /import \{[\s\S]*exportTransientWaveforms[\s\S]*\} from '@\/api\/event\/eventList'/],
['waveform export button is present and disabled without selection', /<el-button[\s\S]*:disabled="!selectedWaveformRows\.length"[\s\S]*@click="handleWaveformExport"[\s\S]*>波形导出<\/el-button>/],
['event export button does not depend on waveform selection', /<el-button type="primary" plain :icon="Download" @click="handleEventExport">事件导出<\/el-button>/],
['waveform selected rows are tracked independently', /const selectedWaveformRows\s*=\s*ref<EventList\.TransientEventRecord\[\]>\(\[\]\)/],
['waveform export payload uses event ids only', /eventIds:\s*exportableRows\.map\(row => row\.eventId\)/],
['waveform export uses server filename download hook', /useDownloadWithServerFileName\(exportTransientWaveforms,\s*'事件波形导出'/],
['waveform selection is cleaned after table reload triggers', /const clearWaveformSelection[\s\S]*selectedWaveformRows\.value\s*=\s*\[\]/],
['waveform export api method exists', /export const exportTransientWaveforms\s*=\s*\(params:\s*EventList\.TransientWaveformExportParams\)/.test(apiSource)],
['waveform export api path is stable', /downloadWithHeaders\('\/event\/list\/transient\/waveform\/export',\s*params\)/.test(apiSource)],
['waveform export params type contains eventIds', /export interface TransientWaveformExportParams\s*\{[\s\S]*eventIds:\s*string\[\][\s\S]*\}/.test(interfaceSource)]
]
const failures = expectations.filter(([, pattern]) => {
if (typeof pattern === 'boolean') return !pattern
return !pattern.test(source)
})
if (failures.length) {
console.error('eventList export contract check failed:')
for (const [name] of failures) {
console.error(`- ${name}`)
}
process.exit(1)
}
console.log('eventList export contract check passed')
```
- [ ] **Step 4: Run contracts and verify red**
Run:
```powershell
node frontend/src/views/event/eventList/check-search-layout-contract.mjs
node frontend/src/views/event/eventList/check-visible-contract.mjs
node frontend/src/views/event/eventList/check-export-contract.mjs
```
Expected:
- Existing checks may pass or fail depending on current user edits.
- `check-export-contract.mjs` must fail with missing waveform export API, button, selection state, and payload expectations.
- [ ] **Step 5: Commit contract red state**
Do not commit a failing red state unless the user explicitly asks for granular commits. Keep it as local verification before implementation.
## Task 2: API Type And Export Method
**Files:**
- Modify: `frontend/src/api/event/eventList/interface/index.ts`
- Modify: `frontend/src/api/event/eventList/index.ts`
- [ ] **Step 1: Add waveform export request type**
In `frontend/src/api/event/eventList/interface/index.ts`, add this interface inside `export namespace EventList` after `TransientPageParams`:
```ts
export interface TransientWaveformExportParams {
eventIds: string[]
}
```
- [ ] **Step 2: Add waveform export API method**
In `frontend/src/api/event/eventList/index.ts`, add:
```ts
export const exportTransientWaveforms = (params: EventList.TransientWaveformExportParams) => {
return http.downloadWithHeaders('/event/list/transient/waveform/export', params)
}
```
- [ ] **Step 3: Run export contract and verify API checks improve**
Run:
```powershell
node frontend/src/views/event/eventList/check-export-contract.mjs
```
Expected: still fails because page UI and handlers are not implemented, but API method and type failures disappear.
## Task 3: Waveform Selection UI And State
**Files:**
- Modify: `frontend/src/views/event/eventList/index.vue`
- [ ] **Step 1: Add imports**
Change imports in `index.vue`:
```ts
import { ElButton, ElCheckbox, ElRadioButton, ElRadioGroup } from 'element-plus'
import { Download, RefreshRight, View } from '@element-plus/icons-vue'
import {
exportTransientEvents,
exportTransientWaveforms,
getTransientEventDetail,
getTransientEventPage
} from '@/api/event/eventList'
```
- [ ] **Step 2: Add waveform selection state**
Add near the existing page refs:
```ts
const selectedWaveformRows = ref<EventList.TransientEventRecord[]>([])
```
- [ ] **Step 3: Add exportable row helpers**
Add before `columns`:
```ts
const isWaveformExportable = (row: EventList.TransientEventRecord) => {
return Boolean(row.eventId) && Number(row.fileFlag) === 1 && Boolean(row.wavePath)
}
const currentWaveformRows = computed(() => proTable.value?.tableData || [])
const currentExportableWaveformRows = computed(() => currentWaveformRows.value.filter(isWaveformExportable))
const selectedWaveformIds = computed(() => selectedWaveformRows.value.map(row => row.eventId))
const selectedWaveformIdSet = computed(() => new Set(selectedWaveformIds.value))
const isAllCurrentWaveformsSelected = computed(() => {
const rows = currentExportableWaveformRows.value
return rows.length > 0 && rows.every(row => selectedWaveformIdSet.value.has(row.eventId))
})
const isCurrentWaveformSelectionIndeterminate = computed(() => {
const rows = currentExportableWaveformRows.value
if (!rows.length) return false
const selectedCount = rows.filter(row => selectedWaveformIdSet.value.has(row.eventId)).length
return selectedCount > 0 && selectedCount < rows.length
})
```
- [ ] **Step 4: Add selection mutators**
Add after the helper computed values:
```ts
const clearWaveformSelection = () => {
selectedWaveformRows.value = []
}
const toggleWaveformRowSelection = (row: EventList.TransientEventRecord, checked: boolean) => {
if (!isWaveformExportable(row)) return
if (checked) {
if (!selectedWaveformIdSet.value.has(row.eventId)) {
selectedWaveformRows.value = [...selectedWaveformRows.value, row]
}
return
}
selectedWaveformRows.value = selectedWaveformRows.value.filter(item => item.eventId !== row.eventId)
}
const toggleCurrentWaveformSelection = (checked: boolean) => {
if (!checked) {
const currentIds = new Set(currentExportableWaveformRows.value.map(row => row.eventId))
selectedWaveformRows.value = selectedWaveformRows.value.filter(row => !currentIds.has(row.eventId))
return
}
const nextRows = [...selectedWaveformRows.value]
const nextIds = new Set(nextRows.map(row => row.eventId))
currentExportableWaveformRows.value.forEach(row => {
if (!nextIds.has(row.eventId)) {
nextRows.push(row)
nextIds.add(row.eventId)
}
})
selectedWaveformRows.value = nextRows
}
```
- [ ] **Step 5: Add render functions for the manual selection column**
Add:
```ts
const renderWaveformSelectionHeader = () =>
h(ElCheckbox, {
modelValue: isAllCurrentWaveformsSelected.value,
indeterminate: isCurrentWaveformSelectionIndeterminate.value,
disabled: currentExportableWaveformRows.value.length === 0,
'onUpdate:modelValue': (value: string | number | boolean) => toggleCurrentWaveformSelection(Boolean(value))
})
const renderWaveformSelectionCell = ({ row }: { row: EventList.TransientEventRecord }) =>
h(ElCheckbox, {
modelValue: selectedWaveformIdSet.value.has(row.eventId),
disabled: !isWaveformExportable(row),
'onUpdate:modelValue': (value: string | number | boolean) => toggleWaveformRowSelection(row, Boolean(value))
})
```
- [ ] **Step 6: Add waveform selection column**
In the `columns` array, add this before the index column:
```ts
{
prop: 'waveformSelection',
label: '波形选择',
fixed: 'left',
width: 90,
isSetting: false,
headerRender: renderWaveformSelectionHeader,
render: renderWaveformSelectionCell
},
```
- [ ] **Step 7: Place amplitude before duration in visible columns**
In the `columns` array, keep the visible event column order as `发生时刻`, `监测点名称`, `暂降/暂升幅值(%)`, `持续时间(s)`, `事件类型`, `相别` by placing the `featureAmplitude` column before the duration column.
- [ ] **Step 8: Clear waveform selection on reset and table request**
Update `handleSearchReset`:
```ts
const handleSearchReset = () => {
eventTimeUnit.value = 'month'
eventTimeBaseDate.value = new Date()
clearWaveformSelection()
syncEventTimeRange()
}
```
Update `getTableList`:
```ts
const getTableList = (params: EventSearchParams) => {
// 分页查询按 API_DEBUG.md 转换时间范围与枚举筛选参数。
clearWaveformSelection()
return getTransientEventPage(buildEventQueryParams(resolveCurrentSearchParams(params)))
}
```
- [ ] **Step 9: Run visible and export contracts**
Run:
```powershell
node frontend/src/views/event/eventList/check-visible-contract.mjs
node frontend/src/views/event/eventList/check-export-contract.mjs
```
Expected: visible contract passes for the new column; export contract still fails until buttons and export handlers are renamed/wired.
## Task 4: Export Buttons And Handlers
**Files:**
- Modify: `frontend/src/views/event/eventList/index.vue`
- [ ] **Step 1: Replace table header buttons**
Replace the existing table header slot with:
```vue
<template #tableHeader>
<el-button type="primary" plain :icon="Download" @click="handleEventExport">事件导出</el-button>
<el-button
type="primary"
plain
:icon="Download"
:disabled="!selectedWaveformRows.length"
@click="handleWaveformExport"
>
波形导出
</el-button>
</template>
```
- [ ] **Step 2: Rename event export handler**
Replace `handleExport` with:
```ts
const handleEventExport = () => {
const searchParam = (proTable.value?.searchParam || {}) as EventSearchParams
useDownloadWithServerFileName(
exportTransientEvents,
'暂态事件列表',
buildEventQueryParams(resolveCurrentSearchParams(searchParam)),
false
)
}
```
- [ ] **Step 3: Add waveform export handler**
Add after `handleEventExport`:
```ts
const handleWaveformExport = () => {
const exportableRows = selectedWaveformRows.value.filter(isWaveformExportable)
if (!exportableRows.length) {
ElMessage.warning('请先选择存在波形的事件')
return
}
useDownloadWithServerFileName(
exportTransientWaveforms,
'事件波形导出',
{
eventIds: exportableRows.map(row => row.eventId)
},
false,
'.zip'
)
}
```
- [ ] **Step 4: Run all eventList contract scripts**
Run:
```powershell
node frontend/src/views/event/eventList/check-search-layout-contract.mjs
node frontend/src/views/event/eventList/check-visible-contract.mjs
node frontend/src/views/event/eventList/check-export-contract.mjs
node frontend/src/views/event/eventList/check-query-params-contract.mjs
node frontend/src/views/event/eventList/check-route-contract.mjs
node frontend/src/views/event/eventList/check-time-range-contract.mjs
```
Expected: all scripts print `passed`.
## Task 5: Type And Lint Verification
**Files:**
- Verify only; no planned file edits.
- [ ] **Step 1: Run TypeScript check**
Run:
```powershell
cd frontend
npm run type-check
```
Expected: command completes without TypeScript errors.
- [ ] **Step 2: Run lint if lightweight enough for the workspace**
Run:
```powershell
cd frontend
npm run lint
```
Expected: command completes without ESLint errors. If lint changes unrelated files because it uses `--fix`, inspect `git status --short` and keep only changes directly related to this task.
- [ ] **Step 3: Inspect final diff**
Run:
```powershell
git diff -- frontend/src/views/event/eventList frontend/src/api/event/eventList
git status --short
```
Expected:
- Event list page contains the new independent waveform selection column.
- API files contain only the waveform export type and method.
- Existing unrelated worktree changes are not reverted or folded into this task.
## Task 6: Commit Implementation
**Files:**
- Commit only files modified for this task.
- [ ] **Step 1: Stage task files only**
Run:
```powershell
git add -- frontend/src/views/event/eventList/index.vue frontend/src/views/event/eventList/check-search-layout-contract.mjs frontend/src/views/event/eventList/check-visible-contract.mjs frontend/src/views/event/eventList/check-export-contract.mjs frontend/src/api/event/eventList/index.ts frontend/src/api/event/eventList/interface/index.ts docs/superpowers/plans/2026-05-15-event-list-export-implementation-plan.md
```
- [ ] **Step 2: Commit**
Run:
```powershell
git commit -m "feat: add event waveform export selection"
```
Expected: commit succeeds and includes only the implementation, contracts, API method, and this plan.