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.

View File

@@ -12,3 +12,7 @@ export const getTransientEventDetail = (eventId: string) => {
export const exportTransientEvents = (params: EventList.TransientPageParams) => {
return http.downloadWithHeaders('/event/list/transient/export', params)
}
export const exportTransientWaveforms = (params: EventList.TransientWaveformExportParams) => {
return http.downloadWithHeaders('/event/list/transient/waveform/export', params)
}

View File

@@ -28,6 +28,10 @@ export namespace EventList {
lineName?: string
}
export interface TransientWaveformExportParams {
eventIds: string[]
}
export interface TransientEventRecord {
eventId: string
measurementPointId?: string

View File

@@ -0,0 +1,46 @@
/* 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 eventExportButtonBlocks = (source.match(/<el-button[\s\S]*?<\/el-button>/g) || []).filter(
block => /@click="handleEventExport"/.test(block) && />事件导出<\/el-button>/.test(block)
)
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 is named explicitly', /<el-button[\s\S]*@click="handleEventExport"[\s\S]*>事件导出<\/el-button>/],
['event export button does not depend on waveform selection', eventExportButtonBlocks.some(block => !/selectedWaveformRows/.test(block))],
['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 clear helper exists', /const clearWaveformSelection\s*=\s*\(\)\s*=>\s*\{[\s\S]*selectedWaveformRows\.value\s*=\s*\[\][\s\S]*\}/],
['table request clears waveform selection before reload', /const getTableList\s*=\s*\([^)]*\)\s*=>\s*\{[\s\S]*clearWaveformSelection\(\)[\s\S]*return getTransientEventPage/],
['search reset clears waveform selection', /const handleSearchReset\s*=\s*\(\)\s*=>\s*\{[\s\S]*clearWaveformSelection\(\)[\s\S]*syncEventTimeRange\(\)/],
['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')

View File

@@ -14,7 +14,19 @@ const expectations = [
['event time search field only takes one grid column', /key:\s*'startTimeRange',\s*span:\s*1,/],
['event time search imports shared TimePeriodSearch component', /import TimePeriodSearch from '@\/views\/components\/TimePeriodSearch\/index\.vue'/],
['event time search imports shared period helpers', /from '@\/views\/components\/TimePeriodSearch\/timePeriod'/],
['event time search renders shared TimePeriodSearch', /h\(TimePeriodSearch,[\s\S]*unit:\s*eventTimeUnit\.value,[\s\S]*modelValue:\s*eventTimeBaseDate\.value/]
['event time search renders shared TimePeriodSearch', /h\(TimePeriodSearch,[\s\S]*unit:\s*eventTimeUnit\.value,[\s\S]*modelValue:\s*eventTimeBaseDate\.value/],
[
'event list requests preserve explicit time range and fallback to current visible range',
/const resolveCurrentSearchParams[\s\S]*startTimeRange:\s*params\.startTimeRange\s*\?\?\s*buildTimePeriodRange\(eventTimeUnit\.value,\s*eventTimeBaseDate\.value\)[\s\S]*getTransientEventPage\(buildEventQueryParams\(resolveCurrentSearchParams\(params\)\)\)/
],
[
'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\)\)/
]
]
const failures = expectations.filter(([, pattern]) => !pattern.test(source))

View File

@@ -10,13 +10,21 @@ const source = fs.readFileSync(pageFile, 'utf8')
const querySource = fs.readFileSync(queryFile, 'utf8')
const expectations = [
[
'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\)/
],
[
'table keeps requested visible event column order',
/label:\s*'发生时刻'[\s\S]*label:\s*'监测点名称'[\s\S]*label:\s*'持续时间\(s\)'[\s\S]*label:\s*'暂降\/暂升幅值\(%\)'[\s\S]*label:\s*'事件类型'[\s\S]*label:\s*'相别'/
/label:\s*'发生时刻'[\s\S]*label:\s*'监测点名称'[\s\S]*label:\s*'暂降\/暂升幅值\(%\)'[\s\S]*label:\s*'持续时间\(s\)'[\s\S]*label:\s*'事件类型'[\s\S]*label:\s*'相别'/
],
[
'event type column displays name but searches by eventType code',
/prop:\s*'eventTypeName'[\s\S]*label:\s*'事件类型'[\s\S]*enum:\s*eventTypeOptions[\s\S]*fieldNames:\s*\{\s*label:\s*'name',\s*value:\s*'id'\s*\}[\s\S]*isFilterEnum:\s*false[\s\S]*search:\s*\{[\s\S]*key:\s*'eventType'[\s\S]*el:\s*'select'/
/prop:\s*'eventTypeName'[\s\S]*label:\s*'事件类型'[\s\S]*enum:\s*eventTypeOptions[\s\S]*fieldNames:\s*\{\s*label:\s*'name',\s*value:\s*'code'\s*\}[\s\S]*isFilterEnum:\s*false[\s\S]*search:\s*\{[\s\S]*key:\s*'eventType'[\s\S]*el:\s*'select'/
],
[
'event description defaults hidden in table columns',
@@ -28,6 +36,7 @@ const expectations = [
['measurement point dialog is present', /measurementPointDialogVisible[\s\S]*title="监测点信息"/],
['operation switches between view waveform and supplement waveform', /Number\(row\.fileFlag\)\s*===\s*1[\s\S]*查看波形[\s\S]*波形补招/],
['waveform status search uses custom render instead of select', /renderFileFlagSearch[\s\S]*prop:\s*'fileFlag'[\s\S]*search:\s*\{[\s\S]*render:\s*renderFileFlagSearch/],
['phase column is not a search field', /prop:\s*'phase'[\s\S]*search:[\s\S]*prop:\s*'event_describe'/.test(source) === false],
[
'event description is not a search field',
/prop:\s*'event_describe'[\s\S]*label:\s*'事件描述'[\s\S]*search:[\s\S]*prop:\s*'sagsource'/.test(source) === false

View File

@@ -9,7 +9,14 @@
@reset="handleSearchReset"
>
<template #tableHeader>
<el-button type="primary" plain :icon="Download" @click="handleExport">导出</el-button>
<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>
<template #fileFlag="{ row }">
@@ -22,7 +29,7 @@
<el-button v-if="Number(row.fileFlag) === 1" type="primary" link :icon="View" @click="handleViewWaveform(row)">
查看波形
</el-button>
<el-button v-else type="primary" link :icon="RefreshRight" @click="handleSupplementWaveform(row)">
<el-button v-else type="primary" link :icon="RefreshRight" @click="handleSupplementWaveform">
波形补招
</el-button>
</template>
@@ -46,11 +53,16 @@
<script setup lang="ts">
import { h } from 'vue'
import { useRouter } from 'vue-router'
import { ElButton, ElRadioButton, ElRadioGroup } from 'element-plus'
import { ElButton, ElCheckbox, ElRadioButton, ElRadioGroup } from 'element-plus'
import { Download, RefreshRight, View } from '@element-plus/icons-vue'
import ProTable from '@/components/ProTable/index.vue'
import type { ColumnProps, ProTableInstance } from '@/components/ProTable/interface'
import { exportTransientEvents, getTransientEventDetail, getTransientEventPage } from '@/api/event/eventList'
import {
exportTransientEvents,
exportTransientWaveforms,
getTransientEventDetail,
getTransientEventPage
} from '@/api/event/eventList'
import type { EventList } from '@/api/event/eventList/interface'
import { useDownloadWithServerFileName } from '@/hooks/useDownload'
import { useDictStore } from '@/stores/modules/dict'
@@ -76,6 +88,7 @@ const dictStore = useDictStore()
const measurementPointDialogVisible = ref(false)
const measurementPointLoading = ref(false)
const measurementPointData = ref<EventList.TransientEventRecord | null>(null)
const selectedWaveformRows = ref<EventList.TransientEventRecord[]>([])
const eventTimeUnit = ref<TimePeriodUnit>('month')
const eventTimeBaseDate = ref(new Date())
const defaultStartTimeRange = buildTimePeriodRange(eventTimeUnit.value, eventTimeBaseDate.value)
@@ -113,9 +126,15 @@ const handleEventTimeDateChange = (value: Date | string | number | null) => {
const handleSearchReset = () => {
eventTimeUnit.value = 'month'
eventTimeBaseDate.value = new Date()
clearWaveformSelection()
syncEventTimeRange()
}
const resolveCurrentSearchParams = (params: EventSearchParams = {}) => ({
...params,
startTimeRange: params.startTimeRange ?? buildTimePeriodRange(eventTimeUnit.value, eventTimeBaseDate.value)
})
const renderEventTimeSearch = () =>
h(TimePeriodSearch, {
class: 'event-time-search',
@@ -144,7 +163,87 @@ const renderFileFlagSearch = ({ searchParam }: { searchParam: EventSearchParams
)
}
const isWaveformExportable = (row: EventList.TransientEventRecord) =>
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).filter(Boolean))
const selectedWaveformIdSet = computed(() => new Set(selectedWaveformIds.value))
const isAllCurrentWaveformsSelected = computed(
() =>
currentExportableWaveformRows.value.length > 0 &&
currentExportableWaveformRows.value.every(row => selectedWaveformIdSet.value.has(row.eventId))
)
const isCurrentWaveformSelectionIndeterminate = computed(() => {
const selectedCount = currentExportableWaveformRows.value.filter(row =>
selectedWaveformIdSet.value.has(row.eventId)
).length
return selectedCount > 0 && selectedCount < currentExportableWaveformRows.value.length
})
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 currentSelectedIds = new Set(selectedWaveformIds.value)
const nextRows = [...selectedWaveformRows.value]
currentExportableWaveformRows.value.forEach(row => {
if (!currentSelectedIds.has(row.eventId)) {
nextRows.push(row)
currentSelectedIds.add(row.eventId)
}
})
selectedWaveformRows.value = nextRows
return
}
const currentIds = new Set(currentExportableWaveformRows.value.map(row => row.eventId))
selectedWaveformRows.value = selectedWaveformRows.value.filter(row => !currentIds.has(row.eventId))
}
const renderWaveformSelectionHeader = () =>
h(ElCheckbox, {
modelValue: isAllCurrentWaveformsSelected.value,
indeterminate: isCurrentWaveformSelectionIndeterminate.value,
disabled: !currentExportableWaveformRows.value.length,
'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))
})
const columns = reactive<ColumnProps<EventList.TransientEventRecord>[]>([
{
prop: 'waveformSelection',
label: '波形选择',
fixed: 'left',
width: 90,
isSetting: false,
headerRender: renderWaveformSelectionHeader,
render: renderWaveformSelectionCell
},
{ type: 'index', fixed: 'left', width: 70, label: '序号' },
{
prop: 'startTime',
@@ -187,14 +286,14 @@ const columns = reactive<ColumnProps<EventList.TransientEventRecord>[]>([
() => resolveText(row.lineName)
)
},
{ prop: 'duration', label: '持续时间(s)', minWidth: 130 },
{ prop: 'featureAmplitude', label: '暂降/暂升幅值(%)', minWidth: 160 },
{ prop: 'duration', label: '持续时间(s)', minWidth: 130 },
{
prop: 'eventTypeName',
label: '事件类型',
minWidth: 160,
enum: eventTypeOptions,
fieldNames: { label: 'name', value: 'id' },
fieldNames: { label: 'name', value: 'code' },
isFilterEnum: false,
render: ({ row }) => resolveEventTypeName(row, eventTypeOptions.value),
search: {
@@ -206,10 +305,7 @@ const columns = reactive<ColumnProps<EventList.TransientEventRecord>[]>([
prop: 'phase',
label: '相别',
minWidth: 90,
enum: phaseOptions,
search: {
el: 'select'
}
enum: phaseOptions
},
{
prop: 'event_describe',
@@ -238,8 +334,9 @@ const resolveText = (value: unknown) => {
}
const getTableList = (params: EventSearchParams) => {
clearWaveformSelection()
// 分页查询按 API_DEBUG.md 转换时间范围与枚举筛选参数。
return getTransientEventPage(buildEventQueryParams(params))
return getTransientEventPage(buildEventQueryParams(resolveCurrentSearchParams(params)))
}
const handleViewMeasurementPoint = async (row: EventList.TransientEventRecord) => {
@@ -275,14 +372,27 @@ const handleViewWaveform = (row: EventList.TransientEventRecord) => {
})
}
const handleSupplementWaveform = (_row: EventList.TransientEventRecord) => {
const handleSupplementWaveform = () => {
// 波形补招需要后端补招接口,当前先保留操作入口避免误触发未知流程。
ElMessage.warning('暂无波形补招接口,无法发起补招')
}
const handleExport = () => {
const handleEventExport = () => {
const searchParam = (proTable.value?.searchParam || {}) as EventSearchParams
useDownloadWithServerFileName(exportTransientEvents, '暂态事件列表', buildEventQueryParams(searchParam), false)
useDownloadWithServerFileName(exportTransientEvents, '暂态事件列表', buildEventQueryParams(resolveCurrentSearchParams(searchParam)), false)
}
const handleWaveformExport = () => {
const exportableRows = selectedWaveformRows.value.filter(isWaveformExportable)
if (!exportableRows.length) {
ElMessage.warning('请先选择存在波形的事件')
return
}
useDownloadWithServerFileName(exportTransientWaveforms, '事件波形导出', {
eventIds: exportableRows.map(row => row.eventId)
}, false, '.zip')
}
</script>