feat(auth): 优化权限模块菜单数据处理逻辑

- 添加showMenuList、flatMenuList和breadcrumbList状态字段
- 修改getter方法直接返回缓存的状态数据
- 新增refreshDerivedMenus方法统一处理菜单衍生数据计算
- 在重置授权存储时清理新增的菜单相关状态
- 避免每次路由跳转时重复深拷贝整个菜单树结构

feat(checksquare): 完善校验功能组件和业务逻辑

- 新增测量点对话框组件用于显示监测点详细信息
- 添加校验台账工具函数解析测量点详情
- 实现任务表格删除功能包括确认提示和数据刷新
- 更新任务表格将缺失率字段替换为数据完整性字段
- 重构详情面板使用标签页展示不同类型的校验详情
- 优化摘要表格样式包括紧凑布局和危险颜色标识
- 统一详情对话框尺寸样式保持界面一致性
- 实现数据完整性字段的百分比单位去除处理

refactor(influxdb): 简化数据库启动流程移除命令行包装器

- 直接通过influxd.exe启动InfluxDB服务
- 移除对cmd.exe包装器的依赖和进程ID记录
- 保持进程管理和停止功能的完整性
This commit is contained in:
2026-06-12 08:44:07 +08:00
parent 8622f25048
commit 81f90ce0f2
26 changed files with 1279 additions and 243 deletions

View File

@@ -1,123 +1,131 @@
<template>
<section class="card checksquare-detail">
<div class="detail-header">
<div>
<div class="section-title">检测项明细</div>
<div class="section-description">
{{ selectedItem ? resolveChecksquareRowName(selectedItem) : '请选择总览表中的指标' }}
</div>
</div>
</div>
<section class="table-main card checksquare-detail">
<el-empty v-if="!selectedItem" description="请选择指标查看明细" />
<template v-else>
<div class="stat-grid">
<div v-for="statType in CHECKSQUARE_STAT_TYPES" :key="statType" class="stat-card">
<span class="stat-name">{{ formatChecksquareStatType(statType) }}</span>
<span class="stat-value">{{ formatStatMissingRate(selectedItem, statType) }}</span>
<el-tabs v-else v-model="detailType" class="detail-tabs" @tab-change="handleDetailTypeChange">
<el-tab-pane label="缺数区间" name="SEGMENT">
<div class="item-overview">
<div class="overview-item">
<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 class="detail-toolbar">
<el-radio-group v-model="detailType" @change="handleDetailTypeChange">
<el-radio-button label="SEGMENT">缺数区间</el-radio-button>
<el-radio-button label="VALUE_ORDER">值关系异常</el-radio-button>
<el-radio-button label="HARMONIC_PARITY">谐波奇偶异常</el-radio-button>
</el-radio-group>
<el-select
v-if="detailType === 'SEGMENT'"
v-model="segmentStatType"
class="stat-select"
@change="handleSegmentStatTypeChange"
<div class="stat-grid">
<div v-for="statType in CHECKSQUARE_STAT_TYPES" :key="statType" class="stat-card">
<span class="stat-name">{{ formatChecksquareStatType(statType) }}</span>
<span class="stat-value">{{ formatStatMissingRate(selectedItem, statType) }}</span>
</div>
</div>
<div class="segment-toolbar">
<el-select v-model="segmentStatType" class="stat-select" @change="handleSegmentStatTypeChange">
<el-option v-for="statType in CHECKSQUARE_STAT_TYPES" :key="statType" :label="formatChecksquareStatType(statType)" :value="statType" />
</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-select>
</div>
<el-table-column prop="time" label="时间" min-width="160" />
<el-table-column prop="phase" label="相别" width="80" />
<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
v-if="detailType === 'SEGMENT'"
v-loading="loading"
class="segment-table"
:data="segments"
size="small"
max-height="300"
empty-text="暂无缺数区间"
>
<el-table-column prop="statType" label="统计类型" width="96">
<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="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>
<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>
</el-tab-pane>
<el-table
v-else-if="detailType === 'VALUE_ORDER'"
v-loading="loading"
:data="itemDetail?.valueOrderDetails || []"
size="small"
max-height="300"
empty-text="暂无值关系异常"
>
<el-table-column prop="time" label="时间" min-width="160" />
<el-table-column prop="phase" label="相别" width="80" />
<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-tab-pane label="谐波奇偶异常" name="HARMONIC_PARITY">
<el-table
v-loading="loading"
class="detail-table"
:data="itemDetail?.harmonicParityDetails || []"
height="100%"
empty-text="暂无谐波奇偶异常"
>
<el-table-column prop="time" label="时间" min-width="160" />
<el-table-column prop="phase" label="相别" width="80" />
<el-table-column prop="statType" label="统计类型" width="100">
<template #default="{ row }">{{ row.statType ? formatChecksquareStatType(row.statType) : '-' }}</template>
</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>
<el-table
v-else
v-loading="loading"
:data="itemDetail?.harmonicParityDetails || []"
size="small"
max-height="300"
empty-text="暂无谐波奇偶异常"
>
<el-table-column prop="time" label="时间" min-width="160" />
<el-table-column prop="phase" label="相别" width="80" />
<el-table-column prop="statType" label="统计类型" width="100">
<template #default="{ row }">{{ row.statType ? formatChecksquareStatType(row.statType) : '-' }}</template>
</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>
<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>
</el-tab-pane>
</el-tabs>
</section>
</template>
@@ -129,6 +137,7 @@ import {
CHECKSQUARE_STAT_TYPES,
collectMissingSegments,
formatChecksquareStatType,
formatDataIntegrity,
formatStatMissingRate,
resolveChecksquareRowName
} from '../utils/checksquareTable'
@@ -244,30 +253,10 @@ watch(
<style scoped lang="scss">
.checksquare-detail {
display: flex;
flex: none;
height: 100%;
min-height: 0;
flex-direction: column;
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 {
@@ -276,6 +265,55 @@ watch(
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 {
display: flex;
align-items: center;
@@ -296,23 +334,33 @@ watch(
color: var(--el-text-color-primary);
}
.detail-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.stat-select {
width: 120px;
}
.detail-pagination {
.segment-toolbar {
display: flex;
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) {
.item-overview,
.stat-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}

View File

@@ -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>

View File

@@ -31,54 +31,55 @@
highlight-current-row
empty-text="暂无校验结果"
>
<el-table-column prop="indicatorName" label="指标名称" min-width="208">
<el-table-column prop="indicatorName" label="指标名称" width="160">
<template #default="{ row }">
<span class="indicator-name" :title="resolveChecksquareRowName(row)">
{{ resolveChecksquareRowName(row) }}
</span>
</template>
</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 }">
<el-tag v-if="row.hasData !== undefined" :type="row.hasData ? 'success' : 'danger'" effect="plain">
{{ formatBooleanText(row.hasData) }}
</el-tag>
<span v-else>-</span>
<span :class="{ 'is-abnormal-count': hasAbnormalCount(row.abnormalPointCount) }">
{{ row.abnormalPointCount ?? '-' }}
</span>
</template>
</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 }">
{{ formatMissingRate(row.missingRate, row.missingRateText) }}
<span :class="{ 'is-abnormal-count': hasAbnormalCount(row.harmonicParityAbnormalPointCount) }">
{{ row.harmonicParityAbnormalPointCount ?? '-' }}
</span>
</template>
</el-table-column>
<el-table-column label="平均值缺失率" min-width="130" align="center">
<template #default="{ row }">{{ formatStatMissingRate(row, 'AVG') }}</template>
<el-table-column label="数据完整性" align="center">
<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 label="最大值缺失率" min-width="130" align="center">
<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">
<el-table-column label="操作" width="130" align="center" fixed="right">
<template #default="{ 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 {
formatBooleanText,
formatMissingRate,
formatDataIntegrity,
formatStatMissingRate,
hasChecksquareDetail,
resolveChecksquareRowName
@@ -114,6 +115,21 @@ const emit = defineEmits<{
refresh: []
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>
<style scoped lang="scss">
@@ -149,4 +165,8 @@ const emit = defineEmits<{
vertical-align: middle;
white-space: nowrap;
}
.is-abnormal-count {
color: var(--el-color-danger);
}
</style>

View File

@@ -11,21 +11,21 @@
</template>
<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>
</ProTable>
</template>
<script setup lang="ts">
import { computed, h, reactive, ref } from 'vue'
import { ElDatePicker, ElTag, ElTreeSelect } from 'element-plus'
import { Plus, View } from '@element-plus/icons-vue'
import { ElButton, ElDatePicker, ElTag, ElTreeSelect } from 'element-plus'
import { Delete, Plus } from '@element-plus/icons-vue'
import ProTable from '@/components/ProTable/index.vue'
import type { ColumnProps, ProTableInstance } from '@/components/ProTable/interface'
import type { SteadyDataView } from '@/api/steady/steadyDataView/interface'
import {
buildChecksquareTaskQueryParams,
formatChecksquarePercent,
formatChecksquareIntegrity,
formatChecksquareTaskStatus,
resolveChecksquareTaskStatusType,
resolveChecksquareText,
@@ -45,6 +45,8 @@ const props = defineProps<{
const emit = defineEmits<{
createTask: []
detail: [row: SteadyDataView.SteadyChecksquareTask]
delete: [row: SteadyDataView.SteadyChecksquareTask]
viewMeasurementPoint: [row: SteadyDataView.SteadyChecksquareTask]
}>()
const proTable = ref<ProTableInstance>()
@@ -192,7 +194,16 @@ const columns = reactive<ColumnProps<SteadyDataView.SteadyChecksquareTask>[]>([
prop: 'lineName',
label: '监测点名称',
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',
@@ -246,7 +257,16 @@ const columns = reactive<ColumnProps<SteadyDataView.SteadyChecksquareTask>[]>([
label: '异常项数',
minWidth: 100,
align: 'center',
render: ({ row }) => resolveChecksquareText(row.abnormalItemCount),
render: ({ row }) =>
h(
ElButton,
{
type: 'primary',
link: true,
onClick: () => emit('detail', row)
},
() => resolveChecksquareText(row.abnormalItemCount)
),
search: {
label: '异常状态',
key: 'hasAbnormal',
@@ -260,11 +280,11 @@ const columns = reactive<ColumnProps<SteadyDataView.SteadyChecksquareTask>[]>([
isFilterEnum: false
},
{
prop: 'maxMissingRate',
label: '最大缺失率',
prop: 'minDataIntegrity',
label: '最低完整性',
minWidth: 120,
align: 'center',
render: ({ row }) => formatChecksquarePercent(row.maxMissingRate)
render: ({ row }) => formatChecksquareIntegrity(row.minDataIntegrity, row.maxMissingRate)
},
{
prop: 'createTime',
@@ -272,7 +292,7 @@ const columns = reactive<ColumnProps<SteadyDataView.SteadyChecksquareTask>[]>([
minWidth: 170,
render: ({ row }) => resolveChecksquareText(row.createTime)
},
{ prop: 'operation', label: '操作', fixed: 'right', width: 96 }
{ prop: 'operation', label: '操作', fixed: 'right', width: 150 }
])
const getTableList = (params: ChecksquareTaskSearchParams) => {

View File

@@ -14,7 +14,9 @@ const files = {
taskTable: path.resolve(rootDir, 'views/steady/checksquare/components/ChecksquareTaskTable.vue'),
summaryTable: path.resolve(rootDir, 'views/steady/checksquare/components/ChecksquareSummaryTable.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'),
ledgerUtils: path.resolve(rootDir, 'views/steady/checksquare/utils/checksquareLedger.ts'),
taskTableUtils: path.resolve(rootDir, 'views/steady/checksquare/utils/checksquareTaskTable.ts'),
table: path.resolve(rootDir, 'views/steady/checksquare/utils/checksquareTable.ts'),
grid: path.resolve(rootDir, 'components/Grid/index.vue'),
@@ -34,11 +36,21 @@ const checks = [
return (
/\/steady\/data-view\/checksquare\/query/.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\/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',
() =>
@@ -70,11 +82,36 @@ const checks = [
'task table has documented task columns',
() => {
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)
)
}
],
[
'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',
() =>
@@ -101,7 +138,10 @@ const checks = [
],
[
'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',
@@ -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',
() => /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)) &&
/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',
() => /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',
() => /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',
() => /getSteadyChecksquareItemDetail/.test(read(files.detailPanel)) && /detailType/.test(read(files.detailPanel))
@@ -232,6 +469,28 @@ const checks = [
const detailPanel = read(files.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
)
)
}
]
]

View File

@@ -7,6 +7,13 @@
:request-api="querySteadyChecksquareTasks"
@create-task="openCreateDialog"
@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>
@@ -43,17 +50,20 @@
</div>
</el-dialog>
<el-dialog v-model="itemDetailDialogVisible" title="检测项明细" width="900px" append-to-body destroy-on-close>
<ChecksquareDetailPanel :selected-item="selectedItem" />
<el-dialog v-model="itemDetailDialogVisible" title="检测项明细" width="1080px" append-to-body destroy-on-close>
<div class="checksquare-detail-dialog">
<ChecksquareDetailPanel :selected-item="selectedItem" />
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
createSteadyChecksquareTask,
deleteSteadyChecksquareTasks,
getSteadyChecksquareDetail,
getSteadyTrendIndicatorTree,
getSteadyTrendLedgerTree,
@@ -69,9 +79,14 @@ import {
} from '@/views/steady/steadyDataView/utils/selectionRules'
import { normalizeSteadyLedgerTree } from '@/views/steady/steadyDataView/utils/ledgerTree'
import ChecksquareDetailPanel from './components/ChecksquareDetailPanel.vue'
import ChecksquareMeasurementPointDialog from './components/ChecksquareMeasurementPointDialog.vue'
import ChecksquareSummaryTable from './components/ChecksquareSummaryTable.vue'
import ChecksquareTaskTable from './components/ChecksquareTaskTable.vue'
import ChecksquareWorkbench from './components/ChecksquareWorkbench.vue'
import {
resolveChecksquareMeasurementPointDetail,
type ChecksquareMeasurementPointDetail
} from './utils/checksquareLedger'
import {
buildSteadyChecksquareCreatePayload,
defaultChecksquareFormState,
@@ -89,6 +104,7 @@ const selectedIndicators = ref<SteadyDataView.SteadyIndicatorNode[]>([])
const taskDetail = ref<SteadyDataView.SteadyChecksquareQueryResult | null>(null)
const selectedTask = ref<SteadyDataView.SteadyChecksquareTask | null>(null)
const selectedItem = ref<SteadyDataView.SteadyChecksquareItem | null>(null)
const measurementPointData = ref<ChecksquareMeasurementPointDetail | null>(null)
const formState = ref(defaultChecksquareFormState())
const ledgerKeyword = ref('')
const ledgerPanelCollapsed = ref(false)
@@ -98,6 +114,7 @@ const defaultIndicatorCheckedKeys = ref<string[]>([])
const createDialogVisible = ref(false)
const detailDialogVisible = ref(false)
const itemDetailDialogVisible = ref(false)
const measurementPointDialogVisible = ref(false)
const taskTableRef = ref<InstanceType<typeof ChecksquareTaskTable>>()
const loading = reactive({
ledger: false,
@@ -216,11 +233,35 @@ const openTaskDetail = async (row: SteadyDataView.SteadyChecksquareTask) => {
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) => {
selectedItem.value = item
itemDetailDialogVisible.value = true
}
const openMeasurementPointDialog = (row: SteadyDataView.SteadyChecksquareTask) => {
measurementPointData.value = resolveChecksquareMeasurementPointDetail(ledgerTree.value, row)
measurementPointDialogVisible.value = true
}
onMounted(() => {
loadLedgerTree()
loadIndicatorTree()

View File

@@ -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
}
}

View File

@@ -24,11 +24,18 @@ export const formatBooleanText = (value?: boolean | null) => {
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 (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 = (
@@ -45,7 +52,7 @@ export const formatStatMissingRate = (
const summary = findStatSummary(item, statType)
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) => {
@@ -221,6 +228,7 @@ const summarizeStatType = (
expectedPointCount,
actualPointCount,
missingPointCount,
dataIntegrity: expectedPointCount ? actualPointCount / expectedPointCount : null,
missingRate: expectedPointCount ? missingPointCount / expectedPointCount : null,
maxContinuousMissingMinutes
}
@@ -253,6 +261,8 @@ export const buildHarmonicParentSummary = (
expectedPointCount,
actualPointCount,
missingPointCount,
dataIntegrity: expectedPointCount ? actualPointCount / expectedPointCount : null,
dataIntegrityText: expectedPointCount ? undefined : '-',
missingRate: expectedPointCount ? missingPointCount / expectedPointCount : null,
missingRateText: expectedPointCount ? undefined : '-',
maxContinuousMissingMinutes,

View File

@@ -33,10 +33,17 @@ export const resolveChecksquareTaskStatusType = (status?: string) => {
return 'info'
}
export const formatChecksquarePercent = (value?: number | null) => {
if (value === null || value === undefined || !Number.isFinite(Number(value))) return '--'
export const formatChecksquareIntegrity = (value?: number | null, fallbackMissingRate?: number | null) => {
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) => {