我叫洪圣文

This commit is contained in:
2026-05-15 16:36:50 +08:00
parent b6006e0dfe
commit 6687cf0339
36 changed files with 2201 additions and 271 deletions

View File

@@ -0,0 +1,117 @@
<template>
<section class="card steady-tree-card indicator-tree">
<div class="panel-header">
<span class="panel-title">稳态指标</span>
<el-button :icon="Refresh" circle :loading="loading" @click="emit('refresh')" />
</div>
<el-scrollbar class="tree-scrollbar">
<el-tree
ref="treeRef"
class="indicator-tree"
:data="normalizedTreeData"
node-key="treeKey"
show-checkbox
default-expand-all
:expand-on-click-node="false"
:props="{ label: 'name', children: 'children' }"
@check="handleCheck"
>
<template #default="{ data }">
<div class="tree-node">
<span class="node-name">{{ data.name }}</span>
<el-tag v-if="data.unit" size="small" effect="plain">{{ data.unit }}</el-tag>
</div>
</template>
</el-tree>
</el-scrollbar>
</section>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { Refresh } from '@element-plus/icons-vue'
import type { TreeInstance } from 'element-plus'
import type { SteadyDataView } from '@/api/steady/steadyDataView/interface'
import { collectLeafIndicators } from '../utils/selectionRules'
defineOptions({
name: 'SteadyIndicatorTree'
})
const props = defineProps<{
treeData: SteadyDataView.SteadyIndicatorNode[]
loading: boolean
}>()
const emit = defineEmits<{
refresh: []
change: [nodes: SteadyDataView.SteadyIndicatorNode[]]
}>()
const treeRef = ref<TreeInstance>()
const normalizedTreeData = computed(() => {
const normalize = (nodes: SteadyDataView.SteadyIndicatorNode[], parentKey = ''): SteadyDataView.SteadyIndicatorNode[] => {
return nodes.map((node, index) => {
const treeKey = node.id || node.indicatorCode || `${parentKey}${node.groupCode || node.name || 'node'}-${index}`
return {
...node,
treeKey,
children: node.children?.length ? normalize(node.children, `${treeKey}-`) : undefined
} as SteadyDataView.SteadyIndicatorNode
})
}
return normalize(props.treeData)
})
const handleCheck = () => {
const checkedNodes = (treeRef.value?.getCheckedNodes(false, true) || []) as SteadyDataView.SteadyIndicatorNode[]
emit('change', collectLeafIndicators(checkedNodes))
}
</script>
<style scoped lang="scss">
.steady-tree-card {
display: flex;
flex-direction: column;
gap: 10px;
min-height: 0;
padding: 12px;
}
.panel-header,
.tree-node {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.panel-title {
font-size: 14px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.tree-scrollbar {
flex: 1;
min-height: 0;
}
.tree-node {
width: 100%;
min-width: 0;
}
.node-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.indicator-tree :deep(.el-tree-node__content) {
min-width: 0;
}
</style>

View File

@@ -0,0 +1,123 @@
<template>
<section class="card steady-tree-card">
<div class="panel-header">
<span class="panel-title">台账监测点</span>
<el-button :icon="Refresh" circle :loading="loading" @click="emit('refresh')" />
</div>
<el-input
:model-value="keyword"
clearable
placeholder="搜索工程、项目、设备、监测点"
@update:model-value="handleKeywordChange"
/>
<el-scrollbar class="tree-scrollbar">
<el-tree
ref="treeRef"
class="ledger-tree"
:data="treeData"
node-key="id"
show-checkbox
default-expand-all
:expand-on-click-node="false"
:props="{ label: 'name', children: 'children' }"
@check="handleCheck"
>
<template #default="{ data }">
<div class="tree-node">
<span class="node-name">{{ data.name }}</span>
<span class="node-count">
<template v-if="Number(data.deviceCount) || Number(data.lineCount)">
{{ Number(data.deviceCount || 0) }} / {{ Number(data.lineCount || 0) }}
</template>
</span>
</div>
</template>
</el-tree>
</el-scrollbar>
</section>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { Refresh } from '@element-plus/icons-vue'
import type { TreeInstance } from 'element-plus'
import type { SteadyDataView } from '@/api/steady/steadyDataView/interface'
defineOptions({
name: 'SteadyLedgerTree'
})
defineProps<{
treeData: SteadyDataView.SteadyLedgerNode[]
loading: boolean
keyword: string
}>()
const emit = defineEmits<{
refresh: []
search: [value: string]
change: [nodes: SteadyDataView.SteadyLedgerNode[]]
}>()
const treeRef = ref<TreeInstance>()
const handleKeywordChange = (value: string) => {
emit('search', value)
}
const handleCheck = () => {
emit('change', (treeRef.value?.getCheckedNodes(false, true) || []) as SteadyDataView.SteadyLedgerNode[])
}
</script>
<style scoped lang="scss">
.steady-tree-card {
display: flex;
flex-direction: column;
gap: 10px;
min-height: 0;
padding: 12px;
}
.panel-header,
.tree-node {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.panel-title {
font-size: 14px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.tree-scrollbar {
flex: 1;
min-height: 0;
}
.tree-node {
width: 100%;
min-width: 0;
}
.node-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.node-count {
flex: none;
color: var(--el-text-color-secondary);
font-size: 12px;
}
.ledger-tree :deep(.el-tree-node__content) {
min-width: 0;
}
</style>

View File

@@ -0,0 +1,74 @@
<template>
<section class="card trend-chart-panel" v-loading="loading">
<div class="panel-header">
<span class="panel-title">趋势图</span>
<span class="panel-meta">
<template v-if="trendResult">
{{ trendResult.bucket || '-' }} / {{ trendResult.displayPointCount || 0 }}
</template>
</span>
</div>
<div v-if="hasSeries" class="chart-body">
<LineChart :options="chartOptions" />
</div>
<el-empty v-else class="chart-empty" description="请选择监测点和指标后查询趋势" />
</section>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import LineChart from '@/components/echarts/line/index.vue'
import type { SteadyDataView } from '@/api/steady/steadyDataView/interface'
import { buildSteadyTrendChartOptions } from '../utils/trendOptions'
defineOptions({
name: 'SteadyTrendChartPanel'
})
const props = defineProps<{
trendResult: SteadyDataView.SteadyTrendQueryResult | null
loading: boolean
}>()
const hasSeries = computed(() => Boolean(props.trendResult?.series?.length))
const chartOptions = computed(() => buildSteadyTrendChartOptions(props.trendResult?.series || []))
</script>
<style scoped lang="scss">
.trend-chart-panel {
display: flex;
flex-direction: column;
min-width: 0;
min-height: 0;
padding: 12px;
}
.panel-header {
display: flex;
flex: none;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 10px;
}
.panel-title {
font-size: 14px;
font-weight: 600;
}
.panel-meta {
color: var(--el-text-color-secondary);
font-size: 12px;
}
.chart-body {
flex: 1;
min-height: 0;
}
.chart-empty {
flex: 1;
}
</style>

View File

@@ -0,0 +1,175 @@
<template>
<section class="card trend-toolbar">
<TimePeriodSearch
class="trend-toolbar__time"
:unit="modelValue.timeUnit"
:model-value="modelValue.timeBaseDate"
@update:unit="handleTimeUnitChange"
@update:model-value="handleTimeBaseDateChange"
/>
<el-select
:model-value="modelValue.phases"
multiple
collapse-tags
collapse-tags-tooltip
placeholder="选择相别"
@update:model-value="updateField('phases', $event)"
>
<el-option v-for="item in phaseOptions" :key="item" :label="resolvePhaseLabel(item)" :value="item" />
</el-select>
<el-select
:model-value="modelValue.statTypes"
multiple
collapse-tags
collapse-tags-tooltip
placeholder="选择统计类型"
@update:model-value="updateField('statTypes', $event)"
>
<el-option v-for="item in statOptions" :key="item" :label="statLabelMap[item]" :value="item" />
</el-select>
<el-select
:model-value="modelValue.bucket"
placeholder="选择时间粒度"
@update:model-value="updateField('bucket', $event)"
>
<el-option v-for="item in bucketOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<el-select
:model-value="modelValue.qualityFlag"
clearable
placeholder="选择数据质量"
@update:model-value="updateField('qualityFlag', $event)"
>
<el-option label="仅有效数据" :value="1" />
<el-option label="仅无效数据" :value="0" />
</el-select>
<el-select
v-if="showHarmonicOrders"
:model-value="modelValue.harmonicOrders"
class="harmonic-select"
multiple
collapse-tags
collapse-tags-tooltip
placeholder="选择谐波次数"
@update:model-value="updateField('harmonicOrders', $event)"
>
<el-option v-for="item in harmonicOrderOptions" :key="item" :label="`${item}次`" :value="item" />
</el-select>
<div class="toolbar-actions">
<el-button type="primary" :loading="loading" @click="emit('query')">查询</el-button>
<el-button @click="emit('reset')">重置</el-button>
</div>
</section>
</template>
<script setup lang="ts">
import type { SteadyDataView } from '@/api/steady/steadyDataView/interface'
import TimePeriodSearch from '@/views/components/TimePeriodSearch/index.vue'
import { buildTimePeriodRange, type TimePeriodUnit } from '@/views/components/TimePeriodSearch/timePeriod'
import type { SteadyTrendFormState } from '../utils/trendPayload'
defineOptions({
name: 'SteadyTrendToolbar'
})
const props = defineProps<{
modelValue: SteadyTrendFormState
phaseOptions: string[]
statOptions: SteadyDataView.SteadyTrendStatType[]
showHarmonicOrders: boolean
loading: boolean
}>()
const emit = defineEmits<{
'update:modelValue': [value: SteadyTrendFormState]
query: []
reset: []
}>()
const bucketOptions = [
{ label: '1分钟', value: '1m' },
{ label: '5分钟', value: '5m' },
{ label: '10分钟', value: '10m' },
{ label: '30分钟', value: '30m' },
{ label: '1小时', value: '1h' }
]
const harmonicOrderOptions = Array.from({ length: 49 }, (_item, index) => index + 2)
const statLabelMap: Record<SteadyDataView.SteadyTrendStatType, string> = {
AVG: '平均值',
MAX: '最大值',
MIN: '最小值',
CP95: '95%概率大值'
}
const phaseLabelMap: Record<string, string> = {
A: 'A相',
B: 'B相',
C: 'C相',
T: '总相'
}
const resolvePhaseLabel = (phase: string) => phaseLabelMap[phase] || `${phase}`
const updateField = <K extends keyof SteadyTrendFormState>(field: K, value: SteadyTrendFormState[K]) => {
emit('update:modelValue', {
...props.modelValue,
[field]: value
})
}
const updateTimeRange = (unit: TimePeriodUnit, baseDate: Date) => {
emit('update:modelValue', {
...props.modelValue,
timeUnit: unit,
timeBaseDate: baseDate,
timeRange: buildTimePeriodRange(unit, baseDate)
})
}
const handleTimeUnitChange = (value: TimePeriodUnit) => {
updateTimeRange(value, props.modelValue.timeBaseDate)
}
const handleTimeBaseDateChange = (value: Date) => {
updateTimeRange(props.modelValue.timeUnit, value)
}
</script>
<style scoped lang="scss">
.trend-toolbar {
display: grid;
grid-template-columns: minmax(260px, 1.3fr) repeat(4, minmax(132px, 0.7fr)) auto;
gap: 10px;
align-items: center;
padding: 12px;
}
.trend-toolbar__time {
min-width: 260px;
}
.harmonic-select {
grid-column: span 2;
}
.toolbar-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
}
@media (max-width: 1280px) {
.trend-toolbar {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.toolbar-actions {
justify-content: flex-start;
}
}
</style>