106网站图像展示,未完成版

This commit is contained in:
YueL1331
2026-01-06 17:35:18 +08:00
parent 45c3d602c0
commit 2c2f9e43e3
4 changed files with 229 additions and 33 deletions

Binary file not shown.

View File

@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.1.0", "@element-plus/icons-vue": "^2.1.0",
"axios": "^1.5.1", "axios": "^1.5.1",
"echarts": "^6.0.0",
"element-plus": "^2.3.14", "element-plus": "^2.3.14",
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-json-viewer": "^3.0.4" "vue-json-viewer": "^3.0.4"
@ -760,6 +761,15 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/echarts": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz",
"integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==",
"dependencies": {
"tslib": "2.3.0",
"zrender": "6.0.0"
}
},
"node_modules/element-plus": { "node_modules/element-plus": {
"version": "2.13.0", "version": "2.13.0",
"resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.13.0.tgz", "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.13.0.tgz",
@ -1177,6 +1187,11 @@
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
}, },
"node_modules/tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
},
"node_modules/vite": { "node_modules/vite": {
"version": "4.5.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz",
@ -1262,6 +1277,14 @@
"peerDependencies": { "peerDependencies": {
"vue": "^3.2.2" "vue": "^3.2.2"
} }
},
"node_modules/zrender": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz",
"integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==",
"dependencies": {
"tslib": "2.3.0"
}
} }
}, },
"dependencies": { "dependencies": {
@ -1719,6 +1742,15 @@
"gopd": "^1.2.0" "gopd": "^1.2.0"
} }
}, },
"echarts": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz",
"integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==",
"requires": {
"tslib": "2.3.0",
"zrender": "6.0.0"
}
},
"element-plus": { "element-plus": {
"version": "2.13.0", "version": "2.13.0",
"resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.13.0.tgz", "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.13.0.tgz",
@ -1999,6 +2031,11 @@
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
}, },
"tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
},
"vite": { "vite": {
"version": "4.5.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz",
@ -2030,6 +2067,14 @@
"requires": { "requires": {
"clipboard": "^2.0.4" "clipboard": "^2.0.4"
} }
},
"zrender": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz",
"integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==",
"requires": {
"tslib": "2.3.0"
}
} }
} }
} }

View File

@ -8,14 +8,15 @@
"build": "vite build" "build": "vite build"
}, },
"dependencies": { "dependencies": {
"vue": "^3.3.4",
"element-plus": "^2.3.14",
"axios": "^1.5.1",
"@element-plus/icons-vue": "^2.1.0", "@element-plus/icons-vue": "^2.1.0",
"axios": "^1.5.1",
"echarts": "^6.0.0",
"element-plus": "^2.3.14",
"vue": "^3.3.4",
"vue-json-viewer": "^3.0.4" "vue-json-viewer": "^3.0.4"
}, },
"devDependencies": { "devDependencies": {
"vite": "4.5.0", "@vitejs/plugin-vue": "4.5.0",
"@vitejs/plugin-vue": "4.5.0" "vite": "4.5.0"
} }
} }

View File

@ -25,7 +25,7 @@
<el-radio-button value="106">106 代理</el-radio-button> <el-radio-button value="106">106 代理</el-radio-button>
<el-radio-button value="82">82 气象站</el-radio-button> <el-radio-button value="82">82 气象站</el-radio-button>
</el-radio-group> </el-radio-group>
<el-input v-model="filters.keyword" placeholder="搜索..." style="width: 200px; margin-left: 15px;" clearable /> <el-input v-model="filters.keyword" placeholder="搜索设备..." style="width: 200px; margin-left: 15px;" clearable />
</div> </div>
<div class="action-section"> <div class="action-section">
<el-checkbox v-model="showHidden" label="显示屏蔽" border style="margin-right: 10px"/> <el-checkbox v-model="showHidden" label="显示屏蔽" border style="margin-right: 10px"/>
@ -38,8 +38,10 @@
<el-table-column prop="source" label="来源" width="100" /> <el-table-column prop="source" label="来源" width="100" />
<el-table-column label="名称" min-width="180"> <el-table-column label="名称" min-width="180">
<template #default="{ row }"> <template #default="{ row }">
<el-link type="primary" underline="hover" @click="showDetails(row)" style="font-weight: bold;">{{ row.name }}</el-link> <el-link type="primary" underline="hover" @click="showDetails(row)" style="font-weight: bold; font-size: 15px;">
<el-tag v-if="isHidden(row.name)" type="info" size="small" style="margin-left:5px">隐藏</el-tag> {{ row.name }}
</el-link>
<el-tag v-if="isHidden(row.name)" type="info" size="small" style="margin-left:5px">已隐藏</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="状态" width="100" align="center"> <el-table-column label="状态" width="100" align="center">
@ -47,7 +49,7 @@
<el-tag :type="getStatusType(row)" effect="dark">{{ getStatusLabel(row) }}</el-tag> <el-tag :type="getStatusType(row)" effect="dark">{{ getStatusLabel(row) }}</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="reason" label="详情"> <el-table-column prop="reason" label="实时详情">
<template #default="{ row }"> <template #default="{ row }">
<span :style="{ color: getStatusColor(row) }">{{ row.reason }}</span> <span :style="{ color: getStatusColor(row) }">{{ row.reason }}</span>
</template> </template>
@ -62,16 +64,33 @@
</el-table> </el-table>
</el-card> </el-card>
<el-drawer v-model="drawerVisible" title="报文详情" size="45%"> <el-drawer v-model="drawerVisible" title="设备数据分析详情" size="80%" @opened="initCharts">
<div v-if="activeDevice" style="padding: 15px"> <div v-if="activeDevice" class="drawer-content">
<el-descriptions :column="1" border> <div class="info-banner">
<el-descriptions-item label="名称">{{ activeDevice.name }}</el-descriptions-item> <el-descriptions :column="3" border size="small">
<el-descriptions-item label="同步日期">{{ activeDevice.latest_time }}</el-descriptions-item> <el-descriptions-item label="设备名称">{{ activeDevice.name }}</el-descriptions-item>
</el-descriptions> <el-descriptions-item label="所属来源">{{ activeDevice.source }}</el-descriptions-item>
<h3 style="margin-top:20px">📦 原始数据</h3> <el-descriptions-item label="最后同步时间">{{ activeDevice.latest_time }}</el-descriptions-item>
<div class="json-box"> </el-descriptions>
<json-viewer v-if="parsedData" :value="parsedData" copyable boxed expand-depth="4" /> </div>
<el-empty v-else description="内容为空" />
<div v-if="is106Site" class="visual-section">
<h3 class="section-title"><el-icon><DataLine /></el-icon> 光谱能量分布分析</h3>
<div v-if="chartModules.length === 0" class="empty-hint">未检测到可解析的光谱数据模块</div>
<div v-for="(module, index) in chartModules" :key="index" class="chart-container">
<div class="chart-header">
<span class="module-tag">{{ module.title }}</span>
<span class="sn-tag">序列号: {{ module.sn }}</span>
</div>
<div :id="'chart-' + index" class="echart-box"></div>
</div>
</div>
<div v-else class="raw-section">
<h3 class="section-title"><el-icon><Document /></el-icon> 原始数据报文</h3>
<div class="json-box">
<json-viewer :value="parsedData" copyable boxed :expand-depth="4" />
</div>
</div> </div>
</div> </div>
</el-drawer> </el-drawer>
@ -79,9 +98,11 @@
</template> </template>
<script setup> <script setup>
import { ref, reactive, computed, onMounted } from 'vue' import { ref, reactive, computed, onMounted, nextTick } from 'vue'
import axios from 'axios' import axios from 'axios'
import * as echarts from 'echarts'
// 基础状态与原有逻辑保持一致
const rawData = ref([]); const isRunning = ref(false); const lastUpdateTime = ref('-') const rawData = ref([]); const isRunning = ref(false); const lastUpdateTime = ref('-')
const selectedRows = ref([]); const showHidden = ref(false); const drawerVisible = ref(false); const activeDevice = ref(null) const selectedRows = ref([]); const showHidden = ref(false); const drawerVisible = ref(false); const activeDevice = ref(null)
const filters = reactive({ site: 'all', keyword: '' }) const filters = reactive({ site: 'all', keyword: '' })
@ -90,9 +111,8 @@ const ignoredList = ref(JSON.parse(localStorage.getItem('hide_list') || '[]'))
const getLevel = (row) => { const getLevel = (row) => {
if (row.reason.includes('离线') || row.reason.includes('失败')) return 3 if (row.reason.includes('离线') || row.reason.includes('失败')) return 3
try { try {
const last = new Date(row.latest_time.split(' ')[0].replace(/_/g, '-')) const last = new Date(row.latest_time.replace(/_/g, '-'))
const diff = (new Date() - last) / (1000 * 3600 * 24) return (new Date() - last) / (1000 * 3600 * 24) > 3 ? 3 : 1
return diff > 3 ? 3 : (diff > 1 ? 2 : 1)
} catch { return 3 } } catch { return 3 }
} }
const getStatusType = (row) => ['','success','warning','danger'][getLevel(row)] const getStatusType = (row) => ['','success','warning','danger'][getLevel(row)]
@ -101,20 +121,134 @@ const getStatusColor = (row) => ['','#67C23A','#E6A23C','#F56C6C'][getLevel(row)
const isHidden = (name) => ignoredList.value.includes(name) const isHidden = (name) => ignoredList.value.includes(name)
const sortedData = computed(() => { const sortedData = computed(() => {
let list = rawData.value.filter(d => { return rawData.value.filter(d => {
const sMatch = filters.site === 'all' || d.source.includes(filters.site) const sMatch = filters.site === 'all' || d.source.includes(filters.site)
const kMatch = d.name.toLowerCase().includes(filters.keyword.toLowerCase()) const kMatch = d.name.toLowerCase().includes(filters.keyword.toLowerCase())
return sMatch && kMatch && (showHidden.value || !isHidden(d.name)) return sMatch && kMatch && (showHidden.value || !isHidden(d.name))
}) }).sort((a, b) => getLevel(b) - getLevel(a))
return list.sort((a, b) => getLevel(b) - getLevel(a))
}) })
const is106Site = computed(() => activeDevice.value?.source.includes('106'))
// 106 数据解析逻辑完善
const chartModules = computed(() => {
if (!is106Site.value || !activeDevice.value?.content) return []
const content = activeDevice.value.content
const modules = []
const blocks = content.match(/(FS\d_Info.*?)(?=FS\d_Info|$)/gs)
if (blocks) {
blocks.forEach(block => {
const snMatch = block.match(/SN,(\d+)/)
const modelMatch = block.match(/Model,([^,]+)/)
const wavelengthMatch = block.match(/Wavelength,([\d\.,]+)/)
if (wavelengthMatch) {
const wavelengths = wavelengthMatch[1].split(',').filter(v => v.trim()).map(Number)
const series = []
const modelName = modelMatch ? modelMatch[1] : 'ISIF'
for (let i = 1; i <= 4; i++) {
const pRegex = new RegExp(`${modelName}_P${i},([\\d\\.,valid]+)`, 'i')
const pMatch = block.match(pRegex)
if (pMatch) {
const values = pMatch[1].split(',').map(v => parseFloat(v)).filter(v => !isNaN(v))
if (values.length > 0) series.push({ name: `通道 P${i}`, data: values })
}
}
if (series.length > 0) {
modules.push({ title: block.split(',')[0], sn: snMatch?.[1] || 'Unknown', xAxis: wavelengths, series })
}
}
})
}
return modules
})
// 图表美化与极简坐标轴
const initCharts = () => {
if (!is106Site.value) return
nextTick(() => {
chartModules.value.forEach((module, index) => {
const chartDom = document.getElementById(`chart-${index}`)
if (!chartDom) return
const myChart = echarts.init(chartDom)
const allValues = module.series.flatMap(s => s.data)
const maxV = Math.max(...allValues); const minV = Math.min(...allValues)
const midV = ((maxV + minV) / 2).toFixed(2)
const waveMax = Math.max(...module.xAxis); const waveMin = Math.min(...module.xAxis)
const waveMid = module.xAxis[Math.floor(module.xAxis.length / 2)]
const colors = [
['#409EFF', 'rgba(64, 158, 255, 0.1)'],
['#67C23A', 'rgba(103, 194, 58, 0.1)'],
['#E6A23C', 'rgba(230, 162, 60, 0.1)'],
['#F56C6C', 'rgba(245, 108, 108, 0.1)']
]
const option = {
backgroundColor: '#fff',
tooltip: { trigger: 'axis', backgroundColor: 'rgba(255, 255, 255, 0.9)', textStyle: { color: '#333' } },
grid: { left: '8%', right: '8%', top: '15%', bottom: '15%', containLabel: false },
xAxis: {
type: 'category',
data: module.xAxis,
axisLine: { lineStyle: { color: '#DCDFE6' } },
axisTick: { show: false },
axisLabel: {
interval: 0,
formatter: (value) => {
const num = Number(value)
if (num === waveMin || num === waveMax || num === waveMid) return num + 'nm'
return ''
},
color: '#909399', fontSize: 11
}
},
yAxis: {
type: 'value',
splitLine: { show: false },
axisLine: { show: true, lineStyle: { color: '#DCDFE6' } },
axisLabel: {
formatter: (value) => {
if (value === 0) return '0'
const v = Number(value).toFixed(2)
if (v == maxV.toFixed(2)) return `Max:${v}`
if (v == minV.toFixed(2)) return `Min:${v}`
if (v == midV) return `Mid:${v}`
return ''
},
color: '#909399', fontSize: 11
}
},
series: module.series.map((s, i) => ({
name: s.name,
type: 'line',
data: s.data,
smooth: true,
showSymbol: false,
lineStyle: { width: 3, color: colors[i % 4][0] },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: colors[i % 4][1] },
{ offset: 1, color: 'transparent' }
])
}
}))
}
myChart.setOption(option)
})
})
}
// 通用方法
const showDetails = (row) => { activeDevice.value = row; drawerVisible.value = true } const showDetails = (row) => { activeDevice.value = row; drawerVisible.value = true }
const parsedData = computed(() => { const parsedData = computed(() => {
if (!activeDevice.value?.content) return null if (!activeDevice.value?.content) return null
try { return JSON.parse(activeDevice.value.content) } catch { return { text: activeDevice.value.content } } try { return JSON.parse(activeDevice.value.content) } catch { return { raw: activeDevice.value.content } }
}) })
const hideSelected = () => { const hideSelected = () => {
ignoredList.value = [...new Set([...ignoredList.value, ...selectedRows.value.map(r => r.name)])] ignoredList.value = [...new Set([...ignoredList.value, ...selectedRows.value.map(r => r.name)])]
localStorage.setItem('hide_list', JSON.stringify(ignoredList.value)) localStorage.setItem('hide_list', JSON.stringify(ignoredList.value))
@ -123,7 +257,6 @@ const restoreDevice = (name) => {
ignoredList.value = ignoredList.value.filter(n => n !== name) ignoredList.value = ignoredList.value.filter(n => n !== name)
localStorage.setItem('hide_list', JSON.stringify(ignoredList.value)) localStorage.setItem('hide_list', JSON.stringify(ignoredList.value))
} }
const fetchLogs = async () => { const fetchLogs = async () => {
const res = await axios.get('/api/logs') const res = await axios.get('/api/logs')
rawData.value = res.data; if (res.data.length) lastUpdateTime.value = res.data[0].check_time rawData.value = res.data; if (res.data.length) lastUpdateTime.value = res.data[0].check_time
@ -131,15 +264,32 @@ const fetchLogs = async () => {
const checkStatus = async () => { const checkStatus = async () => {
const res = await axios.get('/api/status') const res = await axios.get('/api/status')
isRunning.value = res.data.is_running isRunning.value = res.data.is_running
if (isRunning.value) setTimeout(checkStatus, 2000); else fetchLogs() isRunning.value ? setTimeout(checkStatus, 2000) : fetchLogs()
} }
const handleManualRefresh = async () => { await axios.post('/api/run'); isRunning.value = true; checkStatus() } const handleManualRefresh = async () => { await axios.post('/api/run'); isRunning.value = true; checkStatus() }
onMounted(() => { checkStatus(); fetchLogs() }) onMounted(() => { checkStatus(); fetchLogs() })
</script> </script>
<style scoped> <style scoped>
.container { padding: 20px; max-width: 1200px; margin: 0 auto; } .container { padding: 20px; max-width: 1400px; margin: 0 auto; }
.header-row { display: flex; justify-content: space-between; align-items: center; } .header-row { display: flex; justify-content: space-between; align-items: center; }
.toolbar { background: #f9f9f9; padding: 15px; border-radius: 8px; display: flex; justify-content: space-between; margin: 20px 0; border: 1px solid #eee; } .sys-title { margin: 0; font-size: 22px; color: #303133; }
.json-box { border: 1px solid #eee; background: #fafafa; border-radius: 4px; } .sys-status { font-size: 13px; color: #909399; margin-top: 5px; }
.status-running { color: #409EFF; font-weight: bold; }
.toolbar { background: #fff; padding: 15px 20px; border-radius: 8px; display: flex; justify-content: space-between; margin: 20px 0; border: 1px solid #ebeef5; }
.drawer-content { padding: 0 20px 20px; }
.info-banner { margin-bottom: 25px; }
.section-title { border-left: 4px solid #409EFF; padding-left: 10px; margin: 25px 0 15px; font-size: 18px; display: flex; align-items: center; gap: 8px; color: #303133; }
.chart-container { margin-bottom: 30px; border: 1px solid #f0f0f0; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 12px rgba(0,0,0,0.03); }
.chart-header { background: #fafafa; padding: 12px 20px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #f0f0f0; }
.module-tag { font-weight: bold; color: #409EFF; }
.sn-tag { font-size: 12px; color: #909399; background: #eee; padding: 2px 8px; border-radius: 4px; }
.echart-box { width: 100%; height: 450px; }
.empty-hint { text-align: center; padding: 50px; color: #909399; font-style: italic; }
.json-box { border: 1px solid #eee; background: #fafafa; border-radius: 8px; padding: 15px; }
:deep(.el-drawer__header) { margin-bottom: 0; padding: 15px 20px; background: #303133; color: #fff !important; }
</style> </style>