我叫洪圣文
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user