106网站图像展示,未完成版
This commit is contained in:
45
zhandianxinxi/my-vue-app/package-lock.json
generated
45
zhandianxinxi/my-vue-app/package-lock.json
generated
@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"@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"
|
||||
@ -760,6 +761,15 @@
|
||||
"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": {
|
||||
"version": "2.13.0",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz",
|
||||
@ -1262,6 +1277,14 @@
|
||||
"peerDependencies": {
|
||||
"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": {
|
||||
@ -1719,6 +1742,15 @@
|
||||
"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": {
|
||||
"version": "2.13.0",
|
||||
"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",
|
||||
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
|
||||
},
|
||||
"tslib": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
|
||||
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
|
||||
},
|
||||
"vite": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz",
|
||||
@ -2030,6 +2067,14 @@
|
||||
"requires": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,14 +8,15 @@
|
||||
"build": "vite build"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.3.4",
|
||||
"element-plus": "^2.3.14",
|
||||
"axios": "^1.5.1",
|
||||
"@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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "4.5.0",
|
||||
"@vitejs/plugin-vue": "4.5.0"
|
||||
"@vitejs/plugin-vue": "4.5.0",
|
||||
"vite": "4.5.0"
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,7 +25,7 @@
|
||||
<el-radio-button value="106">106 代理</el-radio-button>
|
||||
<el-radio-button value="82">82 气象站</el-radio-button>
|
||||
</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 class="action-section">
|
||||
<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 label="名称" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" underline="hover" @click="showDetails(row)" style="font-weight: bold;">{{ row.name }}</el-link>
|
||||
<el-tag v-if="isHidden(row.name)" type="info" size="small" style="margin-left:5px">隐藏</el-tag>
|
||||
<el-link type="primary" underline="hover" @click="showDetails(row)" style="font-weight: bold; font-size: 15px;">
|
||||
{{ row.name }}
|
||||
</el-link>
|
||||
<el-tag v-if="isHidden(row.name)" type="info" size="small" style="margin-left:5px">已隐藏</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
@ -47,7 +49,7 @@
|
||||
<el-tag :type="getStatusType(row)" effect="dark">{{ getStatusLabel(row) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="reason" label="详情">
|
||||
<el-table-column prop="reason" label="实时详情">
|
||||
<template #default="{ row }">
|
||||
<span :style="{ color: getStatusColor(row) }">{{ row.reason }}</span>
|
||||
</template>
|
||||
@ -62,16 +64,33 @@
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-drawer v-model="drawerVisible" title="报文详情" size="45%">
|
||||
<div v-if="activeDevice" style="padding: 15px">
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="名称">{{ activeDevice.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="同步日期">{{ activeDevice.latest_time }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<h3 style="margin-top:20px">📦 原始数据</h3>
|
||||
<div class="json-box">
|
||||
<json-viewer v-if="parsedData" :value="parsedData" copyable boxed expand-depth="4" />
|
||||
<el-empty v-else description="内容为空" />
|
||||
<el-drawer v-model="drawerVisible" title="设备数据分析详情" size="80%" @opened="initCharts">
|
||||
<div v-if="activeDevice" class="drawer-content">
|
||||
<div class="info-banner">
|
||||
<el-descriptions :column="3" border size="small">
|
||||
<el-descriptions-item label="设备名称">{{ activeDevice.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="所属来源">{{ activeDevice.source }}</el-descriptions-item>
|
||||
<el-descriptions-item label="最后同步时间">{{ activeDevice.latest_time }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</el-drawer>
|
||||
@ -79,9 +98,11 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ref, reactive, computed, onMounted, nextTick } from 'vue'
|
||||
import axios from 'axios'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
// 基础状态与原有逻辑保持一致
|
||||
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 filters = reactive({ site: 'all', keyword: '' })
|
||||
@ -90,9 +111,8 @@ const ignoredList = ref(JSON.parse(localStorage.getItem('hide_list') || '[]'))
|
||||
const getLevel = (row) => {
|
||||
if (row.reason.includes('离线') || row.reason.includes('失败')) return 3
|
||||
try {
|
||||
const last = new Date(row.latest_time.split(' ')[0].replace(/_/g, '-'))
|
||||
const diff = (new Date() - last) / (1000 * 3600 * 24)
|
||||
return diff > 3 ? 3 : (diff > 1 ? 2 : 1)
|
||||
const last = new Date(row.latest_time.replace(/_/g, '-'))
|
||||
return (new Date() - last) / (1000 * 3600 * 24) > 3 ? 3 : 1
|
||||
} catch { return 3 }
|
||||
}
|
||||
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 sortedData = computed(() => {
|
||||
let list = rawData.value.filter(d => {
|
||||
return rawData.value.filter(d => {
|
||||
const sMatch = filters.site === 'all' || d.source.includes(filters.site)
|
||||
const kMatch = d.name.toLowerCase().includes(filters.keyword.toLowerCase())
|
||||
return sMatch && kMatch && (showHidden.value || !isHidden(d.name))
|
||||
})
|
||||
return list.sort((a, b) => getLevel(b) - getLevel(a))
|
||||
}).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 parsedData = computed(() => {
|
||||
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 = () => {
|
||||
ignoredList.value = [...new Set([...ignoredList.value, ...selectedRows.value.map(r => r.name)])]
|
||||
localStorage.setItem('hide_list', JSON.stringify(ignoredList.value))
|
||||
@ -123,7 +257,6 @@ const restoreDevice = (name) => {
|
||||
ignoredList.value = ignoredList.value.filter(n => n !== name)
|
||||
localStorage.setItem('hide_list', JSON.stringify(ignoredList.value))
|
||||
}
|
||||
|
||||
const fetchLogs = async () => {
|
||||
const res = await axios.get('/api/logs')
|
||||
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 res = await axios.get('/api/status')
|
||||
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() }
|
||||
onMounted(() => { checkStatus(); fetchLogs() })
|
||||
</script>
|
||||
|
||||
<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; }
|
||||
.toolbar { background: #f9f9f9; padding: 15px; border-radius: 8px; display: flex; justify-content: space-between; margin: 20px 0; border: 1px solid #eee; }
|
||||
.json-box { border: 1px solid #eee; background: #fafafa; border-radius: 4px; }
|
||||
.sys-title { margin: 0; font-size: 22px; color: #303133; }
|
||||
.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>
|
||||
|
||||
Reference in New Issue
Block a user