docs(design): 删除磁盘监控设计文档并更新前端页面结构规范
- 删除 frontend/src/views/systemMonitor/2026-04-22-disk-monitor-design.md 设计文档 - 删除 frontend/src/views/tools/addLedger/API_DEBUG.md 调试文档 - 在 AGENTS.md 中新增前端页面结构归档章节,规范复杂工具页结构 - 明确 index.vue、components/、utils/ 职责边界和拆分原则 - 规定页面级类型和 contract 脚本管理方式 - 统一复杂页面拆分优先顺序和注意事项
This commit is contained in:
@@ -38,6 +38,11 @@ export namespace EventList {
|
||||
startTime?: string
|
||||
lineName?: string
|
||||
eventDescribe?: string
|
||||
eventDescription?: string
|
||||
eventDesc?: string
|
||||
description?: string
|
||||
describe?: string
|
||||
remark?: string
|
||||
sagsource?: string
|
||||
phase?: string
|
||||
duration?: number
|
||||
|
||||
@@ -51,8 +51,98 @@ let chart: echarts.ECharts | any = null
|
||||
let isPanPointerDown = false
|
||||
let currentGroup: string | undefined
|
||||
|
||||
interface ChartDataZoomPayload {
|
||||
dataZoomId?: string
|
||||
start?: unknown
|
||||
end?: unknown
|
||||
startValue?: unknown
|
||||
endValue?: unknown
|
||||
}
|
||||
|
||||
const getChartViewportRoot = () => chart?.getZr()?.painter?.getViewportRoot?.() as HTMLElement | undefined
|
||||
|
||||
const clampPercent = (value: number) => Math.min(Math.max(value, 0), 100)
|
||||
|
||||
const getFiniteNumber = (value: unknown) => {
|
||||
const numberValue = Number(value)
|
||||
|
||||
return Number.isFinite(numberValue) ? numberValue : undefined
|
||||
}
|
||||
|
||||
const normalizeZoomPercentRange = (start?: unknown, end?: unknown) => {
|
||||
const startPercent = getFiniteNumber(start)
|
||||
const endPercent = getFiniteNumber(end)
|
||||
|
||||
if (startPercent === undefined || endPercent === undefined) return null
|
||||
|
||||
return {
|
||||
start: clampPercent(Math.min(startPercent, endPercent)),
|
||||
end: clampPercent(Math.max(startPercent, endPercent))
|
||||
}
|
||||
}
|
||||
|
||||
const getXAxisData = () => {
|
||||
const xAxisData = props.options?.xAxis?.data
|
||||
|
||||
return Array.isArray(xAxisData) ? xAxisData : []
|
||||
}
|
||||
|
||||
const resolveAxisDataIndex = (value: unknown) => {
|
||||
const xAxisData = getXAxisData()
|
||||
if (!xAxisData.length) return -1
|
||||
|
||||
const numberValue = getFiniteNumber(value)
|
||||
if (numberValue !== undefined) {
|
||||
const roundedIndex = Math.round(numberValue)
|
||||
if (roundedIndex >= 0 && roundedIndex < xAxisData.length) return roundedIndex
|
||||
}
|
||||
|
||||
return xAxisData.findIndex(item => item === value || String(item) === String(value))
|
||||
}
|
||||
|
||||
const resolveZoomRangeFromAxisValues = (startValue: unknown, endValue: unknown) => {
|
||||
const xAxisData = getXAxisData()
|
||||
if (xAxisData.length <= 1) return null
|
||||
|
||||
const startIndex = resolveAxisDataIndex(startValue)
|
||||
const endIndex = resolveAxisDataIndex(endValue)
|
||||
|
||||
if (startIndex < 0 || endIndex < 0) return null
|
||||
|
||||
const maxIndex = xAxisData.length - 1
|
||||
|
||||
return normalizeZoomPercentRange(
|
||||
(Math.min(startIndex, endIndex) / maxIndex) * 100,
|
||||
(Math.max(startIndex, endIndex) / maxIndex) * 100
|
||||
)
|
||||
}
|
||||
|
||||
const resolveCurrentDataZoomRange = (zoomPayload: ChartDataZoomPayload) => {
|
||||
const dataZoomOptions = chart?.getOption?.()?.dataZoom
|
||||
const dataZoomList = Array.isArray(dataZoomOptions) ? dataZoomOptions : dataZoomOptions ? [dataZoomOptions] : []
|
||||
const candidateList = zoomPayload.dataZoomId
|
||||
? dataZoomList.filter((item: { id?: string }) => item.id === zoomPayload.dataZoomId)
|
||||
: dataZoomList
|
||||
|
||||
for (const item of candidateList) {
|
||||
const range = normalizeZoomPercentRange(item?.start, item?.end)
|
||||
if (range) return range
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const resolveChartDataZoomRange = (zoomPayload: ChartDataZoomPayload) => {
|
||||
const percentRange = normalizeZoomPercentRange(zoomPayload?.start, zoomPayload?.end)
|
||||
if (percentRange) return percentRange
|
||||
|
||||
// 框选放大会返回 startValue/endValue,这里统一换算成百分比后再同步给外部工具栏状态。
|
||||
const valueRange = resolveZoomRangeFromAxisValues(zoomPayload?.startValue, zoomPayload?.endValue)
|
||||
if (valueRange) return valueRange
|
||||
|
||||
return resolveCurrentDataZoomRange(zoomPayload)
|
||||
}
|
||||
|
||||
const getAxisPixel = (dataIndex: number) => {
|
||||
const pixelValue = chart?.convertToPixel?.({ xAxisIndex: 0 }, dataIndex)
|
||||
|
||||
@@ -302,15 +392,14 @@ const buildChartOptions = () => {
|
||||
const bindChartEvents = () => {
|
||||
chart.off('datazoom')
|
||||
chart.on('datazoom', (params: any) => {
|
||||
const zoomPayload = Array.isArray(params.batch) ? params.batch[0] : params
|
||||
const start = Number(zoomPayload?.start)
|
||||
const end = Number(zoomPayload?.end)
|
||||
const zoomPayload = (Array.isArray(params.batch) ? params.batch[0] : params) as ChartDataZoomPayload
|
||||
const zoomRange = resolveChartDataZoomRange(zoomPayload)
|
||||
|
||||
if (!Number.isFinite(start) || !Number.isFinite(end)) return
|
||||
if (!zoomRange) return
|
||||
|
||||
emit('chart-data-zoom', {
|
||||
start,
|
||||
end
|
||||
start: zoomRange.start,
|
||||
end: zoomRange.end
|
||||
})
|
||||
})
|
||||
bindPanCursorEvents()
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), '../../../..')
|
||||
|
||||
const read = path => readFileSync(resolve(root, path), 'utf8')
|
||||
|
||||
const staticRouterSource = read('src/routers/modules/staticRouter.ts')
|
||||
const mainSource = read('src/layouts/components/Main/index.vue')
|
||||
const tabsSource = read('src/stores/modules/tabs.ts')
|
||||
|
||||
const routeContracts = [
|
||||
['tools', 'src/views/tools/index.vue'],
|
||||
['toolWaveform', 'src/views/tools/waveform/index.vue'],
|
||||
['toolMmsMapping', 'src/views/tools/mmsMapping/index.vue'],
|
||||
['toolAddData', 'src/views/tools/addData/index.vue'],
|
||||
['toolAddLedger', 'src/views/tools/addLedger/index.vue'],
|
||||
['eventList', 'src/views/event/eventList/index.vue'],
|
||||
['systemMonitor', 'src/views/systemMonitor/index.vue'],
|
||||
['diskMonitor', 'src/views/systemMonitor/diskMonitor/index.vue']
|
||||
]
|
||||
|
||||
const extractRouteBlock = routeName => {
|
||||
const routeNameIndex = staticRouterSource.indexOf(`name: '${routeName}'`)
|
||||
if (routeNameIndex === -1) {
|
||||
throw new Error(`Route ${routeName} was not found in staticRouter.ts`)
|
||||
}
|
||||
|
||||
const nextRouteIndex = staticRouterSource.indexOf('\n {', routeNameIndex + 1)
|
||||
return staticRouterSource.slice(routeNameIndex, nextRouteIndex === -1 ? undefined : nextRouteIndex)
|
||||
}
|
||||
|
||||
const extractComponentName = viewPath => {
|
||||
const viewSource = read(viewPath)
|
||||
const componentName = viewSource.match(/defineOptions\(\s*\{\s*name:\s*'([^']+)'/s)?.[1]
|
||||
|
||||
if (!componentName) {
|
||||
throw new Error(`${viewPath} must define an explicit component name for keep-alive`)
|
||||
}
|
||||
|
||||
return componentName
|
||||
}
|
||||
|
||||
const errors = []
|
||||
|
||||
for (const [routeName, viewPath] of routeContracts) {
|
||||
const routeBlock = extractRouteBlock(routeName)
|
||||
const componentName = extractComponentName(viewPath)
|
||||
const cacheName = routeBlock.match(/cacheName:\s*'([^']+)'/)?.[1]
|
||||
|
||||
if (cacheName !== componentName) {
|
||||
errors.push(`${routeName} meta.cacheName should be ${componentName}, got ${cacheName || '<missing>'}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (!mainSource.includes('v-if="isRouterShow"')) {
|
||||
errors.push('Main router component must use isRouterShow so tab refresh can rebuild the current page')
|
||||
}
|
||||
|
||||
if (!mainSource.includes('keepAliveName')) {
|
||||
errors.push('Main keep-alive include list must come from keepAliveName')
|
||||
}
|
||||
|
||||
if (!tabsSource.includes('cacheName')) {
|
||||
errors.push('Tabs store must keep actual component cacheName values in keepAliveName')
|
||||
}
|
||||
|
||||
if (errors.length) {
|
||||
console.error(errors.join('\n'))
|
||||
process.exit(1)
|
||||
}
|
||||
@@ -5,8 +5,8 @@
|
||||
<router-view v-slot="{ Component, route }" style="height: 100%">
|
||||
<!-- {{ keepAliveName}} -->
|
||||
<!-- <transition name="slide-right" mode="out-in"> -->
|
||||
<keep-alive :include="tabsMenuList">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
<keep-alive :include="keepAliveName">
|
||||
<component v-if="isRouterShow" :is="Component" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
<!-- </transition> -->
|
||||
</router-view>
|
||||
@@ -26,13 +26,14 @@ import Maximize from './components/Maximize.vue'
|
||||
import Tabs from '@/layouts/components/Tabs/index.vue'
|
||||
import Footer from '@/layouts/components/Footer/index.vue'
|
||||
import { useAuthStore } from '@/stores/modules/auth'
|
||||
import { useTabsStore } from '@/stores/modules/tabs'
|
||||
|
||||
const tabStore = useTabsStore()
|
||||
defineOptions({
|
||||
name: 'LayoutMain'
|
||||
})
|
||||
|
||||
const globalStore = useGlobalStore()
|
||||
const tabsMenuList = computed(() => tabStore.tabsMenuList.map(item => item.name))
|
||||
const authStore = useAuthStore()
|
||||
const { maximize, isCollapse, layout, tabs, footer } = storeToRefs(globalStore)
|
||||
const { maximize, isCollapse, layout, tabs } = storeToRefs(globalStore)
|
||||
const keepAliveStore = useKeepAliveStore()
|
||||
const { keepAliveName } = storeToRefs(keepAliveStore)
|
||||
//是否显示导航栏
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
import { resolveBusinessMenuPath } from '@/stores/modules/auth'
|
||||
|
||||
defineProps<{ menuList: Menu.MenuOptions[] }>()
|
||||
const router = useRouter()
|
||||
@@ -29,7 +30,7 @@ const handleClickMenu = async (subItem: Menu.MenuOptions) => {
|
||||
window.open(subItem.meta.isLink, '_blank')
|
||||
return
|
||||
}
|
||||
await router.push(subItem.path)
|
||||
await router.push(resolveBusinessMenuPath(subItem))
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
|
||||
@@ -63,14 +63,22 @@ const currentTabRoute = computed(() => {
|
||||
return router.getRoutes().find(item => item.path === currentTabPath.value)
|
||||
})
|
||||
|
||||
const currentCacheName = computed(() => {
|
||||
return (
|
||||
(route.meta.cacheName as string | undefined) ||
|
||||
(currentTabRoute.value?.meta.cacheName as string | undefined) ||
|
||||
(route.name as string)
|
||||
)
|
||||
})
|
||||
|
||||
// refresh current page
|
||||
const refreshCurrentPage: Function = inject('refresh') as Function
|
||||
const refresh = () => {
|
||||
setTimeout(() => {
|
||||
keepAliveStore.removeKeepAliveName(route.name as string)
|
||||
keepAliveStore.removeKeepAliveName(currentCacheName.value)
|
||||
refreshCurrentPage(false)
|
||||
nextTick(() => {
|
||||
keepAliveStore.addKeepAliveName(route.name as string)
|
||||
keepAliveStore.addKeepAliveName(currentCacheName.value)
|
||||
refreshCurrentPage(true)
|
||||
})
|
||||
}, 0)
|
||||
|
||||
@@ -57,7 +57,7 @@ onMounted(() => {
|
||||
|
||||
const ensureParentTab = () => {
|
||||
const parentPath = resolveCurrentTabPath()
|
||||
if (!parentPath || tabStore.tabsMenuList.some(item => item.path === parentPath)) return
|
||||
if (!parentPath) return
|
||||
|
||||
const parentRoute = router.getRoutes().find(item => item.path === parentPath && item.name)
|
||||
if (!parentRoute) return
|
||||
@@ -69,7 +69,8 @@ const ensureParentTab = () => {
|
||||
path: parentRoute.path,
|
||||
name: parentRoute.name as string,
|
||||
close: !parentRoute.meta.isAffix,
|
||||
isKeepAlive: parentRoute.meta.isKeepAlive as boolean
|
||||
isKeepAlive: (route.meta.isKeepAlive ?? parentRoute.meta.isKeepAlive) as boolean,
|
||||
cacheName: (route.meta.cacheName || parentRoute.meta.cacheName) as string | undefined
|
||||
})
|
||||
}
|
||||
|
||||
@@ -87,6 +88,7 @@ watch(
|
||||
title: route.meta.title as string,
|
||||
path: route.fullPath,
|
||||
name: route.name as string,
|
||||
cacheName: route.meta.cacheName as string | undefined,
|
||||
close: !route.meta.isAffix,
|
||||
isKeepAlive: route.meta.isKeepAlive as boolean
|
||||
}
|
||||
@@ -113,6 +115,7 @@ const initTabs = () => {
|
||||
title: item.meta.title as string,
|
||||
path: item.path,
|
||||
name: item.name as string,
|
||||
cacheName: item.meta.cacheName as string | undefined,
|
||||
close: !item.meta.isAffix,
|
||||
isKeepAlive: item.meta.isKeepAlive as boolean,
|
||||
unshift: true
|
||||
|
||||
@@ -8,6 +8,11 @@ import { useAuthStore } from '@/stores/modules/auth'
|
||||
const modules = import.meta.glob('@/views/**/*.vue')
|
||||
const VIEWS_ALIAS_PREFIX = '@/views'
|
||||
const VIEWS_SRC_PREFIX = '/src/views'
|
||||
const COMPONENT_PATH_ALIASES: Record<string, string> = {
|
||||
// 后端菜单沿用 event-list 模块名时,前端实际页面目录为 eventList。
|
||||
'/event/event-list': '/event/eventList',
|
||||
'/event/event-list/index': '/event/eventList/index'
|
||||
}
|
||||
const STATIC_ROUTE_NAMES = new Set([
|
||||
'layout',
|
||||
'login',
|
||||
@@ -63,7 +68,7 @@ const normalizeComponentPath = (path: string) => {
|
||||
normalizedPath = normalizedPath.slice(0, -1)
|
||||
}
|
||||
|
||||
return normalizedPath
|
||||
return COMPONENT_PATH_ALIASES[normalizedPath] ?? normalizedPath
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,6 +137,9 @@ export const initDynamicRouter = async () => {
|
||||
|
||||
for (const item of authStore.flatMenuListGet) {
|
||||
if (item.children) delete item.children
|
||||
if (item.name && STATIC_ROUTE_NAMES.has(String(item.name))) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 动态菜单组件必须先映射成真实页面模块,否则 addRoute 后会直接落到 404。
|
||||
if (item.component && typeof item.component === 'string') {
|
||||
|
||||
@@ -26,6 +26,7 @@ export const staticRouter: RouteRecordRaw[] = [
|
||||
name: 'home',
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
meta: {
|
||||
cacheName: 'HomeWorkbench',
|
||||
title: '首页',
|
||||
icon: 'HomeFilled',
|
||||
isHide: false,
|
||||
@@ -39,6 +40,7 @@ export const staticRouter: RouteRecordRaw[] = [
|
||||
name: 'tools',
|
||||
component: () => import('@/views/tools/index.vue'),
|
||||
meta: {
|
||||
cacheName: 'ToolsView',
|
||||
title: '工具中心'
|
||||
}
|
||||
},
|
||||
@@ -47,6 +49,7 @@ export const staticRouter: RouteRecordRaw[] = [
|
||||
name: 'toolWaveform',
|
||||
component: () => import('@/views/tools/waveform/index.vue'),
|
||||
meta: {
|
||||
cacheName: 'WaveformView',
|
||||
title: '波形查看'
|
||||
}
|
||||
},
|
||||
@@ -56,6 +59,7 @@ export const staticRouter: RouteRecordRaw[] = [
|
||||
alias: ['/tools/mmsmapping', '/tools/mms-mapping'],
|
||||
component: () => import('@/views/tools/mmsMapping/index.vue'),
|
||||
meta: {
|
||||
cacheName: 'MmsMappingView',
|
||||
title: 'MMS 映射'
|
||||
}
|
||||
},
|
||||
@@ -64,6 +68,7 @@ export const staticRouter: RouteRecordRaw[] = [
|
||||
name: 'toolAddData',
|
||||
component: () => import('@/views/tools/addData/index.vue'),
|
||||
meta: {
|
||||
cacheName: 'AddDataView',
|
||||
title: '模拟数据'
|
||||
}
|
||||
},
|
||||
@@ -72,15 +77,25 @@ export const staticRouter: RouteRecordRaw[] = [
|
||||
name: 'toolAddLedger',
|
||||
component: () => import('@/views/tools/addLedger/index.vue'),
|
||||
meta: {
|
||||
cacheName: 'AddLedgerView',
|
||||
title: '数据台账'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/eventList/index',
|
||||
name: 'eventList',
|
||||
alias: ['/event/eventList'],
|
||||
alias: [
|
||||
'/eventList',
|
||||
'/eventlist',
|
||||
'/eventlist/index',
|
||||
'/event/eventList',
|
||||
'/event/eventList/index',
|
||||
'/event/event-list',
|
||||
'/event/event-list/index'
|
||||
],
|
||||
component: () => import('@/views/event/eventList/index.vue'),
|
||||
meta: {
|
||||
cacheName: 'EventListView',
|
||||
title: '事件列表'
|
||||
}
|
||||
},
|
||||
@@ -113,6 +128,7 @@ export const staticRouter: RouteRecordRaw[] = [
|
||||
name: 'systemMonitor',
|
||||
component: () => import('@/views/systemMonitor/index.vue'),
|
||||
meta: {
|
||||
cacheName: 'SystemMonitorPage',
|
||||
title: '系统监控'
|
||||
}
|
||||
},
|
||||
@@ -121,6 +137,7 @@ export const staticRouter: RouteRecordRaw[] = [
|
||||
name: 'diskMonitor',
|
||||
component: () => import('@/views/systemMonitor/diskMonitor/index.vue'),
|
||||
meta: {
|
||||
cacheName: 'DiskMonitorPage',
|
||||
// 磁盘监控页复用系统监控主标签
|
||||
activeMenu: '/systemMonitor',
|
||||
hideTab: true,
|
||||
|
||||
@@ -42,6 +42,7 @@ export interface TabsMenuProps {
|
||||
title: string
|
||||
path: string
|
||||
name: string
|
||||
cacheName?: string
|
||||
close: boolean
|
||||
isKeepAlive: boolean
|
||||
unshift?: boolean
|
||||
|
||||
@@ -32,7 +32,7 @@ export const useAuthStore = defineStore(AUTH_STORE_KEY, {
|
||||
},
|
||||
async getAuthMenuList() {
|
||||
const { data: menuData } = await getAuthMenuListApi()
|
||||
this.authMenuList = filterBusinessMenus(menuData)
|
||||
this.authMenuList = normalizeBusinessMenus(filterBusinessMenus(menuData))
|
||||
},
|
||||
async setRouteName(name: string) {
|
||||
this.routeName = name
|
||||
@@ -121,6 +121,42 @@ function filterBusinessMenus(menuList: any[]): any[] {
|
||||
})
|
||||
}
|
||||
|
||||
function normalizeBusinessMenus(menuList: any[]): any[] {
|
||||
return menuList.map(menu => normalizeBusinessMenu(menu))
|
||||
}
|
||||
|
||||
function normalizeBusinessMenu(menu: any): any {
|
||||
if (Array.isArray(menu.children) && menu.children.length > 0) {
|
||||
menu.children = normalizeBusinessMenus(menu.children)
|
||||
}
|
||||
|
||||
if (isEventListMenu(menu)) {
|
||||
// 后端菜单历史配置不统一,前端统一收敛到静态 eventList 页面入口。
|
||||
menu.path = '/eventList/index'
|
||||
menu.name = 'eventList'
|
||||
menu.component = '@/views/event/eventList/index.vue'
|
||||
}
|
||||
|
||||
return menu
|
||||
}
|
||||
|
||||
function isEventListMenu(menu: any): boolean {
|
||||
const normalizedName = String(menu?.name ?? '').toLowerCase().replace(/[-_]/g, '')
|
||||
const normalizedPath = String(menu?.path ?? '').toLowerCase().replace(/[-_]/g, '')
|
||||
const normalizedComponent = String(menu?.component ?? '').toLowerCase().replace(/[-_]/g, '')
|
||||
const title = String(menu?.meta?.title ?? menu?.title ?? '')
|
||||
|
||||
if (normalizedName === 'eventlist') return true
|
||||
if (normalizedPath.includes('eventlist')) return true
|
||||
if (normalizedComponent.includes('eventlist')) return true
|
||||
|
||||
return title.includes('事件列表') && (title.includes('暂降') || title.includes('暂态'))
|
||||
}
|
||||
|
||||
export function resolveBusinessMenuPath(menu: Menu.MenuOptions): string {
|
||||
return isEventListMenu(menu) ? '/eventList/index' : menu.path
|
||||
}
|
||||
|
||||
function normalizeActivateInfo(rawData: unknown): Activate.ActivationCodePlaintext {
|
||||
if (!rawData || typeof rawData !== 'object' || Array.isArray(rawData)) {
|
||||
return {}
|
||||
|
||||
@@ -7,6 +7,11 @@ import { TABS_STORE_KEY } from '@/stores/constant'
|
||||
|
||||
const keepAliveStore = useKeepAliveStore()
|
||||
|
||||
const getCacheName = (tabItem: TabsMenuProps) => tabItem.cacheName || tabItem.name
|
||||
const shouldKeepAlive = (tabItem: TabsMenuProps) => tabItem.isKeepAlive !== false
|
||||
const getCacheNameList = (tabsMenuList: TabsMenuProps[]) =>
|
||||
tabsMenuList.filter(shouldKeepAlive).map(getCacheName).filter(Boolean)
|
||||
|
||||
export const useTabsStore = defineStore(TABS_STORE_KEY, {
|
||||
state: (): TabsState => ({
|
||||
tabsMenuList: []
|
||||
@@ -21,12 +26,15 @@ export const useTabsStore = defineStore(TABS_STORE_KEY, {
|
||||
this.tabsMenuList.push(tabItem)
|
||||
}
|
||||
}
|
||||
if (!keepAliveStore.keepAliveName.includes(tabItem.name) && tabItem.isKeepAlive) {
|
||||
await keepAliveStore.addKeepAliveName(tabItem.name)
|
||||
const cacheName = getCacheName(tabItem)
|
||||
if (shouldKeepAlive(tabItem) && cacheName && !keepAliveStore.keepAliveName.includes(cacheName)) {
|
||||
await keepAliveStore.addKeepAliveName(cacheName)
|
||||
}
|
||||
},
|
||||
// Remove Tabs
|
||||
async removeTabs(tabPath: string, isCurrent: boolean = true) {
|
||||
const tabItem = this.tabsMenuList.find(item => item.path === tabPath)
|
||||
|
||||
if (isCurrent) {
|
||||
this.tabsMenuList.forEach((item, index) => {
|
||||
if (item.path !== tabPath) return
|
||||
@@ -37,8 +45,8 @@ export const useTabsStore = defineStore(TABS_STORE_KEY, {
|
||||
}
|
||||
this.tabsMenuList = this.tabsMenuList.filter(item => item.path !== tabPath)
|
||||
// remove keepalive
|
||||
const tabItem = this.tabsMenuList.find(item => item.path === tabPath)
|
||||
tabItem?.isKeepAlive && (await keepAliveStore.removeKeepAliveName(tabItem.name))
|
||||
const cacheName = tabItem ? getCacheName(tabItem) : ''
|
||||
cacheName && (await keepAliveStore.removeKeepAliveName(cacheName))
|
||||
},
|
||||
// Close Tabs On Side
|
||||
async closeTabsOnSide(path: string, type: 'left' | 'right') {
|
||||
@@ -50,8 +58,7 @@ export const useTabsStore = defineStore(TABS_STORE_KEY, {
|
||||
})
|
||||
}
|
||||
// set keepalive
|
||||
const KeepAliveList = this.tabsMenuList.filter(item => item.isKeepAlive)
|
||||
await keepAliveStore.setKeepAliveName(KeepAliveList.map(item => item.name))
|
||||
await keepAliveStore.setKeepAliveName(getCacheNameList(this.tabsMenuList))
|
||||
},
|
||||
// Close MultipleTab
|
||||
async closeMultipleTab(tabsMenuValue?: string) {
|
||||
@@ -59,12 +66,12 @@ export const useTabsStore = defineStore(TABS_STORE_KEY, {
|
||||
return item.path === tabsMenuValue || !item.close
|
||||
})
|
||||
// set keepalive
|
||||
const KeepAliveList = this.tabsMenuList.filter(item => item.isKeepAlive)
|
||||
await keepAliveStore.setKeepAliveName(KeepAliveList.map(item => item.name))
|
||||
await keepAliveStore.setKeepAliveName(getCacheNameList(this.tabsMenuList))
|
||||
},
|
||||
// Set Tabs
|
||||
async setTabs(tabsMenuList: TabsMenuProps[]) {
|
||||
this.tabsMenuList = tabsMenuList
|
||||
await keepAliveStore.setKeepAliveName(getCacheNameList(this.tabsMenuList))
|
||||
},
|
||||
// Set Tabs Title
|
||||
async setTabsTitle(title: string) {
|
||||
|
||||
1
frontend/src/types/global.d.ts
vendored
1
frontend/src/types/global.d.ts
vendored
@@ -11,6 +11,7 @@ declare namespace Menu {
|
||||
interface MetaProps {
|
||||
icon: string;
|
||||
title: string;
|
||||
cacheName?: string;
|
||||
activeMenu?: string;
|
||||
hideTab?: boolean;
|
||||
parentPath?: string;
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import ts from 'typescript'
|
||||
|
||||
const modulePath = path.resolve('src/views/event/eventList/utils/display.ts')
|
||||
|
||||
if (!fs.existsSync(modulePath)) {
|
||||
throw new Error('eventList display helpers must be extracted to utils/display.ts')
|
||||
}
|
||||
|
||||
const source = fs.readFileSync(modulePath, 'utf8')
|
||||
const transpiled = ts.transpileModule(source, {
|
||||
compilerOptions: {
|
||||
module: ts.ModuleKind.ES2020,
|
||||
target: ts.ScriptTarget.ES2020
|
||||
}
|
||||
}).outputText
|
||||
|
||||
const tempDir = path.resolve('node_modules/.cache/event-list-contract')
|
||||
fs.mkdirSync(tempDir, { recursive: true })
|
||||
const tempModulePath = path.join(tempDir, 'display.mjs')
|
||||
fs.writeFileSync(tempModulePath, transpiled, 'utf8')
|
||||
|
||||
const { resolveEventDescription } = await import(pathToFileURL(tempModulePath).href)
|
||||
|
||||
assert.equal(resolveEventDescription({ eventDescribe: '电压暂降' }), '电压暂降')
|
||||
assert.equal(resolveEventDescription({ eventDescription: '描述字段' }), '描述字段')
|
||||
assert.equal(resolveEventDescription({ eventDesc: '简写描述' }), '简写描述')
|
||||
assert.equal(resolveEventDescription({ description: '通用描述' }), '通用描述')
|
||||
assert.equal(resolveEventDescription({ describe: 'describe 字段' }), 'describe 字段')
|
||||
assert.equal(resolveEventDescription({ remark: '备注描述' }), '备注描述')
|
||||
assert.equal(resolveEventDescription({ eventDescribe: '', eventType: 'VOLTAGE_SAG' }), 'VOLTAGE_SAG')
|
||||
assert.equal(resolveEventDescription({}), '--')
|
||||
assert.equal(resolveEventDescription(null), '--')
|
||||
|
||||
console.log('eventList display contract passed')
|
||||
47
frontend/src/views/event/eventList/check-route-contract.mjs
Normal file
47
frontend/src/views/event/eventList/check-route-contract.mjs
Normal file
@@ -0,0 +1,47 @@
|
||||
/* 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 routersDir = path.join(currentDir, '..', '..', '..', 'routers', 'modules')
|
||||
const staticRouterFile = path.join(routersDir, 'staticRouter.ts')
|
||||
const dynamicRouterFile = path.join(routersDir, 'dynamicRouter.ts')
|
||||
const authStoreFile = path.join(currentDir, '..', '..', '..', 'stores', 'modules', 'auth.ts')
|
||||
const subMenuFile = path.join(currentDir, '..', '..', '..', 'layouts', 'components', 'Menu', 'SubMenu.vue')
|
||||
|
||||
const staticRouterSource = fs.readFileSync(staticRouterFile, 'utf8')
|
||||
const dynamicRouterSource = fs.readFileSync(dynamicRouterFile, 'utf8')
|
||||
const authStoreSource = fs.readFileSync(authStoreFile, 'utf8')
|
||||
const subMenuSource = fs.readFileSync(subMenuFile, 'utf8')
|
||||
|
||||
const expectations = [
|
||||
['static route accepts backend kebab event-list path', /['"]\/event\/event-list['"]/],
|
||||
['static route accepts backend kebab event-list index path', /['"]\/event\/event-list\/index['"]/],
|
||||
['dynamic router normalizes event-list component path to eventList', /event\/event-list[\s\S]*event\/eventList/],
|
||||
[
|
||||
'dynamic router keeps static eventList route from being overwritten',
|
||||
/STATIC_ROUTE_NAMES\.has\(String\(item\.name\)\)[\s\S]*continue/
|
||||
],
|
||||
[
|
||||
'auth menu normalizes sag transient event list path',
|
||||
/normalizeBusinessMenu[\s\S]*isEventListMenu[\s\S]*menu\.path\s*=\s*'\/eventList\/index'/
|
||||
],
|
||||
[
|
||||
'menu click resolves business menu path before router push',
|
||||
/resolveBusinessMenuPath[\s\S]*router\.push\(resolveBusinessMenuPath\(subItem\)\)/
|
||||
]
|
||||
]
|
||||
|
||||
const combinedSource = `${staticRouterSource}\n${dynamicRouterSource}\n${authStoreSource}\n${subMenuSource}`
|
||||
const failures = expectations.filter(([, pattern]) => !pattern.test(combinedSource))
|
||||
|
||||
if (failures.length) {
|
||||
console.error('eventList route contract check failed:')
|
||||
for (const [name] of failures) {
|
||||
console.error(`- ${name}`)
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log('eventList route contract check passed')
|
||||
@@ -0,0 +1,32 @@
|
||||
/* 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 source = fs.readFileSync(pageFile, 'utf8')
|
||||
|
||||
const expectations = [
|
||||
['search grid keeps five fields on wide event list screens', /:search-col="\{\s*xs:\s*1,\s*sm:\s*2,\s*md:\s*2,\s*lg:\s*5,\s*xl:\s*5\s*\}"/],
|
||||
['event time table column keeps occurrence time label', /prop:\s*'startTime',\s*label:\s*'发生时刻'/],
|
||||
['event time search label is shortened to time', /search:\s*\{\s*label:\s*'时间',\s*key:\s*'startTimeRange'/],
|
||||
['event time search field only takes one grid column', /key:\s*'startTimeRange',\s*span:\s*1,/],
|
||||
['event time picker uses expanded fixed width', /\.event-time-search__picker[\s\S]*width:\s*112px;\s*flex:\s*0 0 112px;/],
|
||||
['event time search uses compact spacing', /\.event-time-search[\s\S]*gap:\s*4px;/],
|
||||
['event time search imports current time icon', /import \{[^}]*Clock[^}]*\} from '@element-plus\/icons-vue'/],
|
||||
['event time search defines current period handler', /const handleCurrentEventPeriod = \(\) => \{[\s\S]*eventTimeBaseDate\.value = new Date\(\)[\s\S]*syncEventTimeRange\(\)/],
|
||||
['event time search renders current period button', /icon:\s*Clock,[\s\S]*title:\s*`当前\$\{unitLabel\}`,[\s\S]*onClick:\s*handleCurrentEventPeriod/]
|
||||
]
|
||||
|
||||
const failures = expectations.filter(([, pattern]) => !pattern.test(source))
|
||||
|
||||
if (failures.length) {
|
||||
console.error('eventList search layout contract check failed:')
|
||||
for (const [name] of failures) {
|
||||
console.error(`- ${name}`)
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log('eventList search layout contract check passed')
|
||||
@@ -0,0 +1,64 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import ts from 'typescript'
|
||||
|
||||
const modulePath = path.resolve('src/views/event/eventList/eventTimeRange.ts')
|
||||
|
||||
if (!fs.existsSync(modulePath)) {
|
||||
throw new Error('eventTimeRange.ts must provide event list time range helpers')
|
||||
}
|
||||
|
||||
const source = fs.readFileSync(modulePath, 'utf8')
|
||||
const transpiled = ts.transpileModule(source, {
|
||||
compilerOptions: {
|
||||
module: ts.ModuleKind.ES2020,
|
||||
target: ts.ScriptTarget.ES2020
|
||||
}
|
||||
}).outputText
|
||||
|
||||
const tempDir = path.resolve('node_modules/.cache/event-list-contract')
|
||||
fs.mkdirSync(tempDir, { recursive: true })
|
||||
const tempModulePath = path.join(tempDir, 'eventTimeRange.mjs')
|
||||
fs.writeFileSync(tempModulePath, transpiled, 'utf8')
|
||||
|
||||
const { buildEventTimeRange, formatEventOccurrenceTime, shiftEventPeriod } = await import(
|
||||
pathToFileURL(tempModulePath).href
|
||||
)
|
||||
|
||||
assert.deepEqual(buildEventTimeRange('day', new Date(2026, 4, 13)), [
|
||||
'2026-05-13 00:00:00.000',
|
||||
'2026-05-13 23:59:59.999'
|
||||
])
|
||||
|
||||
assert.deepEqual(buildEventTimeRange('month', new Date(2026, 4, 13)), [
|
||||
'2026-05-01 00:00:00.000',
|
||||
'2026-05-31 23:59:59.999'
|
||||
])
|
||||
|
||||
assert.deepEqual(buildEventTimeRange('year', new Date(2026, 4, 13)), [
|
||||
'2026-01-01 00:00:00.000',
|
||||
'2026-12-31 23:59:59.999'
|
||||
])
|
||||
|
||||
assert.deepEqual(buildEventTimeRange('month', shiftEventPeriod('month', new Date(2026, 4, 13), -1)), [
|
||||
'2026-04-01 00:00:00.000',
|
||||
'2026-04-30 23:59:59.999'
|
||||
])
|
||||
|
||||
assert.deepEqual(buildEventTimeRange('year', shiftEventPeriod('year', new Date(2026, 4, 13), 1)), [
|
||||
'2027-01-01 00:00:00.000',
|
||||
'2027-12-31 23:59:59.999'
|
||||
])
|
||||
|
||||
assert.equal(formatEventOccurrenceTime('2026-05-09 10:20:30'), '2026-05-09 10:20:30')
|
||||
assert.equal(formatEventOccurrenceTime('2026-05-09 10:20:30.123'), '2026-05-09 10:20:30.123')
|
||||
assert.equal(formatEventOccurrenceTime('2026-05-09 10:20:30.120'), '2026-05-09 10:20:30.120')
|
||||
assert.equal(formatEventOccurrenceTime('2026-05-09 10:20:30.123456'), '2026-05-09 10:20:30.123456')
|
||||
assert.equal(formatEventOccurrenceTime('2026-05-09 10:20:30.000'), '2026-05-09 10:20:30.000')
|
||||
assert.equal(formatEventOccurrenceTime('2026-05-09T10:20:30.7'), '2026-05-09 10:20:30.7')
|
||||
assert.equal(formatEventOccurrenceTime(''), '--')
|
||||
assert.equal(formatEventOccurrenceTime(null), '--')
|
||||
|
||||
console.log('eventList time range contract passed')
|
||||
@@ -0,0 +1,39 @@
|
||||
/* 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 queryFile = path.join(currentDir, 'utils', 'queryParams.ts')
|
||||
const source = fs.readFileSync(pageFile, 'utf8')
|
||||
const querySource = fs.readFileSync(queryFile, 'utf8')
|
||||
|
||||
const expectations = [
|
||||
['table shows occurrence time before event type', /label:\s*'发生时刻'[\s\S]*label:\s*'事件类型'/],
|
||||
['table shows monitor point before duration', /label:\s*'监测点名称'[\s\S]*label:\s*'持续时间\(s\)'/],
|
||||
['table shows waveform status before event description', /label:\s*'波形文件状态'[\s\S]*label:\s*'事件描述'/],
|
||||
['event location stays in visible table columns', /label:\s*'事件发生位置'/],
|
||||
['monitor point is rendered as a clickable link', /prop:\s*'lineName'[\s\S]*type:\s*'primary'[\s\S]*link:\s*true[\s\S]*handleViewMeasurementPoint\(row\)/],
|
||||
['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/],
|
||||
['event description is not a search field', /prop:\s*'eventDescribe'[\s\S]*label:\s*'事件描述'[\s\S]*search:/.test(source) === false],
|
||||
['ledger names are searched through one keyword field', /ledgerKeyword[\s\S]*label:\s*'台账关键字'/],
|
||||
['query params fan out ledger keyword to ledger name fields', /ledgerKeyword[\s\S]*engineeringName[\s\S]*projectName[\s\S]*equipmentName[\s\S]*lineName/.test(querySource)]
|
||||
]
|
||||
|
||||
const failures = expectations.filter(([, pattern]) => {
|
||||
if (typeof pattern === 'boolean') return !pattern
|
||||
return !pattern.test(source)
|
||||
})
|
||||
|
||||
if (failures.length) {
|
||||
console.error('eventList visible contract check failed:')
|
||||
for (const [name] of failures) {
|
||||
console.error(`- ${name}`)
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log('eventList visible contract check passed')
|
||||
99
frontend/src/views/event/eventList/eventTimeRange.ts
Normal file
99
frontend/src/views/event/eventList/eventTimeRange.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
export type EventTimeUnit = 'day' | 'month' | 'year'
|
||||
|
||||
export const eventTimeUnitOptions: { label: string; value: EventTimeUnit }[] = [
|
||||
{ label: '日', value: 'day' },
|
||||
{ label: '月', value: 'month' },
|
||||
{ label: '年', value: 'year' }
|
||||
]
|
||||
|
||||
const datePickerTypeMap: Record<EventTimeUnit, 'date' | 'month' | 'year'> = {
|
||||
day: 'date',
|
||||
month: 'month',
|
||||
year: 'year'
|
||||
}
|
||||
|
||||
const datePickerFormatMap: Record<EventTimeUnit, string> = {
|
||||
day: 'YYYY-MM-DD',
|
||||
month: 'YYYY-MM',
|
||||
year: 'YYYY'
|
||||
}
|
||||
|
||||
const padTimeValue = (value: number, length = 2) => String(value).padStart(length, '0')
|
||||
|
||||
const formatEventTime = (date: Date) => {
|
||||
const year = date.getFullYear()
|
||||
const month = padTimeValue(date.getMonth() + 1)
|
||||
const day = padTimeValue(date.getDate())
|
||||
const hour = padTimeValue(date.getHours())
|
||||
const minute = padTimeValue(date.getMinutes())
|
||||
const second = padTimeValue(date.getSeconds())
|
||||
const millisecond = padTimeValue(date.getMilliseconds(), 3)
|
||||
|
||||
return `${year}-${month}-${day} ${hour}:${minute}:${second}.${millisecond}`
|
||||
}
|
||||
|
||||
export const formatEventOccurrenceTime = (value: unknown) => {
|
||||
if (value === null || value === undefined) return '--'
|
||||
|
||||
const text = String(value).trim()
|
||||
if (!text) return '--'
|
||||
|
||||
// 发生时刻直接承载事件定位精度:小数秒按接口原始值展示,不补零、不裁剪。
|
||||
const matched = text.match(/^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}:\d{2})(?:\.(\d+))?$/)
|
||||
if (!matched) return text
|
||||
|
||||
const fractionalSecond = matched[3]
|
||||
return `${matched[1]} ${matched[2]}${fractionalSecond ? `.${fractionalSecond}` : ''}`
|
||||
}
|
||||
|
||||
export const getEventDatePickerType = (unit: EventTimeUnit) => datePickerTypeMap[unit]
|
||||
|
||||
export const getEventDatePickerFormat = (unit: EventTimeUnit) => datePickerFormatMap[unit]
|
||||
|
||||
export const resolveEventTimeUnitLabel = (unit: EventTimeUnit) => {
|
||||
return eventTimeUnitOptions.find(item => item.value === unit)?.label ?? ''
|
||||
}
|
||||
|
||||
export const buildEventTimeRange = (unit: EventTimeUnit, date: Date): string[] => {
|
||||
const year = date.getFullYear()
|
||||
const month = date.getMonth()
|
||||
const day = date.getDate()
|
||||
|
||||
if (unit === 'day') {
|
||||
return [
|
||||
formatEventTime(new Date(year, month, day, 0, 0, 0, 0)),
|
||||
formatEventTime(new Date(year, month, day, 23, 59, 59, 999))
|
||||
]
|
||||
}
|
||||
|
||||
if (unit === 'year') {
|
||||
return [
|
||||
formatEventTime(new Date(year, 0, 1, 0, 0, 0, 0)),
|
||||
formatEventTime(new Date(year, 11, 31, 23, 59, 59, 999))
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
formatEventTime(new Date(year, month, 1, 0, 0, 0, 0)),
|
||||
formatEventTime(new Date(year, month + 1, 0, 23, 59, 59, 999))
|
||||
]
|
||||
}
|
||||
|
||||
export const shiftEventPeriod = (unit: EventTimeUnit, date: Date, offset: number) => {
|
||||
const nextDate = new Date(date)
|
||||
|
||||
if (unit === 'day') {
|
||||
nextDate.setDate(nextDate.getDate() + offset)
|
||||
return nextDate
|
||||
}
|
||||
|
||||
if (unit === 'year') {
|
||||
nextDate.setFullYear(nextDate.getFullYear() + offset)
|
||||
return nextDate
|
||||
}
|
||||
|
||||
// 月份切换以 1 日为锚点,避免 31 日切到短月份时发生日期溢出。
|
||||
nextDate.setDate(1)
|
||||
nextDate.setMonth(nextDate.getMonth() + offset)
|
||||
return nextDate
|
||||
}
|
||||
@@ -5,34 +5,38 @@
|
||||
row-key="eventId"
|
||||
:columns="columns"
|
||||
:request-api="getTableList"
|
||||
:search-col="{ xs: 1, sm: 2, md: 2, lg: 3, xl: 4 }"
|
||||
:search-col="{ xs: 1, sm: 2, md: 2, lg: 5, xl: 5 }"
|
||||
@reset="handleSearchReset"
|
||||
>
|
||||
<template #tableHeader>
|
||||
<el-button type="primary" plain :icon="Download" @click="handleExport">导出</el-button>
|
||||
</template>
|
||||
|
||||
<template #fileFlag="{ row }">
|
||||
<el-tag :type="row.fileFlag === 1 ? 'success' : 'info'" effect="light">
|
||||
<el-tag :type="Number(row.fileFlag) === 1 ? 'success' : 'info'" effect="light">
|
||||
{{ resolveFileFlagText(row.fileFlag) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
|
||||
<template #dealFlag="{ row }">
|
||||
<el-tag :type="resolveDealFlagTag(row.dealFlag)" effect="light">
|
||||
{{ resolveDealFlagText(row.dealFlag) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
|
||||
<template #operation="{ row }">
|
||||
<el-button type="primary" link :icon="View" @click="handleViewDetail(row)">详情</el-button>
|
||||
<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>
|
||||
</template>
|
||||
</ProTable>
|
||||
|
||||
<el-dialog v-model="detailDialogVisible" title="暂态事件详情" width="760px">
|
||||
<el-skeleton v-if="detailLoading" :rows="6" animated />
|
||||
<el-dialog v-model="measurementPointDialogVisible" title="监测点信息" width="640px">
|
||||
<el-skeleton v-if="measurementPointLoading" :rows="4" animated />
|
||||
<el-descriptions v-else :column="2" border>
|
||||
<el-descriptions-item v-for="item in detailItems" :key="item.prop" :label="item.label">
|
||||
{{ item.formatter ? item.formatter(detailData?.[item.prop]) : resolveText(detailData?.[item.prop]) }}
|
||||
<el-descriptions-item
|
||||
v-for="item in measurementPointItems"
|
||||
:key="item.prop"
|
||||
:label="item.label"
|
||||
>
|
||||
{{ resolveText(measurementPointData?.[item.prop]) }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
@@ -40,72 +44,174 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Download, View } from '@element-plus/icons-vue'
|
||||
import { h } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElButton, ElDatePicker, ElOption, ElRadioButton, ElRadioGroup, ElSelect } from 'element-plus'
|
||||
import { ArrowLeft, ArrowRight, Clock, 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 type { EventList } from '@/api/event/eventList/interface'
|
||||
import { useDownloadWithServerFileName } from '@/hooks/useDownload'
|
||||
import {
|
||||
buildEventTimeRange,
|
||||
eventTimeUnitOptions,
|
||||
formatEventOccurrenceTime,
|
||||
getEventDatePickerFormat,
|
||||
getEventDatePickerType,
|
||||
resolveEventTimeUnitLabel,
|
||||
shiftEventPeriod,
|
||||
type EventTimeUnit
|
||||
} from './eventTimeRange'
|
||||
import { buildEventQueryParams, type EventSearchParams } from './utils/queryParams'
|
||||
import { resolveEventDescription } from './utils/display'
|
||||
import {
|
||||
fileFlagOptions,
|
||||
phaseOptions,
|
||||
resolveFileFlagText
|
||||
} from './utils/status'
|
||||
|
||||
defineOptions({
|
||||
name: 'EventListView'
|
||||
})
|
||||
|
||||
type EventSearchParams = EventList.TransientPageParams & {
|
||||
startTimeRange?: string[]
|
||||
}
|
||||
|
||||
type DetailItem = {
|
||||
label: string
|
||||
prop: keyof EventList.TransientEventRecord
|
||||
formatter?: (value: unknown) => string
|
||||
}
|
||||
|
||||
const proTable = ref<ProTableInstance>()
|
||||
const detailDialogVisible = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
const detailData = ref<EventList.TransientEventRecord | null>(null)
|
||||
|
||||
const phaseOptions = [
|
||||
{ label: 'A 相', value: 'A' },
|
||||
{ label: 'B 相', value: 'B' },
|
||||
{ label: 'C 相', value: 'C' },
|
||||
{ label: '三相', value: 'ABC' }
|
||||
const router = useRouter()
|
||||
const measurementPointDialogVisible = ref(false)
|
||||
const measurementPointLoading = ref(false)
|
||||
const measurementPointData = ref<EventList.TransientEventRecord | null>(null)
|
||||
const eventTimeUnit = ref<EventTimeUnit>('month')
|
||||
const eventTimeBaseDate = ref(new Date())
|
||||
const defaultStartTimeRange = buildEventTimeRange(eventTimeUnit.value, eventTimeBaseDate.value)
|
||||
const measurementPointItems: { label: string; prop: keyof EventList.TransientEventRecord }[] = [
|
||||
{ label: '工程名称', prop: 'engineeringName' },
|
||||
{ label: '项目名称', prop: 'projectName' },
|
||||
{ label: '设备名称', prop: 'equipmentName' },
|
||||
{ label: '监测点名称', prop: 'lineName' },
|
||||
{ label: '监测点 ID', prop: 'measurementPointId' }
|
||||
]
|
||||
|
||||
const fileFlagOptions = [
|
||||
{ label: '未招', value: 0 },
|
||||
{ label: '已招', value: 1 }
|
||||
]
|
||||
const syncEventTimeRange = () => {
|
||||
const timeRange = buildEventTimeRange(eventTimeUnit.value, eventTimeBaseDate.value)
|
||||
const searchParam = proTable.value?.searchParam as EventSearchParams | undefined
|
||||
|
||||
const dealFlagOptions = [
|
||||
{ label: '未处理', value: 0 },
|
||||
{ label: '已处理', value: 1 },
|
||||
{ label: '已处理无结果', value: 2 },
|
||||
{ label: '计算失败', value: 3 }
|
||||
]
|
||||
if (searchParam) {
|
||||
searchParam.startTimeRange = timeRange
|
||||
}
|
||||
|
||||
return timeRange
|
||||
}
|
||||
|
||||
const handleEventTimeUnitChange = (value: EventTimeUnit) => {
|
||||
eventTimeUnit.value = value
|
||||
syncEventTimeRange()
|
||||
}
|
||||
|
||||
const handleEventTimeDateChange = (value: Date | string | number | null) => {
|
||||
if (!value) return
|
||||
eventTimeBaseDate.value = new Date(value)
|
||||
syncEventTimeRange()
|
||||
}
|
||||
|
||||
const handleShiftEventPeriod = (offset: number) => {
|
||||
eventTimeBaseDate.value = shiftEventPeriod(eventTimeUnit.value, eventTimeBaseDate.value, offset)
|
||||
syncEventTimeRange()
|
||||
}
|
||||
|
||||
const handleCurrentEventPeriod = () => {
|
||||
eventTimeBaseDate.value = new Date()
|
||||
syncEventTimeRange()
|
||||
}
|
||||
|
||||
const handleSearchReset = () => {
|
||||
eventTimeUnit.value = 'month'
|
||||
eventTimeBaseDate.value = new Date()
|
||||
syncEventTimeRange()
|
||||
}
|
||||
|
||||
const renderEventTimeSearch = () => {
|
||||
const unitLabel = resolveEventTimeUnitLabel(eventTimeUnit.value)
|
||||
|
||||
return h('div', { class: 'event-time-search' }, [
|
||||
h(
|
||||
ElSelect,
|
||||
{
|
||||
class: 'event-time-search__unit',
|
||||
modelValue: eventTimeUnit.value,
|
||||
'onUpdate:modelValue': handleEventTimeUnitChange
|
||||
},
|
||||
() =>
|
||||
eventTimeUnitOptions.map(option =>
|
||||
h(ElOption, {
|
||||
key: option.value,
|
||||
label: option.label,
|
||||
value: option.value
|
||||
})
|
||||
)
|
||||
),
|
||||
h(ElButton, {
|
||||
class: 'event-time-search__button',
|
||||
icon: ArrowLeft,
|
||||
title: `上一个${unitLabel}`,
|
||||
onClick: () => handleShiftEventPeriod(-1)
|
||||
}),
|
||||
h(ElDatePicker, {
|
||||
class: 'event-time-search__picker',
|
||||
modelValue: eventTimeBaseDate.value,
|
||||
type: getEventDatePickerType(eventTimeUnit.value),
|
||||
format: getEventDatePickerFormat(eventTimeUnit.value),
|
||||
clearable: false,
|
||||
editable: false,
|
||||
placeholder: `选择${unitLabel}`,
|
||||
'onUpdate:modelValue': handleEventTimeDateChange
|
||||
}),
|
||||
h(ElButton, {
|
||||
class: 'event-time-search__button',
|
||||
icon: ArrowRight,
|
||||
title: `下一个${unitLabel}`,
|
||||
onClick: () => handleShiftEventPeriod(1)
|
||||
}),
|
||||
h(ElButton, {
|
||||
class: 'event-time-search__button',
|
||||
icon: Clock,
|
||||
title: `当前${unitLabel}`,
|
||||
onClick: handleCurrentEventPeriod
|
||||
})
|
||||
])
|
||||
}
|
||||
|
||||
const renderFileFlagSearch = ({ searchParam }: { searchParam: EventSearchParams }) => {
|
||||
return h(
|
||||
ElRadioGroup,
|
||||
{
|
||||
class: 'event-file-flag-search',
|
||||
modelValue: searchParam.fileFlag ?? '',
|
||||
'onUpdate:modelValue': (value: string | number | boolean | undefined) => {
|
||||
searchParam.fileFlag = value === '' || value === undefined ? undefined : Number(value)
|
||||
}
|
||||
},
|
||||
() => [
|
||||
h(ElRadioButton, { label: '' }, () => '全部'),
|
||||
...fileFlagOptions.map(option =>
|
||||
h(ElRadioButton, { key: option.value, label: option.value }, () => option.label)
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
const columns = reactive<ColumnProps<EventList.TransientEventRecord>[]>([
|
||||
{ type: 'index', fixed: 'left', width: 70, label: '序号' },
|
||||
{
|
||||
prop: 'startTime',
|
||||
label: '发生时刻',
|
||||
minWidth: 180,
|
||||
minWidth: 200,
|
||||
render: ({ row }) => formatEventOccurrenceTime(row.startTime),
|
||||
search: {
|
||||
el: 'date-picker',
|
||||
label: '时间',
|
||||
key: 'startTimeRange',
|
||||
props: {
|
||||
type: 'datetimerange',
|
||||
valueFormat: 'YYYY-MM-DD HH:mm:ss'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'eventDescribe',
|
||||
label: '事件描述',
|
||||
minWidth: 180,
|
||||
search: {
|
||||
el: 'input'
|
||||
span: 1,
|
||||
defaultValue: defaultStartTimeRange,
|
||||
render: renderEventTimeSearch
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -126,38 +232,33 @@ const columns = reactive<ColumnProps<EventList.TransientEventRecord>[]>([
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'engineeringName',
|
||||
label: '工程名称',
|
||||
minWidth: 180,
|
||||
prop: 'ledgerKeyword',
|
||||
label: '台账关键字',
|
||||
isShow: false,
|
||||
isSetting: false,
|
||||
search: {
|
||||
el: 'input'
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'projectName',
|
||||
label: '项目名称',
|
||||
minWidth: 180,
|
||||
search: {
|
||||
el: 'input'
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'equipmentName',
|
||||
label: '设备名称',
|
||||
minWidth: 180,
|
||||
search: {
|
||||
el: 'input'
|
||||
el: 'input',
|
||||
label: '台账关键字',
|
||||
props: {
|
||||
placeholder: '工程/项目/设备/监测点'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'lineName',
|
||||
label: '监测点名称',
|
||||
minWidth: 180,
|
||||
search: {
|
||||
el: 'input'
|
||||
}
|
||||
render: ({ row }) =>
|
||||
h(
|
||||
ElButton,
|
||||
{
|
||||
type: 'primary',
|
||||
link: true,
|
||||
onClick: () => handleViewMeasurementPoint(row)
|
||||
},
|
||||
() => resolveText(row.lineName)
|
||||
)
|
||||
},
|
||||
{ prop: 'sagsource', label: '事件发生位置', minWidth: 140 },
|
||||
{ prop: 'duration', label: '持续时间(s)', minWidth: 130 },
|
||||
{ prop: 'featureAmplitude', label: '暂降/暂升幅值(%)', minWidth: 160 },
|
||||
{
|
||||
@@ -166,130 +267,67 @@ const columns = reactive<ColumnProps<EventList.TransientEventRecord>[]>([
|
||||
minWidth: 130,
|
||||
enum: fileFlagOptions,
|
||||
search: {
|
||||
el: 'select'
|
||||
render: renderFileFlagSearch
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'dealFlag',
|
||||
label: '处理状态',
|
||||
minWidth: 140,
|
||||
enum: dealFlagOptions,
|
||||
search: {
|
||||
el: 'select'
|
||||
}
|
||||
prop: 'eventDescribe',
|
||||
label: '事件描述',
|
||||
minWidth: 180,
|
||||
render: ({ row }) => resolveEventDescription(row)
|
||||
},
|
||||
{ prop: 'createTime', label: '创建时间', minWidth: 180 },
|
||||
{ prop: 'operation', label: '操作', fixed: 'right', width: 100 }
|
||||
{ prop: 'sagsource', label: '事件发生位置', minWidth: 140 },
|
||||
{ prop: 'operation', label: '操作', fixed: 'right', width: 130 }
|
||||
])
|
||||
|
||||
const detailItems: DetailItem[] = [
|
||||
{ label: '事件 ID', prop: 'eventId' },
|
||||
{ label: '监测点 ID', prop: 'measurementPointId' },
|
||||
{ label: '事件类型', prop: 'eventType' },
|
||||
{ label: '工程名称', prop: 'engineeringName' },
|
||||
{ label: '项目名称', prop: 'projectName' },
|
||||
{ label: '设备名称', prop: 'equipmentName' },
|
||||
{ label: '监测点名称', prop: 'lineName' },
|
||||
{ label: '发生时刻', prop: 'startTime' },
|
||||
{ label: '事件描述', prop: 'eventDescribe' },
|
||||
{ label: '事件发生位置', prop: 'sagsource' },
|
||||
{ label: '相别', prop: 'phase' },
|
||||
{ label: '持续时间(s)', prop: 'duration' },
|
||||
{ label: '暂降/暂升幅值(%)', prop: 'featureAmplitude' },
|
||||
{ label: '波形文件路径', prop: 'wavePath' },
|
||||
{ label: '波形文件状态', prop: 'fileFlag', formatter: resolveFileFlagText },
|
||||
{ label: '处理状态', prop: 'dealFlag', formatter: resolveDealFlagText },
|
||||
{ label: '创建时间', prop: 'createTime' }
|
||||
]
|
||||
|
||||
const resolveText = (value: unknown) => {
|
||||
if (value === null || value === undefined || value === '') return '--'
|
||||
return String(value)
|
||||
}
|
||||
|
||||
const resolveOptionalText = (value: unknown) => {
|
||||
if (value === null || value === undefined) return undefined
|
||||
const text = String(value).trim()
|
||||
return text || undefined
|
||||
}
|
||||
|
||||
const resolveOptionalNumber = (value: unknown) => {
|
||||
if (value === null || value === undefined || value === '') return undefined
|
||||
const parsed = Number(value)
|
||||
return Number.isFinite(parsed) ? parsed : undefined
|
||||
}
|
||||
|
||||
function resolveFileFlagText(value: unknown) {
|
||||
if (Number(value) === 0) return '未招'
|
||||
if (Number(value) === 1) return '已招'
|
||||
return '--'
|
||||
}
|
||||
|
||||
function resolveDealFlagText(value: unknown) {
|
||||
if (Number(value) === 0) return '未处理'
|
||||
if (Number(value) === 1) return '已处理'
|
||||
if (Number(value) === 2) return '已处理无结果'
|
||||
if (Number(value) === 3) return '计算失败'
|
||||
return '--'
|
||||
}
|
||||
|
||||
const resolveDealFlagTag = (value: unknown) => {
|
||||
if (Number(value) === 0) return 'info'
|
||||
if (Number(value) === 1) return 'success'
|
||||
if (Number(value) === 2) return 'warning'
|
||||
if (Number(value) === 3) return 'danger'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
const pruneEmptyParams = (params: EventList.TransientPageParams) => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(params).filter(([, value]) => value !== undefined && value !== '')
|
||||
) as EventList.TransientPageParams
|
||||
}
|
||||
|
||||
const buildEventQueryParams = (params: EventSearchParams = {}) => {
|
||||
const timeRange = Array.isArray(params.startTimeRange) ? params.startTimeRange : []
|
||||
|
||||
return pruneEmptyParams({
|
||||
pageNum: params.pageNum,
|
||||
pageSize: params.pageSize,
|
||||
startTimeStart: resolveOptionalText(timeRange[0]),
|
||||
startTimeEnd: resolveOptionalText(timeRange[1]),
|
||||
eventType: resolveOptionalText(params.eventType),
|
||||
phase: resolveOptionalText(params.phase),
|
||||
eventDescribe: resolveOptionalText(params.eventDescribe),
|
||||
fileFlag: resolveOptionalNumber(params.fileFlag),
|
||||
dealFlag: resolveOptionalNumber(params.dealFlag),
|
||||
engineeringName: resolveOptionalText(params.engineeringName),
|
||||
projectName: resolveOptionalText(params.projectName),
|
||||
equipmentName: resolveOptionalText(params.equipmentName),
|
||||
lineName: resolveOptionalText(params.lineName)
|
||||
})
|
||||
}
|
||||
|
||||
const getTableList = (params: EventSearchParams) => {
|
||||
// 分页查询按 API_DEBUG.md 转换时间范围与枚举筛选参数。
|
||||
return getTransientEventPage(buildEventQueryParams(params))
|
||||
}
|
||||
|
||||
const handleViewDetail = async (row: EventList.TransientEventRecord) => {
|
||||
const handleViewMeasurementPoint = async (row: EventList.TransientEventRecord) => {
|
||||
if (!row.eventId) {
|
||||
ElMessage.warning('缺少事件 ID,无法查询详情')
|
||||
ElMessage.warning('缺少事件 ID,无法查询监测点信息')
|
||||
return
|
||||
}
|
||||
|
||||
detailDialogVisible.value = true
|
||||
detailLoading.value = true
|
||||
detailData.value = null
|
||||
measurementPointDialogVisible.value = true
|
||||
measurementPointLoading.value = true
|
||||
measurementPointData.value = row
|
||||
|
||||
try {
|
||||
const response = await getTransientEventDetail(row.eventId)
|
||||
detailData.value = response.data
|
||||
measurementPointData.value = response.data
|
||||
} finally {
|
||||
detailLoading.value = false
|
||||
measurementPointLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleViewWaveform = (row: EventList.TransientEventRecord) => {
|
||||
if (!row.wavePath) {
|
||||
ElMessage.warning('缺少波形文件路径,无法查看波形')
|
||||
return
|
||||
}
|
||||
|
||||
router.push({
|
||||
path: '/tools/waveform',
|
||||
query: {
|
||||
eventId: row.eventId,
|
||||
wavePath: row.wavePath
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleSupplementWaveform = (_row: EventList.TransientEventRecord) => {
|
||||
// 波形补招需要后端补招接口,当前先保留操作入口避免误触发未知流程。
|
||||
ElMessage.warning('暂无波形补招接口,无法发起补招')
|
||||
}
|
||||
|
||||
const handleExport = () => {
|
||||
const searchParam = (proTable.value?.searchParam || {}) as EventSearchParams
|
||||
useDownloadWithServerFileName(exportTransientEvents, '暂态事件列表', buildEventQueryParams(searchParam), false)
|
||||
@@ -306,4 +344,31 @@ const handleExport = () => {
|
||||
.event-list-page :deep(.el-descriptions__cell) {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.event-list-page :deep(.event-time-search) {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.event-list-page :deep(.event-time-search__unit) {
|
||||
width: 56px;
|
||||
flex: 0 0 56px;
|
||||
}
|
||||
|
||||
.event-list-page :deep(.event-time-search__picker) {
|
||||
width: 112px;
|
||||
flex: 0 0 112px;
|
||||
}
|
||||
|
||||
.event-list-page :deep(.event-time-search__button) {
|
||||
width: 28px;
|
||||
flex: 0 0 28px;
|
||||
padding: 8px 6px;
|
||||
}
|
||||
|
||||
.event-list-page :deep(.event-file-flag-search) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
30
frontend/src/views/event/eventList/utils/detailItems.ts
Normal file
30
frontend/src/views/event/eventList/utils/detailItems.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { EventList } from '@/api/event/eventList/interface'
|
||||
import { formatEventOccurrenceTime } from '../eventTimeRange'
|
||||
import { resolveEventDescription } from './display'
|
||||
import { resolveDealFlagText, resolveFileFlagText } from './status'
|
||||
|
||||
export type DetailItem = {
|
||||
label: string
|
||||
prop: keyof EventList.TransientEventRecord
|
||||
formatter?: (value: unknown, row?: EventList.TransientEventRecord | null) => string
|
||||
}
|
||||
|
||||
export const detailItems: DetailItem[] = [
|
||||
{ label: '事件 ID', prop: 'eventId' },
|
||||
{ label: '监测点 ID', prop: 'measurementPointId' },
|
||||
{ label: '事件类型', prop: 'eventType' },
|
||||
{ label: '工程名称', prop: 'engineeringName' },
|
||||
{ label: '项目名称', prop: 'projectName' },
|
||||
{ label: '设备名称', prop: 'equipmentName' },
|
||||
{ label: '监测点名称', prop: 'lineName' },
|
||||
{ label: '发生时刻', prop: 'startTime', formatter: formatEventOccurrenceTime },
|
||||
{ label: '事件描述', prop: 'eventDescribe', formatter: (_value, row) => resolveEventDescription(row) },
|
||||
{ label: '事件发生位置', prop: 'sagsource' },
|
||||
{ label: '相别', prop: 'phase' },
|
||||
{ label: '持续时间(s)', prop: 'duration' },
|
||||
{ label: '暂降/暂升幅值(%)', prop: 'featureAmplitude' },
|
||||
{ label: '波形文件路径', prop: 'wavePath' },
|
||||
{ label: '波形文件状态', prop: 'fileFlag', formatter: resolveFileFlagText },
|
||||
{ label: '处理状态', prop: 'dealFlag', formatter: resolveDealFlagText },
|
||||
{ label: '创建时间', prop: 'createTime' }
|
||||
]
|
||||
24
frontend/src/views/event/eventList/utils/display.ts
Normal file
24
frontend/src/views/event/eventList/utils/display.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { EventList } from '@/api/event/eventList/interface'
|
||||
|
||||
type EventRecordLike = Partial<EventList.TransientEventRecord> | null | undefined
|
||||
|
||||
const resolveOptionalText = (value: unknown) => {
|
||||
if (value === null || value === undefined) return ''
|
||||
return String(value).trim()
|
||||
}
|
||||
|
||||
export const resolveEventDescription = (row: EventRecordLike) => {
|
||||
if (!row) return '--'
|
||||
|
||||
const record = row as Record<string, unknown>
|
||||
const description =
|
||||
resolveOptionalText(record.eventDescribe) ||
|
||||
resolveOptionalText(record.eventDescription) ||
|
||||
resolveOptionalText(record.eventDesc) ||
|
||||
resolveOptionalText(record.description) ||
|
||||
resolveOptionalText(record.describe) ||
|
||||
resolveOptionalText(record.remark) ||
|
||||
resolveOptionalText(record.eventType)
|
||||
|
||||
return description || '--'
|
||||
}
|
||||
44
frontend/src/views/event/eventList/utils/queryParams.ts
Normal file
44
frontend/src/views/event/eventList/utils/queryParams.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { EventList } from '@/api/event/eventList/interface'
|
||||
|
||||
export type EventSearchParams = EventList.TransientPageParams & {
|
||||
startTimeRange?: string[]
|
||||
ledgerKeyword?: string
|
||||
}
|
||||
|
||||
const resolveOptionalText = (value: unknown) => {
|
||||
if (value === null || value === undefined) return undefined
|
||||
const text = String(value).trim()
|
||||
return text || undefined
|
||||
}
|
||||
|
||||
const resolveOptionalNumber = (value: unknown) => {
|
||||
if (value === null || value === undefined || value === '') return undefined
|
||||
const parsed = Number(value)
|
||||
return Number.isFinite(parsed) ? parsed : undefined
|
||||
}
|
||||
|
||||
const pruneEmptyParams = (params: EventList.TransientPageParams) => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(params).filter(([, value]) => value !== undefined && value !== '')
|
||||
) as EventList.TransientPageParams
|
||||
}
|
||||
|
||||
export const buildEventQueryParams = (params: EventSearchParams = {}) => {
|
||||
const timeRange = Array.isArray(params.startTimeRange) ? params.startTimeRange : []
|
||||
const ledgerKeyword = resolveOptionalText(params.ledgerKeyword)
|
||||
|
||||
return pruneEmptyParams({
|
||||
pageNum: params.pageNum,
|
||||
pageSize: params.pageSize,
|
||||
startTimeStart: resolveOptionalText(timeRange[0]),
|
||||
startTimeEnd: resolveOptionalText(timeRange[1]),
|
||||
eventType: resolveOptionalText(params.eventType),
|
||||
phase: resolveOptionalText(params.phase),
|
||||
fileFlag: resolveOptionalNumber(params.fileFlag),
|
||||
dealFlag: resolveOptionalNumber(params.dealFlag),
|
||||
engineeringName: ledgerKeyword,
|
||||
projectName: ledgerKeyword,
|
||||
equipmentName: ledgerKeyword,
|
||||
lineName: ledgerKeyword
|
||||
})
|
||||
}
|
||||
40
frontend/src/views/event/eventList/utils/status.ts
Normal file
40
frontend/src/views/event/eventList/utils/status.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
export const phaseOptions = [
|
||||
{ label: 'A 相', value: 'A' },
|
||||
{ label: 'B 相', value: 'B' },
|
||||
{ label: 'C 相', value: 'C' },
|
||||
{ label: '三相', value: 'ABC' }
|
||||
]
|
||||
|
||||
export const fileFlagOptions = [
|
||||
{ label: '未招', value: 0 },
|
||||
{ label: '已招', value: 1 }
|
||||
]
|
||||
|
||||
export const dealFlagOptions = [
|
||||
{ label: '未处理', value: 0 },
|
||||
{ label: '已处理', value: 1 },
|
||||
{ label: '已处理无结果', value: 2 },
|
||||
{ label: '计算失败', value: 3 }
|
||||
]
|
||||
|
||||
export function resolveFileFlagText(value: unknown) {
|
||||
if (Number(value) === 0) return '未招'
|
||||
if (Number(value) === 1) return '已招'
|
||||
return '--'
|
||||
}
|
||||
|
||||
export function resolveDealFlagText(value: unknown) {
|
||||
if (Number(value) === 0) return '未处理'
|
||||
if (Number(value) === 1) return '已处理'
|
||||
if (Number(value) === 2) return '已处理无结果'
|
||||
if (Number(value) === 3) return '计算失败'
|
||||
return '--'
|
||||
}
|
||||
|
||||
export const resolveDealFlagTag = (value: unknown) => {
|
||||
if (Number(value) === 0) return 'info'
|
||||
if (Number(value) === 1) return 'success'
|
||||
if (Number(value) === 2) return 'warning'
|
||||
if (Number(value) === 3) return 'danger'
|
||||
return 'info'
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -92,6 +92,15 @@ import {
|
||||
validateTargetList,
|
||||
validateTargetNotifications
|
||||
} from './utils/form'
|
||||
import {
|
||||
clonePolicy,
|
||||
cloneTarget,
|
||||
normalizeJobRows,
|
||||
normalizeTargetPayload,
|
||||
removeTargetItem,
|
||||
resolveJobId,
|
||||
upsertTargetItem
|
||||
} from './utils/pageState'
|
||||
|
||||
defineOptions({
|
||||
name: 'DiskMonitorPage'
|
||||
@@ -121,30 +130,6 @@ const loading = reactive({
|
||||
})
|
||||
const formBusy = computed(() => loading.init || loading.save || loading.run)
|
||||
|
||||
const resolveJobId = (job: DiskMonitor.JobListItem) => {
|
||||
return job.id ?? job.jobId ?? 0
|
||||
}
|
||||
|
||||
const getTimeValue = (value?: string | null) => {
|
||||
if (!value) return 0
|
||||
const timestamp = new Date(value).getTime()
|
||||
return Number.isNaN(timestamp) ? 0 : timestamp
|
||||
}
|
||||
|
||||
const clonePolicy = (policy: DiskMonitor.PolicyItem): DiskMonitor.PolicyItem => ({
|
||||
...createDefaultPolicy(),
|
||||
...policy
|
||||
})
|
||||
|
||||
const cloneTarget = (target: DiskMonitor.TargetItem): DiskMonitor.TargetItem => {
|
||||
const normalized = normalizeTargetItem(target)
|
||||
return {
|
||||
...normalized,
|
||||
notifyPathList: normalized.notifyPathList.map(item => ({ ...item })),
|
||||
notifyHttpList: normalized.notifyHttpList.map(item => ({ ...item }))
|
||||
}
|
||||
}
|
||||
|
||||
const openPolicyDialog = () => {
|
||||
// 打开弹窗时复制当前策略,避免取消时污染页面已加载状态。
|
||||
editingPolicy.value = clonePolicy(policyForm.value)
|
||||
@@ -168,10 +153,7 @@ const confirmTarget = () => {
|
||||
if (formBusy.value) return
|
||||
|
||||
const normalizedDriveLetter = editingTarget.value.driveLetter.trim().toUpperCase()
|
||||
const payload: DiskMonitor.TargetItem = {
|
||||
...normalizeTargetItem(editingTarget.value),
|
||||
driveLetter: normalizedDriveLetter
|
||||
}
|
||||
const payload = normalizeTargetPayload(editingTarget.value)
|
||||
const exists = targetList.value
|
||||
.filter((_, index) => index !== editingTargetIndex.value)
|
||||
.map(item => item.driveLetter.trim().toUpperCase())
|
||||
@@ -188,15 +170,8 @@ const confirmTarget = () => {
|
||||
return
|
||||
}
|
||||
|
||||
const nextTargetList = targetList.value.map(item => cloneTarget(item))
|
||||
if (editingTargetIndex.value >= 0) {
|
||||
nextTargetList.splice(editingTargetIndex.value, 1, payload)
|
||||
} else {
|
||||
nextTargetList.push(payload)
|
||||
}
|
||||
|
||||
// 目标配置先保留在页面本地状态里,统一通过“全局策略”弹窗中的保存入口提交。
|
||||
targetList.value = nextTargetList.map(item => cloneTarget(item))
|
||||
targetList.value = upsertTargetItem(targetList.value, payload, editingTargetIndex.value)
|
||||
targetDialogVisible.value = false
|
||||
}
|
||||
|
||||
@@ -204,9 +179,7 @@ const removeTarget = (index: number) => {
|
||||
if (formBusy.value) return
|
||||
|
||||
// 删除目标时仅更新本地暂存列表,避免样式调整顺带改成“操作即落库”。
|
||||
targetList.value = targetList.value
|
||||
.filter((_, currentIndex) => currentIndex !== index)
|
||||
.map(item => cloneTarget(item))
|
||||
targetList.value = removeTargetItem(targetList.value, index)
|
||||
}
|
||||
|
||||
const loadPolicyDetail = async () => {
|
||||
@@ -232,13 +205,7 @@ const loadJobList = async () => {
|
||||
sortField: 'startedAt',
|
||||
sortOrder: 'desc'
|
||||
})
|
||||
const records = (response.data?.records || []).map(item => ({
|
||||
...item,
|
||||
id: resolveJobId(item)
|
||||
}))
|
||||
const sortedRecords = [...records].sort((a, b) => {
|
||||
return getTimeValue(b.startedAt) - getTimeValue(a.startedAt)
|
||||
})
|
||||
const sortedRecords = normalizeJobRows(response.data?.records || [])
|
||||
jobList.value = sortedRecords.slice(0, 10)
|
||||
latestJob.value = jobList.value[0] || null
|
||||
} finally {
|
||||
@@ -303,10 +270,7 @@ const persistPolicyAndTargets = async (
|
||||
}
|
||||
|
||||
const normalizedPolicy = clonePolicy(policy)
|
||||
const normalizedTargets = targets.map(item => ({
|
||||
...normalizeTargetItem(item),
|
||||
driveLetter: item.driveLetter.trim().toUpperCase()
|
||||
}))
|
||||
const normalizedTargets = targets.map(item => normalizeTargetPayload(item))
|
||||
const targetListErrorMessage = validateTargetList(normalizedTargets)
|
||||
if (targetListErrorMessage) {
|
||||
ElMessage.warning(targetListErrorMessage)
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { DiskMonitor } from '@/api/system/diskMonitor/interface'
|
||||
import { createDefaultPolicy, normalizeTargetItem } from './form'
|
||||
|
||||
export const resolveJobId = (job: DiskMonitor.JobListItem) => {
|
||||
return job.id ?? job.jobId ?? 0
|
||||
}
|
||||
|
||||
const getTimeValue = (value?: string | null) => {
|
||||
if (!value) return 0
|
||||
const timestamp = new Date(value).getTime()
|
||||
return Number.isNaN(timestamp) ? 0 : timestamp
|
||||
}
|
||||
|
||||
export const normalizeJobRows = (rows: DiskMonitor.JobListItem[]) => {
|
||||
const records = rows.map(item => ({
|
||||
...item,
|
||||
id: resolveJobId(item)
|
||||
}))
|
||||
|
||||
return [...records].sort((a, b) => getTimeValue(b.startedAt) - getTimeValue(a.startedAt))
|
||||
}
|
||||
|
||||
export const clonePolicy = (policy: DiskMonitor.PolicyItem): DiskMonitor.PolicyItem => ({
|
||||
...createDefaultPolicy(),
|
||||
...policy
|
||||
})
|
||||
|
||||
export const cloneTarget = (target: DiskMonitor.TargetItem): DiskMonitor.TargetItem => {
|
||||
const normalized = normalizeTargetItem(target)
|
||||
return {
|
||||
...normalized,
|
||||
notifyPathList: normalized.notifyPathList.map(item => ({ ...item })),
|
||||
notifyHttpList: normalized.notifyHttpList.map(item => ({ ...item }))
|
||||
}
|
||||
}
|
||||
|
||||
export const normalizeTargetPayload = (target: DiskMonitor.TargetItem): DiskMonitor.TargetItem => ({
|
||||
...normalizeTargetItem(target),
|
||||
driveLetter: target.driveLetter.trim().toUpperCase()
|
||||
})
|
||||
|
||||
export const upsertTargetItem = (
|
||||
targets: DiskMonitor.TargetItem[],
|
||||
target: DiskMonitor.TargetItem,
|
||||
editingIndex: number
|
||||
) => {
|
||||
const nextTargetList = targets.map(item => cloneTarget(item))
|
||||
|
||||
if (editingIndex >= 0) {
|
||||
nextTargetList.splice(editingIndex, 1, target)
|
||||
} else {
|
||||
nextTargetList.push(target)
|
||||
}
|
||||
|
||||
return nextTargetList.map(item => cloneTarget(item))
|
||||
}
|
||||
|
||||
export const removeTargetItem = (targets: DiskMonitor.TargetItem[], index: number) => {
|
||||
return targets.filter((_, currentIndex) => currentIndex !== index).map(item => cloneTarget(item))
|
||||
}
|
||||
@@ -42,6 +42,8 @@ import type { AddData } from '@/api/tools/addData/interface'
|
||||
import AddDataTaskPanel from './components/AddDataTaskPanel.vue'
|
||||
import AddDataTaskStatusCard from './components/AddDataTaskStatusCard.vue'
|
||||
import AddDataTemplateTable from './components/AddDataTemplateTable.vue'
|
||||
import { normalizePreview, normalizeTaskStatus, normalizeTemplateItem, resolveText } from './utils/normalize'
|
||||
import { buildPayloadSignature, buildTaskPayload as buildTaskRequestPayload } from './utils/taskPayload'
|
||||
|
||||
defineOptions({
|
||||
name: 'AddDataView'
|
||||
@@ -82,125 +84,13 @@ const handleTaskFormChange = (nextForm: AddData.TaskFormModel) => {
|
||||
taskForm.intervalMinutes = nextForm.intervalMinutes
|
||||
}
|
||||
|
||||
const normalizeLineIds = (lineIds: string[]) => {
|
||||
return Array.from(
|
||||
new Set(
|
||||
(lineIds || [])
|
||||
.map(item => item?.trim())
|
||||
.filter((item): item is string => Boolean(item))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const parseLineIds = (lineIds: string[]) => {
|
||||
return normalizeLineIds(lineIds)
|
||||
}
|
||||
|
||||
const resetPreview = () => {
|
||||
previewSummary.value = null
|
||||
previewSignature.value = ''
|
||||
}
|
||||
|
||||
const resolveNumber = (...values: unknown[]) => {
|
||||
for (const value of values) {
|
||||
if (value === null || value === undefined || value === '') continue
|
||||
const parsed = Number(value)
|
||||
if (Number.isFinite(parsed)) {
|
||||
return parsed
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
const resolveText = (...values: unknown[]) => {
|
||||
for (const value of values) {
|
||||
if (value === null || value === undefined) continue
|
||||
const text = String(value).trim()
|
||||
if (text) {
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
const resolveDisplayRule = (value: unknown, fallback = '--') => {
|
||||
if (typeof value === 'boolean') {
|
||||
return value ? '显示' : '不显示'
|
||||
}
|
||||
|
||||
if (typeof value === 'number') {
|
||||
return value ? '显示' : '不显示'
|
||||
}
|
||||
|
||||
const text = resolveText(value)
|
||||
return text || fallback
|
||||
}
|
||||
|
||||
const normalizePreview = (data?: AddData.PreviewResponse | null): AddData.NormalizedPreview => {
|
||||
const tableStats = (Array.isArray(data?.tableStats) ? data.tableStats : [])
|
||||
.map(item => ({
|
||||
tableName: resolveText(item.tableName) || '--',
|
||||
timePointCount: resolveNumber(item.timePointCount),
|
||||
phaseCount: resolveNumber(item.phaseCount),
|
||||
rowCount: resolveNumber(item.rowCount)
|
||||
}))
|
||||
.sort((left, right) => right.rowCount - left.rowCount)
|
||||
|
||||
return {
|
||||
lineCount: resolveNumber(data?.lineCount),
|
||||
intervalMinutes: resolveNumber(data?.intervalMinutes),
|
||||
totalRowCount: resolveNumber(data?.totalRowCount),
|
||||
tableStats
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeTaskStatus = (data?: AddData.TaskStatusResponse | null): AddData.NormalizedTaskStatus => {
|
||||
return {
|
||||
taskId: resolveText(data?.taskId),
|
||||
status: (resolveText(data?.status) || 'WAITING') as AddData.TaskStatus,
|
||||
currentTableName: resolveText(data?.currentTableName),
|
||||
currentBatchInfo: resolveText(data?.currentBatchInfo),
|
||||
insertedCount: resolveNumber(data?.insertedCount),
|
||||
skippedCount: resolveNumber(data?.skippedCount),
|
||||
failedCount: resolveNumber(data?.failedCount),
|
||||
failureReason: resolveText(data?.failureReason),
|
||||
hourlyTimeResults: Array.isArray(data?.hourlyTimeResults) ? data.hourlyTimeResults.filter(Boolean) : [],
|
||||
startTime: resolveText(data?.startTime),
|
||||
endTime: resolveText(data?.endTime)
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeTemplateItem = (item: AddData.TemplateItem): AddData.NormalizedTemplateItem => {
|
||||
const decimalScale = resolveText(item.decimalScale)
|
||||
|
||||
return {
|
||||
parameterName: resolveText(item.parameterName) || '--',
|
||||
tableName: resolveText(item.tableName) || '--',
|
||||
phaseDisplay: resolveText(item.phaseDisplay) || '--',
|
||||
phaseCodesText: Array.isArray(item.phaseCodes) && item.phaseCodes.length ? item.phaseCodes.join(' / ') : '--',
|
||||
displayText: resolveDisplayRule(item.display),
|
||||
showQualifiedText: resolveDisplayRule(item.showQualified),
|
||||
maxValueRule: resolveText(item.maxValueRule) || '--',
|
||||
minValueRule: resolveText(item.minValueRule) || '--',
|
||||
averageValueRule: resolveText(item.averageValueRule) || '--',
|
||||
cp95ValueRule: resolveText(item.cp95ValueRule) || '--',
|
||||
decimalScaleText: decimalScale ? `${decimalScale} 位小数` : '--'
|
||||
}
|
||||
}
|
||||
|
||||
const buildTaskPayload = (): AddData.TaskRequestParams => {
|
||||
return {
|
||||
lineIds: parseLineIds(taskForm.lineIds),
|
||||
startTime: taskForm.startTime,
|
||||
endTime: taskForm.endTime,
|
||||
intervalMinutes: taskForm.intervalMinutes
|
||||
}
|
||||
}
|
||||
|
||||
const buildPayloadSignature = (payload: AddData.TaskRequestParams) => {
|
||||
return JSON.stringify(payload)
|
||||
return buildTaskRequestPayload(taskForm)
|
||||
}
|
||||
|
||||
const buildPreviewDependencySignature = () => {
|
||||
|
||||
90
frontend/src/views/tools/addData/utils/normalize.ts
Normal file
90
frontend/src/views/tools/addData/utils/normalize.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import type { AddData } from '@/api/tools/addData/interface'
|
||||
|
||||
export const resolveNumber = (...values: unknown[]) => {
|
||||
for (const value of values) {
|
||||
if (value === null || value === undefined || value === '') continue
|
||||
const parsed = Number(value)
|
||||
if (Number.isFinite(parsed)) {
|
||||
return parsed
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
export const resolveText = (...values: unknown[]) => {
|
||||
for (const value of values) {
|
||||
if (value === null || value === undefined) continue
|
||||
const text = String(value).trim()
|
||||
if (text) {
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
const resolveDisplayRule = (value: unknown, fallback = '--') => {
|
||||
if (typeof value === 'boolean') {
|
||||
return value ? '显示' : '不显示'
|
||||
}
|
||||
|
||||
if (typeof value === 'number') {
|
||||
return value ? '显示' : '不显示'
|
||||
}
|
||||
|
||||
const text = resolveText(value)
|
||||
return text || fallback
|
||||
}
|
||||
|
||||
export const normalizePreview = (data?: AddData.PreviewResponse | null): AddData.NormalizedPreview => {
|
||||
const tableStats = (Array.isArray(data?.tableStats) ? data.tableStats : [])
|
||||
.map(item => ({
|
||||
tableName: resolveText(item.tableName) || '--',
|
||||
timePointCount: resolveNumber(item.timePointCount),
|
||||
phaseCount: resolveNumber(item.phaseCount),
|
||||
rowCount: resolveNumber(item.rowCount)
|
||||
}))
|
||||
.sort((left, right) => right.rowCount - left.rowCount)
|
||||
|
||||
return {
|
||||
lineCount: resolveNumber(data?.lineCount),
|
||||
intervalMinutes: resolveNumber(data?.intervalMinutes),
|
||||
totalRowCount: resolveNumber(data?.totalRowCount),
|
||||
tableStats
|
||||
}
|
||||
}
|
||||
|
||||
export const normalizeTaskStatus = (data?: AddData.TaskStatusResponse | null): AddData.NormalizedTaskStatus => {
|
||||
return {
|
||||
taskId: resolveText(data?.taskId),
|
||||
status: (resolveText(data?.status) || 'WAITING') as AddData.TaskStatus,
|
||||
currentTableName: resolveText(data?.currentTableName),
|
||||
currentBatchInfo: resolveText(data?.currentBatchInfo),
|
||||
insertedCount: resolveNumber(data?.insertedCount),
|
||||
skippedCount: resolveNumber(data?.skippedCount),
|
||||
failedCount: resolveNumber(data?.failedCount),
|
||||
failureReason: resolveText(data?.failureReason),
|
||||
hourlyTimeResults: Array.isArray(data?.hourlyTimeResults) ? data.hourlyTimeResults.filter(Boolean) : [],
|
||||
startTime: resolveText(data?.startTime),
|
||||
endTime: resolveText(data?.endTime)
|
||||
}
|
||||
}
|
||||
|
||||
export const normalizeTemplateItem = (item: AddData.TemplateItem): AddData.NormalizedTemplateItem => {
|
||||
const decimalScale = resolveText(item.decimalScale)
|
||||
|
||||
return {
|
||||
parameterName: resolveText(item.parameterName) || '--',
|
||||
tableName: resolveText(item.tableName) || '--',
|
||||
phaseDisplay: resolveText(item.phaseDisplay) || '--',
|
||||
phaseCodesText: Array.isArray(item.phaseCodes) && item.phaseCodes.length ? item.phaseCodes.join(' / ') : '--',
|
||||
displayText: resolveDisplayRule(item.display),
|
||||
showQualifiedText: resolveDisplayRule(item.showQualified),
|
||||
maxValueRule: resolveText(item.maxValueRule) || '--',
|
||||
minValueRule: resolveText(item.minValueRule) || '--',
|
||||
averageValueRule: resolveText(item.averageValueRule) || '--',
|
||||
cp95ValueRule: resolveText(item.cp95ValueRule) || '--',
|
||||
decimalScaleText: decimalScale ? `${decimalScale} 位小数` : '--'
|
||||
}
|
||||
}
|
||||
24
frontend/src/views/tools/addData/utils/taskPayload.ts
Normal file
24
frontend/src/views/tools/addData/utils/taskPayload.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { AddData } from '@/api/tools/addData/interface'
|
||||
|
||||
const normalizeLineIds = (lineIds: string[]) => {
|
||||
return Array.from(
|
||||
new Set(
|
||||
(lineIds || [])
|
||||
.map(item => item?.trim())
|
||||
.filter((item): item is string => Boolean(item))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export const buildTaskPayload = (form: AddData.TaskFormModel): AddData.TaskRequestParams => {
|
||||
return {
|
||||
lineIds: normalizeLineIds(form.lineIds),
|
||||
startTime: form.startTime,
|
||||
endTime: form.endTime,
|
||||
intervalMinutes: form.intervalMinutes
|
||||
}
|
||||
}
|
||||
|
||||
export const buildPayloadSignature = (payload: AddData.TaskRequestParams) => {
|
||||
return JSON.stringify(payload)
|
||||
}
|
||||
@@ -1,484 +0,0 @@
|
||||
# add-ledger API 调试文档
|
||||
|
||||
## 1. 基础信息
|
||||
|
||||
| 项 | 值 |
|
||||
|---|---|
|
||||
| 服务模块 | `tools/add-ledger` |
|
||||
| 默认本地地址 | `http://127.0.0.1:18192` |
|
||||
| 接口前缀 | `/addLedger` |
|
||||
| 数据格式 | `application/json;charset=UTF-8` |
|
||||
| 认证方式 | 走全局认证过滤器,除登录和 Swagger 外均需携带访问令牌 |
|
||||
|
||||
请求头:
|
||||
|
||||
```http
|
||||
Authorization: Bearer ${accessToken}
|
||||
Content-Type: application/json;charset=UTF-8
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `Authorization` 的真实 Header 名称和前缀来自公共组件 `SecurityConstants`,当前过滤器按 `SecurityConstants.AUTHORIZATION_KEY` 和 `SecurityConstants.AUTHORIZATION_PREFIX` 校验。
|
||||
- 以下响应示例只固定 `data` 结构;`code`、`message` 以 `HttpResultUtil` 和 `CommonResponseEnum.SUCCESS` 的运行时序列化结果为准。
|
||||
- 编辑项目、设备、测点时不支持搬迁父节点;如果请求中传父级 ID,后端仍以当前台账节点已有父级关系为准。
|
||||
- 删除父节点前端必须二次确认;后端收到删除请求后直接执行级联软删除。
|
||||
|
||||
统一成功响应结构示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "SUCCESS_CODE",
|
||||
"message": "SUCCESS_MESSAGE",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
## 2. 调试顺序建议
|
||||
|
||||
1. 调用登录接口获取 `accessToken`。
|
||||
2. 新增工程。
|
||||
3. 使用工程 ID 新增项目。
|
||||
4. 使用项目 ID 新增设备。
|
||||
5. 查询设备可用线路号。
|
||||
6. 使用设备 ID 和可用线路号新增测点。
|
||||
7. 调用台账树和详情接口核对层级与字段。
|
||||
8. 调用删除接口验证软删除和级联软删除。
|
||||
|
||||
## 3. 接口总览
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|---|---|---|
|
||||
| `GET` | `/addLedger/tree` | 查询台账树 |
|
||||
| `GET` | `/addLedger/detail` | 查询节点详情 |
|
||||
| `POST` | `/addLedger/engineering/save` | 新增或保存工程 |
|
||||
| `POST` | `/addLedger/project/save` | 新增或保存项目 |
|
||||
| `POST` | `/addLedger/equipment/save` | 新增或保存设备 |
|
||||
| `POST` | `/addLedger/line/save` | 新增或保存测点 |
|
||||
| `GET` | `/addLedger/line/availableLineNos` | 查询设备可用线路号 |
|
||||
| `DELETE` | `/addLedger/node` | 删除节点并级联软删除子节点 |
|
||||
|
||||
## 4. 查询台账树
|
||||
|
||||
### 4.1 请求
|
||||
|
||||
```http
|
||||
GET /addLedger/tree?keyword=测试
|
||||
```
|
||||
|
||||
Query 参数:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `keyword` | `String` | 否 | 按 `cs_ledger.Name` 模糊查询 |
|
||||
|
||||
curl:
|
||||
|
||||
```bash
|
||||
curl -X GET "http://127.0.0.1:18192/addLedger/tree?keyword=测试" \
|
||||
-H "Authorization: Bearer ${accessToken}"
|
||||
```
|
||||
|
||||
### 4.2 响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "SUCCESS_CODE",
|
||||
"message": "SUCCESS_MESSAGE",
|
||||
"data": [
|
||||
{
|
||||
"id": "engineeringId",
|
||||
"parentId": "0",
|
||||
"parentIds": "0",
|
||||
"name": "测试工程",
|
||||
"level": 0,
|
||||
"sort": 0,
|
||||
"children": [
|
||||
{
|
||||
"id": "projectId",
|
||||
"parentId": "engineeringId",
|
||||
"parentIds": "0,engineeringId",
|
||||
"name": "测试项目",
|
||||
"level": 1,
|
||||
"sort": 0,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 5. 查询节点详情
|
||||
|
||||
### 5.1 请求
|
||||
|
||||
```http
|
||||
GET /addLedger/detail?id=engineeringId&level=0
|
||||
```
|
||||
|
||||
Query 参数:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `id` | `String` | 是 | 节点 ID,与业务表主键一致 |
|
||||
| `level` | `Integer` | 是 | `0` 工程,`1` 项目,`2` 设备,`3` 测点 |
|
||||
|
||||
curl:
|
||||
|
||||
```bash
|
||||
curl -X GET "http://127.0.0.1:18192/addLedger/detail?id=engineeringId&level=0" \
|
||||
-H "Authorization: Bearer ${accessToken}"
|
||||
```
|
||||
|
||||
### 5.2 响应
|
||||
|
||||
工程详情:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "SUCCESS_CODE",
|
||||
"message": "SUCCESS_MESSAGE",
|
||||
"data": {
|
||||
"id": "engineeringId",
|
||||
"level": 0,
|
||||
"parentId": "0",
|
||||
"parentIds": "0",
|
||||
"name": "测试工程",
|
||||
"sort": 0,
|
||||
"province": "320000000000",
|
||||
"city": "320100000000",
|
||||
"description": "工程描述"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 6. 新增或保存工程
|
||||
|
||||
### 6.1 请求
|
||||
|
||||
```http
|
||||
POST /addLedger/engineering/save
|
||||
```
|
||||
|
||||
Body 参数:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `id` | `String` | 否 | 为空时新增;非空时编辑 |
|
||||
| `name` | `String` | 是 | 工程名称,同步为树节点名称 |
|
||||
| `province` | `String` | 否 | 省 |
|
||||
| `city` | `String` | 否 | 市 |
|
||||
| `description` | `String` | 否 | 描述 |
|
||||
|
||||
新增示例:
|
||||
|
||||
```bash
|
||||
curl -X POST "http://127.0.0.1:18192/addLedger/engineering/save" \
|
||||
-H "Authorization: Bearer ${accessToken}" \
|
||||
-H "Content-Type: application/json;charset=UTF-8" \
|
||||
-d "{\"name\":\"测试工程\",\"province\":\"320000000000\",\"city\":\"320100000000\",\"description\":\"工程描述\"}"
|
||||
```
|
||||
|
||||
编辑示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "engineeringId",
|
||||
"name": "测试工程-修改",
|
||||
"province": "320000000000",
|
||||
"city": "320100000000",
|
||||
"description": "工程描述修改"
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "SUCCESS_CODE",
|
||||
"message": "SUCCESS_MESSAGE",
|
||||
"data": {
|
||||
"id": "engineeringId",
|
||||
"level": 0,
|
||||
"parentId": "0",
|
||||
"parentIds": "0",
|
||||
"name": "测试工程",
|
||||
"province": "320000000000",
|
||||
"city": "320100000000",
|
||||
"description": "工程描述"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 7. 新增或保存项目
|
||||
|
||||
### 7.1 请求
|
||||
|
||||
```http
|
||||
POST /addLedger/project/save
|
||||
```
|
||||
|
||||
Body 参数:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `id` | `String` | 否 | 为空时新增;非空时编辑 |
|
||||
| `engineeringId` | `String` | 新增必填 | 父级工程 ID |
|
||||
| `name` | `String` | 是 | 项目名称,同步为树节点名称 |
|
||||
| `area` | `String` | 否 | 相对位置 |
|
||||
| `description` | `String` | 否 | 项目描述 |
|
||||
|
||||
新增示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"engineeringId": "engineeringId",
|
||||
"name": "测试项目",
|
||||
"area": "灿能园区",
|
||||
"description": "项目描述"
|
||||
}
|
||||
```
|
||||
|
||||
curl:
|
||||
|
||||
```bash
|
||||
curl -X POST "http://127.0.0.1:18192/addLedger/project/save" \
|
||||
-H "Authorization: Bearer ${accessToken}" \
|
||||
-H "Content-Type: application/json;charset=UTF-8" \
|
||||
-d "{\"engineeringId\":\"engineeringId\",\"name\":\"测试项目\",\"area\":\"灿能园区\",\"description\":\"项目描述\"}"
|
||||
```
|
||||
|
||||
## 8. 新增或保存设备
|
||||
|
||||
### 8.1 请求
|
||||
|
||||
```http
|
||||
POST /addLedger/equipment/save
|
||||
```
|
||||
|
||||
Body 参数:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `id` | `String` | 否 | 为空时新增;非空时编辑 |
|
||||
| `projectId` | `String` | 新增必填 | 父级项目 ID |
|
||||
| `name` | `String` | 是 | 装置名称,同步为树节点名称 |
|
||||
| `ndid` | `String` | 是 | 网络设备 ID |
|
||||
| `mac` | `String` | 是 | 装置 MAC 地址 |
|
||||
| `devType` | `String` | 否 | 装置类型,保存字典数据 ID |
|
||||
| `devModel` | `String` | 是 | 装置型号,保存字典数据 ID |
|
||||
| `devAccessMethod` | `String` | 否 | 装置接入方式 |
|
||||
| `nodeId` | `String` | 否 | 前置服务器 IP |
|
||||
| `nodeProcess` | `Integer` | 否 | 前置进程号 |
|
||||
| `upgrade` | `Integer` | 否 | 是否支持升级,`0` 否,`1` 是,默认 `0` |
|
||||
|
||||
新增示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"projectId": "projectId",
|
||||
"name": "测试设备",
|
||||
"ndid": "00B78D000001",
|
||||
"mac": "00:B7:8D:00:00:01",
|
||||
"devType": "dictDataIdForDeviceType",
|
||||
"devModel": "dictDataIdForDeviceModel",
|
||||
"devAccessMethod": "MQTT",
|
||||
"nodeId": "127.0.0.1",
|
||||
"nodeProcess": 1,
|
||||
"upgrade": 0
|
||||
}
|
||||
```
|
||||
|
||||
curl:
|
||||
|
||||
```bash
|
||||
curl -X POST "http://127.0.0.1:18192/addLedger/equipment/save" \
|
||||
-H "Authorization: Bearer ${accessToken}" \
|
||||
-H "Content-Type: application/json;charset=UTF-8" \
|
||||
-d "{\"projectId\":\"projectId\",\"name\":\"测试设备\",\"ndid\":\"00B78D000001\",\"mac\":\"00:B7:8D:00:00:01\",\"devModel\":\"dictDataIdForDeviceModel\",\"devAccessMethod\":\"MQTT\",\"upgrade\":0}"
|
||||
```
|
||||
|
||||
### 8.2 默认值
|
||||
|
||||
新增设备时后端默认填充:
|
||||
|
||||
| 字段 | 默认值 |
|
||||
|---|---|
|
||||
| `run_status` | `1` |
|
||||
| `status` | `1` |
|
||||
| `process` | `4` |
|
||||
| `usage_status` | `1` |
|
||||
| `sort` | `0` |
|
||||
| `upgrade` | 未传时为 `0` |
|
||||
|
||||
## 9. 查询设备可用线路号
|
||||
|
||||
### 9.1 请求
|
||||
|
||||
```http
|
||||
GET /addLedger/line/availableLineNos?deviceId=equipmentId&lineId=lineId
|
||||
```
|
||||
|
||||
Query 参数:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `deviceId` | `String` | 是 | 设备 ID |
|
||||
| `lineId` | `String` | 否 | 编辑测点时传入当前测点 ID |
|
||||
|
||||
curl:
|
||||
|
||||
```bash
|
||||
curl -X GET "http://127.0.0.1:18192/addLedger/line/availableLineNos?deviceId=equipmentId" \
|
||||
-H "Authorization: Bearer ${accessToken}"
|
||||
```
|
||||
|
||||
### 9.2 响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "SUCCESS_CODE",
|
||||
"message": "SUCCESS_MESSAGE",
|
||||
"data": [1, 2, 3, 4, 5]
|
||||
}
|
||||
```
|
||||
|
||||
## 10. 新增或保存测点
|
||||
|
||||
### 10.1 请求
|
||||
|
||||
```http
|
||||
POST /addLedger/line/save
|
||||
```
|
||||
|
||||
Body 参数:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `lineId` | `String` | 否 | 为空时新增;非空时编辑 |
|
||||
| `deviceId` | `String` | 新增必填 | 父级设备 ID |
|
||||
| `name` | `String` | 是 | 监测点名,同步为树节点名称 |
|
||||
| `lineNo` | `Integer` | 是 | 设备线路,范围 `1-20` |
|
||||
| `conType` | `Integer` | 是 | 接线方式,`0` 星型,`1` 角型,`2` V 型 |
|
||||
| `volGrade` | `Decimal` | 是 | 电压等级,仅支持 `0.38`、`10`、`35`、`110`、`220`、`500` |
|
||||
| `position` | `String` | 否 | 安装位置 |
|
||||
| `ctRatio` | `Decimal` | 是 | CT 一次额定值,非负数 |
|
||||
| `ct2Ratio` | `Decimal` | 是 | CT 二次额定值,非负数 |
|
||||
| `ptRatio` | `Decimal` | 是 | PT 一次额定值,非负数 |
|
||||
| `pt2Ratio` | `Decimal` | 是 | PT 二次额定值,非负数 |
|
||||
| `shortCircuitCapacity` | `Decimal` | 否 | 最小短路容量,非负数 |
|
||||
| `basicCapacity` | `Decimal` | 否 | 基准短路容量,非负数 |
|
||||
| `protocolCapacity` | `Decimal` | 否 | 用户协议容量,非负数 |
|
||||
| `devCapacity` | `Decimal` | 否 | 供电设备容量,非负数 |
|
||||
| `monitorObj` | `String` | 否 | 监测对象类型 |
|
||||
| `isGovern` | `Integer` | 否 | 是否治理,默认 `0` |
|
||||
| `monitorUser` | `String` | 否 | 敏感用户 ID |
|
||||
| `isImportant` | `Integer` | 否 | 是否主要监测点,默认 `0` |
|
||||
|
||||
新增示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"deviceId": "equipmentId",
|
||||
"name": "测试测点1",
|
||||
"lineNo": 1,
|
||||
"conType": 0,
|
||||
"volGrade": 10,
|
||||
"position": "positionDictDataId",
|
||||
"ctRatio": 200,
|
||||
"ct2Ratio": 1,
|
||||
"ptRatio": 10,
|
||||
"pt2Ratio": 0.1,
|
||||
"shortCircuitCapacity": 10,
|
||||
"basicCapacity": 10,
|
||||
"protocolCapacity": 10,
|
||||
"devCapacity": 10,
|
||||
"monitorObj": "monitorObjDictDataId",
|
||||
"isGovern": 0,
|
||||
"monitorUser": "",
|
||||
"isImportant": 0
|
||||
}
|
||||
```
|
||||
|
||||
curl:
|
||||
|
||||
```bash
|
||||
curl -X POST "http://127.0.0.1:18192/addLedger/line/save" \
|
||||
-H "Authorization: Bearer ${accessToken}" \
|
||||
-H "Content-Type: application/json;charset=UTF-8" \
|
||||
-d "{\"deviceId\":\"equipmentId\",\"name\":\"测试测点1\",\"lineNo\":1,\"conType\":0,\"volGrade\":10,\"ctRatio\":200,\"ct2Ratio\":1,\"ptRatio\":10,\"pt2Ratio\":0.1,\"isGovern\":0,\"isImportant\":0}"
|
||||
```
|
||||
|
||||
### 10.2 校验规则
|
||||
|
||||
- `lineNo` 必须在 `1-20` 之间。
|
||||
- 同设备下正常测点的 `lineNo` 不可重复。
|
||||
- `conType` 只能是 `0`、`1`、`2`。
|
||||
- `volGrade` 只能是 `0.38`、`10`、`35`、`110`、`220`、`500`。
|
||||
- CT/PT 字段必填且不能为负数。
|
||||
- 容量字段传入时不能为负数。
|
||||
|
||||
## 11. 删除节点
|
||||
|
||||
### 11.1 请求
|
||||
|
||||
```http
|
||||
DELETE /addLedger/node?id=engineeringId&level=0
|
||||
```
|
||||
|
||||
Query 参数:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `id` | `String` | 是 | 节点 ID |
|
||||
| `level` | `Integer` | 是 | `0` 工程,`1` 项目,`2` 设备,`3` 测点 |
|
||||
|
||||
curl:
|
||||
|
||||
```bash
|
||||
curl -X DELETE "http://127.0.0.1:18192/addLedger/node?id=engineeringId&level=0" \
|
||||
-H "Authorization: Bearer ${accessToken}"
|
||||
```
|
||||
|
||||
### 11.2 响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "SUCCESS_CODE",
|
||||
"message": "SUCCESS_MESSAGE",
|
||||
"data": true
|
||||
}
|
||||
```
|
||||
|
||||
### 11.3 删除影响范围
|
||||
|
||||
| 删除层级 | 后端处理 |
|
||||
|---|---|
|
||||
| 工程 | 软删除工程、下属项目、设备、测点和对应台账节点 |
|
||||
| 项目 | 软删除项目、下属设备、测点和对应台账节点 |
|
||||
| 设备 | 软删除设备、下属测点和对应台账节点 |
|
||||
| 测点 | 软删除测点和对应台账节点 |
|
||||
|
||||
软删除字段:
|
||||
|
||||
| 表 | 字段 | 删除值 |
|
||||
|---|---|---|
|
||||
| `cs_engineering` | `status` | `0` |
|
||||
| `cs_project` | `status` | `0` |
|
||||
| `cs_equipment_delivery` | `run_status` | `0` |
|
||||
| `cs_line` | `status` | `0` |
|
||||
| `cs_ledger` | `State` | `0` |
|
||||
|
||||
## 12. 常见错误
|
||||
|
||||
| 场景 | 典型错误信息 |
|
||||
|---|---|
|
||||
| 未传 token 或 token 格式错误 | 全局过滤器返回 token 解析失败 |
|
||||
| 新增项目未传有效工程 ID | `台账节点不存在或已删除` |
|
||||
| 新增设备未传有效项目 ID | `台账节点不存在或已删除` |
|
||||
| 新增测点未传有效设备 ID | `台账节点不存在或已删除` |
|
||||
| 测点线路号重复 | `同设备下 line_no 不可重复` |
|
||||
| 测点线路号超范围 | `line_no 必须在 1-20 之间` |
|
||||
| 接线方式非法 | `conType 只能是 0、1、2` |
|
||||
| 电压等级非法 | `vol_grade 只能是 0.38、10、35、110、220、500` |
|
||||
@@ -92,6 +92,22 @@ import type { AddLedger } from '@/api/tools/addLedger/interface'
|
||||
import { useDictStore } from '@/stores/modules/dict'
|
||||
import LedgerTreePanel from './components/LedgerTreePanel.vue'
|
||||
import LedgerContextPanel from './components/LedgerContextPanel.vue'
|
||||
import {
|
||||
buildLineNoOptions,
|
||||
createEmptyEngineeringForm,
|
||||
createEmptyEquipmentForm,
|
||||
createEmptyLineForm,
|
||||
createEmptyProjectForm,
|
||||
findNodePath,
|
||||
generateGuidText,
|
||||
normalizeEngineeringDetail,
|
||||
normalizeEquipmentDetail,
|
||||
normalizeLineDetail,
|
||||
normalizeProjectDetail,
|
||||
normalizeTreeNode,
|
||||
resolveContextFromPath,
|
||||
type LedgerContextIds
|
||||
} from './utils/ledgerData'
|
||||
|
||||
defineOptions({
|
||||
name: 'AddLedgerView'
|
||||
@@ -101,12 +117,6 @@ type LedgerContextPanelExpose = {
|
||||
validateActiveForm: (level: AddLedger.NodeLevel | null) => Promise<boolean>
|
||||
}
|
||||
|
||||
type LedgerContextIds = {
|
||||
engineeringId: string
|
||||
projectId: string
|
||||
deviceId: string
|
||||
}
|
||||
|
||||
type LedgerContextItem<T> = {
|
||||
id: string
|
||||
label: string
|
||||
@@ -182,71 +192,6 @@ const emptyStateText = computed(() =>
|
||||
treeData.value.length === 0 ? '台账树为空,请先新增一个工程。' : '从左侧台账树选择工程、项目、设备或监测点。'
|
||||
)
|
||||
|
||||
function createEmptyEngineeringForm(): AddLedger.EngineeringForm {
|
||||
return {
|
||||
id: '',
|
||||
name: '',
|
||||
province: '',
|
||||
city: '',
|
||||
description: ''
|
||||
}
|
||||
}
|
||||
|
||||
function createEmptyProjectForm(parentEngineeringId = ''): AddLedger.ProjectForm {
|
||||
return {
|
||||
id: '',
|
||||
engineeringId: parentEngineeringId,
|
||||
parentId: parentEngineeringId,
|
||||
name: '',
|
||||
area: '',
|
||||
description: ''
|
||||
}
|
||||
}
|
||||
|
||||
function createEmptyEquipmentForm(parentProjectId = '', parentEngineeringId = ''): AddLedger.EquipmentForm {
|
||||
return {
|
||||
id: '',
|
||||
engineeringId: parentEngineeringId,
|
||||
projectId: parentProjectId,
|
||||
parentId: parentProjectId,
|
||||
name: '',
|
||||
ndid: '',
|
||||
mac: '',
|
||||
dev_type: '',
|
||||
dev_model: '',
|
||||
dev_access_method: '',
|
||||
node_id: '',
|
||||
node_process: '',
|
||||
upgrade: 0
|
||||
}
|
||||
}
|
||||
|
||||
function createEmptyLineForm(parentDeviceId = ''): AddLedger.LineForm {
|
||||
return {
|
||||
id: '',
|
||||
line_id: generateGuidText(),
|
||||
deviceId: parentDeviceId,
|
||||
parentId: parentDeviceId,
|
||||
name: '',
|
||||
line_no: undefined,
|
||||
conType: undefined,
|
||||
vol_grade: undefined,
|
||||
position: '',
|
||||
ct_ratio: undefined,
|
||||
ct2_ratio: undefined,
|
||||
pt_ratio: undefined,
|
||||
pt2_ratio: undefined,
|
||||
short_circuit_capacity: undefined,
|
||||
basic_capacity: undefined,
|
||||
protocol_capacity: undefined,
|
||||
dev_capacity: undefined,
|
||||
monitor_obj: '',
|
||||
is_govern: 0,
|
||||
monitor_user: '',
|
||||
is_important: 0
|
||||
}
|
||||
}
|
||||
|
||||
const resolveFallbackDictOptions = (code: LedgerDictCode) =>
|
||||
code === 'ledger_device_type' ? fallbackDeviceTypeOptions : fallbackDeviceModelOptions
|
||||
|
||||
@@ -311,192 +256,17 @@ const loadLedgerDictOptions = async () => {
|
||||
loadLedgerDictOptionsByCode('ledger_device_model')
|
||||
}
|
||||
|
||||
const resolveString = (data: Record<string, unknown>, ...keys: string[]) => {
|
||||
for (const key of keys) {
|
||||
const value = data[key]
|
||||
if (value === null || value === undefined) continue
|
||||
const text = String(value).trim()
|
||||
if (text) return text
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
const resolveNumber = (data: Record<string, unknown>, ...keys: string[]) => {
|
||||
for (const key of keys) {
|
||||
const value = data[key]
|
||||
if (value === null || value === undefined || value === '') continue
|
||||
const parsed = Number(value)
|
||||
if (Number.isFinite(parsed)) return parsed
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
const normalizeLevel = (value: unknown): AddLedger.NodeLevel => {
|
||||
const level = Number(value)
|
||||
if (level === 0 || level === 1 || level === 2 || level === 3) {
|
||||
return level
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
const normalizeTreeNode = (node: AddLedger.LedgerTreeNode): AddLedger.NormalizedTreeNode => {
|
||||
const rawNode = node as Record<string, unknown>
|
||||
const id = resolveString(rawNode, 'id', 'Id')
|
||||
const children = Array.isArray(node.children) ? node.children.map(normalizeTreeNode).filter(item => item.id) : []
|
||||
|
||||
return {
|
||||
id,
|
||||
pid: resolveString(rawNode, 'parentId', 'pid', 'Pid'),
|
||||
pids: resolveString(rawNode, 'parentIds', 'pids', 'Pids'),
|
||||
name: resolveString(rawNode, 'name', 'Name') || id || '--',
|
||||
level: normalizeLevel(rawNode.level ?? rawNode.Level),
|
||||
children,
|
||||
raw: node
|
||||
}
|
||||
}
|
||||
|
||||
const findNodePath = (
|
||||
nodes: AddLedger.NormalizedTreeNode[],
|
||||
id: string,
|
||||
path: AddLedger.NormalizedTreeNode[] = []
|
||||
): AddLedger.NormalizedTreeNode[] => {
|
||||
for (const node of nodes) {
|
||||
const nextPath = [...path, node]
|
||||
if (node.id === id) return nextPath
|
||||
|
||||
const matchedPath = findNodePath(node.children, id, nextPath)
|
||||
if (matchedPath.length) return matchedPath
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
const getCurrentPath = () => {
|
||||
if (!selectedNode.value?.id) return []
|
||||
return findNodePath(treeData.value, selectedNode.value.id)
|
||||
}
|
||||
|
||||
const resolveContextFromPath = (path: AddLedger.NormalizedTreeNode[]): LedgerContextIds => {
|
||||
return {
|
||||
engineeringId: path.find(item => item.level === 0)?.id || '',
|
||||
projectId: path.find(item => item.level === 1)?.id || '',
|
||||
deviceId: path.find(item => item.level === 2)?.id || ''
|
||||
}
|
||||
}
|
||||
|
||||
const resolveContext = () => {
|
||||
const path = getCurrentPath()
|
||||
|
||||
return resolveContextFromPath(path)
|
||||
}
|
||||
|
||||
function generateGuidText() {
|
||||
return window.crypto.randomUUID().replace(/-/g, '')
|
||||
}
|
||||
|
||||
const normalizeEngineeringDetail = (
|
||||
detail: AddLedger.NodeDetail | null,
|
||||
node?: AddLedger.NormalizedTreeNode
|
||||
): AddLedger.EngineeringForm => {
|
||||
const data = (detail || {}) as Record<string, unknown>
|
||||
|
||||
return {
|
||||
id: resolveString(data, 'id', 'engineeringId') || node?.id || '',
|
||||
name: resolveString(data, 'name') || node?.name || '',
|
||||
province: resolveString(data, 'province'),
|
||||
city: resolveString(data, 'city'),
|
||||
description: resolveString(data, 'description')
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeProjectDetail = (
|
||||
detail: AddLedger.NodeDetail | null,
|
||||
node?: AddLedger.NormalizedTreeNode,
|
||||
context = resolveContext()
|
||||
): AddLedger.ProjectForm => {
|
||||
const data = (detail || {}) as Record<string, unknown>
|
||||
|
||||
return {
|
||||
id: resolveString(data, 'id', 'projectId') || node?.id || '',
|
||||
engineeringId: resolveString(data, 'engineeringId', 'associated_engineering') || context.engineeringId,
|
||||
parentId: context.engineeringId,
|
||||
name: resolveString(data, 'name') || node?.name || '',
|
||||
area: resolveString(data, 'area'),
|
||||
description: resolveString(data, 'description')
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeEquipmentDetail = (
|
||||
detail: AddLedger.NodeDetail | null,
|
||||
node?: AddLedger.NormalizedTreeNode,
|
||||
context = resolveContext()
|
||||
): AddLedger.EquipmentForm => {
|
||||
const data = (detail || {}) as Record<string, unknown>
|
||||
|
||||
return {
|
||||
id: resolveString(data, 'id', 'equipmentId') || node?.id || '',
|
||||
engineeringId: resolveString(data, 'engineeringId', 'associated_engineering') || context.engineeringId,
|
||||
projectId: resolveString(data, 'projectId', 'associated_project') || context.projectId,
|
||||
parentId: context.projectId,
|
||||
name: resolveString(data, 'name') || node?.name || '',
|
||||
ndid: resolveString(data, 'ndid'),
|
||||
mac: resolveString(data, 'mac'),
|
||||
dev_type: resolveString(data, 'dev_type', 'devType'),
|
||||
dev_model: resolveString(data, 'dev_model', 'devModel'),
|
||||
dev_access_method: resolveString(data, 'dev_access_method', 'devAccessMethod'),
|
||||
node_id: resolveString(data, 'node_id', 'nodeId'),
|
||||
node_process: resolveString(data, 'node_process', 'nodeProcess'),
|
||||
upgrade: resolveNumber(data, 'upgrade') ?? 0
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeLineDetail = (
|
||||
detail: AddLedger.NodeDetail | null,
|
||||
node?: AddLedger.NormalizedTreeNode,
|
||||
context = resolveContext()
|
||||
): AddLedger.LineForm => {
|
||||
const data = (detail || {}) as Record<string, unknown>
|
||||
const lineId = resolveString(data, 'line_id', 'lineId', 'id') || node?.id || generateGuidText()
|
||||
|
||||
return {
|
||||
id: resolveString(data, 'id') || lineId,
|
||||
line_id: lineId,
|
||||
deviceId: resolveString(data, 'deviceId', 'device_id') || context.deviceId,
|
||||
parentId: context.deviceId,
|
||||
name: resolveString(data, 'name') || node?.name || '',
|
||||
line_no: resolveNumber(data, 'line_no', 'lineNo'),
|
||||
conType: resolveNumber(data, 'conType'),
|
||||
vol_grade: resolveNumber(data, 'vol_grade', 'volGrade'),
|
||||
position: resolveString(data, 'position'),
|
||||
ct_ratio: resolveNumber(data, 'ct_ratio', 'ctRatio'),
|
||||
ct2_ratio: resolveNumber(data, 'ct2_ratio', 'ct2Ratio'),
|
||||
pt_ratio: resolveNumber(data, 'pt_ratio', 'ptRatio'),
|
||||
pt2_ratio: resolveNumber(data, 'pt2_ratio', 'pt2Ratio'),
|
||||
short_circuit_capacity: resolveNumber(data, 'short_circuit_capacity', 'shortCircuitCapacity'),
|
||||
basic_capacity: resolveNumber(data, 'basic_capacity', 'basicCapacity'),
|
||||
protocol_capacity: resolveNumber(data, 'protocol_capacity', 'protocolCapacity'),
|
||||
dev_capacity: resolveNumber(data, 'dev_capacity', 'devCapacity'),
|
||||
monitor_obj: resolveString(data, 'monitor_obj', 'monitorObj'),
|
||||
is_govern: resolveNumber(data, 'is_govern', 'isGovern') ?? 0,
|
||||
monitor_user: resolveString(data, 'monitor_user', 'monitorUser'),
|
||||
is_important: resolveNumber(data, 'is_important', 'isImportant') ?? 0
|
||||
}
|
||||
}
|
||||
|
||||
const buildLineNoOptions = (lineNos: number[], currentLineNo?: number) => {
|
||||
const values = new Set(lineNos.filter(item => Number.isInteger(item) && item >= 1 && item <= 20))
|
||||
if (currentLineNo && currentLineNo >= 1 && currentLineNo <= 20) {
|
||||
values.add(currentLineNo)
|
||||
}
|
||||
|
||||
return Array.from(values)
|
||||
.sort((left, right) => left - right)
|
||||
.map(item => ({ label: `${item} 号线路`, value: item }))
|
||||
}
|
||||
|
||||
const allLineNoOptions = computed(() => buildLineNoOptions(Array.from({ length: 20 }, (_item, index) => index + 1)))
|
||||
|
||||
const loadAvailableLineNoOptions = async (deviceId: string, lineId = '', currentLineNo?: number) => {
|
||||
|
||||
247
frontend/src/views/tools/addLedger/utils/ledgerData.ts
Normal file
247
frontend/src/views/tools/addLedger/utils/ledgerData.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
import type { AddLedger } from '@/api/tools/addLedger/interface'
|
||||
|
||||
export type LedgerContextIds = {
|
||||
engineeringId: string
|
||||
projectId: string
|
||||
deviceId: string
|
||||
}
|
||||
|
||||
export function generateGuidText() {
|
||||
return window.crypto.randomUUID().replace(/-/g, '')
|
||||
}
|
||||
|
||||
export function createEmptyEngineeringForm(): AddLedger.EngineeringForm {
|
||||
return {
|
||||
id: '',
|
||||
name: '',
|
||||
province: '',
|
||||
city: '',
|
||||
description: ''
|
||||
}
|
||||
}
|
||||
|
||||
export function createEmptyProjectForm(parentEngineeringId = ''): AddLedger.ProjectForm {
|
||||
return {
|
||||
id: '',
|
||||
engineeringId: parentEngineeringId,
|
||||
parentId: parentEngineeringId,
|
||||
name: '',
|
||||
area: '',
|
||||
description: ''
|
||||
}
|
||||
}
|
||||
|
||||
export function createEmptyEquipmentForm(parentProjectId = '', parentEngineeringId = ''): AddLedger.EquipmentForm {
|
||||
return {
|
||||
id: '',
|
||||
engineeringId: parentEngineeringId,
|
||||
projectId: parentProjectId,
|
||||
parentId: parentProjectId,
|
||||
name: '',
|
||||
ndid: '',
|
||||
mac: '',
|
||||
dev_type: '',
|
||||
dev_model: '',
|
||||
dev_access_method: '',
|
||||
node_id: '',
|
||||
node_process: '',
|
||||
upgrade: 0
|
||||
}
|
||||
}
|
||||
|
||||
export function createEmptyLineForm(parentDeviceId = ''): AddLedger.LineForm {
|
||||
return {
|
||||
id: '',
|
||||
line_id: generateGuidText(),
|
||||
deviceId: parentDeviceId,
|
||||
parentId: parentDeviceId,
|
||||
name: '',
|
||||
line_no: undefined,
|
||||
conType: undefined,
|
||||
vol_grade: undefined,
|
||||
position: '',
|
||||
ct_ratio: undefined,
|
||||
ct2_ratio: undefined,
|
||||
pt_ratio: undefined,
|
||||
pt2_ratio: undefined,
|
||||
short_circuit_capacity: undefined,
|
||||
basic_capacity: undefined,
|
||||
protocol_capacity: undefined,
|
||||
dev_capacity: undefined,
|
||||
monitor_obj: '',
|
||||
is_govern: 0,
|
||||
monitor_user: '',
|
||||
is_important: 0
|
||||
}
|
||||
}
|
||||
|
||||
const resolveString = (data: Record<string, unknown>, ...keys: string[]) => {
|
||||
for (const key of keys) {
|
||||
const value = data[key]
|
||||
if (value === null || value === undefined) continue
|
||||
const text = String(value).trim()
|
||||
if (text) return text
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
const resolveNumber = (data: Record<string, unknown>, ...keys: string[]) => {
|
||||
for (const key of keys) {
|
||||
const value = data[key]
|
||||
if (value === null || value === undefined || value === '') continue
|
||||
const parsed = Number(value)
|
||||
if (Number.isFinite(parsed)) return parsed
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
const normalizeLevel = (value: unknown): AddLedger.NodeLevel => {
|
||||
const level = Number(value)
|
||||
if (level === 0 || level === 1 || level === 2 || level === 3) {
|
||||
return level
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
export const normalizeTreeNode = (node: AddLedger.LedgerTreeNode): AddLedger.NormalizedTreeNode => {
|
||||
const rawNode = node as Record<string, unknown>
|
||||
const id = resolveString(rawNode, 'id', 'Id')
|
||||
const children = Array.isArray(node.children) ? node.children.map(normalizeTreeNode).filter(item => item.id) : []
|
||||
|
||||
return {
|
||||
id,
|
||||
pid: resolveString(rawNode, 'parentId', 'pid', 'Pid'),
|
||||
pids: resolveString(rawNode, 'parentIds', 'pids', 'Pids'),
|
||||
name: resolveString(rawNode, 'name', 'Name') || id || '--',
|
||||
level: normalizeLevel(rawNode.level ?? rawNode.Level),
|
||||
children,
|
||||
raw: node
|
||||
}
|
||||
}
|
||||
|
||||
export const findNodePath = (
|
||||
nodes: AddLedger.NormalizedTreeNode[],
|
||||
id: string,
|
||||
path: AddLedger.NormalizedTreeNode[] = []
|
||||
): AddLedger.NormalizedTreeNode[] => {
|
||||
for (const node of nodes) {
|
||||
const nextPath = [...path, node]
|
||||
if (node.id === id) return nextPath
|
||||
|
||||
const matchedPath = findNodePath(node.children, id, nextPath)
|
||||
if (matchedPath.length) return matchedPath
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
export const resolveContextFromPath = (path: AddLedger.NormalizedTreeNode[]): LedgerContextIds => {
|
||||
return {
|
||||
engineeringId: path.find(item => item.level === 0)?.id || '',
|
||||
projectId: path.find(item => item.level === 1)?.id || '',
|
||||
deviceId: path.find(item => item.level === 2)?.id || ''
|
||||
}
|
||||
}
|
||||
|
||||
export const normalizeEngineeringDetail = (
|
||||
detail: AddLedger.NodeDetail | null,
|
||||
node?: AddLedger.NormalizedTreeNode
|
||||
): AddLedger.EngineeringForm => {
|
||||
const data = (detail || {}) as Record<string, unknown>
|
||||
|
||||
return {
|
||||
id: resolveString(data, 'id', 'engineeringId') || node?.id || '',
|
||||
name: resolveString(data, 'name') || node?.name || '',
|
||||
province: resolveString(data, 'province'),
|
||||
city: resolveString(data, 'city'),
|
||||
description: resolveString(data, 'description')
|
||||
}
|
||||
}
|
||||
|
||||
export const normalizeProjectDetail = (
|
||||
detail: AddLedger.NodeDetail | null,
|
||||
node?: AddLedger.NormalizedTreeNode,
|
||||
context: LedgerContextIds = { engineeringId: '', projectId: '', deviceId: '' }
|
||||
): AddLedger.ProjectForm => {
|
||||
const data = (detail || {}) as Record<string, unknown>
|
||||
|
||||
return {
|
||||
id: resolveString(data, 'id', 'projectId') || node?.id || '',
|
||||
engineeringId: resolveString(data, 'engineeringId', 'associated_engineering') || context.engineeringId,
|
||||
parentId: context.engineeringId,
|
||||
name: resolveString(data, 'name') || node?.name || '',
|
||||
area: resolveString(data, 'area'),
|
||||
description: resolveString(data, 'description')
|
||||
}
|
||||
}
|
||||
|
||||
export const normalizeEquipmentDetail = (
|
||||
detail: AddLedger.NodeDetail | null,
|
||||
node?: AddLedger.NormalizedTreeNode,
|
||||
context: LedgerContextIds = { engineeringId: '', projectId: '', deviceId: '' }
|
||||
): AddLedger.EquipmentForm => {
|
||||
const data = (detail || {}) as Record<string, unknown>
|
||||
|
||||
return {
|
||||
id: resolveString(data, 'id', 'equipmentId') || node?.id || '',
|
||||
engineeringId: resolveString(data, 'engineeringId', 'associated_engineering') || context.engineeringId,
|
||||
projectId: resolveString(data, 'projectId', 'associated_project') || context.projectId,
|
||||
parentId: context.projectId,
|
||||
name: resolveString(data, 'name') || node?.name || '',
|
||||
ndid: resolveString(data, 'ndid'),
|
||||
mac: resolveString(data, 'mac'),
|
||||
dev_type: resolveString(data, 'dev_type', 'devType'),
|
||||
dev_model: resolveString(data, 'dev_model', 'devModel'),
|
||||
dev_access_method: resolveString(data, 'dev_access_method', 'devAccessMethod'),
|
||||
node_id: resolveString(data, 'node_id', 'nodeId'),
|
||||
node_process: resolveString(data, 'node_process', 'nodeProcess'),
|
||||
upgrade: resolveNumber(data, 'upgrade') ?? 0
|
||||
}
|
||||
}
|
||||
|
||||
export const normalizeLineDetail = (
|
||||
detail: AddLedger.NodeDetail | null,
|
||||
node?: AddLedger.NormalizedTreeNode,
|
||||
context: LedgerContextIds = { engineeringId: '', projectId: '', deviceId: '' }
|
||||
): AddLedger.LineForm => {
|
||||
const data = (detail || {}) as Record<string, unknown>
|
||||
const lineId = resolveString(data, 'line_id', 'lineId', 'id') || node?.id || generateGuidText()
|
||||
|
||||
return {
|
||||
id: resolveString(data, 'id') || lineId,
|
||||
line_id: lineId,
|
||||
deviceId: resolveString(data, 'deviceId', 'device_id') || context.deviceId,
|
||||
parentId: context.deviceId,
|
||||
name: resolveString(data, 'name') || node?.name || '',
|
||||
line_no: resolveNumber(data, 'line_no', 'lineNo'),
|
||||
conType: resolveNumber(data, 'conType'),
|
||||
vol_grade: resolveNumber(data, 'vol_grade', 'volGrade'),
|
||||
position: resolveString(data, 'position'),
|
||||
ct_ratio: resolveNumber(data, 'ct_ratio', 'ctRatio'),
|
||||
ct2_ratio: resolveNumber(data, 'ct2_ratio', 'ct2Ratio'),
|
||||
pt_ratio: resolveNumber(data, 'pt_ratio', 'ptRatio'),
|
||||
pt2_ratio: resolveNumber(data, 'pt2_ratio', 'pt2Ratio'),
|
||||
short_circuit_capacity: resolveNumber(data, 'short_circuit_capacity', 'shortCircuitCapacity'),
|
||||
basic_capacity: resolveNumber(data, 'basic_capacity', 'basicCapacity'),
|
||||
protocol_capacity: resolveNumber(data, 'protocol_capacity', 'protocolCapacity'),
|
||||
dev_capacity: resolveNumber(data, 'dev_capacity', 'devCapacity'),
|
||||
monitor_obj: resolveString(data, 'monitor_obj', 'monitorObj'),
|
||||
is_govern: resolveNumber(data, 'is_govern', 'isGovern') ?? 0,
|
||||
monitor_user: resolveString(data, 'monitor_user', 'monitorUser'),
|
||||
is_important: resolveNumber(data, 'is_important', 'isImportant') ?? 0
|
||||
}
|
||||
}
|
||||
|
||||
export const buildLineNoOptions = (lineNos: number[], currentLineNo?: number) => {
|
||||
const values = new Set(lineNos.filter(item => Number.isInteger(item) && item >= 1 && item <= 20))
|
||||
if (currentLineNo && currentLineNo >= 1 && currentLineNo <= 20) {
|
||||
values.add(currentLineNo)
|
||||
}
|
||||
|
||||
return Array.from(values)
|
||||
.sort((left, right) => left - right)
|
||||
.map(item => ({ label: `${item} 号线路`, value: item }))
|
||||
}
|
||||
@@ -1,756 +0,0 @@
|
||||
{
|
||||
"ReportList": [
|
||||
{
|
||||
"desc": "统计数据",
|
||||
"inst": "01",
|
||||
"TrgOps": "96",
|
||||
"Select": "DataStatFileMap",
|
||||
"DataSetList": [
|
||||
"dsStatisticData",
|
||||
"dsStHarm",
|
||||
"dsStIHarm",
|
||||
"dsStMMXU",
|
||||
"dsStMSQI"
|
||||
],
|
||||
"LnInstList": [
|
||||
"最大值",
|
||||
"最小值",
|
||||
"平均值",
|
||||
"95值",
|
||||
"方均根值",
|
||||
"间谐波最大值",
|
||||
"间谐波最小值",
|
||||
"间谐波平均值",
|
||||
"间谐波95值",
|
||||
"间谐波方均根值"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "波动闪变",
|
||||
"inst": "01",
|
||||
"TrgOps": "96",
|
||||
"Select": "FlickerFileMap",
|
||||
"DataSetList": [
|
||||
"dsFlickerData",
|
||||
"dsPST"
|
||||
],
|
||||
"LnInstList": [
|
||||
"波动闪变值"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "实时数据",
|
||||
"inst": "01",
|
||||
"TrgOps": "40",
|
||||
"Select": "DataRealFileMap",
|
||||
"DataSetList": [
|
||||
"dsRealTimeData",
|
||||
"dsRtHarm",
|
||||
"dsRtIHarm",
|
||||
"dsRtMMXU",
|
||||
"dsRtMSQI",
|
||||
"dsRtFre"
|
||||
],
|
||||
"LnInstList": [
|
||||
"实时数据",
|
||||
"间谐波实时数据"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "暂态事件",
|
||||
"inst": "01",
|
||||
"TrgOps": "96",
|
||||
"Select": "QVVR",
|
||||
"DataSetList": [
|
||||
"dsEveQVVR"
|
||||
],
|
||||
"LnInstList": [
|
||||
"电压变动A",
|
||||
"电压变动B",
|
||||
"电压变动C"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "录波状态",
|
||||
"inst": "01",
|
||||
"TrgOps": "96",
|
||||
"Select": "RDRE",
|
||||
"DataSetList": [
|
||||
"dsEveRDRE"
|
||||
],
|
||||
"LnInstList": [
|
||||
"录波文件"
|
||||
]
|
||||
}
|
||||
],
|
||||
"LnClassList": [
|
||||
{
|
||||
"desc": "基本数据",
|
||||
"nameList": [
|
||||
"MMXU"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "序分量值",
|
||||
"nameList": [
|
||||
"MSQI"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "谐波/间谐波数据",
|
||||
"nameList": [
|
||||
"MHAI"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "波动闪变",
|
||||
"nameList": [
|
||||
"MFLK"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电压变动",
|
||||
"nameList": [
|
||||
"QVVR"
|
||||
]
|
||||
}
|
||||
],
|
||||
"PhaseList": [
|
||||
{
|
||||
"desc": "无相别",
|
||||
"nameList": [
|
||||
"null"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "正序",
|
||||
"nameList": [
|
||||
"c1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "负序",
|
||||
"nameList": [
|
||||
"c2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "零序",
|
||||
"nameList": [
|
||||
"c3"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "A相",
|
||||
"nameList": [
|
||||
"phsA",
|
||||
"phsAHar"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "B相",
|
||||
"nameList": [
|
||||
"phsB",
|
||||
"phsBHar"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "C相",
|
||||
"nameList": [
|
||||
"phsC",
|
||||
"phsCHar"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "AB线",
|
||||
"nameList": [
|
||||
"phsAB",
|
||||
"phsABHar"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "BC线",
|
||||
"nameList": [
|
||||
"phsBC",
|
||||
"phsBCHar"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "CA线",
|
||||
"nameList": [
|
||||
"phsCA",
|
||||
"phsCAHar"
|
||||
]
|
||||
}
|
||||
],
|
||||
"MultiplierList": [
|
||||
{
|
||||
"multiplier": 1,
|
||||
"nameList": [
|
||||
"null"
|
||||
]
|
||||
},
|
||||
{
|
||||
"multiplier": 1000,
|
||||
"nameList": [
|
||||
"k"
|
||||
]
|
||||
}
|
||||
],
|
||||
"UnitList": [
|
||||
{
|
||||
"desc": "other",
|
||||
"nameList": [
|
||||
"null"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "v",
|
||||
"nameList": [
|
||||
"V"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "a",
|
||||
"nameList": [
|
||||
"A"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "w",
|
||||
"nameList": [
|
||||
"W",
|
||||
"VAr",
|
||||
"VA"
|
||||
]
|
||||
}
|
||||
],
|
||||
"TypeList": [
|
||||
{
|
||||
"desc": "值",
|
||||
"nameList": [
|
||||
"mag"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "角度",
|
||||
"nameList": [
|
||||
"ang"
|
||||
]
|
||||
}
|
||||
],
|
||||
"DataObjectsList": [
|
||||
{
|
||||
"desc": "非间谐波数据",
|
||||
"LnInstList": [
|
||||
"最大值",
|
||||
"最小值",
|
||||
"平均值",
|
||||
"95值",
|
||||
"实时数据"
|
||||
],
|
||||
"ObjectList": [
|
||||
{
|
||||
"desc": "频率",
|
||||
"nameList": [
|
||||
"Hz"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "线电压总有效值",
|
||||
"nameList": [
|
||||
"PPV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "相电压总有效值",
|
||||
"nameList": [
|
||||
"PhV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电流总有效值",
|
||||
"nameList": [
|
||||
"A"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "有功功率",
|
||||
"nameList": [
|
||||
"W"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "无功功率",
|
||||
"nameList": [
|
||||
"VAr"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "视在功率",
|
||||
"nameList": [
|
||||
"VA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "功率因数",
|
||||
"nameList": [
|
||||
"PF"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "位移功率因数",
|
||||
"nameList": [
|
||||
"DF"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "三相总有功功率",
|
||||
"nameList": [
|
||||
"TotW"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "三相总无功功率",
|
||||
"nameList": [
|
||||
"TotVAr"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "三相总视在功率",
|
||||
"nameList": [
|
||||
"TotVA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "三相功率因数",
|
||||
"nameList": [
|
||||
"TotPF"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "三相位移功率因数",
|
||||
"nameList": [
|
||||
"TotDF"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "频率偏差",
|
||||
"nameList": [
|
||||
"HzDev"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "相电压偏差",
|
||||
"nameList": [
|
||||
"PhVDev"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "线电压偏差",
|
||||
"nameList": [
|
||||
"PPVDev"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "正序负序和零序电压",
|
||||
"nameList": [
|
||||
"SeqV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "正序负序和零序电流",
|
||||
"nameList": [
|
||||
"SeqA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电压负序不平衡度",
|
||||
"nameList": [
|
||||
"ImbNgV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电流负序不平衡度",
|
||||
"nameList": [
|
||||
"ImbNgA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电压零序不平衡度",
|
||||
"nameList": [
|
||||
"ImbZroV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电流零序不平衡度",
|
||||
"nameList": [
|
||||
"ImbZroA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "相电压谐波总畸变率",
|
||||
"nameList": [
|
||||
"ThdPhV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "相电压总偶次谐波畸变率",
|
||||
"nameList": [
|
||||
"ThdEvnPhV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "相电压总奇次谐波畸变率",
|
||||
"nameList": [
|
||||
"ThdOddPhV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "线电压谐波总畸变率",
|
||||
"nameList": [
|
||||
"ThdPPV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "线电压总偶次谐波畸变率",
|
||||
"nameList": [
|
||||
"ThdEvnPPV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "线电压总奇次谐波畸变率",
|
||||
"nameList": [
|
||||
"ThdOddPPV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "相电压谐波含有率序列",
|
||||
"baseflag": 1,
|
||||
"basecount": 49,
|
||||
"nameList": [
|
||||
"HRPhV",
|
||||
"HPhVMag"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "线电压谐波含有率序列",
|
||||
"baseflag": 1,
|
||||
"basecount": 49,
|
||||
"nameList": [
|
||||
"HRPPV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电流总谐波畸变率",
|
||||
"nameList": [
|
||||
"ThdA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电流总偶次谐波畸变率",
|
||||
"nameList": [
|
||||
"ThdEvnA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电流总奇次谐波畸变率",
|
||||
"nameList": [
|
||||
"ThdOddA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "谐波电流有效值序列",
|
||||
"baseflag": 1,
|
||||
"basecount": 49,
|
||||
"nameList": [
|
||||
"HA",
|
||||
"HAMag"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "谐波电压有效值序列",
|
||||
"baseflag": 1,
|
||||
"basecount": 49,
|
||||
"nameList": [
|
||||
"HPhV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "2~50次谐波有功功率序列",
|
||||
"baseflag": 1,
|
||||
"basecount": 49,
|
||||
"nameList": [
|
||||
"HW"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "2~50次谐波无功功率序列",
|
||||
"baseflag": 1,
|
||||
"basecount": 49,
|
||||
"nameList": [
|
||||
"HVAr"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "2~50次谐波视在功率序列",
|
||||
"baseflag": 1,
|
||||
"basecount": 49,
|
||||
"nameList": [
|
||||
"HVA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "三相总谐波视在功率",
|
||||
"nameList": [
|
||||
"TotHVA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "三相总谐波无功功率",
|
||||
"nameList": [
|
||||
"TotHVAr"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "三相总谐波有功功率",
|
||||
"nameList": [
|
||||
"TotHW"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "相电压基波有效值",
|
||||
"baseflag": 2,
|
||||
"queuecount": 49,
|
||||
"nameList": [
|
||||
"HFundPhV",
|
||||
"FundPhV"
|
||||
],
|
||||
"queueList":[
|
||||
"HPhV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "线电压基波有效值",
|
||||
"baseflag": 2,
|
||||
"queuecount": 49,
|
||||
"nameList": [
|
||||
"HFundPPV"
|
||||
],
|
||||
"queueList":[
|
||||
"HPPV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电流基波有效值",
|
||||
"baseflag": 2,
|
||||
"queuecount": 49,
|
||||
"nameList": [
|
||||
|
||||
],
|
||||
"queueList":[
|
||||
"HA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "基波有功功率",
|
||||
"baseflag": 2,
|
||||
"queuecount": 49,
|
||||
"nameList": [
|
||||
|
||||
],
|
||||
"queueList":[
|
||||
"HW"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "基波无功功率",
|
||||
"baseflag": 2,
|
||||
"queuecount": 49,
|
||||
"nameList": [
|
||||
|
||||
],
|
||||
"queueList":[
|
||||
"HVAr"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "基波视在功率",
|
||||
"baseflag": 2,
|
||||
"queuecount": 49,
|
||||
"nameList": [
|
||||
|
||||
],
|
||||
"queueList":[
|
||||
"HVA"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "间谐波数据",
|
||||
"LnInstList": [
|
||||
"间谐波最大值",
|
||||
"间谐波最小值",
|
||||
"间谐波平均值",
|
||||
"间谐波95值",
|
||||
"间谐波实时数据"
|
||||
],
|
||||
"ObjectList": [
|
||||
{
|
||||
"desc": "相电压间谐波含有率序列",
|
||||
"baseflag": 1,
|
||||
"basecount": 50,
|
||||
"nameList": [
|
||||
"HPhV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "线电压间谐波含有率序列",
|
||||
"baseflag": 1,
|
||||
"basecount": 50,
|
||||
"nameList": [
|
||||
"HRPPV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "间谐波电流有效值序列",
|
||||
"baseflag": 1,
|
||||
"basecount": 50,
|
||||
"nameList": [
|
||||
"HA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "间谐波电压有效值序列",
|
||||
"baseflag": 1,
|
||||
"basecount": 50,
|
||||
"nameList": [
|
||||
"HRPhV"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电压变动",
|
||||
"LnInstList": [
|
||||
"电压变动A",
|
||||
"电压变动B",
|
||||
"电压变动C"
|
||||
],
|
||||
"ObjectList": [
|
||||
{
|
||||
"desc": "电压扰动事件启动",
|
||||
"nameList": [
|
||||
"VarStr"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电压暂降事件启动",
|
||||
"nameList": [
|
||||
"DipStr"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电压暂升事件启动",
|
||||
"nameList": [
|
||||
"SwlStr"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电压中断事件启动",
|
||||
"nameList": [
|
||||
"IntrStr"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电压扰动事件特征幅值",
|
||||
"nameList": [
|
||||
"VVa"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电压扰动事件持续时间",
|
||||
"nameList": [
|
||||
"VVaTm"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电压暂降启动定值",
|
||||
"nameList": [
|
||||
"DipStrVal"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电压暂升启动定值",
|
||||
"nameList": [
|
||||
"SwlStrVal"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电压中断启动定值",
|
||||
"nameList": [
|
||||
"IntrStrVal"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "其余数据",
|
||||
"LnInstList": [
|
||||
"波动闪变值",
|
||||
"录波文件"
|
||||
],
|
||||
"ObjectList": [
|
||||
{
|
||||
"desc": "线电压短时闪变值",
|
||||
"nameList": [
|
||||
"PPPst"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "相电压短时闪变值",
|
||||
"nameList": [
|
||||
"PhPst"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "线电压长时闪变值",
|
||||
"nameList": [
|
||||
"PPPlt"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "相电压长时闪变值",
|
||||
"nameList": [
|
||||
"PhPlt"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "线电压电压变动幅值",
|
||||
"nameList": [
|
||||
"PPFluc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "相电压电压变动幅值",
|
||||
"nameList": [
|
||||
"PhFluc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "线电压电压变动频度",
|
||||
"nameList": [
|
||||
"PPFlucf"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "相电压电压变动频度",
|
||||
"nameList": [
|
||||
"PhFlucf"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/* 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 viewFile = path.join(currentDir, 'index.vue')
|
||||
const viewSource = fs.readFileSync(viewFile, 'utf8')
|
||||
|
||||
const expectations = [
|
||||
['download data waits for image capture', /case\s+'download-data':[\s\S]*await\s+downloadTrendData\(\)/],
|
||||
['downloadTrendData is async', /const\s+downloadTrendData\s*=\s*async\s*\(\)\s*=>/],
|
||||
['trend export captures images for both metrics', /captureTrendExportImages[\s\S]*instant[\s\S]*rms/],
|
||||
['export uses mhtml multipart workbook', /multipart\/related;\s*boundary=/],
|
||||
['export workbook declares data worksheet source', /<x:Name>数据<\/x:Name>[\s\S]*<x:WorksheetSource\s+HRef="data\.htm"\/>/],
|
||||
['export workbook declares image worksheet source', /<x:Name>图片<\/x:Name>[\s\S]*<x:WorksheetSource\s+HRef="image\.htm"\/>/],
|
||||
['export writes content location header', /`Content-Location:\s*\$\{contentLocation\}`/],
|
||||
['export contains data html part', /'data\.htm'[\s\S]*buildTrendExportDataSheet/],
|
||||
['export contains image html part', /'image\.htm'[\s\S]*buildTrendExportImageSheet/],
|
||||
['export contains merged image resource', /Content-Location:\s*trend-images\.png/],
|
||||
['export combines metric screenshots before embedding', /mergeTrendExportImages[\s\S]*loadedImages\.forEach[\s\S]*drawImage/],
|
||||
['image worksheet contains instant chart title', /瞬时图/],
|
||||
['image worksheet contains rms chart title', /RMS图/],
|
||||
['captured image draws trend metric title', /drawTrendExportImageTitle[\s\S]*fillText\([\s\S]*title/],
|
||||
['instant captured image uses instant value title', /captureTrendExportTargetImage\(trendExportMetricLabelMap\[metric\]\)/],
|
||||
['image worksheet embeds merged resource image', /<img\s+src="trend-images\.png"/],
|
||||
['export no longer embeds per-metric image resources', /trendExportImageLocationMap/, true],
|
||||
['export no longer uses inline worksheet div markers', /mso-element:worksheet/, true]
|
||||
]
|
||||
|
||||
const failures = expectations.filter(([, pattern, shouldBeMissing]) => {
|
||||
const exists = pattern.test(viewSource)
|
||||
return shouldBeMissing ? exists : !exists
|
||||
})
|
||||
|
||||
if (failures.length) {
|
||||
console.error('waveform trend export contract check failed:')
|
||||
for (const [name] of failures) {
|
||||
console.error(`- ${name}`)
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log('waveform trend export contract check passed')
|
||||
@@ -0,0 +1,27 @@
|
||||
/* 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 lineChartFile = path.join(currentDir, '..', '..', '..', 'components', 'echarts', 'line', 'index.vue')
|
||||
const lineChartSource = fs.readFileSync(lineChartFile, 'utf8')
|
||||
|
||||
const expectations = [
|
||||
['line chart reads toolbox dataZoom startValue', /zoomPayload\?\.startValue/],
|
||||
['line chart reads toolbox dataZoom endValue', /zoomPayload\?\.endValue/],
|
||||
['line chart falls back to current dataZoom option range', /getOption\?\.\(\)\?\.dataZoom/],
|
||||
['line chart emits normalized chart zoom range', /emit\('chart-data-zoom'[\s\S]*start:[\s\S]*end:/]
|
||||
]
|
||||
|
||||
const failures = expectations.filter(([, pattern]) => !pattern.test(lineChartSource))
|
||||
|
||||
if (failures.length) {
|
||||
console.error('waveform trend zoom contract check failed:')
|
||||
for (const [name] of failures) {
|
||||
console.error(`- ${name}`)
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log('waveform trend zoom contract check passed')
|
||||
@@ -7,11 +7,13 @@ const currentDir = path.dirname(fileURLToPath(import.meta.url))
|
||||
const viewFile = path.join(currentDir, 'index.vue')
|
||||
const toolbarFile = path.join(currentDir, 'components', 'WaveformToolbar.vue')
|
||||
const trendPanelFile = path.join(currentDir, 'components', 'WaveformTrendPanel.vue')
|
||||
const optionsFile = path.join(currentDir, 'utils', 'options.ts')
|
||||
const interfaceFile = path.join(currentDir, '..', '..', '..', 'api', 'tools', 'waveform', 'interface', 'index.ts')
|
||||
|
||||
const viewSource = fs.readFileSync(viewFile, 'utf8')
|
||||
const toolbarSource = fs.readFileSync(toolbarFile, 'utf8')
|
||||
const trendPanelSource = fs.readFileSync(trendPanelFile, 'utf8')
|
||||
const optionsSource = fs.readFileSync(optionsFile, 'utf8')
|
||||
const interfaceSource = fs.readFileSync(interfaceFile, 'utf8')
|
||||
|
||||
const expectations = [
|
||||
@@ -124,10 +126,16 @@ const expectations = [
|
||||
/key:\s*'marker-view'[\s\S]*mark[\s\S]*fullscreen[\s\S]*key:\s*'export'/
|
||||
],
|
||||
['trend tools third group contains image and data downloads', /key:\s*'export'[\s\S]*download-image[\s\S]*download-data/],
|
||||
['parse type supports image display value', /export\s+type\s+ParseType\s*=\s*0\s*\|\s*1\s*\|\s*2\s*\|\s*3\s*\|\s*4/],
|
||||
['parse type options include image display', /label:\s*'图片展示'\s*,\s*value:\s*4/],
|
||||
['parse type change accepts image display value', /value\s*===\s*0[\s\S]*value\s*===\s*1[\s\S]*value\s*===\s*2[\s\S]*value\s*===\s*3[\s\S]*value\s*===\s*4/],
|
||||
['trend tool group separator is dashed vertical line', /\.trend-tool-group\s*\+\s*\.trend-tool-group[\s\S]*border-left:\s*1px\s+dashed/]
|
||||
]
|
||||
|
||||
const combinedSource = `${viewSource}\n${toolbarSource}\n${trendPanelSource}\n${interfaceSource}`
|
||||
const typesFile = path.join(currentDir, 'components', 'types.ts')
|
||||
const typesSource = fs.readFileSync(typesFile, 'utf8')
|
||||
|
||||
const combinedSource = `${viewSource}\n${toolbarSource}\n${trendPanelSource}\n${optionsSource}\n${interfaceSource}\n${typesSource}`
|
||||
const failures = expectations.filter(([, pattern, shouldBeMissing]) => {
|
||||
const exists = pattern.test(combinedSource)
|
||||
return shouldBeMissing ? exists : !exists
|
||||
|
||||
@@ -247,7 +247,7 @@ const handleValueModeChange = (value: string | number | boolean | undefined) =>
|
||||
}
|
||||
|
||||
const handleParseTypeChange = (value: string | number | boolean | undefined) => {
|
||||
if (value === 0 || value === 1 || value === 2 || value === 3) {
|
||||
if (value === 0 || value === 1 || value === 2 || value === 3 || value === 4) {
|
||||
emit('update:activeParseType', value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ export type TrendTabValue = 'instant' | 'rms'
|
||||
export type ValueMode = 'primary' | 'secondary'
|
||||
export type DisplayMode = 'single-channel' | 'multi-channel'
|
||||
export type ChannelSelectValue = number | 'all'
|
||||
export type ParseType = 0 | 1 | 2 | 3
|
||||
export type ParseType = 0 | 1 | 2 | 3 | 4
|
||||
export type TrendToolAction =
|
||||
| 'x-zoom-in'
|
||||
| 'x-zoom-out'
|
||||
@@ -77,3 +77,33 @@ export interface TrendChartClickPayload {
|
||||
dataIndex: number
|
||||
axisValue: string | number
|
||||
}
|
||||
|
||||
export interface WaveformSeriesItem {
|
||||
name: string
|
||||
data: number[]
|
||||
}
|
||||
|
||||
export interface WaveformTrendPayload {
|
||||
timeLabels: string[]
|
||||
unit: string
|
||||
min?: number
|
||||
max?: number
|
||||
series: WaveformSeriesItem[]
|
||||
}
|
||||
|
||||
export interface TrendChartLayoutOptions {
|
||||
showTimeAxis?: boolean
|
||||
showMarkerLabel?: boolean
|
||||
yAxisSplitCount?: number
|
||||
}
|
||||
|
||||
export interface TrendZoomRange {
|
||||
start: number
|
||||
end: number
|
||||
}
|
||||
|
||||
export interface TrendMarker {
|
||||
name: 'T1' | 'T2'
|
||||
dataIndex: number
|
||||
axisValue: string | number
|
||||
}
|
||||
|
||||
@@ -57,7 +57,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
import dayjs from 'dayjs'
|
||||
import html2canvas from 'html2canvas'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { parseComtradeApi, parseComtradeVectorApi } from '@/api/tools/waveform'
|
||||
@@ -76,47 +75,38 @@ import type {
|
||||
SummaryItem,
|
||||
TrendChartClickPayload,
|
||||
TrendChartZoomPayload,
|
||||
TrendChartLayoutOptions,
|
||||
TrendMarker,
|
||||
TrendToolAction,
|
||||
TrendTabValue,
|
||||
TrendZoomRange,
|
||||
ValueMode,
|
||||
WaveformDetailOption,
|
||||
WaveformRatioValue
|
||||
WaveformRatioValue,
|
||||
WaveformSeriesItem,
|
||||
WaveformTrendPayload
|
||||
} from './components/types'
|
||||
import {
|
||||
displayModeOptions,
|
||||
parseTypeOptions,
|
||||
trendLabelMap,
|
||||
trendTabs,
|
||||
valueModeLabelMap,
|
||||
valueModeOptions
|
||||
} from './utils/options'
|
||||
import { axisLineColor, axisTextColor, defaultPhaseColors } from './utils/theme'
|
||||
import {
|
||||
formatNumber,
|
||||
formatTrendTimeLabel,
|
||||
formatWaveformTime,
|
||||
getWaveformParseErrorMessage,
|
||||
safeNumber
|
||||
} from './utils/format'
|
||||
|
||||
defineOptions({
|
||||
name: 'WaveformView'
|
||||
})
|
||||
|
||||
interface WaveformSeriesItem {
|
||||
name: string
|
||||
data: number[]
|
||||
}
|
||||
|
||||
interface WaveformTrendPayload {
|
||||
timeLabels: string[]
|
||||
unit: string
|
||||
min?: number
|
||||
max?: number
|
||||
series: WaveformSeriesItem[]
|
||||
}
|
||||
|
||||
interface TrendChartLayoutOptions {
|
||||
showTimeAxis?: boolean
|
||||
showMarkerLabel?: boolean
|
||||
yAxisSplitCount?: number
|
||||
}
|
||||
|
||||
interface TrendZoomRange {
|
||||
start: number
|
||||
end: number
|
||||
}
|
||||
|
||||
interface TrendMarker {
|
||||
name: 'T1' | 'T2'
|
||||
dataIndex: number
|
||||
axisValue: string | number
|
||||
}
|
||||
|
||||
const activeTrendTab = ref<TrendTabValue>('instant')
|
||||
const activeValueMode = ref<ValueMode>('primary')
|
||||
const activeDisplayMode = ref<DisplayMode>('single-channel')
|
||||
@@ -140,76 +130,11 @@ const trendYZoomScale = ref(1)
|
||||
const activeTrendInteractionMode = ref<'none' | 'box-zoom' | 'pan' | 'mark'>('none')
|
||||
const trendMarkers = ref<TrendMarker[]>([])
|
||||
|
||||
const trendTabs: LabelValueOption<TrendTabValue>[] = [
|
||||
{ value: 'instant', label: '瞬时波形' },
|
||||
{ value: 'rms', label: 'RMS 波形' }
|
||||
]
|
||||
|
||||
const valueModeOptions: LabelValueOption<ValueMode>[] = [
|
||||
{ label: '一次值', value: 'primary' },
|
||||
{ label: '二次值', value: 'secondary' }
|
||||
]
|
||||
|
||||
const displayModeOptions: LabelValueOption<DisplayMode>[] = [
|
||||
{ label: '单通道', value: 'single-channel' },
|
||||
{ label: '多通道', value: 'multi-channel' }
|
||||
]
|
||||
|
||||
const parseTypeOptions: LabelValueOption<ParseType>[] = [
|
||||
{ label: '高级算法(32-128)', value: 0 },
|
||||
{ label: '普通展示(多采样率取最小)', value: 1 },
|
||||
{ label: 'App抽点(32)', value: 2 },
|
||||
{ label: '原始波形', value: 3 }
|
||||
]
|
||||
|
||||
const trendLabelMap: Record<TrendTabValue, string> = {
|
||||
instant: '瞬时波形',
|
||||
rms: 'RMS 波形'
|
||||
}
|
||||
|
||||
const valueModeLabelMap: Record<ValueMode, string> = {
|
||||
primary: '一次值',
|
||||
secondary: '二次值'
|
||||
}
|
||||
|
||||
const readThemeColor = (name: string, fallback: string) => {
|
||||
if (typeof window === 'undefined') return fallback
|
||||
const value = getComputedStyle(document.documentElement).getPropertyValue(name).trim()
|
||||
return value || fallback
|
||||
}
|
||||
|
||||
const phaseColors = {
|
||||
a: readThemeColor('--cn-color-phase-a', '#daa520'),
|
||||
b: readThemeColor('--cn-color-phase-b', '#2e8b57'),
|
||||
c: readThemeColor('--cn-color-phase-c', '#a52a2a')
|
||||
}
|
||||
|
||||
const defaultPhaseColors = [phaseColors.a, phaseColors.b, phaseColors.c]
|
||||
const axisTextColor = readThemeColor('--el-text-color-regular', '#606266')
|
||||
const axisLineColor = readThemeColor('--el-border-color', '#dcdfe6')
|
||||
|
||||
const selectedWaveformFileName = computed(() => {
|
||||
const fileNames = [selectedCfgFile.value?.name, selectedDatFile.value?.name].filter(Boolean)
|
||||
return fileNames.join(' / ')
|
||||
})
|
||||
|
||||
const getWaveformParseErrorMessage = (error: unknown) => {
|
||||
if (!error || typeof error !== 'object') {
|
||||
return '波形解析失败,请检查 cfg 和 dat 文件内容'
|
||||
}
|
||||
|
||||
const businessError = error as {
|
||||
message?: string
|
||||
response?: {
|
||||
data?: {
|
||||
message?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return businessError.response?.data?.message || businessError.message || '波形解析失败,请检查 cfg 和 dat 文件内容'
|
||||
}
|
||||
|
||||
const hasParsedWaveform = computed(() => !!waveformParseResult.value?.waveData)
|
||||
|
||||
const buildSeriesPoints = (list: number[][] | undefined, valueIndex: number) => {
|
||||
@@ -333,37 +258,6 @@ const getValueScale = (detail: Waveform.WaveDataDetail | null) => {
|
||||
|
||||
const activeValueScale = computed(() => getValueScale(activeWaveDetail.value))
|
||||
|
||||
const safeNumber = (value: unknown) => {
|
||||
const numberValue = Number(value)
|
||||
return Number.isFinite(numberValue) ? numberValue : 0
|
||||
}
|
||||
|
||||
const formatNumber = (value: unknown, fractionDigits = 3) => {
|
||||
const numberValue = Number(value)
|
||||
|
||||
if (!Number.isFinite(numberValue)) return '--'
|
||||
if (Number.isInteger(numberValue)) return `${numberValue}`
|
||||
|
||||
return `${Number(numberValue.toFixed(fractionDigits))}`
|
||||
}
|
||||
|
||||
const formatTrendTimeLabel = (value: unknown) => {
|
||||
const numberValue = Number(value)
|
||||
|
||||
if (!Number.isFinite(numberValue)) {
|
||||
return value === undefined || value === null || value === '' ? '--' : `${value}`
|
||||
}
|
||||
|
||||
return numberValue.toFixed(3)
|
||||
}
|
||||
|
||||
const formatWaveformTime = (value?: string) => {
|
||||
if (!value) return '--'
|
||||
|
||||
const parsedValue = dayjs(value)
|
||||
return parsedValue.isValid() ? parsedValue.format('YYYY-MM-DD HH:mm:ss.SSS') : value
|
||||
}
|
||||
|
||||
const buildChannelLabel = (detail: Waveform.WaveDataDetail, index: number) => {
|
||||
if (detail.title) return detail.title
|
||||
if (detail.channelName && detail.unit) return `${detail.channelName} (${detail.unit})`
|
||||
@@ -1192,7 +1086,7 @@ const handleTrendToolAction = async (action: TrendToolAction) => {
|
||||
await downloadTrendImage()
|
||||
break
|
||||
case 'download-data':
|
||||
downloadTrendData()
|
||||
await downloadTrendData()
|
||||
break
|
||||
default:
|
||||
break
|
||||
@@ -1375,9 +1269,7 @@ const buildTrendExportFileName = (extension: string, includeTrendLabel = true) =
|
||||
}
|
||||
|
||||
const downloadTrendImage = async () => {
|
||||
await nextTick()
|
||||
|
||||
const targetElement = document.querySelector('.waveform-trend-export-target') as HTMLElement | null
|
||||
const targetElement = await waitTrendExportTargetElement()
|
||||
|
||||
if (!targetElement) {
|
||||
ElMessage.warning('未找到可导出的趋势图区域')
|
||||
@@ -1410,6 +1302,16 @@ interface TrendExportPayloadGroup {
|
||||
|
||||
type TrendExportMetric = 'instant' | 'rms'
|
||||
|
||||
interface TrendExportImage {
|
||||
metric: TrendExportMetric
|
||||
title: string
|
||||
imageUrl: string
|
||||
}
|
||||
|
||||
interface TrendExportMergedImage {
|
||||
imageUrl: string
|
||||
}
|
||||
|
||||
interface TrendExportColumn {
|
||||
metric: TrendExportMetric
|
||||
channelName: string
|
||||
@@ -1428,10 +1330,141 @@ const trendExportMetricLabelMap: Record<TrendExportMetric, string> = {
|
||||
rms: 'RMS值'
|
||||
}
|
||||
|
||||
const trendExportImageTitleMap: Record<TrendExportMetric, string> = {
|
||||
instant: '瞬时图',
|
||||
rms: 'RMS图'
|
||||
}
|
||||
|
||||
const hasTrendExportSeries = (group: TrendExportPayloadGroup) => {
|
||||
return group.instantPayload.series.length > 0 || group.rmsPayload.series.length > 0
|
||||
}
|
||||
|
||||
const waitAnimationFrame = () => {
|
||||
return new Promise<void>(resolve => {
|
||||
requestAnimationFrame(() => resolve())
|
||||
})
|
||||
}
|
||||
|
||||
const waitTrendExportTargetElement = async () => {
|
||||
await nextTick()
|
||||
await waitAnimationFrame()
|
||||
|
||||
return document.querySelector('.waveform-trend-export-target') as HTMLElement | null
|
||||
}
|
||||
|
||||
const drawTrendExportImageTitle = (sourceCanvas: HTMLCanvasElement, title: string) => {
|
||||
const titleHeight = 52
|
||||
const targetCanvas = document.createElement('canvas')
|
||||
const context = targetCanvas.getContext('2d')
|
||||
|
||||
targetCanvas.width = sourceCanvas.width
|
||||
targetCanvas.height = sourceCanvas.height + titleHeight
|
||||
|
||||
if (!context) return sourceCanvas.toDataURL('image/png')
|
||||
|
||||
context.fillStyle = '#ffffff'
|
||||
context.fillRect(0, 0, targetCanvas.width, targetCanvas.height)
|
||||
context.fillStyle = '#303133'
|
||||
context.font = '600 24px sans-serif'
|
||||
context.textBaseline = 'middle'
|
||||
context.fillText(title, 24, titleHeight / 2)
|
||||
context.drawImage(sourceCanvas, 0, titleHeight)
|
||||
|
||||
return targetCanvas.toDataURL('image/png')
|
||||
}
|
||||
|
||||
const captureTrendExportTargetImage = async (title: string) => {
|
||||
const targetElement = await waitTrendExportTargetElement()
|
||||
|
||||
if (!targetElement) return ''
|
||||
|
||||
const canvas = await html2canvas(targetElement, {
|
||||
backgroundColor: '#ffffff',
|
||||
scale: window.devicePixelRatio || 1,
|
||||
useCORS: true
|
||||
})
|
||||
|
||||
return drawTrendExportImageTitle(canvas, title)
|
||||
}
|
||||
|
||||
const captureTrendExportImages = async (): Promise<TrendExportImage[]> => {
|
||||
const previousTrendTab = activeTrendTab.value
|
||||
const previousXZoomRange = { ...trendXZoomRange.value }
|
||||
const previousYZoomScale = trendYZoomScale.value
|
||||
const previousInteractionMode = activeTrendInteractionMode.value
|
||||
const previousMarkers = [...trendMarkers.value]
|
||||
const metrics: TrendExportMetric[] = ['instant', 'rms']
|
||||
|
||||
try {
|
||||
const images: TrendExportImage[] = []
|
||||
|
||||
for (const metric of metrics) {
|
||||
// 导出数据时临时切换趋势页签截图,保证“图片”sheet 同时包含瞬时图和 RMS 图。
|
||||
activeTrendTab.value = metric
|
||||
|
||||
const imageUrl = await captureTrendExportTargetImage(trendExportMetricLabelMap[metric])
|
||||
if (!imageUrl) continue
|
||||
|
||||
images.push({
|
||||
metric,
|
||||
title: trendExportImageTitleMap[metric],
|
||||
imageUrl
|
||||
})
|
||||
}
|
||||
|
||||
return images
|
||||
} finally {
|
||||
activeTrendTab.value = previousTrendTab
|
||||
await nextTick()
|
||||
trendXZoomRange.value = previousXZoomRange
|
||||
trendYZoomScale.value = previousYZoomScale
|
||||
activeTrendInteractionMode.value = previousInteractionMode
|
||||
trendMarkers.value = previousMarkers
|
||||
}
|
||||
}
|
||||
|
||||
const loadTrendExportImage = (imageUrl: string) => {
|
||||
return new Promise<HTMLImageElement>((resolve, reject) => {
|
||||
const image = new Image()
|
||||
|
||||
image.onload = () => resolve(image)
|
||||
image.onerror = () => reject(new Error('趋势图图片加载失败'))
|
||||
image.src = imageUrl
|
||||
})
|
||||
}
|
||||
|
||||
const mergeTrendExportImages = async (images: TrendExportImage[]): Promise<TrendExportMergedImage | null> => {
|
||||
const loadedImages = await Promise.all(images.map(item => loadTrendExportImage(item.imageUrl)))
|
||||
if (!loadedImages.length) return null
|
||||
|
||||
const imageGap = 24
|
||||
const targetCanvas = document.createElement('canvas')
|
||||
const context = targetCanvas.getContext('2d')
|
||||
const targetWidth = Math.max(...loadedImages.map(item => item.naturalWidth || item.width))
|
||||
const targetHeight =
|
||||
loadedImages.reduce((height, item) => height + (item.naturalHeight || item.height), 0) +
|
||||
imageGap * Math.max(loadedImages.length - 1, 0)
|
||||
|
||||
targetCanvas.width = targetWidth
|
||||
targetCanvas.height = targetHeight
|
||||
|
||||
if (!context) return null
|
||||
|
||||
context.fillStyle = '#ffffff'
|
||||
context.fillRect(0, 0, targetCanvas.width, targetCanvas.height)
|
||||
|
||||
let offsetTop = 0
|
||||
|
||||
loadedImages.forEach((image, index) => {
|
||||
context.drawImage(image, 0, offsetTop)
|
||||
offsetTop += (image.naturalHeight || image.height) + (index === loadedImages.length - 1 ? 0 : imageGap)
|
||||
})
|
||||
|
||||
return {
|
||||
imageUrl: targetCanvas.toDataURL('image/png')
|
||||
}
|
||||
}
|
||||
|
||||
const buildTrendExportPayloadGroup = (detail: Waveform.WaveDataDetail | null, index: number) => {
|
||||
const scale = getValueScale(detail)
|
||||
|
||||
@@ -1596,7 +1629,20 @@ const buildTrendExportRows = (timeLabels: string[], columns: TrendExportColumn[]
|
||||
.join('')
|
||||
}
|
||||
|
||||
const buildTrendExportExcelHtml = (timeLabels: string[], columns: TrendExportColumn[]) => {
|
||||
const buildTrendExportWorkbookXml = () => {
|
||||
return [
|
||||
'<xml>',
|
||||
'<x:ExcelWorkbook>',
|
||||
'<x:ExcelWorksheets>',
|
||||
'<x:ExcelWorksheet><x:Name>数据</x:Name><x:WorksheetSource HRef="data.htm"/><x:WorksheetOptions><x:DisplayGridlines/></x:WorksheetOptions></x:ExcelWorksheet>',
|
||||
'<x:ExcelWorksheet><x:Name>图片</x:Name><x:WorksheetSource HRef="image.htm"/><x:WorksheetOptions><x:DisplayGridlines/></x:WorksheetOptions></x:ExcelWorksheet>',
|
||||
'</x:ExcelWorksheets>',
|
||||
'</x:ExcelWorkbook>',
|
||||
'</xml>'
|
||||
].join('')
|
||||
}
|
||||
|
||||
const buildTrendExportDataSheet = (timeLabels: string[], columns: TrendExportColumn[]) => {
|
||||
return [
|
||||
'<!DOCTYPE html>',
|
||||
'<html>',
|
||||
@@ -1628,7 +1674,102 @@ const buildTrendExportExcelHtml = (timeLabels: string[], columns: TrendExportCol
|
||||
].join('')
|
||||
}
|
||||
|
||||
const downloadTrendData = () => {
|
||||
const buildTrendExportImageSheet = () => {
|
||||
return [
|
||||
'<!DOCTYPE html>',
|
||||
'<html>',
|
||||
'<head>',
|
||||
'<meta charset="UTF-8" />',
|
||||
'<style>',
|
||||
'table{border-collapse:collapse;}',
|
||||
'th,td{border:1px solid #999;padding:4px 8px;white-space:nowrap;}',
|
||||
'th{background:#f5f7fa;font-weight:700;text-align:left;font-size:16px;}',
|
||||
'.image-cell{padding:12px;text-align:left;}',
|
||||
'.image-cell img{width:1100px;height:auto;display:block;}',
|
||||
'</style>',
|
||||
'</head>',
|
||||
'<body>',
|
||||
'<table class="image-table">',
|
||||
'<tbody>',
|
||||
'<tr><td class="image-cell"><img src="trend-images.png" alt="瞬时图和RMS图" /></td></tr>',
|
||||
'</tbody>',
|
||||
'</table>',
|
||||
'</body>',
|
||||
'</html>'
|
||||
].join('')
|
||||
}
|
||||
|
||||
const buildTrendExportWorkbookHtml = () => {
|
||||
return [
|
||||
'<!DOCTYPE html>',
|
||||
'<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel">',
|
||||
'<head>',
|
||||
'<meta charset="UTF-8" />',
|
||||
buildTrendExportWorkbookXml(),
|
||||
'</head>',
|
||||
'<body></body>',
|
||||
'</html>'
|
||||
].join('')
|
||||
}
|
||||
|
||||
const getTrendExportImageBase64 = (imageUrl: string) => {
|
||||
return imageUrl.replace(/^data:image\/png;base64,/, '')
|
||||
}
|
||||
|
||||
const splitBase64Lines = (value: string) => {
|
||||
return value.replace(/(.{76})/g, '$1\r\n')
|
||||
}
|
||||
|
||||
const buildTrendExportMhtmlPart = (boundary: string, contentType: string, contentLocation: string, content: string) => {
|
||||
return [`--${boundary}`, `Content-Type: ${contentType}`, `Content-Location: ${contentLocation}`, '', content].join(
|
||||
'\r\n'
|
||||
)
|
||||
}
|
||||
|
||||
const buildTrendExportImagePart = (boundary: string, image: TrendExportMergedImage) => {
|
||||
return [
|
||||
`--${boundary}`,
|
||||
'Content-Type: image/png',
|
||||
'Content-Transfer-Encoding: base64',
|
||||
'Content-Location: trend-images.png',
|
||||
'',
|
||||
splitBase64Lines(getTrendExportImageBase64(image.imageUrl))
|
||||
].join('\r\n')
|
||||
}
|
||||
|
||||
const buildTrendExportExcelHtml = (
|
||||
timeLabels: string[],
|
||||
columns: TrendExportColumn[],
|
||||
image: TrendExportMergedImage
|
||||
) => {
|
||||
const boundary = `----=_NextPart_${Date.now()}_${Math.random().toString(16).slice(2)}`
|
||||
const parts = [
|
||||
buildTrendExportMhtmlPart(boundary, 'text/html; charset="utf-8"', 'workbook.htm', buildTrendExportWorkbookHtml()),
|
||||
buildTrendExportMhtmlPart(
|
||||
boundary,
|
||||
'text/html; charset="utf-8"',
|
||||
'data.htm',
|
||||
buildTrendExportDataSheet(timeLabels, columns)
|
||||
),
|
||||
buildTrendExportMhtmlPart(
|
||||
boundary,
|
||||
'text/html; charset="utf-8"',
|
||||
'image.htm',
|
||||
buildTrendExportImageSheet()
|
||||
),
|
||||
buildTrendExportImagePart(boundary, image),
|
||||
`--${boundary}--`
|
||||
]
|
||||
|
||||
return [
|
||||
'MIME-Version: 1.0',
|
||||
`Content-Type: multipart/related; boundary="${boundary}"; type="text/html"`,
|
||||
'',
|
||||
...parts
|
||||
].join('\r\n')
|
||||
}
|
||||
|
||||
const downloadTrendData = async () => {
|
||||
if (!hasWaveformData.value) {
|
||||
ElMessage.warning('暂无可导出的波形数据')
|
||||
return
|
||||
@@ -1650,8 +1791,22 @@ const downloadTrendData = () => {
|
||||
return
|
||||
}
|
||||
|
||||
const excelContent = buildTrendExportExcelHtml(timeLabels, exportColumns)
|
||||
const blob = new Blob([`\uFEFF${excelContent}`], { type: 'application/vnd.ms-excel;charset=utf-8;' })
|
||||
const exportImages = await captureTrendExportImages()
|
||||
|
||||
if (exportImages.length < 2) {
|
||||
ElMessage.warning('趋势图图片生成失败,请稍后重试')
|
||||
return
|
||||
}
|
||||
|
||||
const exportImage = await mergeTrendExportImages(exportImages)
|
||||
|
||||
if (!exportImage) {
|
||||
ElMessage.warning('趋势图图片生成失败,请稍后重试')
|
||||
return
|
||||
}
|
||||
|
||||
const excelContent = buildTrendExportExcelHtml(timeLabels, exportColumns, exportImage)
|
||||
const blob = new Blob([excelContent], { type: 'application/vnd.ms-excel;charset=utf-8;' })
|
||||
const blobUrl = URL.createObjectURL(blob)
|
||||
const exportFile = document.createElement('a')
|
||||
const fileName = buildTrendExportFileName('xls', false)
|
||||
|
||||
49
frontend/src/views/tools/waveform/utils/format.ts
Normal file
49
frontend/src/views/tools/waveform/utils/format.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
export const getWaveformParseErrorMessage = (error: unknown) => {
|
||||
if (!error || typeof error !== 'object') {
|
||||
return '波形解析失败,请检查 cfg 和 dat 文件内容'
|
||||
}
|
||||
|
||||
const businessError = error as {
|
||||
message?: string
|
||||
response?: {
|
||||
data?: {
|
||||
message?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return businessError.response?.data?.message || businessError.message || '波形解析失败,请检查 cfg 和 dat 文件内容'
|
||||
}
|
||||
|
||||
export const safeNumber = (value: unknown) => {
|
||||
const numberValue = Number(value)
|
||||
return Number.isFinite(numberValue) ? numberValue : 0
|
||||
}
|
||||
|
||||
export const formatNumber = (value: unknown, fractionDigits = 3) => {
|
||||
const numberValue = Number(value)
|
||||
|
||||
if (!Number.isFinite(numberValue)) return '--'
|
||||
if (Number.isInteger(numberValue)) return `${numberValue}`
|
||||
|
||||
return `${Number(numberValue.toFixed(fractionDigits))}`
|
||||
}
|
||||
|
||||
export const formatTrendTimeLabel = (value: unknown) => {
|
||||
const numberValue = Number(value)
|
||||
|
||||
if (!Number.isFinite(numberValue)) {
|
||||
return value === undefined || value === null || value === '' ? '--' : `${value}`
|
||||
}
|
||||
|
||||
return numberValue.toFixed(3)
|
||||
}
|
||||
|
||||
export const formatWaveformTime = (value?: string) => {
|
||||
if (!value) return '--'
|
||||
|
||||
const parsedValue = dayjs(value)
|
||||
return parsedValue.isValid() ? parsedValue.format('YYYY-MM-DD HH:mm:ss.SSS') : value
|
||||
}
|
||||
34
frontend/src/views/tools/waveform/utils/options.ts
Normal file
34
frontend/src/views/tools/waveform/utils/options.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { DisplayMode, LabelValueOption, ParseType, TrendTabValue, ValueMode } from '../components/types'
|
||||
|
||||
export const trendTabs: LabelValueOption<TrendTabValue>[] = [
|
||||
{ value: 'instant', label: '瞬时波形' },
|
||||
{ value: 'rms', label: 'RMS 波形' }
|
||||
]
|
||||
|
||||
export const valueModeOptions: LabelValueOption<ValueMode>[] = [
|
||||
{ label: '一次值', value: 'primary' },
|
||||
{ label: '二次值', value: 'secondary' }
|
||||
]
|
||||
|
||||
export const displayModeOptions: LabelValueOption<DisplayMode>[] = [
|
||||
{ label: '单通道', value: 'single-channel' },
|
||||
{ label: '多通道', value: 'multi-channel' }
|
||||
]
|
||||
|
||||
export const parseTypeOptions: LabelValueOption<ParseType>[] = [
|
||||
{ label: '高级算法(32-128)', value: 0 },
|
||||
{ label: '普通展示(多采样率取最小)', value: 1 },
|
||||
{ label: 'App抽点(32)', value: 2 },
|
||||
{ label: '原始波形', value: 3 },
|
||||
{ label: '图片展示', value: 4 }
|
||||
]
|
||||
|
||||
export const trendLabelMap: Record<TrendTabValue, string> = {
|
||||
instant: '瞬时波形',
|
||||
rms: 'RMS 波形'
|
||||
}
|
||||
|
||||
export const valueModeLabelMap: Record<ValueMode, string> = {
|
||||
primary: '一次值',
|
||||
secondary: '二次值'
|
||||
}
|
||||
15
frontend/src/views/tools/waveform/utils/theme.ts
Normal file
15
frontend/src/views/tools/waveform/utils/theme.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
const readThemeColor = (name: string, fallback: string) => {
|
||||
if (typeof window === 'undefined') return fallback
|
||||
const value = getComputedStyle(document.documentElement).getPropertyValue(name).trim()
|
||||
return value || fallback
|
||||
}
|
||||
|
||||
const phaseColors = {
|
||||
a: readThemeColor('--cn-color-phase-a', '#daa520'),
|
||||
b: readThemeColor('--cn-color-phase-b', '#2e8b57'),
|
||||
c: readThemeColor('--cn-color-phase-c', '#a52a2a')
|
||||
}
|
||||
|
||||
export const defaultPhaseColors = [phaseColors.a, phaseColors.b, phaseColors.c]
|
||||
export const axisTextColor = readThemeColor('--el-text-color-regular', '#606266')
|
||||
export const axisLineColor = readThemeColor('--el-border-color', '#dcdfe6')
|
||||
Reference in New Issue
Block a user