feat(steady): 完善稳态数据视图功能
- 更新纵坐标刻度算法,优化小数趋势图范围显示 - 添加稳态趋势图全屏模式和共享工具组件 - 实现多图联动的鼠标悬停竖线同步功能 - 调整主线线宽分档策略,降低最大线宽限制 - 重构稳态趋势工具栏,优化谐波次数选择逻辑 - 添加周时间周期搜索支持和自定义时间范围选择 - 完善稳态数据表格和指示器浮动面板功能 - 优化稳态趋势图性能,添加LTB采样和动画控制 - 修复数据表格打开前的趋势数据验证问题 - 统一时间轴标签格式化和网格对齐处理
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<aside class="indicator-floating-panel" :class="{ 'is-collapsed': collapsed }">
|
||||
<aside class="indicator-floating-panel" :class="[`is-${mode}`, { 'is-collapsed': collapsed }]">
|
||||
<el-button
|
||||
class="indicator-toggle"
|
||||
type="primary"
|
||||
@@ -11,9 +11,7 @@
|
||||
<SteadyIndicatorTree
|
||||
:key="selectorResetKey"
|
||||
:tree-data="treeData"
|
||||
:loading="loading"
|
||||
:default-checked-keys="defaultCheckedKeys"
|
||||
@refresh="emit('refresh')"
|
||||
@change="emit('change', $event)"
|
||||
/>
|
||||
</div>
|
||||
@@ -29,17 +27,21 @@ defineOptions({
|
||||
name: 'SteadyIndicatorFloatingPanel'
|
||||
})
|
||||
|
||||
defineProps<{
|
||||
collapsed: boolean
|
||||
treeData: SteadyDataView.SteadyIndicatorNode[]
|
||||
loading: boolean
|
||||
defaultCheckedKeys: string[]
|
||||
selectorResetKey: number
|
||||
}>()
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
collapsed: boolean
|
||||
treeData: SteadyDataView.SteadyIndicatorNode[]
|
||||
defaultCheckedKeys: string[]
|
||||
selectorResetKey: number
|
||||
mode?: 'floating' | 'inline'
|
||||
}>(),
|
||||
{
|
||||
mode: 'floating'
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:collapsed': [value: boolean]
|
||||
refresh: []
|
||||
change: [nodes: SteadyDataView.SteadyIndicatorNode[]]
|
||||
}>()
|
||||
</script>
|
||||
@@ -59,6 +61,19 @@ const emit = defineEmits<{
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.indicator-floating-panel.is-inline {
|
||||
position: relative;
|
||||
top: auto;
|
||||
right: auto;
|
||||
bottom: auto;
|
||||
width: 300px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.indicator-floating-panel.is-inline.is-collapsed {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.indicator-toggle {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
@@ -84,5 +99,9 @@ const emit = defineEmits<{
|
||||
.indicator-floating-panel {
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.indicator-floating-panel.is-inline {
|
||||
width: 280px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
<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">
|
||||
@@ -31,7 +30,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, ref, watch } 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'
|
||||
@@ -42,12 +40,10 @@ defineOptions({
|
||||
|
||||
const props = defineProps<{
|
||||
treeData: SteadyDataView.SteadyIndicatorNode[]
|
||||
loading: boolean
|
||||
defaultCheckedKeys: string[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
refresh: []
|
||||
change: [nodes: SteadyDataView.SteadyIndicatorNode[]]
|
||||
}>()
|
||||
|
||||
|
||||
@@ -1,40 +1,23 @@
|
||||
<template>
|
||||
<section class="card trend-chart-panel" v-loading="loading">
|
||||
<section class="card trend-chart-panel">
|
||||
<div class="panel-header">
|
||||
<span v-if="trendResult" class="panel-meta">{{ trendResult.displayPointCount || 0 }} 点</span>
|
||||
<span v-else class="panel-meta">趋势图</span>
|
||||
|
||||
<div class="trend-tool-groups">
|
||||
<div v-for="group in trendToolGroups" :key="group.key" class="trend-tool-group">
|
||||
<el-tooltip
|
||||
v-for="item in group.items"
|
||||
:key="item.action"
|
||||
:content="getTrendToolTooltip(item)"
|
||||
placement="top"
|
||||
>
|
||||
<el-button
|
||||
:type="isTrendToolActive(item.action) ? 'primary' : 'default'"
|
||||
:icon="item.icon"
|
||||
:disabled="isTrendToolDisabled(item.action)"
|
||||
circle
|
||||
@click="handleTrendToolAction(item.action)"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<SteadyTrendChartTools
|
||||
:tool-groups="trendToolGroups"
|
||||
:is-tool-active="isTrendToolActive"
|
||||
:is-tool-disabled="isTrendToolDisabled"
|
||||
:get-tool-tooltip="getTrendToolTooltip"
|
||||
@tool-action="handleTrendToolAction"
|
||||
/>
|
||||
<span v-if="trendResult" class="panel-meta">总点数:{{ trendResult.displayPointCount || 0 }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="hasSeries" ref="chartExportTargetRef" class="chart-list steady-trend-export-target">
|
||||
<div v-for="group in chartGroups" :key="group.key" class="chart-group">
|
||||
<div class="chart-body">
|
||||
<LineChart :options="group.options" :group="group.group" @chart-data-zoom="handleChartDataZoom" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-else class="chart-empty" description="请选择监测点和指标后查询趋势" />
|
||||
|
||||
<el-dialog v-model="fullscreenVisible" title="趋势图全屏展示" fullscreen append-to-body destroy-on-close>
|
||||
<div v-if="hasSeries" class="fullscreen-chart-body">
|
||||
<div class="chart-panel-body" v-loading="loading">
|
||||
<div
|
||||
v-if="hasChartFrame"
|
||||
ref="chartExportTargetRef"
|
||||
class="chart-list steady-trend-export-target"
|
||||
:style="{ '--steady-trend-visible-chart-count': normalVisibleChartCount }"
|
||||
>
|
||||
<div v-for="group in chartGroups" :key="group.key" class="chart-group">
|
||||
<div class="chart-body">
|
||||
<LineChart
|
||||
@@ -45,8 +28,21 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-else-if="hasQueriedWithoutData" class="chart-empty" description="暂无数据" />
|
||||
<el-empty v-else class="chart-empty" description="请选择监测点和指标后查询趋势" />
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
||||
<SteadyTrendFullscreen
|
||||
v-model="fullscreenVisible"
|
||||
:chart-groups="chartGroups"
|
||||
:visible-chart-count="fullscreenVisibleChartCount"
|
||||
:tool-groups="fullscreenToolGroups"
|
||||
:is-tool-active="isTrendToolActive"
|
||||
:is-tool-disabled="isTrendToolDisabled"
|
||||
:get-tool-tooltip="getTrendToolTooltip"
|
||||
@chart-data-zoom="handleChartDataZoom"
|
||||
@tool-action="handleTrendToolAction"
|
||||
/>
|
||||
|
||||
<SteadyTrendDataTableDialog v-model="dataTableVisible" :trend-result="trendResult" />
|
||||
</section>
|
||||
@@ -60,6 +56,7 @@ import {
|
||||
ArrowUpBold,
|
||||
Crop,
|
||||
DataAnalysis,
|
||||
DataLine,
|
||||
FullScreen,
|
||||
Mouse,
|
||||
Picture,
|
||||
@@ -72,8 +69,10 @@ import { computed, nextTick, ref, watch } from 'vue'
|
||||
import LineChart from '@/components/echarts/line/index.vue'
|
||||
import type { SteadyDataView } from '@/api/steady/steadyDataView/interface'
|
||||
import { buildSteadyTrendChartGroups, type SteadyTrendZoomRange } from '../utils/trendOptions'
|
||||
import type { SteadyTrendToolAction, SteadyTrendToolGroup, SteadyTrendToolItem } from './chartTools'
|
||||
import SteadyTrendChartTools from './SteadyTrendChartTools.vue'
|
||||
import SteadyTrendDataTableDialog from './SteadyTrendDataTableDialog.vue'
|
||||
import type { Component } from 'vue'
|
||||
import SteadyTrendFullscreen from './SteadyTrendFullscreen.vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'SteadyTrendChartPanel'
|
||||
@@ -85,43 +84,24 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
type SteadyTrendInteractionMode = 'none' | 'box-zoom' | 'pan'
|
||||
type SteadyTrendToolAction =
|
||||
| 'x-zoom-in'
|
||||
| 'x-zoom-out'
|
||||
| 'y-zoom-in'
|
||||
| 'y-zoom-out'
|
||||
| 'box-zoom'
|
||||
| 'wheel-zoom'
|
||||
| 'reset'
|
||||
| 'pan'
|
||||
| 'fullscreen'
|
||||
| 'download-image'
|
||||
| 'query-data'
|
||||
|
||||
const trendToolGroups: Array<{
|
||||
key: string
|
||||
items: Array<{
|
||||
action: SteadyTrendToolAction
|
||||
label: string
|
||||
icon: Component
|
||||
}>
|
||||
}> = [
|
||||
const trendToolGroups: SteadyTrendToolGroup[] = [
|
||||
{
|
||||
key: 'viewport',
|
||||
items: [
|
||||
{ action: 'wheel-zoom', label: '滚轮缩放', icon: Mouse },
|
||||
{ action: 'x-zoom-in', label: 'X坐标放大', icon: ArrowRightBold },
|
||||
{ action: 'x-zoom-out', label: 'X坐标缩小', icon: ArrowLeftBold },
|
||||
{ action: 'y-zoom-in', label: 'Y坐标放大', icon: ArrowUpBold },
|
||||
{ action: 'y-zoom-out', label: 'Y坐标缩小', icon: ArrowDownBold },
|
||||
{ action: 'box-zoom', label: '框选放大', icon: Crop },
|
||||
{ action: 'reset', label: '恢复', icon: RefreshLeft },
|
||||
{ action: 'pan', label: '平移', icon: Pointer }
|
||||
{ action: 'pan', label: '平移', icon: Pointer },
|
||||
{ action: 'wheel-zoom', label: '滚轮缩放', icon: Mouse }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'view',
|
||||
items: [
|
||||
{ action: 'missing-data', label: '缺失数据', icon: DataLine },
|
||||
{ action: 'fullscreen', label: '全屏展示', icon: FullScreen },
|
||||
{ action: 'download-image', label: '下载图片', icon: Picture },
|
||||
{ action: 'query-data', label: '数据查询', icon: DataAnalysis }
|
||||
@@ -130,22 +110,51 @@ const trendToolGroups: Array<{
|
||||
]
|
||||
|
||||
const clampPercent = (value: number) => Math.min(Math.max(value, 0), 100)
|
||||
const DEFAULT_STEADY_TREND_X_ZOOM_RANGE: SteadyTrendZoomRange = { start: 0, end: 10 }
|
||||
const DEFAULT_STEADY_TREND_X_ZOOM_RANGE: SteadyTrendZoomRange = { start: 0, end: 100 }
|
||||
const STEADY_TREND_DAY_MS = 24 * 60 * 60 * 1000
|
||||
const STEADY_TREND_HALF_RANGE_DAYS = 20
|
||||
const STEADY_TREND_QUARTER_RANGE_DAYS = 30
|
||||
const STEADY_TREND_TENTH_RANGE_DAYS = 60
|
||||
const trendXZoomRange = ref<SteadyTrendZoomRange>({ ...DEFAULT_STEADY_TREND_X_ZOOM_RANGE })
|
||||
const defaultTrendXZoomRange = ref<SteadyTrendZoomRange>({ ...DEFAULT_STEADY_TREND_X_ZOOM_RANGE })
|
||||
const trendYZoomScale = ref(1)
|
||||
const activeTrendInteractionMode = ref<SteadyTrendInteractionMode>('none')
|
||||
const wheelZoomEnabled = ref(false)
|
||||
const missingDataEnabled = ref(true)
|
||||
const fullscreenVisible = ref(false)
|
||||
const dataTableVisible = ref(false)
|
||||
const chartExportTargetRef = ref<HTMLElement>()
|
||||
const hasSeries = computed(() => Boolean(props.trendResult?.series?.length))
|
||||
const hasQueryTimeRange = computed(() => Boolean(props.trendResult?.queryTimeStart && props.trendResult?.queryTimeEnd))
|
||||
const hasDataPoints = computed(() =>
|
||||
Boolean(
|
||||
props.trendResult?.series?.some(series =>
|
||||
(series.points || []).some(point => typeof point.value === 'number' && Number.isFinite(point.value))
|
||||
)
|
||||
)
|
||||
)
|
||||
const hasChartFrame = computed(() => hasSeries.value || (hasQueryTimeRange.value && !props.trendResult?.queryCompleted))
|
||||
const hasQueriedWithoutData = computed(() => Boolean(props.trendResult?.queryCompleted && !hasDataPoints.value))
|
||||
const chartGroups = computed(() =>
|
||||
buildSteadyTrendChartGroups(props.trendResult?.series || [], trendXZoomRange.value, {
|
||||
activeTool: activeTrendInteractionMode.value,
|
||||
wheelZoomEnabled: wheelZoomEnabled.value,
|
||||
yZoomScale: trendYZoomScale.value
|
||||
showMissingData: missingDataEnabled.value,
|
||||
yZoomScale: trendYZoomScale.value,
|
||||
queryTimeStart: props.trendResult?.queryTimeStart,
|
||||
queryTimeEnd: props.trendResult?.queryTimeEnd
|
||||
})
|
||||
)
|
||||
const normalVisibleChartCount = computed(() => Math.max(Math.min(chartGroups.value.length, 3), 1))
|
||||
const fullscreenVisibleChartCount = computed(() => Math.max(Math.min(chartGroups.value.length, 6), 1))
|
||||
const fullscreenToolGroups = computed(() =>
|
||||
trendToolGroups
|
||||
.map(group => ({
|
||||
...group,
|
||||
items: group.items.filter(item => item.action !== 'fullscreen')
|
||||
}))
|
||||
.filter(group => group.items.length)
|
||||
)
|
||||
const canPanTrendChart = computed(() => {
|
||||
const { start, end } = trendXZoomRange.value
|
||||
|
||||
@@ -153,19 +162,60 @@ const canPanTrendChart = computed(() => {
|
||||
})
|
||||
const isDefaultTrendXZoomRange = computed(() => {
|
||||
const { start, end } = trendXZoomRange.value
|
||||
const defaultRange = defaultTrendXZoomRange.value
|
||||
|
||||
return start === DEFAULT_STEADY_TREND_X_ZOOM_RANGE.start && end === DEFAULT_STEADY_TREND_X_ZOOM_RANGE.end
|
||||
return start === defaultRange.start && end === defaultRange.end
|
||||
})
|
||||
const canResetTrendChart = computed(() => {
|
||||
const changedYZoom = trendYZoomScale.value !== 1
|
||||
const changedInteractionMode = activeTrendInteractionMode.value !== 'none'
|
||||
const changedWheelZoom = wheelZoomEnabled.value
|
||||
|
||||
return hasSeries.value && (!isDefaultTrendXZoomRange.value || changedYZoom || changedInteractionMode || changedWheelZoom)
|
||||
return (
|
||||
hasSeries.value &&
|
||||
(!isDefaultTrendXZoomRange.value || changedYZoom || changedInteractionMode || changedWheelZoom)
|
||||
)
|
||||
})
|
||||
|
||||
const parseSteadyTrendTime = (value?: string) => {
|
||||
if (!value) return null
|
||||
|
||||
const timestamp = Date.parse(value.replace(' ', 'T'))
|
||||
|
||||
return Number.isFinite(timestamp) ? timestamp : null
|
||||
}
|
||||
|
||||
const resolveSteadyTrendTimeRangeMs = (trendResult: SteadyDataView.SteadyTrendQueryResult | null) => {
|
||||
let minTime = Number.POSITIVE_INFINITY
|
||||
let maxTime = Number.NEGATIVE_INFINITY
|
||||
|
||||
trendResult?.series?.forEach(series => {
|
||||
series.points?.forEach(point => {
|
||||
const timestamp = parseSteadyTrendTime(point.time)
|
||||
|
||||
if (timestamp === null) return
|
||||
|
||||
minTime = Math.min(minTime, timestamp)
|
||||
maxTime = Math.max(maxTime, timestamp)
|
||||
})
|
||||
})
|
||||
|
||||
return Number.isFinite(minTime) && Number.isFinite(maxTime) && maxTime > minTime ? maxTime - minTime : 0
|
||||
}
|
||||
|
||||
const resolveSteadyTrendDefaultZoomRange = (trendResult: SteadyDataView.SteadyTrendQueryResult | null) => {
|
||||
const timeRangeMs = resolveSteadyTrendTimeRangeMs(trendResult)
|
||||
const timeRangeDays = timeRangeMs / STEADY_TREND_DAY_MS
|
||||
|
||||
if (timeRangeDays > STEADY_TREND_TENTH_RANGE_DAYS) return { start: 0, end: 10 }
|
||||
if (timeRangeDays > STEADY_TREND_QUARTER_RANGE_DAYS) return { start: 0, end: 25 }
|
||||
if (timeRangeDays > STEADY_TREND_HALF_RANGE_DAYS) return { start: 0, end: 50 }
|
||||
|
||||
return { ...DEFAULT_STEADY_TREND_X_ZOOM_RANGE }
|
||||
}
|
||||
|
||||
const resetTrendToolState = () => {
|
||||
trendXZoomRange.value = { ...DEFAULT_STEADY_TREND_X_ZOOM_RANGE }
|
||||
trendXZoomRange.value = { ...defaultTrendXZoomRange.value }
|
||||
trendYZoomScale.value = 1
|
||||
activeTrendInteractionMode.value = 'none'
|
||||
wheelZoomEnabled.value = false
|
||||
@@ -194,6 +244,7 @@ const zoomTrendXAxis = (ratio: number) => {
|
||||
const isTrendToolActive = (action: SteadyTrendToolAction) => {
|
||||
if (action === 'fullscreen') return fullscreenVisible.value
|
||||
if (action === 'wheel-zoom') return wheelZoomEnabled.value
|
||||
if (action === 'missing-data') return missingDataEnabled.value
|
||||
if (action === 'box-zoom' || action === 'pan') return activeTrendInteractionMode.value === action
|
||||
|
||||
return false
|
||||
@@ -201,14 +252,14 @@ const isTrendToolActive = (action: SteadyTrendToolAction) => {
|
||||
|
||||
const isTrendToolDisabled = (action: SteadyTrendToolAction) => {
|
||||
if (action === 'query-data') return false
|
||||
if (!hasSeries.value) return true
|
||||
if (!hasChartFrame.value) return true
|
||||
if (action === 'pan') return !canPanTrendChart.value
|
||||
if (action === 'reset') return !canResetTrendChart.value
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
const getTrendToolTooltip = (item: { action: SteadyTrendToolAction; label: string }) => {
|
||||
const getTrendToolTooltip = (item: SteadyTrendToolItem) => {
|
||||
if (item.action === 'pan' && isTrendToolDisabled(item.action) && hasSeries.value) {
|
||||
return '请先放大 X 轴或框选局部区域后再平移'
|
||||
}
|
||||
@@ -219,7 +270,9 @@ const getTrendToolTooltip = (item: { action: SteadyTrendToolAction; label: strin
|
||||
const downloadSteadyTrendImage = async () => {
|
||||
await nextTick()
|
||||
|
||||
const targetElement = chartExportTargetRef.value
|
||||
const targetElement = fullscreenVisible.value
|
||||
? (document.querySelector('.steady-trend-fullscreen__chart-list') as HTMLElement | null)
|
||||
: chartExportTargetRef.value
|
||||
|
||||
if (!targetElement) {
|
||||
ElMessage.warning('暂无可下载的趋势图')
|
||||
@@ -266,6 +319,9 @@ const handleTrendToolAction = async (action: SteadyTrendToolAction) => {
|
||||
case 'wheel-zoom':
|
||||
wheelZoomEnabled.value = !wheelZoomEnabled.value
|
||||
break
|
||||
case 'missing-data':
|
||||
missingDataEnabled.value = !missingDataEnabled.value
|
||||
break
|
||||
case 'pan':
|
||||
if (!canPanTrendChart.value) {
|
||||
ElMessage.info('请先放大 X 轴或框选局部区域后再平移')
|
||||
@@ -309,7 +365,8 @@ const handleChartDataZoom = (value: SteadyTrendZoomRange) => {
|
||||
watch(
|
||||
() => props.trendResult,
|
||||
() => {
|
||||
// 新查询结果应回到完整时间范围,避免沿用上一批数据的局部缩放窗口。
|
||||
// 新查询结果按当前数据量重置默认窗口,避免沿用上一批数据的局部缩放范围。
|
||||
defaultTrendXZoomRange.value = resolveSteadyTrendDefaultZoomRange(props.trendResult)
|
||||
resetTrendToolState()
|
||||
}
|
||||
)
|
||||
@@ -317,6 +374,9 @@ watch(
|
||||
|
||||
<style scoped lang="scss">
|
||||
.trend-chart-panel {
|
||||
--steady-trend-chart-gap: 8px;
|
||||
--steady-trend-visible-chart-count: 3;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
@@ -330,63 +390,44 @@ watch(
|
||||
flex: none;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 12px;
|
||||
gap: 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.panel-meta {
|
||||
margin-left: 15px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.trend-tool-groups {
|
||||
.chart-panel-body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.trend-tool-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.trend-tool-group + .trend-tool-group {
|
||||
padding-left: 8px;
|
||||
border-left: 1px dashed var(--el-border-color);
|
||||
}
|
||||
|
||||
.trend-tool-group :deep(.el-button.is-circle) {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 6px;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.chart-list {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
gap: var(--steady-trend-chart-gap);
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.fullscreen-chart-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
height: calc(100vh - 96px);
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.chart-group {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex: 1 0 240px;
|
||||
flex: 0 0
|
||||
calc(
|
||||
(100% - var(--steady-trend-chart-gap) * (var(--steady-trend-visible-chart-count) - 1)) /
|
||||
var(--steady-trend-visible-chart-count)
|
||||
);
|
||||
flex-direction: column;
|
||||
min-height: 220px;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 4px;
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<div class="trend-tool-groups">
|
||||
<div v-for="group in toolGroups" :key="group.key" class="trend-tool-group">
|
||||
<el-tooltip v-for="item in group.items" :key="item.action" :content="getToolTooltip(item)" placement="top">
|
||||
<el-button
|
||||
:type="isToolActive(item.action) ? 'primary' : 'default'"
|
||||
:icon="item.icon"
|
||||
:disabled="isToolDisabled(item.action)"
|
||||
circle
|
||||
@click="emit('tool-action', item.action)"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { SteadyTrendToolAction, SteadyTrendToolGroup, SteadyTrendToolItem } from './chartTools'
|
||||
|
||||
defineOptions({
|
||||
name: 'SteadyTrendChartTools'
|
||||
})
|
||||
|
||||
defineProps<{
|
||||
toolGroups: SteadyTrendToolGroup[]
|
||||
isToolActive: (action: SteadyTrendToolAction) => boolean
|
||||
isToolDisabled: (action: SteadyTrendToolAction) => boolean
|
||||
getToolTooltip: (item: SteadyTrendToolItem) => string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'tool-action': [action: SteadyTrendToolAction]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.trend-tool-groups {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.trend-tool-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.trend-tool-group + .trend-tool-group {
|
||||
padding-left: 8px;
|
||||
border-left: 1px dashed var(--el-border-color);
|
||||
}
|
||||
|
||||
.trend-tool-group :deep(.el-button.is-circle) {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 6px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,192 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div v-if="modelValue" class="steady-trend-fullscreen">
|
||||
<header class="steady-trend-fullscreen__header">
|
||||
<span class="steady-trend-fullscreen__title">趋势图全屏展示</span>
|
||||
<el-button class="steady-trend-fullscreen__close" :icon="Close" text circle @click="closeFullscreen" />
|
||||
</header>
|
||||
|
||||
<main class="steady-trend-fullscreen__body">
|
||||
<div class="steady-trend-fullscreen__tool-row">
|
||||
<SteadyTrendChartTools
|
||||
class="steady-trend-fullscreen__tools"
|
||||
:tool-groups="toolGroups"
|
||||
:is-tool-active="isToolActive"
|
||||
:is-tool-disabled="isToolDisabled"
|
||||
:get-tool-tooltip="getToolTooltip"
|
||||
@tool-action="emit('tool-action', $event)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="chartGroups.length"
|
||||
class="steady-trend-fullscreen__chart-list"
|
||||
:style="{ '--steady-trend-visible-chart-count': visibleChartCount }"
|
||||
>
|
||||
<div v-for="group in chartGroups" :key="group.key" class="steady-trend-fullscreen__chart-group">
|
||||
<div class="steady-trend-fullscreen__chart-body">
|
||||
<LineChart
|
||||
:options="group.options"
|
||||
:group="group.group"
|
||||
@chart-data-zoom="handleChartDataZoom"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-else class="steady-trend-fullscreen__empty" description="请选择监测点和指标后查询趋势" />
|
||||
</main>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Close } from '@element-plus/icons-vue'
|
||||
import { onBeforeUnmount, onMounted } from 'vue'
|
||||
import LineChart from '@/components/echarts/line/index.vue'
|
||||
import type { SteadyTrendToolAction, SteadyTrendToolGroup, SteadyTrendToolItem } from './chartTools'
|
||||
import SteadyTrendChartTools from './SteadyTrendChartTools.vue'
|
||||
import type { SteadyTrendChartGroup, SteadyTrendZoomRange } from '../utils/trendOptions'
|
||||
|
||||
defineOptions({
|
||||
name: 'SteadyTrendFullscreen'
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
chartGroups: SteadyTrendChartGroup[]
|
||||
visibleChartCount: number
|
||||
toolGroups: SteadyTrendToolGroup[]
|
||||
isToolActive: (action: SteadyTrendToolAction) => boolean
|
||||
isToolDisabled: (action: SteadyTrendToolAction) => boolean
|
||||
getToolTooltip: (item: SteadyTrendToolItem) => string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
'chart-data-zoom': [value: SteadyTrendZoomRange]
|
||||
'tool-action': [action: SteadyTrendToolAction]
|
||||
}>()
|
||||
|
||||
const closeFullscreen = () => {
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
if (!props.modelValue || event.key !== 'Escape') return
|
||||
|
||||
closeFullscreen()
|
||||
}
|
||||
|
||||
const handleChartDataZoom = (value: SteadyTrendZoomRange) => {
|
||||
emit('chart-data-zoom', value)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('keydown', handleKeydown)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('keydown', handleKeydown)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.steady-trend-fullscreen {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 3000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
background: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.steady-trend-fullscreen__header {
|
||||
display: flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 44px;
|
||||
padding: 0 10px;
|
||||
color: #ffffff;
|
||||
background: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.steady-trend-fullscreen__title {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.steady-trend-fullscreen__close {
|
||||
flex: none;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.steady-trend-fullscreen__body {
|
||||
--steady-trend-chart-gap: 8px;
|
||||
--steady-trend-visible-chart-count: 6;
|
||||
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
min-height: 0;
|
||||
padding: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.steady-trend-fullscreen__tool-row {
|
||||
position: static;
|
||||
display: flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.steady-trend-fullscreen__tools {
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.steady-trend-fullscreen__chart-list {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
gap: var(--steady-trend-chart-gap);
|
||||
min-height: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.steady-trend-fullscreen__chart-group {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex: 0 0
|
||||
calc(
|
||||
(100% - var(--steady-trend-chart-gap) * (var(--steady-trend-visible-chart-count) - 1)) /
|
||||
var(--steady-trend-visible-chart-count)
|
||||
);
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 4px;
|
||||
background: var(--cn-color-canvas-bg);
|
||||
}
|
||||
|
||||
.steady-trend-fullscreen__chart-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 0 8px 8px;
|
||||
}
|
||||
|
||||
.steady-trend-fullscreen__empty {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -23,16 +23,18 @@
|
||||
</div>
|
||||
|
||||
<div class="toolbar-field">
|
||||
<span class="toolbar-field__label">数据:</span>
|
||||
<el-select
|
||||
:model-value="modelValue.qualityFlag"
|
||||
clearable
|
||||
placeholder="选择数据质量"
|
||||
@update:model-value="updateField('qualityFlag', $event)"
|
||||
>
|
||||
<el-option label="仅有效数据" :value="0" />
|
||||
<el-option label="仅无效数据" :value="1" />
|
||||
</el-select>
|
||||
<span class="toolbar-field__label">数据质量:</span>
|
||||
<el-switch
|
||||
:model-value="modelValue.qualityFlag ?? 0"
|
||||
class="quality-switch"
|
||||
width="72"
|
||||
inline-prompt
|
||||
active-text="有效"
|
||||
inactive-text="无效"
|
||||
:active-value="0"
|
||||
:inactive-value="1"
|
||||
@update:model-value="handleQualityFlagChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="showHarmonicOrders" class="toolbar-field harmonic-select">
|
||||
@@ -40,10 +42,8 @@
|
||||
<el-select
|
||||
:model-value="modelValue.harmonicOrders"
|
||||
multiple
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
placeholder="选择谐波次数"
|
||||
@update:model-value="updateField('harmonicOrders', $event)"
|
||||
@update:model-value="handleHarmonicOrdersChange"
|
||||
>
|
||||
<el-option v-for="item in harmonicOrderOptions" :key="item" :label="`${item}次`" :value="item" />
|
||||
</el-select>
|
||||
@@ -57,9 +57,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage } from 'element-plus'
|
||||
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 { MAX_HARMONIC_ORDER_COUNT } from '../utils/selectionRules'
|
||||
import type { SteadyTrendFormState } from '../utils/trendPayload'
|
||||
|
||||
defineOptions({
|
||||
@@ -94,6 +96,40 @@ const updateField = <K extends keyof SteadyTrendFormState>(field: K, value: Stea
|
||||
})
|
||||
}
|
||||
|
||||
const handleQualityFlagChange = (value: string | number | boolean) => {
|
||||
updateField('qualityFlag', Number(value))
|
||||
}
|
||||
|
||||
const normalizeHarmonicOrders = (orders: number[]) => {
|
||||
return Array.from(
|
||||
new Set(
|
||||
orders
|
||||
.map(item => Number(item))
|
||||
.filter(item => Number.isInteger(item) && item >= 2 && item <= 50)
|
||||
)
|
||||
).sort((left, right) => left - right)
|
||||
}
|
||||
|
||||
const updateHarmonicOrders = (orders: number[]) => {
|
||||
const nextOrders = normalizeHarmonicOrders(orders)
|
||||
|
||||
if (nextOrders.length > MAX_HARMONIC_ORDER_COUNT) {
|
||||
ElMessage.warning(`谐波次数最多选择 ${MAX_HARMONIC_ORDER_COUNT} 个`)
|
||||
const currentOrders = normalizeHarmonicOrders(props.modelValue.harmonicOrders)
|
||||
updateField(
|
||||
'harmonicOrders',
|
||||
currentOrders.length ? currentOrders : nextOrders.slice(0, MAX_HARMONIC_ORDER_COUNT)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
updateField('harmonicOrders', nextOrders)
|
||||
}
|
||||
|
||||
const handleHarmonicOrdersChange = (value: number[]) => {
|
||||
updateHarmonicOrders(value)
|
||||
}
|
||||
|
||||
const updateTimeRange = (unit: TimePeriodUnit, baseDate: Date) => {
|
||||
emit('update:modelValue', {
|
||||
...props.modelValue,
|
||||
@@ -144,6 +180,10 @@ const handleTimeBaseDateChange = (value: Date) => {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.quality-switch {
|
||||
min-width: 72px;
|
||||
}
|
||||
|
||||
.trend-toolbar__time {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
|
||||
@@ -34,9 +34,7 @@
|
||||
v-model:collapsed="indicatorPanelCollapsedProxy"
|
||||
:selector-reset-key="selectorResetKey"
|
||||
:tree-data="indicatorTree"
|
||||
:loading="loading.indicator"
|
||||
:default-checked-keys="defaultIndicatorCheckedKeys"
|
||||
@refresh="emit('refreshIndicator')"
|
||||
@change="emit('indicatorChange', $event)"
|
||||
/>
|
||||
</div>
|
||||
@@ -83,7 +81,6 @@ const emit = defineEmits<{
|
||||
refreshLedger: []
|
||||
ledgerSearch: [value: string]
|
||||
ledgerChange: [nodes: SteadyDataView.SteadyLedgerNode[]]
|
||||
refreshIndicator: []
|
||||
indicatorChange: [nodes: SteadyDataView.SteadyIndicatorNode[]]
|
||||
queryTrend: []
|
||||
resetTrend: []
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { Component } from 'vue'
|
||||
|
||||
export type SteadyTrendToolAction =
|
||||
| 'x-zoom-in'
|
||||
| 'x-zoom-out'
|
||||
| 'y-zoom-in'
|
||||
| 'y-zoom-out'
|
||||
| 'box-zoom'
|
||||
| 'wheel-zoom'
|
||||
| 'reset'
|
||||
| 'pan'
|
||||
| 'fullscreen'
|
||||
| 'download-image'
|
||||
| 'query-data'
|
||||
| 'missing-data'
|
||||
|
||||
export interface SteadyTrendToolItem {
|
||||
action: SteadyTrendToolAction
|
||||
label: string
|
||||
icon: Component
|
||||
}
|
||||
|
||||
export interface SteadyTrendToolGroup {
|
||||
key: string
|
||||
items: SteadyTrendToolItem[]
|
||||
}
|
||||
Reference in New Issue
Block a user