855 lines
26 KiB
Vue
855 lines
26 KiB
Vue
|
|
<template>
|
|||
|
|
<div class="h-full flex flex-col relative overflow-hidden p-1 group">
|
|||
|
|
<!-- Tech Border/Background effects -->
|
|||
|
|
<div class="absolute inset-0 border border-cyan-500/20 pointer-events-none z-0"></div>
|
|||
|
|
<!-- Corner Accents -->
|
|||
|
|
<div
|
|||
|
|
class="absolute top-0 left-0 w-4 h-4 border-t-2 border-l-2 border-cyan-500 z-0 opacity-60 group-hover:opacity-100 transition-opacity"
|
|||
|
|
></div>
|
|||
|
|
<div
|
|||
|
|
class="absolute top-0 right-0 w-4 h-4 border-t-2 border-r-2 border-cyan-500 z-0 opacity-60 group-hover:opacity-100 transition-opacity"
|
|||
|
|
></div>
|
|||
|
|
<div
|
|||
|
|
class="absolute bottom-0 left-0 w-4 h-4 border-b-2 border-l-2 border-cyan-500 z-0 opacity-60 group-hover:opacity-100 transition-opacity"
|
|||
|
|
></div>
|
|||
|
|
<div
|
|||
|
|
class="absolute bottom-0 right-0 w-4 h-4 border-b-2 border-r-2 border-cyan-500 z-0 opacity-60 group-hover:opacity-100 transition-opacity"
|
|||
|
|
></div>
|
|||
|
|
|
|||
|
|
<div class="relative z-10 flex flex-col h-full p-4">
|
|||
|
|
<div class="flex justify-between items-center mb-6">
|
|||
|
|
<div class="flex items-center gap-3">
|
|||
|
|
<div
|
|||
|
|
class="w-1 h-6 bg-gradient-to-b from-cyan-400 to-blue-600 shadow-[0_0_10px_rgba(34,211,238,0.5)]"
|
|||
|
|
></div>
|
|||
|
|
<h3
|
|||
|
|
class="text-xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-cyan-100 to-blue-200 tracking-wider font-mono"
|
|||
|
|
>
|
|||
|
|
数据趋势分析
|
|||
|
|
<span class="text-xs text-cyan-500/50 ml-2 font-normal">/// TREND ANALYSIS</span>
|
|||
|
|
</h3>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="flex items-center gap-4">
|
|||
|
|
<el-radio-group
|
|||
|
|
v-model="chartType"
|
|||
|
|
size="small"
|
|||
|
|
@change="handleTypeChange"
|
|||
|
|
class="tech-radio-group"
|
|||
|
|
>
|
|||
|
|
<el-radio-button value="stress">应力</el-radio-button>
|
|||
|
|
<el-radio-button value="temp">温度</el-radio-button>
|
|||
|
|
<el-radio-button value="load">载荷</el-radio-button>
|
|||
|
|
<el-radio-button value="displacement">位移</el-radio-button>
|
|||
|
|
</el-radio-group>
|
|||
|
|
|
|||
|
|
<div class="flex items-center border-l border-cyan-500/20 pl-4">
|
|||
|
|
<!-- 3D View Button -->
|
|||
|
|
<div
|
|||
|
|
class="flex justify-center items-center px-4 py-1 mr-2 bg-[#0f172a] border border-cyan-500/10 rounded cursor-pointer hover:border-cyan-500/50 hover:bg-[#0f172a]/80 transition-all group"
|
|||
|
|
@click="showViewer = true"
|
|||
|
|
>
|
|||
|
|
<span
|
|||
|
|
class="text-sm text-cyan-400 font-mono group-hover:text-cyan-300 transition-colors"
|
|||
|
|
>数据模型</span
|
|||
|
|
>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<el-tooltip content="查看详细数据" placement="bottom" effect="dark">
|
|||
|
|
<button class="tech-icon-btn" @click="showTableDialog = true">
|
|||
|
|
<el-icon><List /></el-icon>
|
|||
|
|
</button>
|
|||
|
|
</el-tooltip>
|
|||
|
|
|
|||
|
|
<el-tooltip :content="isMaximized ? '还原' : '最大化'" placement="left" effect="dark">
|
|||
|
|
<button class="tech-icon-btn" @click="emit('toggleMaximize')">
|
|||
|
|
<el-icon><FullScreen /></el-icon>
|
|||
|
|
</button>
|
|||
|
|
</el-tooltip>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div
|
|||
|
|
class="flex-1 h-full min-h-0 relative bg-[#0f172a]/40 border border-cyan-500/10 rounded backdrop-blur-sm shadow-inner"
|
|||
|
|
>
|
|||
|
|
<div
|
|||
|
|
v-if="isLoading"
|
|||
|
|
class="absolute inset-0 flex items-center justify-center bg-[#0b1120]/80 z-10 backdrop-blur-sm"
|
|||
|
|
>
|
|||
|
|
<div class="flex flex-col items-center gap-3">
|
|||
|
|
<el-icon class="is-loading text-cyan-400 text-3xl"><Loading /></el-icon>
|
|||
|
|
<span class="text-cyan-400/80 text-xs tracking-[0.2em] font-mono">LOADING DATA...</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<client-only>
|
|||
|
|
<v-chart class="chart" :option="option" autoresize />
|
|||
|
|
</client-only>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 数据表格弹窗 -->
|
|||
|
|
<el-dialog
|
|||
|
|
v-model="showTableDialog"
|
|||
|
|
:title="`${currentMetricName} - 详细数据查询`"
|
|||
|
|
width="1200px"
|
|||
|
|
append-to-body
|
|||
|
|
class="tech-dialog-modal"
|
|||
|
|
>
|
|||
|
|
<div class="mb-4 p-4 bg-[#0f172a] border border-cyan-500/20 rounded">
|
|||
|
|
<div class="flex flex-wrap gap-4 items-end">
|
|||
|
|
<div class="flex flex-col gap-1">
|
|||
|
|
<span class="text-xs text-gray-500">时间范围</span>
|
|||
|
|
<el-date-picker
|
|||
|
|
v-model="query.dateRange"
|
|||
|
|
type="daterange"
|
|||
|
|
range-separator="至"
|
|||
|
|
start-placeholder="开始日期"
|
|||
|
|
end-placeholder="结束日期"
|
|||
|
|
size="small"
|
|||
|
|
value-format="YYYY-MM-DD"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="flex flex-col gap-1">
|
|||
|
|
<span class="text-xs text-gray-500">数值范围 ({{ currentUnit }})</span>
|
|||
|
|
<div class="flex items-center gap-2">
|
|||
|
|
<el-input-number
|
|||
|
|
v-model="query.minValue"
|
|||
|
|
size="small"
|
|||
|
|
:placeholder="'Min'"
|
|||
|
|
:controls="false"
|
|||
|
|
class="!w-24"
|
|||
|
|
/>
|
|||
|
|
<span class="text-gray-400">-</span>
|
|||
|
|
<el-input-number
|
|||
|
|
v-model="query.maxValue"
|
|||
|
|
size="small"
|
|||
|
|
:placeholder="'Max'"
|
|||
|
|
:controls="false"
|
|||
|
|
class="!w-24"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<el-button type="primary" size="small" @click="handleQuery">
|
|||
|
|
<el-icon class="mr-1"><Search /></el-icon> 查询
|
|||
|
|
</el-button>
|
|||
|
|
<el-button size="small" @click="resetQuery">重置</el-button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<el-table :data="filteredTableData" height="600" stripe style="width: 100%">
|
|||
|
|
<el-table-column prop="date" label="时间" width="180" sortable />
|
|||
|
|
<el-table-column prop="value" :label="`数值 (${currentUnit})`" sortable>
|
|||
|
|
<template #default="scope">
|
|||
|
|
<span :class="getValueClass(scope.row.value)">{{ scope.row.value }}</span>
|
|||
|
|
</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
<el-table-column label="状态" width="100">
|
|||
|
|
<template #default="scope">
|
|||
|
|
<el-tag :type="getStatusType(scope.row.value)" size="small">
|
|||
|
|
{{ getStatusText(scope.row.value) }}
|
|||
|
|
</el-tag>
|
|||
|
|
</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
</el-table>
|
|||
|
|
</el-dialog>
|
|||
|
|
|
|||
|
|
<!-- 运行状态记录弹窗 (Moved from ModelPropertiesPanel) -->
|
|||
|
|
<el-dialog
|
|||
|
|
v-model="showRunStateDialog"
|
|||
|
|
:title="currentParamItem ? `${currentParamItem.label} - 历史记录` : '运行状态记录'"
|
|||
|
|
width="900px"
|
|||
|
|
append-to-body
|
|||
|
|
class="tech-dialog-modal"
|
|||
|
|
>
|
|||
|
|
<div class="mb-4 flex flex-wrap gap-4 items-center">
|
|||
|
|
<div class="text-sm text-cyan-400 font-mono">
|
|||
|
|
<span class="mr-2">当前节点:</span>
|
|||
|
|
<el-tag size="small" class="tech-tag">{{ store.currentNodeId || 'Unknown' }}</el-tag>
|
|||
|
|
</div>
|
|||
|
|
<div class="flex-1"></div>
|
|||
|
|
<el-button
|
|||
|
|
size="small"
|
|||
|
|
type="primary"
|
|||
|
|
class="tech-button"
|
|||
|
|
@click="() => generateMockData()"
|
|||
|
|
>
|
|||
|
|
<el-icon class="mr-1"><Refresh /></el-icon> 刷新数据
|
|||
|
|
</el-button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<el-table :data="runStateTableData" height="500" stripe style="width: 100%">
|
|||
|
|
<el-table-column prop="time" label="记录时间" width="200" />
|
|||
|
|
<el-table-column prop="parameter" label="监测参数" width="180" />
|
|||
|
|
<el-table-column prop="value" label="监测数值" />
|
|||
|
|
<el-table-column prop="status" label="状态判定" width="120">
|
|||
|
|
<template #default="scope">
|
|||
|
|
<el-tag :type="scope.row.statusType" size="small">{{ scope.row.status }}</el-tag>
|
|||
|
|
</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
<el-table-column prop="operator" label="记录人" width="120" />
|
|||
|
|
</el-table>
|
|||
|
|
</el-dialog>
|
|||
|
|
|
|||
|
|
<!-- 3D Viewer 弹窗 (Implicitly moved for 3D Button) -->
|
|||
|
|
<el-dialog
|
|||
|
|
v-model="showViewer"
|
|||
|
|
title="3D数据查看"
|
|||
|
|
width="1000px"
|
|||
|
|
top="5vh"
|
|||
|
|
append-to-body
|
|||
|
|
class="tech-dialog-modal"
|
|||
|
|
destroy-on-close
|
|||
|
|
>
|
|||
|
|
<div
|
|||
|
|
class="h-[700px] w-full border border-gray-700/50 rounded overflow-hidden relative bg-[#0b1120]"
|
|||
|
|
>
|
|||
|
|
<Viewer v-if="showViewer" />
|
|||
|
|
</div>
|
|||
|
|
</el-dialog>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup lang="ts">
|
|||
|
|
import { FullScreen, List, Loading, Refresh, Search } from '@element-plus/icons-vue'
|
|||
|
|
import { LineChart } from 'echarts/charts'
|
|||
|
|
import {
|
|||
|
|
DataZoomComponent,
|
|||
|
|
GridComponent,
|
|||
|
|
LegendComponent,
|
|||
|
|
TitleComponent,
|
|||
|
|
TooltipComponent,
|
|||
|
|
} from 'echarts/components'
|
|||
|
|
import { graphic, use } from 'echarts/core'
|
|||
|
|
import { CanvasRenderer } from 'echarts/renderers'
|
|||
|
|
import { computed, onMounted, ref, watch } from 'vue'
|
|||
|
|
import VChart from 'vue-echarts'
|
|||
|
|
import Viewer from '~/components/Viewer.vue'
|
|||
|
|
import { usePowerStationStore } from '~/stores/powerStation'
|
|||
|
|
|
|||
|
|
// 注册 ECharts 组件
|
|||
|
|
use([
|
|||
|
|
CanvasRenderer,
|
|||
|
|
LineChart,
|
|||
|
|
TitleComponent,
|
|||
|
|
TooltipComponent,
|
|||
|
|
GridComponent,
|
|||
|
|
LegendComponent,
|
|||
|
|
DataZoomComponent,
|
|||
|
|
])
|
|||
|
|
|
|||
|
|
// 定义组件属性
|
|||
|
|
defineProps<{
|
|||
|
|
isMaximized?: boolean // 是否最大化显示
|
|||
|
|
}>()
|
|||
|
|
|
|||
|
|
// 定义组件事件
|
|||
|
|
const emit = defineEmits(['toggleMaximize'])
|
|||
|
|
|
|||
|
|
// 状态管理
|
|||
|
|
const store = usePowerStationStore()
|
|||
|
|
const chartType = ref('stress') // 图表类型
|
|||
|
|
const showTableDialog = ref(false)
|
|||
|
|
const showRunStateDialog = ref(false) // Renamed from showTableDialog to avoid conflict
|
|||
|
|
const showViewer = ref(false)
|
|||
|
|
|
|||
|
|
// 运行状态表格相关
|
|||
|
|
const runStateTableData = ref<any[]>([])
|
|||
|
|
const currentParamItem = ref<any>(null)
|
|||
|
|
|
|||
|
|
const generateMockData = (item?: any) => {
|
|||
|
|
const targetItem = item || currentParamItem.value || { label: '未命名参数' }
|
|||
|
|
// if (!targetItem) return // Allow running without item for demo
|
|||
|
|
|
|||
|
|
const label = targetItem.label
|
|||
|
|
const operators = ['系统自动', '操作员A', '操作员B']
|
|||
|
|
|
|||
|
|
// 根据不同的标签生成不同的模拟数据
|
|||
|
|
runStateTableData.value = Array.from({ length: 20 }).map((_, i) => {
|
|||
|
|
let value = ''
|
|||
|
|
let statusObj = { label: '正常', type: 'success' }
|
|||
|
|
|
|||
|
|
// 基于标签的简单的模拟逻辑
|
|||
|
|
if (label.includes('温度')) {
|
|||
|
|
const val = 50 + Math.random() * 20
|
|||
|
|
value = val.toFixed(1) + ' °C'
|
|||
|
|
if (val > 68) statusObj = { label: '高温警告', type: 'warning' }
|
|||
|
|
} else if (label.includes('压力')) {
|
|||
|
|
const val = 10 + Math.random() * 5
|
|||
|
|
value = val.toFixed(2) + ' MPa'
|
|||
|
|
if (val > 14.5) statusObj = { label: '高压', type: 'warning' }
|
|||
|
|
} else if (label.includes('健康')) {
|
|||
|
|
// 健康度
|
|||
|
|
const val = 90 + Math.random() * 10
|
|||
|
|
value = Math.min(100, val).toFixed(1) + '%'
|
|||
|
|
if (val < 92) statusObj = { label: '需关注', type: 'warning' }
|
|||
|
|
} else if (label.includes('状态') || label.includes('运行')) {
|
|||
|
|
const states = ['正常运行', '以正常运行', '低负荷运行']
|
|||
|
|
value = states[Math.floor(Math.random() * states.length)] || '正常运行'
|
|||
|
|
// 偶尔出个异常
|
|||
|
|
if (Math.random() > 0.9) {
|
|||
|
|
value = '停机维护'
|
|||
|
|
statusObj = { label: '停机', type: 'info' }
|
|||
|
|
}
|
|||
|
|
} else if (label.includes('报警')) {
|
|||
|
|
const subStatus = Math.random()
|
|||
|
|
if (subStatus > 0.8) {
|
|||
|
|
value = '轻微报警'
|
|||
|
|
statusObj = { label: '报警', type: 'warning' }
|
|||
|
|
} else {
|
|||
|
|
value = '无报警'
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
// 默认回退
|
|||
|
|
value = (Math.random() * 100).toFixed(2)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 随机插入一些异常
|
|||
|
|
if (Math.random() > 0.95 && statusObj.type === 'success') {
|
|||
|
|
statusObj = { label: '异常', type: 'danger' }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
time: new Date(Date.now() - i * 1000 * 60 * 30).toLocaleString(),
|
|||
|
|
parameter: label,
|
|||
|
|
value: value,
|
|||
|
|
status: statusObj.label,
|
|||
|
|
// @ts-ignore - Element Plus tag type
|
|||
|
|
statusType: statusObj.type || 'success',
|
|||
|
|
operator: operators[i % operators.length],
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 数据定义
|
|||
|
|
interface ChartDataPoint {
|
|||
|
|
date: string
|
|||
|
|
value: number
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const rawData = ref<ChartDataPoint[]>([]) // 原始数据
|
|||
|
|
const chartData = ref({
|
|||
|
|
dates: [] as string[],
|
|||
|
|
values: [] as number[],
|
|||
|
|
unit: '',
|
|||
|
|
name: '',
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 查询状态
|
|||
|
|
const query = ref({
|
|||
|
|
dateRange: [] as string[],
|
|||
|
|
minValue: undefined as number | undefined,
|
|||
|
|
maxValue: undefined as number | undefined,
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 图表加载状态
|
|||
|
|
const isLoading = ref(false)
|
|||
|
|
|
|||
|
|
// 计算属性
|
|||
|
|
const currentUnit = computed(() => chartData.value.unit)
|
|||
|
|
const currentMetricName = computed(() => chartData.value.name)
|
|||
|
|
|
|||
|
|
// 过滤后的表格数据
|
|||
|
|
const filteredTableData = computed(() => {
|
|||
|
|
let data = [...rawData.value]
|
|||
|
|
|
|||
|
|
// 日期过滤
|
|||
|
|
if (query.value.dateRange && query.value.dateRange.length === 2) {
|
|||
|
|
const start = new Date(query.value.dateRange[0]).getTime()
|
|||
|
|
const end = new Date(query.value.dateRange[1]).getTime() + 86400000 // 包含结束当天
|
|||
|
|
data = data.filter(item => {
|
|||
|
|
const itemTime = new Date(item.date).getTime()
|
|||
|
|
return itemTime >= start && itemTime < end
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 数值过滤
|
|||
|
|
if (query.value.minValue !== undefined) {
|
|||
|
|
data = data.filter(item => item.value >= query.value.minValue!)
|
|||
|
|
}
|
|||
|
|
if (query.value.maxValue !== undefined) {
|
|||
|
|
data = data.filter(item => item.value <= query.value.maxValue!)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return data
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 辅助函数:获取状态样式
|
|||
|
|
const getStatusType = (val: number) => {
|
|||
|
|
// 这里只是简单的mock逻辑,实际应根据不同指标的阈值判断
|
|||
|
|
if (chartType.value === 'stress' && val > 140) return 'danger'
|
|||
|
|
if (chartType.value === 'temp' && val > 575) return 'warning'
|
|||
|
|
if (chartType.value === 'load' && val > 4500) return 'warning'
|
|||
|
|
if (chartType.value === 'displacement' && val > 40) return 'warning'
|
|||
|
|
return 'success'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const getStatusText = (val: number) => {
|
|||
|
|
const type = getStatusType(val)
|
|||
|
|
if (type === 'danger') return '告警'
|
|||
|
|
if (type === 'warning') return '注意'
|
|||
|
|
return '正常'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const getValueClass = (val: number) => {
|
|||
|
|
const type = getStatusType(val)
|
|||
|
|
if (type === 'danger') return 'text-red-400 font-bold'
|
|||
|
|
if (type === 'warning') return 'text-orange-400'
|
|||
|
|
return 'text-slate-300'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 查询处理
|
|||
|
|
const handleQuery = () => {
|
|||
|
|
// 实际上computed会自动处理,这里可以加一些loading效果或者后端查询逻辑
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const resetQuery = () => {
|
|||
|
|
query.value = {
|
|||
|
|
dateRange: [],
|
|||
|
|
minValue: undefined,
|
|||
|
|
maxValue: undefined,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取图表数据函数
|
|||
|
|
const fetchChartData = async (type: string) => {
|
|||
|
|
if (!store.currentNodeId) {
|
|||
|
|
// 清空图表数据
|
|||
|
|
chartData.value = {
|
|||
|
|
dates: [],
|
|||
|
|
values: [],
|
|||
|
|
unit: '',
|
|||
|
|
name: '',
|
|||
|
|
}
|
|||
|
|
rawData.value = []
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
isLoading.value = true
|
|||
|
|
try {
|
|||
|
|
// 尝试调用API (这里假设API会根据type返回对应数据)
|
|||
|
|
// 实际项目中可能需要根据type调整API路径或参数
|
|||
|
|
const response = await $fetch('/api/power-station/chart-data', {
|
|||
|
|
params: {
|
|||
|
|
nodeId: store.currentNodeId,
|
|||
|
|
type: type,
|
|||
|
|
},
|
|||
|
|
}).catch(() => null) // 捕获错误以便使用Mock数据
|
|||
|
|
|
|||
|
|
if (response && response.success && response.data) {
|
|||
|
|
const data = response.data as any
|
|||
|
|
chartData.value = {
|
|||
|
|
...data,
|
|||
|
|
dates: (data.dates || []).map((d: any) => String(d)),
|
|||
|
|
}
|
|||
|
|
// 同步 rawData
|
|||
|
|
rawData.value = chartData.value.dates.map((date: string, index: number) => ({
|
|||
|
|
date,
|
|||
|
|
value: chartData.value.values[index],
|
|||
|
|
}))
|
|||
|
|
} else {
|
|||
|
|
throw new Error('Use mock data')
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
// 使用模拟数据
|
|||
|
|
const dates = []
|
|||
|
|
const values = []
|
|||
|
|
const rawPoints: ChartDataPoint[] = []
|
|||
|
|
const now = new Date()
|
|||
|
|
|
|||
|
|
let unit = ''
|
|||
|
|
let name = ''
|
|||
|
|
let baseValue = 0
|
|||
|
|
let range = 0
|
|||
|
|
|
|||
|
|
switch (type) {
|
|||
|
|
case 'stress':
|
|||
|
|
unit = 'MPa'
|
|||
|
|
name = '历史应力 (Stress)'
|
|||
|
|
baseValue = 100
|
|||
|
|
range = 50
|
|||
|
|
break
|
|||
|
|
case 'temp':
|
|||
|
|
unit = '°C'
|
|||
|
|
name = '历史温度 (Temp)'
|
|||
|
|
baseValue = 560
|
|||
|
|
range = 20
|
|||
|
|
break
|
|||
|
|
case 'load':
|
|||
|
|
unit = 'N'
|
|||
|
|
name = '实时载荷 (Load)'
|
|||
|
|
baseValue = 3000
|
|||
|
|
range = 2000 // 3000-5000
|
|||
|
|
break
|
|||
|
|
case 'displacement':
|
|||
|
|
unit = 'mm'
|
|||
|
|
name = '实时位移 (Displacement)'
|
|||
|
|
baseValue = 10
|
|||
|
|
range = 40 // 10-50
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for (let i = 0; i < 30; i++) {
|
|||
|
|
const date = new Date(now.getTime() - (29 - i) * 24 * 60 * 60 * 1000)
|
|||
|
|
const dateStr = date.toISOString().split('T')[0]
|
|||
|
|
const val = Math.floor(Math.random() * range) + baseValue
|
|||
|
|
|
|||
|
|
dates.push(dateStr)
|
|||
|
|
values.push(val)
|
|||
|
|
rawPoints.push({ date: dateStr, value: val })
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
chartData.value = {
|
|||
|
|
dates,
|
|||
|
|
values,
|
|||
|
|
unit,
|
|||
|
|
name,
|
|||
|
|
}
|
|||
|
|
rawData.value = rawPoints
|
|||
|
|
} finally {
|
|||
|
|
isLoading.value = false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理图表类型变更
|
|||
|
|
const handleTypeChange = (val: string | number | boolean | undefined) => {
|
|||
|
|
if (store.currentNodeId && typeof val === 'string') {
|
|||
|
|
resetQuery() // 切换类型时重置查询条件
|
|||
|
|
fetchChartData(val)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 监听currentNodeId变化,重新计算图表数据
|
|||
|
|
watch(
|
|||
|
|
() => store.currentNodeId,
|
|||
|
|
newId => {
|
|||
|
|
if (newId) {
|
|||
|
|
// 当选择的节点ID变化时,重新获取图表数据
|
|||
|
|
fetchChartData(chartType.value)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// 计算图表配置选项
|
|||
|
|
const option = computed(() => {
|
|||
|
|
const data = chartData.value
|
|||
|
|
|
|||
|
|
// 颜色配置
|
|||
|
|
const colors = {
|
|||
|
|
stress: { main: '#06b6d4', area: ['rgba(6,182,212,0.5)', 'rgba(6,182,212,0.01)'] }, // Cyan
|
|||
|
|
temp: { main: '#f43f5e', area: ['rgba(244,63,94,0.5)', 'rgba(244,63,94,0.01)'] }, // Rose
|
|||
|
|
load: { main: '#f59e0b', area: ['rgba(245,158,11,0.5)', 'rgba(245,158,11,0.01)'] }, // Amber
|
|||
|
|
displacement: { main: '#10b981', area: ['rgba(16,185,129,0.5)', 'rgba(16,185,129,0.01)'] }, // Emerald
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const currentColors = colors[chartType.value as keyof typeof colors] || colors.stress
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
backgroundColor: 'transparent',
|
|||
|
|
title: {
|
|||
|
|
text: data.name || '历史数据',
|
|||
|
|
left: 'center',
|
|||
|
|
textStyle: {
|
|||
|
|
color: '#e2e8f0',
|
|||
|
|
fontSize: 16,
|
|||
|
|
fontWeight: 'normal',
|
|||
|
|
fontFamily: 'monospace',
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
tooltip: {
|
|||
|
|
trigger: 'axis',
|
|||
|
|
backgroundColor: 'rgba(15, 23, 42, 0.9)',
|
|||
|
|
borderColor: '#06b6d4',
|
|||
|
|
textStyle: {
|
|||
|
|
color: '#e2e8f0',
|
|||
|
|
},
|
|||
|
|
axisPointer: {
|
|||
|
|
type: 'cross',
|
|||
|
|
label: {
|
|||
|
|
backgroundColor: '#0891b2',
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
grid: {
|
|||
|
|
left: '3%',
|
|||
|
|
right: '4%',
|
|||
|
|
bottom: '10%',
|
|||
|
|
containLabel: true,
|
|||
|
|
borderColor: '#1e293b',
|
|||
|
|
},
|
|||
|
|
xAxis: {
|
|||
|
|
type: 'category',
|
|||
|
|
boundaryGap: false,
|
|||
|
|
data: data.dates,
|
|||
|
|
axisLine: {
|
|||
|
|
lineStyle: {
|
|||
|
|
color: '#334155',
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
axisLabel: {
|
|||
|
|
color: '#94a3b8',
|
|||
|
|
fontFamily: 'monospace',
|
|||
|
|
},
|
|||
|
|
splitLine: {
|
|||
|
|
show: true,
|
|||
|
|
lineStyle: {
|
|||
|
|
color: 'rgba(51, 65, 85, 0.3)',
|
|||
|
|
type: 'dashed',
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
yAxis: {
|
|||
|
|
type: 'value',
|
|||
|
|
name: data.unit,
|
|||
|
|
nameTextStyle: {
|
|||
|
|
color: '#94a3b8',
|
|||
|
|
align: 'right',
|
|||
|
|
padding: [0, 10, 0, 0],
|
|||
|
|
},
|
|||
|
|
axisLine: {
|
|||
|
|
show: true,
|
|||
|
|
lineStyle: {
|
|||
|
|
color: '#334155',
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
axisLabel: {
|
|||
|
|
color: '#94a3b8',
|
|||
|
|
fontFamily: 'monospace',
|
|||
|
|
},
|
|||
|
|
splitLine: {
|
|||
|
|
lineStyle: {
|
|||
|
|
color: 'rgba(51, 65, 85, 0.3)',
|
|||
|
|
type: 'dashed',
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
dataZoom: [
|
|||
|
|
{
|
|||
|
|
type: 'inside',
|
|||
|
|
start: 0,
|
|||
|
|
end: 100,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
start: 0,
|
|||
|
|
end: 100,
|
|||
|
|
backgroundColor: 'rgba(15, 23, 42, 0.5)',
|
|||
|
|
dataBackground: {
|
|||
|
|
lineStyle: { color: '#0891b2' },
|
|||
|
|
areaStyle: { color: '#0891b2', opacity: 0.2 },
|
|||
|
|
},
|
|||
|
|
selectedDataBackground: {
|
|||
|
|
lineStyle: { color: '#22d3ee' },
|
|||
|
|
areaStyle: { color: '#22d3ee', opacity: 0.4 },
|
|||
|
|
},
|
|||
|
|
fillerColor: 'rgba(34, 211, 238, 0.1)',
|
|||
|
|
handleStyle: {
|
|||
|
|
color: '#22d3ee',
|
|||
|
|
borderColor: '#0891b2',
|
|||
|
|
},
|
|||
|
|
textStyle: {
|
|||
|
|
color: '#94a3b8',
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
],
|
|||
|
|
series: [
|
|||
|
|
{
|
|||
|
|
name: data.name,
|
|||
|
|
type: 'line',
|
|||
|
|
smooth: true,
|
|||
|
|
symbol: 'circle',
|
|||
|
|
symbolSize: 6,
|
|||
|
|
showSymbol: false,
|
|||
|
|
data: data.values,
|
|||
|
|
lineStyle: {
|
|||
|
|
width: 3,
|
|||
|
|
color: currentColors.main,
|
|||
|
|
shadowColor: currentColors.main,
|
|||
|
|
shadowBlur: 10,
|
|||
|
|
},
|
|||
|
|
areaStyle: {
|
|||
|
|
opacity: 0.8,
|
|||
|
|
color: new graphic.LinearGradient(0, 0, 0, 1, [
|
|||
|
|
{
|
|||
|
|
offset: 0,
|
|||
|
|
color: currentColors.area[0],
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
offset: 1,
|
|||
|
|
color: currentColors.area[1],
|
|||
|
|
},
|
|||
|
|
]),
|
|||
|
|
},
|
|||
|
|
itemStyle: {
|
|||
|
|
color: currentColors.main,
|
|||
|
|
borderColor: '#fff',
|
|||
|
|
borderWidth: 2,
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
],
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 组件挂载时初始化数据
|
|||
|
|
onMounted(() => {
|
|||
|
|
if (store.currentNodeId) {
|
|||
|
|
fetchChartData(chartType.value)
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.chart {
|
|||
|
|
height: 100%;
|
|||
|
|
width: 100%;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Tech Radio Group Styles */
|
|||
|
|
:deep(.tech-radio-group .el-radio-button__inner) {
|
|||
|
|
background-color: transparent;
|
|||
|
|
border: 1px solid rgba(34, 211, 238, 0.2);
|
|||
|
|
color: #94a3b8;
|
|||
|
|
font-family: monospace;
|
|||
|
|
transition: all 0.3s ease;
|
|||
|
|
padding: 6px 16px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
:deep(.tech-radio-group .el-radio-button__original-radio:checked + .el-radio-button__inner) {
|
|||
|
|
background-color: rgba(6, 182, 212, 0.15);
|
|||
|
|
border-color: #06b6d4;
|
|||
|
|
color: #22d3ee;
|
|||
|
|
box-shadow: 0 0 10px rgba(6, 182, 212, 0.3);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
:deep(.tech-radio-group .el-radio-button:first-child .el-radio-button__inner) {
|
|||
|
|
border-radius: 4px 0 0 4px;
|
|||
|
|
}
|
|||
|
|
:deep(.tech-radio-group .el-radio-button:last-child .el-radio-button__inner) {
|
|||
|
|
border-radius: 0 4px 4px 0;
|
|||
|
|
}
|
|||
|
|
:deep(.tech-radio-group .el-radio-button__inner:hover) {
|
|||
|
|
color: #22d3ee;
|
|||
|
|
background-color: rgba(34, 211, 238, 0.05);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Tech Icon Button */
|
|||
|
|
.tech-icon-btn {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
width: 32px;
|
|||
|
|
height: 32px;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
background: transparent;
|
|||
|
|
border: 1px solid rgba(34, 211, 238, 0.3);
|
|||
|
|
color: #22d3ee;
|
|||
|
|
cursor: pointer;
|
|||
|
|
transition: all 0.3s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tech-icon-btn:hover {
|
|||
|
|
background: rgba(6, 182, 212, 0.2);
|
|||
|
|
box-shadow: 0 0 10px rgba(6, 182, 212, 0.4);
|
|||
|
|
transform: scale(1.05);
|
|||
|
|
border-color: #22d3ee;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tech-icon-btn:active {
|
|||
|
|
transform: scale(0.95);
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
|
|||
|
|
<style>
|
|||
|
|
/* Global overrides for the tech dialog */
|
|||
|
|
.tech-dialog-modal {
|
|||
|
|
background-color: #0b1120 !important;
|
|||
|
|
border: 1px solid #06b6d4 !important;
|
|||
|
|
box-shadow: 0 0 20px rgba(6, 182, 212, 0.2) !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tech-dialog-modal .el-dialog__header {
|
|||
|
|
border-bottom: 1px solid rgba(34, 211, 238, 0.1);
|
|||
|
|
margin-right: 0 !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tech-dialog-modal .el-dialog__title {
|
|||
|
|
color: #22d3ee !important;
|
|||
|
|
font-family: monospace;
|
|||
|
|
font-weight: bold;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tech-dialog-modal .el-dialog__body {
|
|||
|
|
background-color: #0b1120 !important;
|
|||
|
|
color: #94a3b8 !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tech-dialog-modal .el-dialog__close {
|
|||
|
|
color: #22d3ee !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Table overrides inside dialog */
|
|||
|
|
.tech-dialog-modal .el-table {
|
|||
|
|
background-color: transparent !important;
|
|||
|
|
--el-table-tr-bg-color: transparent !important;
|
|||
|
|
--el-table-header-bg-color: rgba(15, 23, 42, 0.8) !important;
|
|||
|
|
--el-table-row-hover-bg-color: rgba(6, 182, 212, 0.1) !important;
|
|||
|
|
--el-table-border-color: rgba(34, 211, 238, 0.1) !important;
|
|||
|
|
color: #94a3b8 !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tech-dialog-modal .el-table th.el-table__cell {
|
|||
|
|
background-color: rgba(15, 23, 42, 0.8) !important;
|
|||
|
|
color: #22d3ee !important;
|
|||
|
|
border-bottom: 1px solid rgba(34, 211, 238, 0.2) !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tech-dialog-modal .el-table td.el-table__cell {
|
|||
|
|
border-bottom: 1px solid rgba(34, 211, 238, 0.1) !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tech-dialog-modal .el-table--striped .el-table__body tr.el-table__row--striped td.el-table__cell {
|
|||
|
|
background-color: rgba(30, 41, 59, 0.3) !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Input/Select overrides */
|
|||
|
|
.tech-dialog-modal .el-input__wrapper,
|
|||
|
|
.tech-dialog-modal .el-date-editor {
|
|||
|
|
background-color: rgba(15, 23, 42, 0.5) !important;
|
|||
|
|
box-shadow: 0 0 0 1px rgba(34, 211, 238, 0.2) inset !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tech-dialog-modal .el-input__inner {
|
|||
|
|
color: #e2e8f0 !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tech-dialog-modal .el-range-input {
|
|||
|
|
color: #e2e8f0 !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tech-dialog-modal .el-range-separator {
|
|||
|
|
color: #94a3b8 !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Tag overrides */
|
|||
|
|
.tech-dialog-modal .el-tag {
|
|||
|
|
background-color: rgba(15, 23, 42, 0.8) !important;
|
|||
|
|
border-color: rgba(34, 211, 238, 0.3) !important;
|
|||
|
|
}
|
|||
|
|
.tech-dialog-modal .el-tag--success {
|
|||
|
|
color: #34d399 !important;
|
|||
|
|
background-color: rgba(16, 185, 129, 0.1) !important;
|
|||
|
|
border-color: rgba(16, 185, 129, 0.2) !important;
|
|||
|
|
}
|
|||
|
|
.tech-dialog-modal .el-tag--warning {
|
|||
|
|
color: #fbbf24 !important;
|
|||
|
|
background-color: rgba(245, 158, 11, 0.1) !important;
|
|||
|
|
border-color: rgba(245, 158, 11, 0.2) !important;
|
|||
|
|
}
|
|||
|
|
.tech-dialog-modal .el-tag--danger {
|
|||
|
|
color: #f87171 !important;
|
|||
|
|
background-color: rgba(239, 68, 68, 0.1) !important;
|
|||
|
|
border-color: rgba(239, 68, 68, 0.2) !important;
|
|||
|
|
}
|
|||
|
|
</style>
|