feat(auth): 优化权限模块菜单数据处理逻辑
- 添加showMenuList、flatMenuList和breadcrumbList状态字段 - 修改getter方法直接返回缓存的状态数据 - 新增refreshDerivedMenus方法统一处理菜单衍生数据计算 - 在重置授权存储时清理新增的菜单相关状态 - 避免每次路由跳转时重复深拷贝整个菜单树结构 feat(checksquare): 完善校验功能组件和业务逻辑 - 新增测量点对话框组件用于显示监测点详细信息 - 添加校验台账工具函数解析测量点详情 - 实现任务表格删除功能包括确认提示和数据刷新 - 更新任务表格将缺失率字段替换为数据完整性字段 - 重构详情面板使用标签页展示不同类型的校验详情 - 优化摘要表格样式包括紧凑布局和危险颜色标识 - 统一详情对话框尺寸样式保持界面一致性 - 实现数据完整性字段的百分比单位去除处理 refactor(influxdb): 简化数据库启动流程移除命令行包装器 - 直接通过influxd.exe启动InfluxDB服务 - 移除对cmd.exe包装器的依赖和进程ID记录 - 保持进程管理和停止功能的完整性
This commit is contained in:
Binary file not shown.
@@ -29,6 +29,12 @@ export const createSteadyChecksquareTask = (params: SteadyDataView.SteadyChecksq
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const deleteSteadyChecksquareTasks = (taskIds: SteadyDataView.SteadyChecksquareDeleteParams) => {
|
||||||
|
return http.post<boolean>('/steady/data-view/checksquare/delete', taskIds, {
|
||||||
|
loading: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export const getSteadyChecksquareDetail = (taskId: string) => {
|
export const getSteadyChecksquareDetail = (taskId: string) => {
|
||||||
return http.get<SteadyDataView.SteadyChecksquareQueryResult>('/steady/data-view/checksquare/detail', { taskId }, { loading: false })
|
return http.get<SteadyDataView.SteadyChecksquareQueryResult>('/steady/data-view/checksquare/detail', { taskId }, { loading: false })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,6 +103,8 @@ export namespace SteadyDataView {
|
|||||||
timeEnd: string
|
timeEnd: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SteadyChecksquareDeleteParams = string[]
|
||||||
|
|
||||||
export interface SteadyChecksquareTask {
|
export interface SteadyChecksquareTask {
|
||||||
taskId: string
|
taskId: string
|
||||||
taskNo?: string
|
taskNo?: string
|
||||||
@@ -114,6 +116,7 @@ export namespace SteadyDataView {
|
|||||||
taskStatus?: 'SUCCESS' | string
|
taskStatus?: 'SUCCESS' | string
|
||||||
itemCount?: number
|
itemCount?: number
|
||||||
abnormalItemCount?: number
|
abnormalItemCount?: number
|
||||||
|
minDataIntegrity?: number | null
|
||||||
maxMissingRate?: number | null
|
maxMissingRate?: number | null
|
||||||
createTime?: string
|
createTime?: string
|
||||||
}
|
}
|
||||||
@@ -146,6 +149,8 @@ export namespace SteadyDataView {
|
|||||||
expectedPointCount?: number
|
expectedPointCount?: number
|
||||||
actualPointCount?: number
|
actualPointCount?: number
|
||||||
missingPointCount?: number
|
missingPointCount?: number
|
||||||
|
dataIntegrity?: number | null
|
||||||
|
dataIntegrityText?: string | null
|
||||||
missingRate?: number | null
|
missingRate?: number | null
|
||||||
missingRateText?: string | null
|
missingRateText?: string | null
|
||||||
maxContinuousMissingMinutes?: number
|
maxContinuousMissingMinutes?: number
|
||||||
@@ -167,6 +172,8 @@ export namespace SteadyDataView {
|
|||||||
expectedPointCount?: number
|
expectedPointCount?: number
|
||||||
actualPointCount?: number
|
actualPointCount?: number
|
||||||
missingPointCount?: number
|
missingPointCount?: number
|
||||||
|
dataIntegrity?: number | null
|
||||||
|
dataIntegrityText?: string | null
|
||||||
missingRate?: number | null
|
missingRate?: number | null
|
||||||
missingRateText?: string | null
|
missingRateText?: string | null
|
||||||
maxContinuousMissingMinutes?: number
|
maxContinuousMissingMinutes?: number
|
||||||
|
|||||||
@@ -9,6 +9,22 @@ const buildIcdFormData = (icdFile: File) => {
|
|||||||
return formData
|
return formData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const listDeviceTypesApi = () => {
|
||||||
|
return http.get<MmsMapping.DeviceType[]>('/api/mms-mapping/dev-types')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createDeviceTypeApi = (params: MmsMapping.CreateDeviceTypeRequest) => {
|
||||||
|
return http.post<MmsMapping.DeviceType>('/api/mms-mapping/dev-types', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const saveIcdCheckResultApi = (id: string, params: MmsMapping.SaveIcdCheckResultRequest) => {
|
||||||
|
return http.post<boolean>(`/api/mms-mapping/dev-types/${id}/icd-check-result`, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pqdifCheckApi = (id: string) => {
|
||||||
|
return http.post<MmsMapping.PqdifCheckPlaceholder>(`/api/mms-mapping/dev-types/${id}/pqdif-check`)
|
||||||
|
}
|
||||||
|
|
||||||
export const getIcdApi = (params: MmsMapping.GetIcdParams) => {
|
export const getIcdApi = (params: MmsMapping.GetIcdParams) => {
|
||||||
const formData = buildIcdFormData(params.icdFile)
|
const formData = buildIcdFormData(params.icdFile)
|
||||||
|
|
||||||
|
|||||||
@@ -128,4 +128,37 @@ export namespace MmsMapping {
|
|||||||
version: string
|
version: string
|
||||||
author: string
|
author: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DeviceType {
|
||||||
|
id?: string
|
||||||
|
name?: string
|
||||||
|
icdId?: string
|
||||||
|
icdName?: string
|
||||||
|
icdPath?: string
|
||||||
|
icdResult?: number
|
||||||
|
icdMsg?: string
|
||||||
|
reportName?: string
|
||||||
|
canCheckIcd?: boolean
|
||||||
|
canCheckPqdif?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateDeviceTypeRequest {
|
||||||
|
name: string
|
||||||
|
icdId?: string
|
||||||
|
icdName?: string
|
||||||
|
icdPath?: string
|
||||||
|
reportName?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SaveIcdCheckResultRequest {
|
||||||
|
mappingJson?: string
|
||||||
|
xml?: string
|
||||||
|
result: number
|
||||||
|
msg?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PqdifCheckPlaceholder {
|
||||||
|
status?: string
|
||||||
|
message?: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,12 +89,6 @@ router.beforeEach(async (to, from, next) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
syncHomeStateWithMenus()
|
|
||||||
logRouterPerf('before-each-menu-sync', guardStart, {
|
|
||||||
path: to.path,
|
|
||||||
from: from.path,
|
|
||||||
hasActivateInfo: authStore.activateInfoLoadedGet
|
|
||||||
})
|
|
||||||
await authStore.setRouteName(to.name as string)
|
await authStore.setRouteName(to.name as string)
|
||||||
|
|
||||||
if (!authStore.activateInfoLoadedGet) {
|
if (!authStore.activateInfoLoadedGet) {
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
|
const currentDir = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
const srcRoot = path.resolve(currentDir, '../..')
|
||||||
|
|
||||||
|
const files = {
|
||||||
|
router: path.resolve(srcRoot, 'routers/index.ts'),
|
||||||
|
authStore: path.resolve(srcRoot, 'stores/modules/auth.ts'),
|
||||||
|
utils: path.resolve(srcRoot, 'utils/index.ts')
|
||||||
|
}
|
||||||
|
|
||||||
|
const read = file => fs.readFileSync(file, 'utf8')
|
||||||
|
|
||||||
|
const routerSource = read(files.router)
|
||||||
|
const authStoreSource = read(files.authStore)
|
||||||
|
const utilsSource = read(files.utils)
|
||||||
|
|
||||||
|
const expectations = [
|
||||||
|
{
|
||||||
|
name: 'auth store owns cached flat/show/breadcrumb menu state',
|
||||||
|
pass:
|
||||||
|
/flatMenuList:\s*\[\]/.test(authStoreSource) &&
|
||||||
|
/showMenuList:\s*\[\]/.test(authStoreSource) &&
|
||||||
|
/breadcrumbList:\s*\{\}/.test(authStoreSource)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'auth store refreshes derived menus when auth menu list changes',
|
||||||
|
pass: /refreshDerivedMenus\(\)/.test(authStoreSource) && /this\.refreshDerivedMenus\(\)/.test(authStoreSource)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'menu derivation helpers avoid JSON stringify deep clone',
|
||||||
|
pass:
|
||||||
|
!/getFlatMenuList[\s\S]*JSON\.parse\(JSON\.stringify/.test(utilsSource) &&
|
||||||
|
!/getShowMenuList[\s\S]*JSON\.parse\(JSON\.stringify/.test(utilsSource)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'router beforeEach does not sync home state on every navigation',
|
||||||
|
pass: !/before-each-menu-sync/.test(routerSource)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'router syncs home state after dynamic menu initialization',
|
||||||
|
pass: /syncHomeStateWithMenus\(\)/.test(routerSource) && /first-sync-home-state/.test(routerSource)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const failures = expectations.filter(item => !item.pass)
|
||||||
|
|
||||||
|
if (failures.length) {
|
||||||
|
console.error('Menu navigation performance contract failed:')
|
||||||
|
failures.forEach(item => console.error(`- ${item.name}`))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Menu navigation performance contract passed')
|
||||||
@@ -63,6 +63,16 @@ export const staticRouter: RouteRecordRaw[] = [
|
|||||||
title: 'MMS 映射'
|
title: 'MMS 映射'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/tools/mmsMapping/deviceTypes',
|
||||||
|
name: 'toolMmsMappingDeviceTypes',
|
||||||
|
alias: ['/tools/mmsmapping/deviceTypes', '/tools/mms-mapping/device-types'],
|
||||||
|
component: () => import('@/views/tools/mmsMapping/deviceTypes/index.vue'),
|
||||||
|
meta: {
|
||||||
|
cacheName: 'MmsDeviceTypesView',
|
||||||
|
title: '设备类型校验'
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/tools/addData',
|
path: '/tools/addData',
|
||||||
name: 'toolAddData',
|
name: 'toolAddData',
|
||||||
|
|||||||
@@ -60,6 +60,9 @@ export interface AuthState {
|
|||||||
[key: string]: string[]
|
[key: string]: string[]
|
||||||
}
|
}
|
||||||
authMenuList: Menu.MenuOptions[]
|
authMenuList: Menu.MenuOptions[]
|
||||||
|
showMenuList: Menu.MenuOptions[]
|
||||||
|
flatMenuList: Menu.MenuOptions[]
|
||||||
|
breadcrumbList: { [key: string]: Menu.MenuOptions[] }
|
||||||
showMenuFlag: boolean
|
showMenuFlag: boolean
|
||||||
activateInfo: Activate.ActivationCodePlaintext
|
activateInfo: Activate.ActivationCodePlaintext
|
||||||
activateInfoLoaded: boolean
|
activateInfoLoaded: boolean
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ export const useAuthStore = defineStore(AUTH_STORE_KEY, {
|
|||||||
state: (): AuthState => ({
|
state: (): AuthState => ({
|
||||||
authButtonList: {},
|
authButtonList: {},
|
||||||
authMenuList: [],
|
authMenuList: [],
|
||||||
|
showMenuList: [],
|
||||||
|
flatMenuList: [],
|
||||||
|
breadcrumbList: {},
|
||||||
routeName: '',
|
routeName: '',
|
||||||
showMenuFlag: localStorage.getItem('showMenuFlag') === 'true',
|
showMenuFlag: localStorage.getItem('showMenuFlag') === 'true',
|
||||||
activateInfo: {} as Activate.ActivationCodePlaintext,
|
activateInfo: {} as Activate.ActivationCodePlaintext,
|
||||||
@@ -18,9 +21,9 @@ export const useAuthStore = defineStore(AUTH_STORE_KEY, {
|
|||||||
getters: {
|
getters: {
|
||||||
authButtonListGet: state => state.authButtonList,
|
authButtonListGet: state => state.authButtonList,
|
||||||
authMenuListGet: state => state.authMenuList,
|
authMenuListGet: state => state.authMenuList,
|
||||||
showMenuListGet: state => getShowMenuList(state.authMenuList),
|
showMenuListGet: state => state.showMenuList,
|
||||||
flatMenuListGet: state => getFlatMenuList(state.authMenuList),
|
flatMenuListGet: state => state.flatMenuList,
|
||||||
breadcrumbListGet: state => getAllBreadcrumbList(state.authMenuList),
|
breadcrumbListGet: state => state.breadcrumbList,
|
||||||
showMenuFlagGet: state => state.showMenuFlag,
|
showMenuFlagGet: state => state.showMenuFlag,
|
||||||
activateInfoGet: state => state.activateInfo,
|
activateInfoGet: state => state.activateInfo,
|
||||||
activateInfoLoadedGet: state => state.activateInfoLoaded
|
activateInfoLoadedGet: state => state.activateInfoLoaded
|
||||||
@@ -33,6 +36,13 @@ export const useAuthStore = defineStore(AUTH_STORE_KEY, {
|
|||||||
async getAuthMenuList() {
|
async getAuthMenuList() {
|
||||||
const { data: menuData } = await getAuthMenuListApi()
|
const { data: menuData } = await getAuthMenuListApi()
|
||||||
this.authMenuList = normalizeBusinessMenus(filterBusinessMenus(menuData))
|
this.authMenuList = normalizeBusinessMenus(filterBusinessMenus(menuData))
|
||||||
|
// 菜单派生数据只在菜单源数据变化时重算,避免每次路由跳转都深拷贝整棵菜单。
|
||||||
|
this.refreshDerivedMenus()
|
||||||
|
},
|
||||||
|
refreshDerivedMenus() {
|
||||||
|
this.showMenuList = getShowMenuList(this.authMenuList)
|
||||||
|
this.flatMenuList = getFlatMenuList(this.authMenuList)
|
||||||
|
this.breadcrumbList = getAllBreadcrumbList(this.authMenuList)
|
||||||
},
|
},
|
||||||
async setRouteName(name: string) {
|
async setRouteName(name: string) {
|
||||||
this.routeName = name
|
this.routeName = name
|
||||||
@@ -40,6 +50,9 @@ export const useAuthStore = defineStore(AUTH_STORE_KEY, {
|
|||||||
async resetAuthStore() {
|
async resetAuthStore() {
|
||||||
this.authButtonList = {}
|
this.authButtonList = {}
|
||||||
this.authMenuList = []
|
this.authMenuList = []
|
||||||
|
this.showMenuList = []
|
||||||
|
this.flatMenuList = []
|
||||||
|
this.breadcrumbList = {}
|
||||||
this.routeName = ''
|
this.routeName = ''
|
||||||
this.showMenuFlag = false
|
this.showMenuFlag = false
|
||||||
this.activateInfo = {}
|
this.activateInfo = {}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { isArray, isNumber } from '@/utils/is'
|
import { isArray, isNumber } from '@/utils/is'
|
||||||
import { FieldNamesProps } from '@/components/ProTable/interface'
|
import type { FieldNamesProps } from '@/components/ProTable/interface'
|
||||||
|
|
||||||
const mode = import.meta.env.VITE_ROUTER_MODE
|
const mode = import.meta.env.VITE_ROUTER_MODE
|
||||||
|
|
||||||
@@ -152,8 +152,7 @@ export function getUrlWithParams() {
|
|||||||
* @returns {Array}
|
* @returns {Array}
|
||||||
*/
|
*/
|
||||||
export function getFlatMenuList(menuList: Menu.MenuOptions[]): Menu.MenuOptions[] {
|
export function getFlatMenuList(menuList: Menu.MenuOptions[]): Menu.MenuOptions[] {
|
||||||
const newMenuList: Menu.MenuOptions[] = JSON.parse(JSON.stringify(menuList))
|
return menuList.flatMap(item => [item, ...(item.children?.length ? getFlatMenuList(item.children) : [])])
|
||||||
return newMenuList.flatMap(item => [item, ...(item.children ? getFlatMenuList(item.children) : [])])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -161,12 +160,13 @@ export function getFlatMenuList(menuList: Menu.MenuOptions[]): Menu.MenuOptions[
|
|||||||
* @param {Array} menuList 菜单列表
|
* @param {Array} menuList 菜单列表
|
||||||
* @returns {Array}
|
* @returns {Array}
|
||||||
* */
|
* */
|
||||||
export function getShowMenuList(menuList: Menu.MenuOptions[]) {
|
export function getShowMenuList(menuList: Menu.MenuOptions[]): Menu.MenuOptions[] {
|
||||||
const newMenuList: Menu.MenuOptions[] = JSON.parse(JSON.stringify(menuList))
|
return menuList
|
||||||
return newMenuList.filter(item => {
|
.filter(item => !item.meta?.isHide)
|
||||||
item.children?.length && (item.children = getShowMenuList(item.children))
|
.map(item => ({
|
||||||
return !item.meta?.isHide
|
...item,
|
||||||
})
|
children: item.children?.length ? getShowMenuList(item.children) : item.children
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getFirstMenuPath(menuList: Menu.MenuOptions[]): string {
|
export function getFirstMenuPath(menuList: Menu.MenuOptions[]): string {
|
||||||
|
|||||||
@@ -1,123 +1,131 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="card checksquare-detail">
|
<section class="table-main card checksquare-detail">
|
||||||
<div class="detail-header">
|
|
||||||
<div>
|
|
||||||
<div class="section-title">检测项明细</div>
|
|
||||||
<div class="section-description">
|
|
||||||
{{ selectedItem ? resolveChecksquareRowName(selectedItem) : '请选择总览表中的指标' }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-empty v-if="!selectedItem" description="请选择指标查看明细" />
|
<el-empty v-if="!selectedItem" description="请选择指标查看明细" />
|
||||||
|
|
||||||
<template v-else>
|
<el-tabs v-else v-model="detailType" class="detail-tabs" @tab-change="handleDetailTypeChange">
|
||||||
<div class="stat-grid">
|
<el-tab-pane label="缺数区间" name="SEGMENT">
|
||||||
<div v-for="statType in CHECKSQUARE_STAT_TYPES" :key="statType" class="stat-card">
|
<div class="item-overview">
|
||||||
<span class="stat-name">{{ formatChecksquareStatType(statType) }}</span>
|
<div class="overview-item">
|
||||||
<span class="stat-value">{{ formatStatMissingRate(selectedItem, statType) }}</span>
|
<span class="overview-label">数据完整性</span>
|
||||||
|
<span class="overview-value">
|
||||||
|
{{ formatDataIntegrity(selectedItem.dataIntegrity, selectedItem.dataIntegrityText, selectedItem.missingRate) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="overview-item">
|
||||||
|
<span class="overview-label">期望点数</span>
|
||||||
|
<span class="overview-value">{{ selectedItem.expectedPointCount ?? '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="overview-item">
|
||||||
|
<span class="overview-label">实际点数</span>
|
||||||
|
<span class="overview-value">{{ selectedItem.actualPointCount ?? '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="overview-item">
|
||||||
|
<span class="overview-label">缺失点数</span>
|
||||||
|
<span class="overview-value">{{ selectedItem.missingPointCount ?? '-' }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="detail-toolbar">
|
<div class="stat-grid">
|
||||||
<el-radio-group v-model="detailType" @change="handleDetailTypeChange">
|
<div v-for="statType in CHECKSQUARE_STAT_TYPES" :key="statType" class="stat-card">
|
||||||
<el-radio-button label="SEGMENT">缺数区间</el-radio-button>
|
<span class="stat-name">{{ formatChecksquareStatType(statType) }}</span>
|
||||||
<el-radio-button label="VALUE_ORDER">值关系异常</el-radio-button>
|
<span class="stat-value">{{ formatStatMissingRate(selectedItem, statType) }}</span>
|
||||||
<el-radio-button label="HARMONIC_PARITY">谐波奇偶异常</el-radio-button>
|
</div>
|
||||||
</el-radio-group>
|
</div>
|
||||||
<el-select
|
|
||||||
v-if="detailType === 'SEGMENT'"
|
<div class="segment-toolbar">
|
||||||
v-model="segmentStatType"
|
<el-select v-model="segmentStatType" class="stat-select" @change="handleSegmentStatTypeChange">
|
||||||
class="stat-select"
|
<el-option v-for="statType in CHECKSQUARE_STAT_TYPES" :key="statType" :label="formatChecksquareStatType(statType)" :value="statType" />
|
||||||
@change="handleSegmentStatTypeChange"
|
</el-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" class="detail-table" :data="segments" height="100%" empty-text="暂无缺数区间">
|
||||||
|
<el-table-column prop="statType" label="统计类型" width="100">
|
||||||
|
<template #default="{ row }">{{ formatChecksquareStatType(row.statType) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="status" label="状态" width="90">
|
||||||
|
<template #default="{ row }">{{ formatSegmentStatus(row.status) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="startTime" label="统计时间" min-width="220" />
|
||||||
|
<el-table-column prop="harmonicOrder" label="谐波次数" width="100" align="right">
|
||||||
|
<template #default="{ row }">{{ row.harmonicOrder ?? '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="missingPointCount" label="缺失点数" width="110" align="right">
|
||||||
|
<template #default="{ row }">{{ row.missingPointCount ?? '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="durationMinutes" label="持续分钟" width="110" align="right">
|
||||||
|
<template #default="{ row }">{{ row.durationMinutes ?? '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="值关系异常" name="VALUE_ORDER">
|
||||||
|
<el-table
|
||||||
|
v-loading="loading"
|
||||||
|
class="detail-table"
|
||||||
|
:data="itemDetail?.valueOrderDetails || []"
|
||||||
|
height="100%"
|
||||||
|
empty-text="暂无值关系异常"
|
||||||
>
|
>
|
||||||
<el-option v-for="statType in CHECKSQUARE_STAT_TYPES" :key="statType" :label="formatChecksquareStatType(statType)" :value="statType" />
|
<el-table-column prop="time" label="时间" min-width="160" />
|
||||||
</el-select>
|
<el-table-column prop="phase" label="相别" width="80" />
|
||||||
</div>
|
<el-table-column prop="harmonicOrder" label="谐波次数" width="96" align="right">
|
||||||
|
<template #default="{ row }">{{ row.harmonicOrder ?? '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="maxValue" label="最大值" min-width="100" align="right" />
|
||||||
|
<el-table-column prop="cp95Value" label="CP95" min-width="100" align="right" />
|
||||||
|
<el-table-column prop="avgValue" label="平均值" min-width="100" align="right" />
|
||||||
|
<el-table-column prop="minValue" label="最小值" min-width="100" align="right" />
|
||||||
|
</el-table>
|
||||||
|
|
||||||
<el-table
|
<div v-if="showDetailPagination" class="detail-pagination">
|
||||||
v-if="detailType === 'SEGMENT'"
|
<el-pagination
|
||||||
v-loading="loading"
|
background
|
||||||
class="segment-table"
|
layout="total, prev, pager, next"
|
||||||
:data="segments"
|
:current-page="detailPageNum"
|
||||||
size="small"
|
:page-size="DETAIL_PAGE_SIZE"
|
||||||
max-height="300"
|
:total="detailTotal"
|
||||||
empty-text="暂无缺数区间"
|
@current-change="handleDetailPageChange"
|
||||||
>
|
/>
|
||||||
<el-table-column prop="statType" label="统计类型" width="96">
|
</div>
|
||||||
<template #default="{ row }">{{ formatChecksquareStatType(row.statType) }}</template>
|
</el-tab-pane>
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="status" label="状态" width="90">
|
|
||||||
<template #default="{ row }">{{ formatSegmentStatus(row.status) }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="startTime" label="开始时间" min-width="160" />
|
|
||||||
<el-table-column prop="endTime" label="结束时间" min-width="160" />
|
|
||||||
<el-table-column prop="harmonicOrder" label="谐波次数" width="96" align="right">
|
|
||||||
<template #default="{ row }">{{ row.harmonicOrder ?? '-' }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="missingPointCount" label="缺失点数" width="100" align="right">
|
|
||||||
<template #default="{ row }">{{ row.missingPointCount ?? '-' }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="durationMinutes" label="持续分钟" width="100" align="right">
|
|
||||||
<template #default="{ row }">{{ row.durationMinutes ?? '-' }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
|
|
||||||
<el-table
|
<el-tab-pane label="谐波奇偶异常" name="HARMONIC_PARITY">
|
||||||
v-else-if="detailType === 'VALUE_ORDER'"
|
<el-table
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
:data="itemDetail?.valueOrderDetails || []"
|
class="detail-table"
|
||||||
size="small"
|
:data="itemDetail?.harmonicParityDetails || []"
|
||||||
max-height="300"
|
height="100%"
|
||||||
empty-text="暂无值关系异常"
|
empty-text="暂无谐波奇偶异常"
|
||||||
>
|
>
|
||||||
<el-table-column prop="time" label="时间" min-width="160" />
|
<el-table-column prop="time" label="时间" min-width="160" />
|
||||||
<el-table-column prop="phase" label="相别" width="80" />
|
<el-table-column prop="phase" label="相别" width="80" />
|
||||||
<el-table-column prop="harmonicOrder" label="谐波次数" width="96" align="right">
|
<el-table-column prop="statType" label="统计类型" width="100">
|
||||||
<template #default="{ row }">{{ row.harmonicOrder ?? '-' }}</template>
|
<template #default="{ row }">{{ row.statType ? formatChecksquareStatType(row.statType) : '-' }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="maxValue" label="最大值" min-width="100" align="right" />
|
<el-table-column prop="evenHarmonicOrder" label="偶次" width="80" align="right" />
|
||||||
<el-table-column prop="cp95Value" label="CP95" min-width="100" align="right" />
|
<el-table-column prop="evenValue" label="偶次值" min-width="100" align="right" />
|
||||||
<el-table-column prop="avgValue" label="平均值" min-width="100" align="right" />
|
<el-table-column prop="oddHarmonicOrders" label="奇次" min-width="110">
|
||||||
<el-table-column prop="minValue" label="最小值" min-width="100" align="right" />
|
<template #default="{ row }">{{ formatDetailArray(row.oddHarmonicOrders) }}</template>
|
||||||
</el-table>
|
</el-table-column>
|
||||||
|
<el-table-column prop="oddValues" label="奇次值" min-width="130">
|
||||||
|
<template #default="{ row }">{{ formatDetailArray(row.oddValues) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="oddMedianValue" label="奇次中位值" min-width="120" align="right" />
|
||||||
|
<el-table-column prop="thresholdMultiplier" label="阈值倍数" min-width="100" align="right" />
|
||||||
|
</el-table>
|
||||||
|
|
||||||
<el-table
|
<div v-if="showDetailPagination" class="detail-pagination">
|
||||||
v-else
|
<el-pagination
|
||||||
v-loading="loading"
|
background
|
||||||
:data="itemDetail?.harmonicParityDetails || []"
|
layout="total, prev, pager, next"
|
||||||
size="small"
|
:current-page="detailPageNum"
|
||||||
max-height="300"
|
:page-size="DETAIL_PAGE_SIZE"
|
||||||
empty-text="暂无谐波奇偶异常"
|
:total="detailTotal"
|
||||||
>
|
@current-change="handleDetailPageChange"
|
||||||
<el-table-column prop="time" label="时间" min-width="160" />
|
/>
|
||||||
<el-table-column prop="phase" label="相别" width="80" />
|
</div>
|
||||||
<el-table-column prop="statType" label="统计类型" width="100">
|
</el-tab-pane>
|
||||||
<template #default="{ row }">{{ row.statType ? formatChecksquareStatType(row.statType) : '-' }}</template>
|
</el-tabs>
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="evenHarmonicOrder" label="偶次" width="80" align="right" />
|
|
||||||
<el-table-column prop="evenValue" label="偶次值" min-width="100" align="right" />
|
|
||||||
<el-table-column prop="oddHarmonicOrders" label="奇次" min-width="110">
|
|
||||||
<template #default="{ row }">{{ formatDetailArray(row.oddHarmonicOrders) }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="oddValues" label="奇次值" min-width="130">
|
|
||||||
<template #default="{ row }">{{ formatDetailArray(row.oddValues) }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="oddMedianValue" label="奇次中位值" min-width="120" align="right" />
|
|
||||||
<el-table-column prop="thresholdMultiplier" label="阈值倍数" min-width="100" align="right" />
|
|
||||||
</el-table>
|
|
||||||
|
|
||||||
<div v-if="showDetailPagination" class="detail-pagination">
|
|
||||||
<el-pagination
|
|
||||||
background
|
|
||||||
layout="total, prev, pager, next"
|
|
||||||
:current-page="detailPageNum"
|
|
||||||
:page-size="DETAIL_PAGE_SIZE"
|
|
||||||
:total="detailTotal"
|
|
||||||
@current-change="handleDetailPageChange"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -129,6 +137,7 @@ import {
|
|||||||
CHECKSQUARE_STAT_TYPES,
|
CHECKSQUARE_STAT_TYPES,
|
||||||
collectMissingSegments,
|
collectMissingSegments,
|
||||||
formatChecksquareStatType,
|
formatChecksquareStatType,
|
||||||
|
formatDataIntegrity,
|
||||||
formatStatMissingRate,
|
formatStatMissingRate,
|
||||||
resolveChecksquareRowName
|
resolveChecksquareRowName
|
||||||
} from '../utils/checksquareTable'
|
} from '../utils/checksquareTable'
|
||||||
@@ -244,30 +253,10 @@ watch(
|
|||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.checksquare-detail {
|
.checksquare-detail {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: none;
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
min-height: 0;
|
|
||||||
padding: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--el-text-color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-description {
|
|
||||||
margin-top: 4px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--el-text-color-regular);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-grid {
|
.stat-grid {
|
||||||
@@ -276,6 +265,55 @@ watch(
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detail-tabs {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-tabs :deep(.el-tabs__content) {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-tabs :deep(.el-tab-pane) {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-overview {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-item {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-value {
|
||||||
|
overflow: hidden;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.stat-card {
|
.stat-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -296,23 +334,33 @@ watch(
|
|||||||
color: var(--el-text-color-primary);
|
color: var(--el-text-color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-toolbar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-select {
|
.stat-select {
|
||||||
width: 120px;
|
width: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-pagination {
|
.segment-toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detail-table {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-table :deep(.el-table__header .cell),
|
||||||
|
.detail-table :deep(.el-table__body .cell) {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-pagination {
|
||||||
|
display: flex;
|
||||||
|
flex: none;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 1200px) {
|
@media (max-width: 1200px) {
|
||||||
|
.item-overview,
|
||||||
.stat-grid {
|
.stat-grid {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog :model-value="visible" :title="dialogTitle" width="640px" @update:model-value="emit('update:visible', $event)">
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item v-for="item in measurementPointItems" :key="item.prop" :label="item.label">
|
||||||
|
{{ resolveText(data?.[item.prop]) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { ChecksquareMeasurementPointDetail } from '../utils/checksquareLedger'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'ChecksquareMeasurementPointDialog'
|
||||||
|
})
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
visible: boolean
|
||||||
|
data: ChecksquareMeasurementPointDetail | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:visible': [value: boolean]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const dialogTitle = '监测点信息'
|
||||||
|
const measurementPointItems: { label: string; prop: keyof ChecksquareMeasurementPointDetail }[] = [
|
||||||
|
{ label: '工程名称', prop: 'engineeringName' },
|
||||||
|
{ label: '项目名称', prop: 'projectName' },
|
||||||
|
{ label: '设备名称', prop: 'equipmentName' },
|
||||||
|
{ label: '网络参数', prop: 'networkParam' },
|
||||||
|
{ label: '监测点名称', prop: 'lineName' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const resolveText = (value: unknown) => {
|
||||||
|
if (value === null || value === undefined || value === '') return '--'
|
||||||
|
return String(value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -31,54 +31,55 @@
|
|||||||
highlight-current-row
|
highlight-current-row
|
||||||
empty-text="暂无校验结果"
|
empty-text="暂无校验结果"
|
||||||
>
|
>
|
||||||
<el-table-column prop="indicatorName" label="指标名称" min-width="208">
|
<el-table-column prop="indicatorName" label="指标名称" width="160">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span class="indicator-name" :title="resolveChecksquareRowName(row)">
|
<span class="indicator-name" :title="resolveChecksquareRowName(row)">
|
||||||
{{ resolveChecksquareRowName(row) }}
|
{{ resolveChecksquareRowName(row) }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="hasData" label="是否有数据" min-width="120" align="center">
|
<el-table-column prop="abnormalPointCount" label="值关系异常点" width="88" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag v-if="row.hasData !== undefined" :type="row.hasData ? 'success' : 'danger'" effect="plain">
|
<span :class="{ 'is-abnormal-count': hasAbnormalCount(row.abnormalPointCount) }">
|
||||||
{{ formatBooleanText(row.hasData) }}
|
{{ row.abnormalPointCount ?? '-' }}
|
||||||
</el-tag>
|
</span>
|
||||||
<span v-else>-</span>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="missingRate" label="总缺失率" min-width="130" align="center">
|
<el-table-column prop="harmonicParityAbnormalPointCount" label="谐波奇偶异常点" width="88" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ formatMissingRate(row.missingRate, row.missingRateText) }}
|
<span :class="{ 'is-abnormal-count': hasAbnormalCount(row.harmonicParityAbnormalPointCount) }">
|
||||||
|
{{ row.harmonicParityAbnormalPointCount ?? '-' }}
|
||||||
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="平均值缺失率" min-width="130" align="center">
|
<el-table-column label="数据完整性" align="center">
|
||||||
<template #default="{ row }">{{ formatStatMissingRate(row, 'AVG') }}</template>
|
<el-table-column prop="hasData" label="是否有数据" width="88" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag v-if="row.hasData !== undefined" :type="row.hasData ? 'success' : 'danger'" effect="plain">
|
||||||
|
{{ formatBooleanText(row.hasData) }}
|
||||||
|
</el-tag>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="dataIntegrity" label="总体(%)" width="88" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatSummaryDataIntegrity(row) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="平均值(%)" width="88" align="center">
|
||||||
|
<template #default="{ row }">{{ formatSummaryStatIntegrity(row, 'AVG') }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="最大值(%)" width="88" align="center">
|
||||||
|
<template #default="{ row }">{{ formatSummaryStatIntegrity(row, 'MAX') }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="最小值(%)" width="88" align="center">
|
||||||
|
<template #default="{ row }">{{ formatSummaryStatIntegrity(row, 'MIN') }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="CP95值(%)" width="88" align="center">
|
||||||
|
<template #default="{ row }">{{ formatSummaryStatIntegrity(row, 'CP95') }}</template>
|
||||||
|
</el-table-column>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="最大值缺失率" min-width="130" align="center">
|
<el-table-column label="操作" width="130" align="center" fixed="right">
|
||||||
<template #default="{ row }">{{ formatStatMissingRate(row, 'MAX') }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="最小值缺失率" min-width="130" align="center">
|
|
||||||
<template #default="{ row }">{{ formatStatMissingRate(row, 'MIN') }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="CP95缺失率" min-width="140" align="center">
|
|
||||||
<template #default="{ row }">{{ formatStatMissingRate(row, 'CP95') }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="maxContinuousMissingMinutes" label="最大连续缺失" min-width="150" align="center">
|
|
||||||
<template #default="{ row }">
|
|
||||||
{{ row.maxContinuousMissingMinutes ?? '-' }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="abnormalPointCount" label="值关系异常点" min-width="130" align="center">
|
|
||||||
<template #default="{ row }">
|
|
||||||
{{ row.abnormalPointCount ?? '-' }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="harmonicParityAbnormalPointCount" label="谐波奇偶异常点" min-width="150" align="center">
|
|
||||||
<template #default="{ row }">
|
|
||||||
{{ row.harmonicParityAbnormalPointCount ?? '-' }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" width="96" align="center" fixed="right">
|
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button type="primary" link :disabled="!hasChecksquareDetail(row)" @click="emit('detail', row)">
|
<el-button type="primary" link :disabled="!hasChecksquareDetail(row)" @click="emit('detail', row)">
|
||||||
详情
|
详情
|
||||||
@@ -94,7 +95,7 @@ import { Refresh } from '@element-plus/icons-vue'
|
|||||||
import type { SteadyDataView } from '@/api/steady/steadyDataView/interface'
|
import type { SteadyDataView } from '@/api/steady/steadyDataView/interface'
|
||||||
import {
|
import {
|
||||||
formatBooleanText,
|
formatBooleanText,
|
||||||
formatMissingRate,
|
formatDataIntegrity,
|
||||||
formatStatMissingRate,
|
formatStatMissingRate,
|
||||||
hasChecksquareDetail,
|
hasChecksquareDetail,
|
||||||
resolveChecksquareRowName
|
resolveChecksquareRowName
|
||||||
@@ -114,6 +115,21 @@ const emit = defineEmits<{
|
|||||||
refresh: []
|
refresh: []
|
||||||
detail: [row: SteadyDataView.SteadyChecksquareItem]
|
detail: [row: SteadyDataView.SteadyChecksquareItem]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const hasAbnormalCount = (value?: number | null) => Number(value || 0) > 0
|
||||||
|
|
||||||
|
const stripPercentUnit = (value: string) => value.replace(/%$/, '')
|
||||||
|
|
||||||
|
const formatSummaryDataIntegrity = (row: SteadyDataView.SteadyChecksquareItem) => {
|
||||||
|
return stripPercentUnit(formatDataIntegrity(row.dataIntegrity, row.dataIntegrityText, row.missingRate))
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatSummaryStatIntegrity = (
|
||||||
|
row: SteadyDataView.SteadyChecksquareItem,
|
||||||
|
statType: SteadyDataView.SteadyTrendStatType
|
||||||
|
) => {
|
||||||
|
return stripPercentUnit(formatStatMissingRate(row, statType))
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@@ -149,4 +165,8 @@ const emit = defineEmits<{
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.is-abnormal-count {
|
||||||
|
color: var(--el-color-danger);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -11,21 +11,21 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #operation="{ row }">
|
<template #operation="{ row }">
|
||||||
<el-button type="primary" link :icon="View" @click="emit('detail', row)">详情</el-button>
|
<el-button type="danger" link :icon="Delete" @click="emit('delete', row)">删除</el-button>
|
||||||
</template>
|
</template>
|
||||||
</ProTable>
|
</ProTable>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, h, reactive, ref } from 'vue'
|
import { computed, h, reactive, ref } from 'vue'
|
||||||
import { ElDatePicker, ElTag, ElTreeSelect } from 'element-plus'
|
import { ElButton, ElDatePicker, ElTag, ElTreeSelect } from 'element-plus'
|
||||||
import { Plus, View } from '@element-plus/icons-vue'
|
import { Delete, Plus } from '@element-plus/icons-vue'
|
||||||
import ProTable from '@/components/ProTable/index.vue'
|
import ProTable from '@/components/ProTable/index.vue'
|
||||||
import type { ColumnProps, ProTableInstance } from '@/components/ProTable/interface'
|
import type { ColumnProps, ProTableInstance } from '@/components/ProTable/interface'
|
||||||
import type { SteadyDataView } from '@/api/steady/steadyDataView/interface'
|
import type { SteadyDataView } from '@/api/steady/steadyDataView/interface'
|
||||||
import {
|
import {
|
||||||
buildChecksquareTaskQueryParams,
|
buildChecksquareTaskQueryParams,
|
||||||
formatChecksquarePercent,
|
formatChecksquareIntegrity,
|
||||||
formatChecksquareTaskStatus,
|
formatChecksquareTaskStatus,
|
||||||
resolveChecksquareTaskStatusType,
|
resolveChecksquareTaskStatusType,
|
||||||
resolveChecksquareText,
|
resolveChecksquareText,
|
||||||
@@ -45,6 +45,8 @@ const props = defineProps<{
|
|||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
createTask: []
|
createTask: []
|
||||||
detail: [row: SteadyDataView.SteadyChecksquareTask]
|
detail: [row: SteadyDataView.SteadyChecksquareTask]
|
||||||
|
delete: [row: SteadyDataView.SteadyChecksquareTask]
|
||||||
|
viewMeasurementPoint: [row: SteadyDataView.SteadyChecksquareTask]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const proTable = ref<ProTableInstance>()
|
const proTable = ref<ProTableInstance>()
|
||||||
@@ -192,7 +194,16 @@ const columns = reactive<ColumnProps<SteadyDataView.SteadyChecksquareTask>[]>([
|
|||||||
prop: 'lineName',
|
prop: 'lineName',
|
||||||
label: '监测点名称',
|
label: '监测点名称',
|
||||||
minWidth: 160,
|
minWidth: 160,
|
||||||
render: ({ row }) => resolveChecksquareText(row.lineName || row.lineId)
|
render: ({ row }) =>
|
||||||
|
h(
|
||||||
|
ElButton,
|
||||||
|
{
|
||||||
|
type: 'primary',
|
||||||
|
link: true,
|
||||||
|
onClick: () => emit('viewMeasurementPoint', row)
|
||||||
|
},
|
||||||
|
() => resolveChecksquareText(row.lineName || row.lineId)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'indicatorCode',
|
prop: 'indicatorCode',
|
||||||
@@ -246,7 +257,16 @@ const columns = reactive<ColumnProps<SteadyDataView.SteadyChecksquareTask>[]>([
|
|||||||
label: '异常项数',
|
label: '异常项数',
|
||||||
minWidth: 100,
|
minWidth: 100,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
render: ({ row }) => resolveChecksquareText(row.abnormalItemCount),
|
render: ({ row }) =>
|
||||||
|
h(
|
||||||
|
ElButton,
|
||||||
|
{
|
||||||
|
type: 'primary',
|
||||||
|
link: true,
|
||||||
|
onClick: () => emit('detail', row)
|
||||||
|
},
|
||||||
|
() => resolveChecksquareText(row.abnormalItemCount)
|
||||||
|
),
|
||||||
search: {
|
search: {
|
||||||
label: '异常状态',
|
label: '异常状态',
|
||||||
key: 'hasAbnormal',
|
key: 'hasAbnormal',
|
||||||
@@ -260,11 +280,11 @@ const columns = reactive<ColumnProps<SteadyDataView.SteadyChecksquareTask>[]>([
|
|||||||
isFilterEnum: false
|
isFilterEnum: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'maxMissingRate',
|
prop: 'minDataIntegrity',
|
||||||
label: '最大缺失率',
|
label: '最低完整性',
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
render: ({ row }) => formatChecksquarePercent(row.maxMissingRate)
|
render: ({ row }) => formatChecksquareIntegrity(row.minDataIntegrity, row.maxMissingRate)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prop: 'createTime',
|
prop: 'createTime',
|
||||||
@@ -272,7 +292,7 @@ const columns = reactive<ColumnProps<SteadyDataView.SteadyChecksquareTask>[]>([
|
|||||||
minWidth: 170,
|
minWidth: 170,
|
||||||
render: ({ row }) => resolveChecksquareText(row.createTime)
|
render: ({ row }) => resolveChecksquareText(row.createTime)
|
||||||
},
|
},
|
||||||
{ prop: 'operation', label: '操作', fixed: 'right', width: 96 }
|
{ prop: 'operation', label: '操作', fixed: 'right', width: 150 }
|
||||||
])
|
])
|
||||||
|
|
||||||
const getTableList = (params: ChecksquareTaskSearchParams) => {
|
const getTableList = (params: ChecksquareTaskSearchParams) => {
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ const files = {
|
|||||||
taskTable: path.resolve(rootDir, 'views/steady/checksquare/components/ChecksquareTaskTable.vue'),
|
taskTable: path.resolve(rootDir, 'views/steady/checksquare/components/ChecksquareTaskTable.vue'),
|
||||||
summaryTable: path.resolve(rootDir, 'views/steady/checksquare/components/ChecksquareSummaryTable.vue'),
|
summaryTable: path.resolve(rootDir, 'views/steady/checksquare/components/ChecksquareSummaryTable.vue'),
|
||||||
detailPanel: path.resolve(rootDir, 'views/steady/checksquare/components/ChecksquareDetailPanel.vue'),
|
detailPanel: path.resolve(rootDir, 'views/steady/checksquare/components/ChecksquareDetailPanel.vue'),
|
||||||
|
measurementPointDialog: path.resolve(rootDir, 'views/steady/checksquare/components/ChecksquareMeasurementPointDialog.vue'),
|
||||||
payload: path.resolve(rootDir, 'views/steady/checksquare/utils/checksquarePayload.ts'),
|
payload: path.resolve(rootDir, 'views/steady/checksquare/utils/checksquarePayload.ts'),
|
||||||
|
ledgerUtils: path.resolve(rootDir, 'views/steady/checksquare/utils/checksquareLedger.ts'),
|
||||||
taskTableUtils: path.resolve(rootDir, 'views/steady/checksquare/utils/checksquareTaskTable.ts'),
|
taskTableUtils: path.resolve(rootDir, 'views/steady/checksquare/utils/checksquareTaskTable.ts'),
|
||||||
table: path.resolve(rootDir, 'views/steady/checksquare/utils/checksquareTable.ts'),
|
table: path.resolve(rootDir, 'views/steady/checksquare/utils/checksquareTable.ts'),
|
||||||
grid: path.resolve(rootDir, 'components/Grid/index.vue'),
|
grid: path.resolve(rootDir, 'components/Grid/index.vue'),
|
||||||
@@ -34,11 +36,21 @@ const checks = [
|
|||||||
return (
|
return (
|
||||||
/\/steady\/data-view\/checksquare\/query/.test(api) &&
|
/\/steady\/data-view\/checksquare\/query/.test(api) &&
|
||||||
/\/steady\/data-view\/checksquare\/create/.test(api) &&
|
/\/steady\/data-view\/checksquare\/create/.test(api) &&
|
||||||
|
/\/steady\/data-view\/checksquare\/delete/.test(api) &&
|
||||||
/\/steady\/data-view\/checksquare\/detail/.test(api) &&
|
/\/steady\/data-view\/checksquare\/detail/.test(api) &&
|
||||||
/\/steady\/data-view\/checksquare\/item-detail/.test(api)
|
/\/steady\/data-view\/checksquare\/item-detail/.test(api)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'checksquare delete api accepts documented task id array body',
|
||||||
|
() =>
|
||||||
|
/export const deleteSteadyChecksquareTasks = \(taskIds: SteadyDataView\.SteadyChecksquareDeleteParams\)/.test(
|
||||||
|
read(files.api)
|
||||||
|
) &&
|
||||||
|
/http\.post<boolean>\('\/steady\/data-view\/checksquare\/delete', taskIds/.test(read(files.api)) &&
|
||||||
|
/export type SteadyChecksquareDeleteParams = string\[\]/.test(read(files.apiTypes))
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'checksquare task query params support page and filters',
|
'checksquare task query params support page and filters',
|
||||||
() =>
|
() =>
|
||||||
@@ -70,11 +82,36 @@ const checks = [
|
|||||||
'task table has documented task columns',
|
'task table has documented task columns',
|
||||||
() => {
|
() => {
|
||||||
const source = read(files.taskTable)
|
const source = read(files.taskTable)
|
||||||
return ['taskNo', 'lineName', 'timeStart', 'timeEnd', 'taskStatus', 'itemCount', 'abnormalItemCount', 'maxMissingRate', 'createTime'].every(
|
return ['taskNo', 'lineName', 'timeStart', 'timeEnd', 'taskStatus', 'itemCount', 'abnormalItemCount', 'minDataIntegrity', 'createTime'].every(
|
||||||
prop => new RegExp(`prop:\\s*'${prop}'`).test(source)
|
prop => new RegExp(`prop:\\s*'${prop}'`).test(source)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'task table exposes row delete action without duplicate detail operation',
|
||||||
|
() => {
|
||||||
|
const taskTable = read(files.taskTable)
|
||||||
|
const operationSlot = taskTable.match(/<template #operation="\{ row \}">[\s\S]*?<\/template>/)?.[0] || ''
|
||||||
|
return (
|
||||||
|
/emit\('delete', row\)/.test(operationSlot) &&
|
||||||
|
!/emit\('detail', row\)/.test(operationSlot) &&
|
||||||
|
/delete: \[row: SteadyDataView\.SteadyChecksquareTask\]/.test(taskTable) &&
|
||||||
|
/Delete/.test(taskTable)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'task detail opens from abnormal item count value',
|
||||||
|
() => {
|
||||||
|
const taskTable = read(files.taskTable)
|
||||||
|
return (
|
||||||
|
/prop:\s*'abnormalItemCount'[\s\S]*ElButton[\s\S]*type:\s*'primary'[\s\S]*link:\s*true[\s\S]*emit\('detail', row\)/.test(
|
||||||
|
taskTable
|
||||||
|
) &&
|
||||||
|
/resolveChecksquareText\(row\.abnormalItemCount\)/.test(taskTable)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'task table query params convert time range and abnormal filter',
|
'task table query params convert time range and abnormal filter',
|
||||||
() =>
|
() =>
|
||||||
@@ -101,7 +138,10 @@ const checks = [
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
'page renders task table as first screen',
|
'page renders task table as first screen',
|
||||||
() => /<ChecksquareTaskTable[\s\S]*@create-task="openCreateDialog"[\s\S]*@detail="openTaskDetail"/.test(read(files.page))
|
() =>
|
||||||
|
/<ChecksquareTaskTable[\s\S]*@create-task="openCreateDialog"[\s\S]*@detail="openTaskDetail"[\s\S]*@delete="handleDeleteTask"/.test(
|
||||||
|
read(files.page)
|
||||||
|
)
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'page passes steady ledger and indicator trees to task table filters',
|
'page passes steady ledger and indicator trees to task table filters',
|
||||||
@@ -157,6 +197,63 @@ const checks = [
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'task table monitor point name opens measurement point dialog like event list',
|
||||||
|
() => {
|
||||||
|
const taskTable = read(files.taskTable)
|
||||||
|
return (
|
||||||
|
/viewMeasurementPoint:\s*\[row: SteadyDataView\.SteadyChecksquareTask\]/.test(taskTable) &&
|
||||||
|
/prop:\s*'lineName'[\s\S]*ElButton[\s\S]*type:\s*'primary'[\s\S]*link:\s*true[\s\S]*emit\('viewMeasurementPoint', row\)/.test(
|
||||||
|
taskTable
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'checksquare measurement point dialog matches event list fields',
|
||||||
|
() => {
|
||||||
|
const dialog = read(files.measurementPointDialog)
|
||||||
|
return (
|
||||||
|
exists(files.measurementPointDialog) &&
|
||||||
|
/name:\s*'ChecksquareMeasurementPointDialog'/.test(dialog) &&
|
||||||
|
/dialogTitle\s*=\s*'监测点信息'/.test(dialog) &&
|
||||||
|
/工程名称/.test(dialog) &&
|
||||||
|
/项目名称/.test(dialog) &&
|
||||||
|
/设备名称/.test(dialog) &&
|
||||||
|
/网络参数/.test(dialog) &&
|
||||||
|
/监测点名称/.test(dialog) &&
|
||||||
|
/resolveText/.test(dialog)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'checksquare ledger utils resolve monitor point detail from loaded ledger tree',
|
||||||
|
() => {
|
||||||
|
const source = read(files.ledgerUtils)
|
||||||
|
return (
|
||||||
|
exists(files.ledgerUtils) &&
|
||||||
|
/resolveChecksquareMeasurementPointDetail/.test(source) &&
|
||||||
|
/engineeringName/.test(source) &&
|
||||||
|
/projectName/.test(source) &&
|
||||||
|
/equipmentName/.test(source) &&
|
||||||
|
/lineName/.test(source) &&
|
||||||
|
/networkParam/.test(source)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'page wires checksquare measurement point dialog to task table',
|
||||||
|
() => {
|
||||||
|
const page = read(files.page)
|
||||||
|
return (
|
||||||
|
/@view-measurement-point="openMeasurementPointDialog"/.test(page) &&
|
||||||
|
/<ChecksquareMeasurementPointDialog[\s\S]*v-model:visible="measurementPointDialogVisible"[\s\S]*:data="measurementPointData"/.test(
|
||||||
|
page
|
||||||
|
) &&
|
||||||
|
/resolveChecksquareMeasurementPointDetail/.test(page)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'search grid keeps third filter visible when operation column exactly fills first row',
|
'search grid keeps third filter visible when operation column exactly fills first row',
|
||||||
() => /Number\(prev\)\s*>\s*props\.collapsedRows \* gridCols\.value - suffixCols/.test(read(files.grid))
|
() => /Number\(prev\)\s*>\s*props\.collapsedRows \* gridCols\.value - suffixCols/.test(read(files.grid))
|
||||||
@@ -182,14 +279,154 @@ const checks = [
|
|||||||
/taskTableRef\.value\?\.refresh\(\)/.test(read(files.page)) &&
|
/taskTableRef\.value\?\.refresh\(\)/.test(read(files.page)) &&
|
||||||
/createDialogVisible\.value = false/.test(read(files.page))
|
/createDialogVisible\.value = false/.test(read(files.page))
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'page delete flow confirms, calls delete api and refreshes task table',
|
||||||
|
() =>
|
||||||
|
/deleteSteadyChecksquareTasks/.test(read(files.page)) &&
|
||||||
|
/ElMessageBox\.confirm/.test(read(files.page)) &&
|
||||||
|
/deleteSteadyChecksquareTasks\(\[row\.taskId\]\)/.test(read(files.page)) &&
|
||||||
|
/taskTableRef\.value\?\.refresh\(\)/.test(read(files.page))
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'page detail flow calls detail api',
|
'page detail flow calls detail api',
|
||||||
() => /getSteadyChecksquareDetail/.test(read(files.page)) && /detailDialogVisible\.value = true/.test(read(files.page))
|
() => /getSteadyChecksquareDetail/.test(read(files.page)) && /detailDialogVisible\.value = true/.test(read(files.page))
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'task detail and item detail dialogs use the same size',
|
||||||
|
() => {
|
||||||
|
const page = read(files.page)
|
||||||
|
return (
|
||||||
|
/v-model="detailDialogVisible"[\s\S]*?width="1080px"/.test(page) &&
|
||||||
|
/v-model="itemDetailDialogVisible"[\s\S]*?width="1080px"/.test(page) &&
|
||||||
|
/v-model="detailDialogVisible"[\s\S]*?class="checksquare-detail-dialog"/.test(page) &&
|
||||||
|
/v-model="itemDetailDialogVisible"[\s\S]*?class="checksquare-detail-dialog"/.test(page) &&
|
||||||
|
/\.checksquare-detail-dialog\s*\{[\s\S]*height:\s*560px/.test(page)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'summary table supports persisted abnormal fields',
|
'summary table supports persisted abnormal fields',
|
||||||
() => /abnormalPointCount/.test(read(files.summaryTable)) && /harmonicParityAbnormalPointCount/.test(read(files.summaryTable))
|
() => /abnormalPointCount/.test(read(files.summaryTable)) && /harmonicParityAbnormalPointCount/.test(read(files.summaryTable))
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'summary table renders positive abnormal counts in danger color',
|
||||||
|
() => {
|
||||||
|
const summaryTable = read(files.summaryTable)
|
||||||
|
return (
|
||||||
|
/hasAbnormalCount/.test(summaryTable) &&
|
||||||
|
/:class="\{\s*'is-abnormal-count': hasAbnormalCount\(row\.abnormalPointCount\)\s*\}"/.test(summaryTable) &&
|
||||||
|
/:class="\{\s*'is-abnormal-count': hasAbnormalCount\(row\.harmonicParityAbnormalPointCount\)\s*\}"/.test(
|
||||||
|
summaryTable
|
||||||
|
) &&
|
||||||
|
/\.is-abnormal-count\s*\{[\s\S]*color:\s*var\(--el-color-danger\)/.test(summaryTable)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'summary table groups data integrity columns under compact double header',
|
||||||
|
() => {
|
||||||
|
const summaryTable = read(files.summaryTable)
|
||||||
|
const dataIntegrityGroup =
|
||||||
|
summaryTable.match(/<el-table-column label="数据完整性"[\s\S]*?<\/el-table-column>\s*<el-table-column label="操作"/)?.[0] ||
|
||||||
|
''
|
||||||
|
|
||||||
|
return (
|
||||||
|
/label="数据完整性"/.test(dataIntegrityGroup) &&
|
||||||
|
/prop="hasData" label="是否有数据" width="88"/.test(dataIntegrityGroup) &&
|
||||||
|
/prop="dataIntegrity" label="总体\(%\)" width="88"/.test(dataIntegrityGroup) &&
|
||||||
|
/label="平均值\(%\)" width="88"/.test(dataIntegrityGroup) &&
|
||||||
|
/label="最大值\(%\)" width="88"/.test(dataIntegrityGroup) &&
|
||||||
|
/label="最小值\(%\)" width="88"/.test(dataIntegrityGroup) &&
|
||||||
|
/label="CP95值\(%\)" width="88"/.test(dataIntegrityGroup)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'summary table removes percent unit from data integrity cell values',
|
||||||
|
() => {
|
||||||
|
const summaryTable = read(files.summaryTable)
|
||||||
|
return (
|
||||||
|
/stripPercentUnit/.test(summaryTable) &&
|
||||||
|
/formatSummaryDataIntegrity/.test(summaryTable) &&
|
||||||
|
/formatSummaryStatIntegrity/.test(summaryTable) &&
|
||||||
|
/formatSummaryDataIntegrity\(row\)/.test(summaryTable) &&
|
||||||
|
/formatSummaryStatIntegrity\(row,\s*'AVG'\)/.test(summaryTable) &&
|
||||||
|
/formatSummaryStatIntegrity\(row,\s*'MAX'\)/.test(summaryTable) &&
|
||||||
|
/formatSummaryStatIntegrity\(row,\s*'MIN'\)/.test(summaryTable) &&
|
||||||
|
/formatSummaryStatIntegrity\(row,\s*'CP95'\)/.test(summaryTable)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'summary table keeps abnormal and operation columns compact',
|
||||||
|
() => {
|
||||||
|
const summaryTable = read(files.summaryTable)
|
||||||
|
return (
|
||||||
|
/prop="abnormalPointCount" label="值关系异常点" width="88"/.test(summaryTable) &&
|
||||||
|
/prop="harmonicParityAbnormalPointCount" label="谐波奇偶异常点" width="88"/.test(summaryTable) &&
|
||||||
|
/<el-table-column label="操作" width="130"/.test(summaryTable)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'summary table keeps indicator name column at configured width',
|
||||||
|
() => /prop="indicatorName" label="指标名称" width="160"/.test(read(files.summaryTable))
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'summary table places abnormal fields after indicator name and hides max continuous missing column',
|
||||||
|
() => {
|
||||||
|
const summaryTable = read(files.summaryTable)
|
||||||
|
const indicatorIndex = summaryTable.indexOf('prop="indicatorName"')
|
||||||
|
const valueOrderIndex = summaryTable.indexOf('prop="abnormalPointCount"')
|
||||||
|
const harmonicParityIndex = summaryTable.indexOf('prop="harmonicParityAbnormalPointCount"')
|
||||||
|
const hasDataIndex = summaryTable.indexOf('prop="hasData"')
|
||||||
|
|
||||||
|
return (
|
||||||
|
!/prop="maxContinuousMissingMinutes"/.test(summaryTable) &&
|
||||||
|
indicatorIndex >= 0 &&
|
||||||
|
valueOrderIndex > indicatorIndex &&
|
||||||
|
harmonicParityIndex > valueOrderIndex &&
|
||||||
|
hasDataIndex > harmonicParityIndex
|
||||||
|
)
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'checksquare display uses documented data integrity fields instead of missing rate fields',
|
||||||
|
() => {
|
||||||
|
const types = read(files.apiTypes)
|
||||||
|
const taskTable = read(files.taskTable)
|
||||||
|
const summaryTable = read(files.summaryTable)
|
||||||
|
const tableUtils = read(files.table)
|
||||||
|
const taskTableUtils = read(files.taskTableUtils)
|
||||||
|
|
||||||
|
return (
|
||||||
|
/minDataIntegrity\?: number \| null/.test(types) &&
|
||||||
|
/dataIntegrity\?: number \| null/.test(types) &&
|
||||||
|
/dataIntegrityText\?: string \| null/.test(types) &&
|
||||||
|
/prop:\s*'minDataIntegrity'/.test(taskTable) &&
|
||||||
|
/formatChecksquareIntegrity/.test(taskTableUtils) &&
|
||||||
|
/prop="dataIntegrity"/.test(summaryTable) &&
|
||||||
|
/formatDataIntegrity/.test(tableUtils) &&
|
||||||
|
!/prop:\s*'maxMissingRate'/.test(taskTable) &&
|
||||||
|
!/prop="missingRate"/.test(summaryTable)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'detail dialog shows integrity overview and documented point counts',
|
||||||
|
() => {
|
||||||
|
const detailPanel = read(files.detailPanel)
|
||||||
|
|
||||||
|
return (
|
||||||
|
/formatDataIntegrity/.test(detailPanel) &&
|
||||||
|
/selectedItem\.dataIntegrity/.test(detailPanel) &&
|
||||||
|
/selectedItem\.dataIntegrityText/.test(detailPanel) &&
|
||||||
|
/selectedItem\.expectedPointCount/.test(detailPanel) &&
|
||||||
|
/selectedItem\.actualPointCount/.test(detailPanel) &&
|
||||||
|
/selectedItem\.missingPointCount/.test(detailPanel)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'detail panel loads item details on demand',
|
'detail panel loads item details on demand',
|
||||||
() => /getSteadyChecksquareItemDetail/.test(read(files.detailPanel)) && /detailType/.test(read(files.detailPanel))
|
() => /getSteadyChecksquareItemDetail/.test(read(files.detailPanel)) && /detailType/.test(read(files.detailPanel))
|
||||||
@@ -232,6 +469,28 @@ const checks = [
|
|||||||
const detailPanel = read(files.detailPanel)
|
const detailPanel = read(files.detailPanel)
|
||||||
return /prop="status"[\s\S]*状态/.test(detailPanel) && /oddHarmonicOrders/.test(detailPanel) && /oddValues/.test(detailPanel)
|
return /prop="status"[\s\S]*状态/.test(detailPanel) && /oddHarmonicOrders/.test(detailPanel) && /oddValues/.test(detailPanel)
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'detail panel table follows task detail table container style',
|
||||||
|
() => {
|
||||||
|
const detailPanel = read(files.detailPanel)
|
||||||
|
return (
|
||||||
|
/<section class="table-main card checksquare-detail">/.test(detailPanel) &&
|
||||||
|
/<el-tabs/.test(detailPanel) &&
|
||||||
|
/<el-tab-pane label="缺数区间" name="SEGMENT">/.test(detailPanel) &&
|
||||||
|
/<el-tab-pane label="值关系异常" name="VALUE_ORDER">/.test(detailPanel) &&
|
||||||
|
/<el-tab-pane label="谐波奇偶异常" name="HARMONIC_PARITY">/.test(detailPanel) &&
|
||||||
|
/class="detail-table"/.test(detailPanel) &&
|
||||||
|
/height="100%"/.test(detailPanel) &&
|
||||||
|
!/max-height="300"/.test(detailPanel) &&
|
||||||
|
!/section-title/.test(detailPanel) &&
|
||||||
|
!/section-description/.test(detailPanel) &&
|
||||||
|
!/Refresh/.test(detailPanel) &&
|
||||||
|
/\.detail-table :deep\(\.el-table__header \.cell\),[\s\S]*\.detail-table :deep\(\.el-table__body \.cell\)[\s\S]*text-align:\s*center/.test(
|
||||||
|
detailPanel
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,13 @@
|
|||||||
:request-api="querySteadyChecksquareTasks"
|
:request-api="querySteadyChecksquareTasks"
|
||||||
@create-task="openCreateDialog"
|
@create-task="openCreateDialog"
|
||||||
@detail="openTaskDetail"
|
@detail="openTaskDetail"
|
||||||
|
@delete="handleDeleteTask"
|
||||||
|
@view-measurement-point="openMeasurementPointDialog"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ChecksquareMeasurementPointDialog
|
||||||
|
v-model:visible="measurementPointDialogVisible"
|
||||||
|
:data="measurementPointData"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<el-dialog v-model="createDialogVisible" title="新增校验任务" width="1120px" append-to-body destroy-on-close>
|
<el-dialog v-model="createDialogVisible" title="新增校验任务" width="1120px" append-to-body destroy-on-close>
|
||||||
@@ -43,17 +50,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog v-model="itemDetailDialogVisible" title="检测项明细" width="900px" append-to-body destroy-on-close>
|
<el-dialog v-model="itemDetailDialogVisible" title="检测项明细" width="1080px" append-to-body destroy-on-close>
|
||||||
<ChecksquareDetailPanel :selected-item="selectedItem" />
|
<div class="checksquare-detail-dialog">
|
||||||
|
<ChecksquareDetailPanel :selected-item="selectedItem" />
|
||||||
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import {
|
import {
|
||||||
createSteadyChecksquareTask,
|
createSteadyChecksquareTask,
|
||||||
|
deleteSteadyChecksquareTasks,
|
||||||
getSteadyChecksquareDetail,
|
getSteadyChecksquareDetail,
|
||||||
getSteadyTrendIndicatorTree,
|
getSteadyTrendIndicatorTree,
|
||||||
getSteadyTrendLedgerTree,
|
getSteadyTrendLedgerTree,
|
||||||
@@ -69,9 +79,14 @@ import {
|
|||||||
} from '@/views/steady/steadyDataView/utils/selectionRules'
|
} from '@/views/steady/steadyDataView/utils/selectionRules'
|
||||||
import { normalizeSteadyLedgerTree } from '@/views/steady/steadyDataView/utils/ledgerTree'
|
import { normalizeSteadyLedgerTree } from '@/views/steady/steadyDataView/utils/ledgerTree'
|
||||||
import ChecksquareDetailPanel from './components/ChecksquareDetailPanel.vue'
|
import ChecksquareDetailPanel from './components/ChecksquareDetailPanel.vue'
|
||||||
|
import ChecksquareMeasurementPointDialog from './components/ChecksquareMeasurementPointDialog.vue'
|
||||||
import ChecksquareSummaryTable from './components/ChecksquareSummaryTable.vue'
|
import ChecksquareSummaryTable from './components/ChecksquareSummaryTable.vue'
|
||||||
import ChecksquareTaskTable from './components/ChecksquareTaskTable.vue'
|
import ChecksquareTaskTable from './components/ChecksquareTaskTable.vue'
|
||||||
import ChecksquareWorkbench from './components/ChecksquareWorkbench.vue'
|
import ChecksquareWorkbench from './components/ChecksquareWorkbench.vue'
|
||||||
|
import {
|
||||||
|
resolveChecksquareMeasurementPointDetail,
|
||||||
|
type ChecksquareMeasurementPointDetail
|
||||||
|
} from './utils/checksquareLedger'
|
||||||
import {
|
import {
|
||||||
buildSteadyChecksquareCreatePayload,
|
buildSteadyChecksquareCreatePayload,
|
||||||
defaultChecksquareFormState,
|
defaultChecksquareFormState,
|
||||||
@@ -89,6 +104,7 @@ const selectedIndicators = ref<SteadyDataView.SteadyIndicatorNode[]>([])
|
|||||||
const taskDetail = ref<SteadyDataView.SteadyChecksquareQueryResult | null>(null)
|
const taskDetail = ref<SteadyDataView.SteadyChecksquareQueryResult | null>(null)
|
||||||
const selectedTask = ref<SteadyDataView.SteadyChecksquareTask | null>(null)
|
const selectedTask = ref<SteadyDataView.SteadyChecksquareTask | null>(null)
|
||||||
const selectedItem = ref<SteadyDataView.SteadyChecksquareItem | null>(null)
|
const selectedItem = ref<SteadyDataView.SteadyChecksquareItem | null>(null)
|
||||||
|
const measurementPointData = ref<ChecksquareMeasurementPointDetail | null>(null)
|
||||||
const formState = ref(defaultChecksquareFormState())
|
const formState = ref(defaultChecksquareFormState())
|
||||||
const ledgerKeyword = ref('')
|
const ledgerKeyword = ref('')
|
||||||
const ledgerPanelCollapsed = ref(false)
|
const ledgerPanelCollapsed = ref(false)
|
||||||
@@ -98,6 +114,7 @@ const defaultIndicatorCheckedKeys = ref<string[]>([])
|
|||||||
const createDialogVisible = ref(false)
|
const createDialogVisible = ref(false)
|
||||||
const detailDialogVisible = ref(false)
|
const detailDialogVisible = ref(false)
|
||||||
const itemDetailDialogVisible = ref(false)
|
const itemDetailDialogVisible = ref(false)
|
||||||
|
const measurementPointDialogVisible = ref(false)
|
||||||
const taskTableRef = ref<InstanceType<typeof ChecksquareTaskTable>>()
|
const taskTableRef = ref<InstanceType<typeof ChecksquareTaskTable>>()
|
||||||
const loading = reactive({
|
const loading = reactive({
|
||||||
ledger: false,
|
ledger: false,
|
||||||
@@ -216,11 +233,35 @@ const openTaskDetail = async (row: SteadyDataView.SteadyChecksquareTask) => {
|
|||||||
await refreshTaskDetail()
|
await refreshTaskDetail()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleDeleteTask = async (row: SteadyDataView.SteadyChecksquareTask) => {
|
||||||
|
if (!row.taskId) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确认删除该校验任务吗?删除后历史列表将不再显示该任务。', '删除确认', {
|
||||||
|
confirmButtonText: '删除',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除接口按任务 ID 数组批量处理;列表行操作只传当前行任务 ID。
|
||||||
|
await deleteSteadyChecksquareTasks([row.taskId])
|
||||||
|
ElMessage.success('删除校验任务成功')
|
||||||
|
taskTableRef.value?.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
const openItemDetail = (item: SteadyDataView.SteadyChecksquareItem) => {
|
const openItemDetail = (item: SteadyDataView.SteadyChecksquareItem) => {
|
||||||
selectedItem.value = item
|
selectedItem.value = item
|
||||||
itemDetailDialogVisible.value = true
|
itemDetailDialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openMeasurementPointDialog = (row: SteadyDataView.SteadyChecksquareTask) => {
|
||||||
|
measurementPointData.value = resolveChecksquareMeasurementPointDetail(ledgerTree.value, row)
|
||||||
|
measurementPointDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadLedgerTree()
|
loadLedgerTree()
|
||||||
loadIndicatorTree()
|
loadIndicatorTree()
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import type { SteadyDataView } from '@/api/steady/steadyDataView/interface'
|
||||||
|
|
||||||
|
export interface ChecksquareMeasurementPointDetail {
|
||||||
|
engineeringName?: string
|
||||||
|
projectName?: string
|
||||||
|
equipmentName?: string
|
||||||
|
networkParam?: string
|
||||||
|
lineName?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveText = (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 collectLedgerPath = (
|
||||||
|
nodes: SteadyDataView.SteadyLedgerNode[],
|
||||||
|
lineId: string,
|
||||||
|
parents: SteadyDataView.SteadyLedgerNode[] = []
|
||||||
|
): SteadyDataView.SteadyLedgerNode[] => {
|
||||||
|
for (const node of nodes) {
|
||||||
|
const nextPath = [...parents, node]
|
||||||
|
|
||||||
|
if (node.id === lineId) {
|
||||||
|
return nextPath
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.children?.length) {
|
||||||
|
const matchedPath = collectLedgerPath(node.children, lineId, nextPath)
|
||||||
|
if (matchedPath.length) return matchedPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
export const resolveChecksquareMeasurementPointDetail = (
|
||||||
|
ledgerTree: SteadyDataView.SteadyLedgerNode[],
|
||||||
|
row: Pick<SteadyDataView.SteadyChecksquareTask, 'lineId' | 'lineName'>
|
||||||
|
): ChecksquareMeasurementPointDetail => {
|
||||||
|
const lineId = row.lineId || ''
|
||||||
|
const ledgerPath = lineId ? collectLedgerPath(ledgerTree, lineId) : []
|
||||||
|
const engineeringNode = ledgerPath.find(item => item.level === 0)
|
||||||
|
const projectNode = ledgerPath.find(item => item.level === 1)
|
||||||
|
const equipmentNode = ledgerPath.find(item => item.level === 2)
|
||||||
|
const lineNode = ledgerPath.find(item => item.level === 3)
|
||||||
|
const rawEquipmentNode = (equipmentNode || {}) as SteadyDataView.SteadyLedgerNode & Record<string, unknown>
|
||||||
|
const rawLineNode = (lineNode || {}) as SteadyDataView.SteadyLedgerNode & Record<string, unknown>
|
||||||
|
|
||||||
|
// 数据校验任务只返回监测点 ID/名称,弹窗所需层级信息从已加载的台账树回溯补齐。
|
||||||
|
return {
|
||||||
|
engineeringName: engineeringNode?.name,
|
||||||
|
projectName: projectNode?.name,
|
||||||
|
equipmentName: equipmentNode?.name,
|
||||||
|
networkParam: resolveText(rawEquipmentNode, 'mac', 'ndid', 'unnid') || resolveText(rawLineNode, 'mac', 'ndid', 'unnid'),
|
||||||
|
lineName: lineNode?.name || row.lineName || row.lineId
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,11 +24,18 @@ export const formatBooleanText = (value?: boolean | null) => {
|
|||||||
return value ? '是' : '否'
|
return value ? '是' : '否'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const formatMissingRate = (value?: number | null, text?: string | null) => {
|
export const formatDataIntegrity = (value?: number | null, text?: string | null, fallbackMissingRate?: number | null) => {
|
||||||
if (text) return text
|
if (text) return text
|
||||||
if (value === null || value === undefined || !Number.isFinite(Number(value))) return '-'
|
const integrityValue =
|
||||||
|
value === null || value === undefined || !Number.isFinite(Number(value))
|
||||||
|
? fallbackMissingRate === null || fallbackMissingRate === undefined || !Number.isFinite(Number(fallbackMissingRate))
|
||||||
|
? null
|
||||||
|
: 1 - Number(fallbackMissingRate)
|
||||||
|
: Number(value)
|
||||||
|
|
||||||
return `${(Number(value) * 100).toFixed(2)}%`
|
if (integrityValue === null) return '-'
|
||||||
|
|
||||||
|
return `${(integrityValue * 100).toFixed(2)}%`
|
||||||
}
|
}
|
||||||
|
|
||||||
export const findStatSummary = (
|
export const findStatSummary = (
|
||||||
@@ -45,7 +52,7 @@ export const formatStatMissingRate = (
|
|||||||
const summary = findStatSummary(item, statType)
|
const summary = findStatSummary(item, statType)
|
||||||
if (!summary || summary.supported === false) return '-'
|
if (!summary || summary.supported === false) return '-'
|
||||||
|
|
||||||
return formatMissingRate(summary.missingRate, summary.missingRateText)
|
return formatDataIntegrity(summary.dataIntegrity, summary.dataIntegrityText, summary.missingRate)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const resolveChecksquareRowName = (item: SteadyDataView.SteadyChecksquareItem) => {
|
export const resolveChecksquareRowName = (item: SteadyDataView.SteadyChecksquareItem) => {
|
||||||
@@ -221,6 +228,7 @@ const summarizeStatType = (
|
|||||||
expectedPointCount,
|
expectedPointCount,
|
||||||
actualPointCount,
|
actualPointCount,
|
||||||
missingPointCount,
|
missingPointCount,
|
||||||
|
dataIntegrity: expectedPointCount ? actualPointCount / expectedPointCount : null,
|
||||||
missingRate: expectedPointCount ? missingPointCount / expectedPointCount : null,
|
missingRate: expectedPointCount ? missingPointCount / expectedPointCount : null,
|
||||||
maxContinuousMissingMinutes
|
maxContinuousMissingMinutes
|
||||||
}
|
}
|
||||||
@@ -253,6 +261,8 @@ export const buildHarmonicParentSummary = (
|
|||||||
expectedPointCount,
|
expectedPointCount,
|
||||||
actualPointCount,
|
actualPointCount,
|
||||||
missingPointCount,
|
missingPointCount,
|
||||||
|
dataIntegrity: expectedPointCount ? actualPointCount / expectedPointCount : null,
|
||||||
|
dataIntegrityText: expectedPointCount ? undefined : '-',
|
||||||
missingRate: expectedPointCount ? missingPointCount / expectedPointCount : null,
|
missingRate: expectedPointCount ? missingPointCount / expectedPointCount : null,
|
||||||
missingRateText: expectedPointCount ? undefined : '-',
|
missingRateText: expectedPointCount ? undefined : '-',
|
||||||
maxContinuousMissingMinutes,
|
maxContinuousMissingMinutes,
|
||||||
|
|||||||
@@ -33,10 +33,17 @@ export const resolveChecksquareTaskStatusType = (status?: string) => {
|
|||||||
return 'info'
|
return 'info'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const formatChecksquarePercent = (value?: number | null) => {
|
export const formatChecksquareIntegrity = (value?: number | null, fallbackMissingRate?: number | null) => {
|
||||||
if (value === null || value === undefined || !Number.isFinite(Number(value))) return '--'
|
const integrityValue =
|
||||||
|
value === null || value === undefined || !Number.isFinite(Number(value))
|
||||||
|
? fallbackMissingRate === null || fallbackMissingRate === undefined || !Number.isFinite(Number(fallbackMissingRate))
|
||||||
|
? null
|
||||||
|
: 1 - Number(fallbackMissingRate)
|
||||||
|
: Number(value)
|
||||||
|
|
||||||
return `${(Number(value) * 100).toFixed(2)}%`
|
if (integrityValue === null) return '--'
|
||||||
|
|
||||||
|
return `${(integrityValue * 100).toFixed(2)}%`
|
||||||
}
|
}
|
||||||
|
|
||||||
export const resolveChecksquareText = (value: unknown) => {
|
export const resolveChecksquareText = (value: unknown) => {
|
||||||
|
|||||||
@@ -19,6 +19,11 @@
|
|||||||
<div class="tool-text">进入 MMS 映射页面,后续可继续补充映射配置和预览能力。</div>
|
<div class="tool-text">进入 MMS 映射页面,后续可继续补充映射配置和预览能力。</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button class="tool-item" type="button" @click="handleNavigate('/tools/mmsMapping/deviceTypes')">
|
||||||
|
<div class="tool-name">设备类型校验</div>
|
||||||
|
<div class="tool-text">维护设备类型,并从列表进入 ICD 一致性校验和 PQDIF 预留校验。</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
<button class="tool-item" type="button" @click="handleNavigate('/tools/addData')">
|
<button class="tool-item" type="button" @click="handleNavigate('/tools/addData')">
|
||||||
<div class="tool-name">addData</div>
|
<div class="tool-name">addData</div>
|
||||||
<div class="tool-text">进入 addData 页面壳子,后续在此扩展补录数据能力和业务交互。</div>
|
<div class="tool-text">进入 addData 页面壳子,后续在此扩展补录数据能力和业务交互。</div>
|
||||||
|
|||||||
287
frontend/src/views/tools/mmsMapping/deviceTypes/index.vue
Normal file
287
frontend/src/views/tools/mmsMapping/deviceTypes/index.vue
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
<template>
|
||||||
|
<div class="table-box mms-device-type-page">
|
||||||
|
<section class="table-main card mms-device-type-card">
|
||||||
|
<div class="table-header">
|
||||||
|
<div class="header-button-lf">
|
||||||
|
<el-button type="primary" :icon="Plus" @click="openCreateDialog">新增设备类型</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="header-button-ri">
|
||||||
|
<el-button circle :icon="Refresh" :loading="loading" @click="loadDeviceTypes" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="device-type-table-body">
|
||||||
|
<el-table v-loading="loading" :data="deviceTypes" border stripe height="100%">
|
||||||
|
<el-table-column
|
||||||
|
prop="name"
|
||||||
|
label="设备类型名称"
|
||||||
|
min-width="180"
|
||||||
|
fixed="left"
|
||||||
|
show-overflow-tooltip
|
||||||
|
/>
|
||||||
|
<el-table-column prop="icdName" label="ICD 名称" min-width="180" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="icdPath" label="ICD 路径" min-width="220" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="reportName" label="报告模板" min-width="160" show-overflow-tooltip />
|
||||||
|
<el-table-column label="ICD 校验结论" min-width="150" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="getIcdResultTagType(row.icdResult)" effect="light">
|
||||||
|
{{ getIcdResultText(row.icdResult) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="icdMsg" label="结论描述" min-width="220" show-overflow-tooltip />
|
||||||
|
<el-table-column label="操作" width="230" fixed="right" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
:icon="Connection"
|
||||||
|
:disabled="!row.canCheckIcd"
|
||||||
|
@click="handleIcdCheck(row)"
|
||||||
|
>
|
||||||
|
ICD一致性校验
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
:icon="DocumentChecked"
|
||||||
|
:loading="pqdifCheckingId === row.id"
|
||||||
|
:disabled="!row.canCheckPqdif"
|
||||||
|
@click="handlePqdifCheck(row)"
|
||||||
|
>
|
||||||
|
PQDIF校验
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<el-dialog v-model="createDialogVisible" title="新增设备类型" width="520px" destroy-on-close>
|
||||||
|
<el-form ref="createFormRef" :model="createForm" :rules="createRules" label-width="110px">
|
||||||
|
<el-form-item label="设备类型名称" prop="name">
|
||||||
|
<el-input v-model="createForm.name" maxlength="80" clearable placeholder="请输入设备类型名称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="ICD ID">
|
||||||
|
<el-input v-model="createForm.icdId" maxlength="80" clearable placeholder="可选,关联 ICD ID" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="ICD 名称">
|
||||||
|
<el-input v-model="createForm.icdName" maxlength="120" clearable placeholder="可选,ICD 名称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="ICD 路径">
|
||||||
|
<el-input v-model="createForm.icdPath" maxlength="260" clearable placeholder="可选,ICD 存储路径" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="报告模板">
|
||||||
|
<el-input
|
||||||
|
v-model="createForm.reportName"
|
||||||
|
maxlength="120"
|
||||||
|
clearable
|
||||||
|
placeholder="可选,报告模板名称"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="createDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="creating" @click="handleCreateDeviceType">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Connection, DocumentChecked, Plus, Refresh } from '@element-plus/icons-vue'
|
||||||
|
import { ElMessage, type FormInstance, type FormRules, type TagProps } from 'element-plus'
|
||||||
|
import { onMounted, reactive, ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import type { ResultData } from '@/api/interface'
|
||||||
|
import { createDeviceTypeApi, listDeviceTypesApi, pqdifCheckApi } from '@/api/tools/mmsmapping'
|
||||||
|
import type { MmsMapping } from '@/api/tools/mmsmapping/interface'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'MmsDeviceTypesView'
|
||||||
|
})
|
||||||
|
|
||||||
|
type TagType = TagProps['type']
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const loading = ref(false)
|
||||||
|
const creating = ref(false)
|
||||||
|
const pqdifCheckingId = ref('')
|
||||||
|
const createDialogVisible = ref(false)
|
||||||
|
const createFormRef = ref<FormInstance>()
|
||||||
|
const deviceTypes = ref<MmsMapping.DeviceType[]>([])
|
||||||
|
const createForm = reactive<MmsMapping.CreateDeviceTypeRequest>({
|
||||||
|
name: '',
|
||||||
|
icdId: '',
|
||||||
|
icdName: '',
|
||||||
|
icdPath: '',
|
||||||
|
reportName: ''
|
||||||
|
})
|
||||||
|
const createRules: FormRules<MmsMapping.CreateDeviceTypeRequest> = {
|
||||||
|
name: [{ required: true, message: '请输入设备类型名称', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
function unwrapApiPayload<T>(response: ResultData<T> | T): T {
|
||||||
|
if (response && typeof response === 'object' && 'data' in response) {
|
||||||
|
return (response as ResultData<T>).data
|
||||||
|
}
|
||||||
|
|
||||||
|
return response as T
|
||||||
|
}
|
||||||
|
|
||||||
|
const getErrorMessage = (error: unknown) => {
|
||||||
|
if (error instanceof Error && error.message) return error.message
|
||||||
|
return '接口调用失败,请检查后端服务和请求参数'
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimOptionalText = (value?: string) => {
|
||||||
|
const text = value?.trim()
|
||||||
|
|
||||||
|
return text || undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetCreateForm = () => {
|
||||||
|
createForm.name = ''
|
||||||
|
createForm.icdId = ''
|
||||||
|
createForm.icdName = ''
|
||||||
|
createForm.icdPath = ''
|
||||||
|
createForm.reportName = ''
|
||||||
|
createFormRef.value?.clearValidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
const openCreateDialog = () => {
|
||||||
|
resetCreateForm()
|
||||||
|
createDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const getIcdResultText = (value?: number) => {
|
||||||
|
if (value === 1) return '一致'
|
||||||
|
if (value === 0) return '不一致'
|
||||||
|
return '未校验'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getIcdResultTagType = (value?: number): TagType => {
|
||||||
|
if (value === 1) return 'success'
|
||||||
|
if (value === 0) return 'danger'
|
||||||
|
return 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadDeviceTypes = async () => {
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await listDeviceTypesApi()
|
||||||
|
|
||||||
|
deviceTypes.value = unwrapApiPayload<MmsMapping.DeviceType[]>(response) || []
|
||||||
|
} catch (error) {
|
||||||
|
deviceTypes.value = []
|
||||||
|
ElMessage.error(getErrorMessage(error))
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCreateDeviceType = async () => {
|
||||||
|
const valid = await createFormRef.value?.validate().catch(() => false)
|
||||||
|
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
creating.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
await createDeviceTypeApi({
|
||||||
|
name: createForm.name.trim(),
|
||||||
|
icdId: trimOptionalText(createForm.icdId),
|
||||||
|
icdName: trimOptionalText(createForm.icdName),
|
||||||
|
icdPath: trimOptionalText(createForm.icdPath),
|
||||||
|
reportName: trimOptionalText(createForm.reportName)
|
||||||
|
})
|
||||||
|
createDialogVisible.value = false
|
||||||
|
ElMessage.success('设备类型新增成功')
|
||||||
|
await loadDeviceTypes()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(getErrorMessage(error))
|
||||||
|
} finally {
|
||||||
|
creating.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleIcdCheck = async (row: MmsMapping.DeviceType) => {
|
||||||
|
if (!row.id) {
|
||||||
|
ElMessage.warning('当前设备类型缺少 ID,不能执行 ICD 一致性校验')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关键业务节点:设备类型页只负责传递校验上下文,ICD 文件选择、索引确认和映射生成复用现有 MMS 映射页流程。
|
||||||
|
await router.push({
|
||||||
|
path: '/tools/mmsMapping',
|
||||||
|
query: {
|
||||||
|
deviceTypeId: row.id,
|
||||||
|
deviceTypeName: row.name || '',
|
||||||
|
fromDeviceTypeCheck: '1'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePqdifCheck = async (row: MmsMapping.DeviceType) => {
|
||||||
|
if (!row.id) {
|
||||||
|
ElMessage.warning('当前设备类型缺少 ID,不能执行 PQDIF 校验')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pqdifCheckingId.value = row.id
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await pqdifCheckApi(row.id)
|
||||||
|
const payload = unwrapApiPayload<MmsMapping.PqdifCheckPlaceholder>(response)
|
||||||
|
|
||||||
|
ElMessage.info(payload?.message || 'PQDIF校验功能待实现')
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(getErrorMessage(error))
|
||||||
|
} finally {
|
||||||
|
pqdifCheckingId.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadDeviceTypes()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.mms-device-type-page {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mms-device-type-card {
|
||||||
|
padding: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-button-lf,
|
||||||
|
.header-button-ri {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-type-table-body {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
margin-top: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-type-table-body :deep(.el-table__inner-wrapper) {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -52,9 +52,14 @@
|
|||||||
:can-generate-xml-mapping="canGenerateXmlMapping"
|
:can-generate-xml-mapping="canGenerateXmlMapping"
|
||||||
:is-generating-xml="isGeneratingXml"
|
:is-generating-xml="isGeneratingXml"
|
||||||
:show-xml-mapping-tab="showXmlMappingTab"
|
:show-xml-mapping-tab="showXmlMappingTab"
|
||||||
|
:show-save-icd-check-result="showSaveIcdCheckResult"
|
||||||
|
:can-save-icd-check-result="canSaveIcdCheckResult"
|
||||||
|
:is-saving-icd-check-result="isSavingIcdCheckResult"
|
||||||
|
:save-icd-check-result-text="saveIcdCheckResultText"
|
||||||
@export-mapping="handleExportMapping"
|
@export-mapping="handleExportMapping"
|
||||||
@generate-xml-mapping="handleGenerateXmlMapping"
|
@generate-xml-mapping="handleGenerateXmlMapping"
|
||||||
@update-mapping-json="handleUpdateMappingJson"
|
@update-mapping-json="handleUpdateMappingJson"
|
||||||
|
@save-icd-check-result="handleSaveIcdCheckResult"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -70,6 +75,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import type { ResultData } from '@/api/interface'
|
import type { ResultData } from '@/api/interface'
|
||||||
import {
|
import {
|
||||||
@@ -77,7 +83,8 @@ import {
|
|||||||
buildIndexSelectionApi,
|
buildIndexSelectionApi,
|
||||||
getIcdApi,
|
getIcdApi,
|
||||||
getIcdMmsJsonApi,
|
getIcdMmsJsonApi,
|
||||||
getXmlFromJsonApi
|
getXmlFromJsonApi,
|
||||||
|
saveIcdCheckResultApi
|
||||||
} from '@/api/tools/mmsmapping'
|
} from '@/api/tools/mmsmapping'
|
||||||
import type { MmsMapping } from '@/api/tools/mmsmapping/interface'
|
import type { MmsMapping } from '@/api/tools/mmsmapping/interface'
|
||||||
import MappingRequestPanel from './components/MappingRequestPanel.vue'
|
import MappingRequestPanel from './components/MappingRequestPanel.vue'
|
||||||
@@ -112,7 +119,9 @@ const isParsing = ref(false)
|
|||||||
const isConfirmingSelection = ref(false)
|
const isConfirmingSelection = ref(false)
|
||||||
const isGenerating = ref(false)
|
const isGenerating = ref(false)
|
||||||
const isGeneratingXml = ref(false)
|
const isGeneratingXml = ref(false)
|
||||||
|
const isSavingIcdCheckResult = ref(false)
|
||||||
const icdFileAccept = '.icd,.cid,.scd,.xml'
|
const icdFileAccept = '.icd,.cid,.scd,.xml'
|
||||||
|
const route = useRoute()
|
||||||
const problemEmptyText = '当前返回未包含 problems'
|
const problemEmptyText = '当前返回未包含 problems'
|
||||||
|
|
||||||
function unwrapApiPayload<T>(response: ResultData<T> | T): T {
|
function unwrapApiPayload<T>(response: ResultData<T> | T): T {
|
||||||
@@ -202,7 +211,12 @@ const parsedIndexSelectionState = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const isSubmitting = computed(
|
const isSubmitting = computed(
|
||||||
() => isParsing.value || isConfirmingSelection.value || isGenerating.value || isGeneratingXml.value
|
() =>
|
||||||
|
isParsing.value ||
|
||||||
|
isConfirmingSelection.value ||
|
||||||
|
isGenerating.value ||
|
||||||
|
isGeneratingXml.value ||
|
||||||
|
isSavingIcdCheckResult.value
|
||||||
)
|
)
|
||||||
const canResetPage = computed(() =>
|
const canResetPage = computed(() =>
|
||||||
Boolean(
|
Boolean(
|
||||||
@@ -216,7 +230,12 @@ const canResetPage = computed(() =>
|
|||||||
const indexSelectionError = computed(() => parsedIndexSelectionState.value.error)
|
const indexSelectionError = computed(() => parsedIndexSelectionState.value.error)
|
||||||
const canParseIcd = computed(() => Boolean(selectedIcdFile.value && !isSubmitting.value))
|
const canParseIcd = computed(() => Boolean(selectedIcdFile.value && !isSubmitting.value))
|
||||||
const canGenerate = computed(() =>
|
const canGenerate = computed(() =>
|
||||||
Boolean(selectedIcdFile.value && indexSelectionJsonText.value.trim() && !indexSelectionError.value && !isSubmitting.value)
|
Boolean(
|
||||||
|
selectedIcdFile.value &&
|
||||||
|
indexSelectionJsonText.value.trim() &&
|
||||||
|
!indexSelectionError.value &&
|
||||||
|
!isSubmitting.value
|
||||||
|
)
|
||||||
)
|
)
|
||||||
// 关键业务节点:请求配置区只在用户已经选择 ICD 后展示,避免初始态暴露无效的请求编辑区。
|
// 关键业务节点:请求配置区只在用户已经选择 ICD 后展示,避免初始态暴露无效的请求编辑区。
|
||||||
const showConfigPanel = computed(() => Boolean(selectedIcdFile.value))
|
const showConfigPanel = computed(() => Boolean(selectedIcdFile.value))
|
||||||
@@ -307,6 +326,23 @@ const canGenerateXmlMapping = computed(() => Boolean(mappingJsonPreview.value &&
|
|||||||
const showXmlMappingTab = computed(() =>
|
const showXmlMappingTab = computed(() =>
|
||||||
Boolean(xmlResponsePayload.value && xmlResponsePayload.value.status !== 'FAILED')
|
Boolean(xmlResponsePayload.value && xmlResponsePayload.value.status !== 'FAILED')
|
||||||
)
|
)
|
||||||
|
const deviceTypeCheckId = computed(() => {
|
||||||
|
const value = route.query.deviceTypeId
|
||||||
|
|
||||||
|
return typeof value === 'string' ? value.trim() : ''
|
||||||
|
})
|
||||||
|
const deviceTypeCheckName = computed(() => {
|
||||||
|
const value = route.query.deviceTypeName
|
||||||
|
|
||||||
|
return typeof value === 'string' ? value.trim() : ''
|
||||||
|
})
|
||||||
|
const showSaveIcdCheckResult = computed(() => Boolean(deviceTypeCheckId.value))
|
||||||
|
const canSaveIcdCheckResult = computed(() =>
|
||||||
|
Boolean(deviceTypeCheckId.value && mappingJsonPreview.value && !isSubmitting.value)
|
||||||
|
)
|
||||||
|
const saveIcdCheckResultText = computed(() =>
|
||||||
|
deviceTypeCheckName.value ? `入库:${deviceTypeCheckName.value}` : '校验结果入库'
|
||||||
|
)
|
||||||
|
|
||||||
const xmlMappingPreview = computed(() => {
|
const xmlMappingPreview = computed(() => {
|
||||||
const source = xmlContentForExport.value
|
const source = xmlContentForExport.value
|
||||||
@@ -632,6 +668,44 @@ const handleExportMapping = (type: ExportMappingType) => {
|
|||||||
ElMessage.success('XML 映射已导出')
|
ElMessage.success('XML 映射已导出')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSaveIcdCheckResult = async () => {
|
||||||
|
if (!deviceTypeCheckId.value) {
|
||||||
|
ElMessage.warning('当前缺少设备类型 ID,不能入库校验结果')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const mappingJson = responsePayload.value?.mappingJson?.trim() || mappingJsonPreview.value
|
||||||
|
|
||||||
|
if (!mappingJson) {
|
||||||
|
ElMessage.warning('请先生成 JSON 映射后再入库')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isSavingIcdCheckResult.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 关键业务节点:设备类型 ICD 校验入库只保存本次生成的映射结果,XML 已生成时一并回写。
|
||||||
|
const response = await saveIcdCheckResultApi(deviceTypeCheckId.value, {
|
||||||
|
mappingJson,
|
||||||
|
xml: xmlContentForExport.value || undefined,
|
||||||
|
result: 1,
|
||||||
|
msg: xmlContentForExport.value ? 'ICD一致性校验通过,JSON/XML已生成' : 'ICD一致性校验通过,JSON已生成'
|
||||||
|
})
|
||||||
|
const saved = unwrapApiPayload<boolean>(response)
|
||||||
|
|
||||||
|
if (!saved) {
|
||||||
|
ElMessage.warning('校验结果入库未返回成功')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ElMessage.success('校验结果已入库')
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(getErrorMessage(error))
|
||||||
|
} finally {
|
||||||
|
isSavingIcdCheckResult.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleUpdateMappingJson = (mappingJson: string) => {
|
const handleUpdateMappingJson = (mappingJson: string) => {
|
||||||
if (!responsePayload.value) return
|
if (!responsePayload.value) return
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,17 @@
|
|||||||
>
|
>
|
||||||
生成XML映射
|
生成XML映射
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="showSaveIcdCheckResult"
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
:icon="Finished"
|
||||||
|
:loading="isSavingIcdCheckResult"
|
||||||
|
:disabled="!canSaveIcdCheckResult"
|
||||||
|
@click="emit('save-icd-check-result')"
|
||||||
|
>
|
||||||
|
{{ saveIcdCheckResultText }}
|
||||||
|
</el-button>
|
||||||
<div class="export-actions">
|
<div class="export-actions">
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
@@ -128,13 +139,7 @@
|
|||||||
<el-empty v-else :description="problemEmptyText" />
|
<el-empty v-else :description="problemEmptyText" />
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog
|
<el-dialog v-model="matchResultDialogVisible" title="匹配结果展示" width="640px" destroy-on-close top="12vh">
|
||||||
v-model="matchResultDialogVisible"
|
|
||||||
title="匹配结果展示"
|
|
||||||
width="640px"
|
|
||||||
destroy-on-close
|
|
||||||
top="12vh"
|
|
||||||
>
|
|
||||||
<div class="match-result-detail">
|
<div class="match-result-detail">
|
||||||
{{ methodDescribe || '当前接口返回未包含 methodDescribe' }}
|
{{ methodDescribe || '当前接口返回未包含 methodDescribe' }}
|
||||||
</div>
|
</div>
|
||||||
@@ -179,7 +184,9 @@
|
|||||||
>
|
>
|
||||||
<div class="sequence-type-header">
|
<div class="sequence-type-header">
|
||||||
<div class="sequence-type-title">{{ typeGroup.typeName }}</div>
|
<div class="sequence-type-title">{{ typeGroup.typeName }}</div>
|
||||||
<el-tag type="primary" effect="plain" size="small">{{ typeGroup.rows.length }} 项</el-tag>
|
<el-tag type="primary" effect="plain" size="small">
|
||||||
|
{{ typeGroup.rows.length }} 项
|
||||||
|
</el-tag>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-for="row in typeGroup.rows" :key="row.id" class="sequence-config-item">
|
<div v-for="row in typeGroup.rows" :key="row.id" class="sequence-config-item">
|
||||||
@@ -220,7 +227,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ArrowDown, Connection, Document, Download, Search, Setting, Warning } from '@element-plus/icons-vue'
|
import { ArrowDown, Connection, Document, Download, Finished, Search, Setting, Warning } from '@element-plus/icons-vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import JsonMappingTree from './JsonMappingTree.vue'
|
import JsonMappingTree from './JsonMappingTree.vue'
|
||||||
@@ -282,6 +289,10 @@ const props = defineProps<{
|
|||||||
canGenerateXmlMapping: boolean
|
canGenerateXmlMapping: boolean
|
||||||
isGeneratingXml: boolean
|
isGeneratingXml: boolean
|
||||||
showXmlMappingTab: boolean
|
showXmlMappingTab: boolean
|
||||||
|
showSaveIcdCheckResult: boolean
|
||||||
|
canSaveIcdCheckResult: boolean
|
||||||
|
isSavingIcdCheckResult: boolean
|
||||||
|
saveIcdCheckResultText: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -289,6 +300,7 @@ const emit = defineEmits<{
|
|||||||
(event: 'export-mapping', value: ExportMappingType): void
|
(event: 'export-mapping', value: ExportMappingType): void
|
||||||
(event: 'generate-xml-mapping'): void
|
(event: 'generate-xml-mapping'): void
|
||||||
(event: 'update-mapping-json', value: string): void
|
(event: 'update-mapping-json', value: string): void
|
||||||
|
(event: 'save-icd-check-result'): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const activeTabProxy = computed({
|
const activeTabProxy = computed({
|
||||||
@@ -322,9 +334,17 @@ const filteredSequenceRows = computed(() => {
|
|||||||
if (!keyword) return sequenceConfigRows.value
|
if (!keyword) return sequenceConfigRows.value
|
||||||
|
|
||||||
return sequenceConfigRows.value.filter(row =>
|
return sequenceConfigRows.value.filter(row =>
|
||||||
[row.topKey, row.topDesc, row.desc, row.parentDesc, row.name, row.baseflag, row.pathText, row.start, row.end].some(
|
[
|
||||||
value => value.toLowerCase().includes(keyword)
|
row.topKey,
|
||||||
)
|
row.topDesc,
|
||||||
|
row.desc,
|
||||||
|
row.parentDesc,
|
||||||
|
row.name,
|
||||||
|
row.baseflag,
|
||||||
|
row.pathText,
|
||||||
|
row.start,
|
||||||
|
row.end
|
||||||
|
].some(value => value.toLowerCase().includes(keyword))
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
const sequenceConfigGroups = computed<SequenceConfigGroup[]>(() => {
|
const sequenceConfigGroups = computed<SequenceConfigGroup[]>(() => {
|
||||||
@@ -365,7 +385,8 @@ const handleExportCommand = (command: string | number | object) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isRecord = (value: unknown): value is JsonObject => Boolean(value && typeof value === 'object' && !Array.isArray(value))
|
const isRecord = (value: unknown): value is JsonObject =>
|
||||||
|
Boolean(value && typeof value === 'object' && !Array.isArray(value))
|
||||||
|
|
||||||
const toDisplayText = (value: unknown, fallback: string) => {
|
const toDisplayText = (value: unknown, fallback: string) => {
|
||||||
if (typeof value === 'string' && value.trim()) return value.trim()
|
if (typeof value === 'string' && value.trim()) return value.trim()
|
||||||
@@ -392,7 +413,12 @@ const isConfigurableBaseflag = (value: unknown) => {
|
|||||||
return ['1', '2'].includes(value.trim())
|
return ['1', '2'].includes(value.trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
const collectSequenceRows = (source: unknown, path: JsonPath = [], parentDesc = '', rootSource = source): SequenceConfigRow[] => {
|
const collectSequenceRows = (
|
||||||
|
source: unknown,
|
||||||
|
path: JsonPath = [],
|
||||||
|
parentDesc = '',
|
||||||
|
rootSource = source
|
||||||
|
): SequenceConfigRow[] => {
|
||||||
if (Array.isArray(source)) {
|
if (Array.isArray(source)) {
|
||||||
return source.flatMap((item, index) => collectSequenceRows(item, [...path, index], parentDesc, rootSource))
|
return source.flatMap((item, index) => collectSequenceRows(item, [...path, index], parentDesc, rootSource))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ if (!fs.existsSync(path.join(rootDir, files.manager))) {
|
|||||||
const source = read(files.manager)
|
const source = read(files.manager)
|
||||||
const checks = [
|
const checks = [
|
||||||
['manager exports InfluxDBProcessManager', /module\.exports\s*=\s*InfluxDBProcessManager/.test(source)],
|
['manager exports InfluxDBProcessManager', /module\.exports\s*=\s*InfluxDBProcessManager/.test(source)],
|
||||||
['manager starts InfluxDB through cmd.exe and start-influxdb.bat', /cmd\.exe/.test(source) && /start-influxdb\.bat/.test(source) && /spawn\(/.test(source)],
|
['manager starts InfluxDB directly through influxd.exe', /spawn\(influxd,\s*\[\s*'-config'/.test(source)],
|
||||||
|
['manager does not record cmd.exe wrapper pid as InfluxDB owner', !/spawn\('cmd\.exe'/.test(source)],
|
||||||
['manager records process ownership', /\.running-process\.json/.test(source)],
|
['manager records process ownership', /\.running-process\.json/.test(source)],
|
||||||
['manager can stop tracked process', /stopInfluxDBProcess/.test(source) && /terminateTrackedProcess/.test(source)]
|
['manager can stop tracked process', /stopInfluxDBProcess/.test(source) && /terminateTrackedProcess/.test(source)]
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -164,13 +164,12 @@ class InfluxDBProcessManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const expectedExe = this.normalizeComparablePath(path.join(this.influxdbPath, 'influxd.exe'));
|
const expectedExe = this.normalizeComparablePath(path.join(this.influxdbPath, 'influxd.exe'));
|
||||||
const expectedBat = this.normalizeComparablePath(path.join(this.influxdbPath, 'start-influxdb.bat'));
|
|
||||||
const expectedConfig = this.normalizeComparablePath((record && record.configFile) || this.configFile);
|
const expectedConfig = this.normalizeComparablePath((record && record.configFile) || this.configFile);
|
||||||
const executablePath = this.normalizeComparablePath(processInfo.executablePath);
|
const executablePath = this.normalizeComparablePath(processInfo.executablePath);
|
||||||
const commandLine = this.normalizeComparablePath(processInfo.commandLine);
|
const commandLine = this.normalizeComparablePath(processInfo.commandLine);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
(executablePath === expectedExe || commandLine.includes(expectedBat)) &&
|
executablePath === expectedExe &&
|
||||||
(!expectedConfig || commandLine.includes(expectedConfig))
|
(!expectedConfig || commandLine.includes(expectedConfig))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -268,24 +267,20 @@ bind-address = "127.0.0.1:8088"
|
|||||||
async startInfluxDBProcess(port) {
|
async startInfluxDBProcess(port) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const influxd = path.join(this.influxdbPath, 'influxd.exe');
|
const influxd = path.join(this.influxdbPath, 'influxd.exe');
|
||||||
const startBat = path.join(this.influxdbPath, 'start-influxdb.bat');
|
|
||||||
|
|
||||||
this.log('system', '正在启动 InfluxDB 进程...');
|
this.log('system', '正在启动 InfluxDB 进程...');
|
||||||
this.log('system', `可执行文件: ${influxd}`);
|
this.log('system', `可执行文件: ${influxd}`);
|
||||||
this.log('system', `启动脚本: ${startBat}`);
|
|
||||||
this.log('system', `配置文件: ${this.configFile}`);
|
this.log('system', `配置文件: ${this.configFile}`);
|
||||||
|
|
||||||
if (!fs.existsSync(influxd)) {
|
if (!fs.existsSync(influxd)) {
|
||||||
return reject(new Error(`influxd.exe 不存在: ${influxd}`));
|
return reject(new Error(`influxd.exe 不存在: ${influxd}`));
|
||||||
}
|
}
|
||||||
if (!fs.existsSync(startBat)) {
|
|
||||||
return reject(new Error(`start-influxdb.bat 不存在: ${startBat}`));
|
|
||||||
}
|
|
||||||
if (!fs.existsSync(this.configFile)) {
|
if (!fs.existsSync(this.configFile)) {
|
||||||
return reject(new Error(`配置文件不存在: ${this.configFile}`));
|
return reject(new Error(`配置文件不存在: ${this.configFile}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
const influxdbProcess = spawn('cmd.exe', ['/d', '/s', '/c', 'call', startBat, this.configFile], {
|
// 直接启动 influxd.exe,进程标记中的 PID 才能精确对应实际 InfluxDB 进程。
|
||||||
|
const influxdbProcess = spawn(influxd, ['-config', this.configFile], {
|
||||||
cwd: this.influxdbPath,
|
cwd: this.influxdbPath,
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
windowsHide: true
|
windowsHide: true
|
||||||
|
|||||||
Reference in New Issue
Block a user