-
光谱能量分布分析
-
未检测到可解析的光谱数据模块
-
-
@@ -102,22 +114,110 @@ 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)
+// 1. 基础响应式变量
+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: '' })
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.replace(/_/g, '-'))
- return (new Date() - last) / (1000 * 3600 * 24) > 3 ? 3 : 1
- } catch { return 3 }
+// --- UI 格式化工具 ---
+const formatDisplayName = (name) => {
+ if (!name) return ''
+ return name.split('_').map(part => {
+ const lower = part.toLowerCase()
+ return lower.charAt(0).toUpperCase() + lower.slice(1)
+ }).join('_')
}
-const getStatusType = (row) => ['','success','warning','danger'][getLevel(row)]
-const getStatusLabel = (row) => ['','正常','滞后','异常'][getLevel(row)]
-const getStatusColor = (row) => ['','#67C23A','#E6A23C','#F56C6C'][getLevel(row)]
+
+// --- 核心逻辑 ---
+const isContentEmpty = (row) => {
+ return !row.content ||
+ row.content.toString().trim() === '' ||
+ row.content === '{}' ||
+ row.content === '[]'
+}
+
+const isSameDay = (d1, d2) => {
+ return d1.getFullYear() === d2.getFullYear() &&
+ d1.getMonth() === d2.getMonth() &&
+ d1.getDate() === d2.getDate()
+}
+
+const parseFlexibleDate = (dateStr) => {
+ if (!dateStr) return null
+ try {
+ let cleanStr = dateStr.toString().replace(/[_/]/g, '-')
+ if (cleanStr.includes(' ')) {
+ const parts = cleanStr.split(' ')
+ if (parts[1] && parts[1].includes('-') && !parts[1].includes(':')) {
+ parts[1] = parts[1].replace(/-/g, ':')
+ }
+ cleanStr = parts.join(' ')
+ }
+ const d = new Date(cleanStr)
+ return isNaN(d.getTime()) ? null : d
+ } catch {
+ return null
+ }
+}
+
+const getLevel = (row) => {
+ const errorKeywords = ['离线', '失败', '无法访问', '无数据', '异常', 'Empty']
+ if (row.reason && errorKeywords.some(kw => row.reason.includes(kw))) return 4
+ if (isContentEmpty(row)) return 4
+ const last = parseFlexibleDate(row.latest_time)
+ if (!last) return 4
+ const now = new Date()
+ const diffHours = (now - last) / (1000 * 3600)
+ const diffDays = diffHours / 24
+ if (diffDays > 7) return 4
+ if (diffDays > 2) return 3
+ if (diffHours > 24) return 2
+ if (!isSameDay(now, last)) return 2
+ return 1
+}
+
+const getStatusLabel = (row) => {
+ const level = getLevel(row)
+ if (level === 4) {
+ if (isContentEmpty(row) && (!row.reason || !row.reason.includes('离线'))) return '数据缺失'
+ return '离线/异常'
+ }
+ if (level === 2) {
+ const last = parseFlexibleDate(row.latest_time)
+ const now = new Date()
+ if ((now - last) / (1000 * 3600) <= 24) return '待今日更新(<24h)'
+ return '滞后(1-2d)'
+ }
+ return ['','正常','滞后(1-2d)','滞后(2-7d)'][level]
+}
+
+const getStatusColor = (row) => {
+ const level = getLevel(row)
+ if (level === 1) return '#67C23A'
+ if (level === 2) return '#E6A23C'
+ if (level === 3) return '#ff8c00'
+ return '#F56C6C'
+}
+
+const getStatusTagStyle = (row) => {
+ const color = getStatusColor(row)
+ return { backgroundColor: color, borderColor: color, color: 'white', border: 'none' }
+}
+
+const formatReason = (row) => {
+ if (row.reason) return row.reason
+ const level = getLevel(row)
+ if (level === 4 && isContentEmpty(row)) return '❌ 数据内容为空'
+ if (level === 2 && getStatusLabel(row).includes('待今日更新')) return '⚠️ 数据仍为昨日'
+ return '同步正常'
+}
+
const isHidden = (name) => ignoredList.value.includes(name)
const sortedData = computed(() => {
@@ -128,168 +228,211 @@ const sortedData = computed(() => {
}).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 []
+// 106 数据解析逻辑:增加对 65534/65535 的饱和值过滤
+const chartModules106 = computed(() => {
+ if (!is106Site.value || isContentEmpty(activeDevice.value)) 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 })
- }
- }
+ const infoRegex = /FS\d_Info,Model,([^,]+),SN,([^,]+).*?Wavelength,([\d\.,\s]+)/gs
+ let infoMap = []
+ let match
+ while ((match = infoRegex.exec(content)) !== null) {
+ infoMap.push({
+ model: match[1].trim(),
+ sn: match[2].trim(),
+ wavelengths: match[3].split(',').map(v => v.trim()).filter(v => v && !isNaN(v)).map(Number)
})
}
+ infoMap.forEach(info => {
+ const series = []
+ for (let p = 1; p <= 4; p++) {
+ const dataRegex = new RegExp(`${info.model}_P${p}[^0-9-]*([\\d\\.,\\s-]+)`, 'i')
+ const dataMatch = content.match(dataRegex)
+ if (dataMatch) {
+ const rawStr = dataMatch[1]
+ const vals = rawStr.split(',').map(v => {
+ const num = parseFloat(v)
+ if (isNaN(num)) return null
+ // 【核心修复】如果数值超过 65500 (通常是 65534/65535 饱和值),
+ // 视为 null 无效点。这能解决 20000 的数据被 65534 撑大的问题。
+ return num > 65500 ? null : num
+ })
+
+ // 过滤掉全是 null 的数组,避免报错
+ if (vals.some(v => v !== null)) {
+ series.push({ name: `通道 P${p}`, data: vals })
+ }
+ }
+ }
+ if (series.length > 0) {
+ modules.push({ model: info.model, sn: info.sn, xAxis: info.wavelengths, series: 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)']
+// 82 数据解析逻辑 (保持逻辑不变)
+const chartModules82 = computed(() => {
+ if (is106Site.value || isContentEmpty(activeDevice.value)) return []
+ try {
+ const data = JSON.parse(activeDevice.value.content)
+ if (!Array.isArray(data.wavelenth) || !Array.isArray(data.downspec) || !Array.isArray(data.upspec)) {
+ return []
+ }
+ return [{
+ title: formatDisplayName(activeDevice.value.name),
+ xAxis: data.wavelenth,
+ series: [
+ { name: 'DownSpec (下行光谱)', data: data.downspec, color: '#409EFF' },
+ { name: 'UpSpec (上行光谱)', data: data.upspec, color: '#67C23A' }
]
+ }]
+ } catch (e) {
+ return []
+ }
+})
+
+const currentChartModules = computed(() => {
+ return is106Site.value ? chartModules106.value : chartModules82.value
+})
+
+// --- ECharts 渲染 ---
+const initCharts = () => {
+ nextTick(() => {
+ const modules = currentChartModules.value
+ if (modules.length === 0) return
+
+ modules.forEach((module, index) => {
+ const dom = document.getElementById(`chart-${index}`)
+ if (!dom) return
+ if (echarts.getInstanceByDom(dom)) echarts.getInstanceByDom(dom).dispose()
+
+ const chart = echarts.init(dom)
+ const defaultColors = ['#409EFF', '#67C23A', '#E6A23C', '#F56C6C']
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
- }
+ title: {
+ text: is106Site.value ? `SN: ${module.sn}` : module.title,
+ left: 'center',
+ top: is106Site.value ? 5 : 15,
+ textStyle: { fontSize: 14, color: '#606266' }
},
+ tooltip: { trigger: 'axis' },
+ legend: { top: is106Site.value ? '8%' : '12%' },
+ grid: { left: '3%', right: '4%', bottom: '3%', top: '22%', containLabel: true },
+ xAxis: { type: 'category', boundaryGap: false, data: module.xAxis, axisLabel: { color: '#909399', fontSize: 10 } },
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
- }
+ scale: true,
+ // 核心点:紧贴 dataMin 和 dataMax
+ // 由于上面已经把 65535 变成了 null,这里的 dataMax 将是 20000 左右
+ min: 'dataMin',
+ max: 'dataMax',
+ splitLine: { lineStyle: { type: 'dashed' } }
},
- 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' }
- ])
+ series: module.series.map((s, i) => {
+ const lineColor = s.color || defaultColors[i % 4]
+ return {
+ name: s.name,
+ type: 'line',
+ data: s.data,
+ // 连线策略:spanGaps 为 false 表示遇到 null 断开,这样能清楚看到哪里数据过曝了
+ // 如果想连起来,可以改为 true
+ connectNulls: false,
+ smooth: true,
+ showSymbol: false,
+ lineStyle: { width: 2, color: lineColor },
+ areaStyle: {
+ color: new echarts.graphic.LinearGradient(0,0,0,1,[{offset:0,color:lineColor+'33'},{offset:1,color:'transparent'}])
+ }
}
- }))
+ })
}
- myChart.setOption(option)
+
+ chart.setOption(option)
+ window.addEventListener('resize', () => chart.resize())
})
})
}
-// 通用方法
-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 { raw: activeDevice.value.content } }
-})
+// --- API 交互 (保持不变) ---
+const fetchLogs = async () => {
+ try {
+ const res = await axios.get('/api/logs')
+ rawData.value = res.data
+ if (res.data.length) lastUpdateTime.value = res.data[0].check_time || new Date().toLocaleString()
+ } catch (e) {
+ console.error("获取日志失败", e)
+ }
+}
+
+const checkStatus = async () => {
+ try {
+ const res = await axios.get('/api/status')
+ isRunning.value = res.data.is_running
+ if (isRunning.value) setTimeout(checkStatus, 2000)
+ else fetchLogs()
+ } catch (e) {
+ isRunning.value = false
+ }
+}
+
+const handleManualRefresh = async () => {
+ try {
+ isRunning.value = true
+ await axios.post('/api/run')
+ checkStatus()
+ } catch (e) {
+ isRunning.value = false
+ }
+}
+
+const showDetails = (row) => {
+ activeDevice.value = row
+ drawerVisible.value = true
+}
+
const hideSelected = () => {
ignoredList.value = [...new Set([...ignoredList.value, ...selectedRows.value.map(r => r.name)])]
localStorage.setItem('hide_list', JSON.stringify(ignoredList.value))
+ selectedRows.value = []
}
+
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
-}
-const checkStatus = async () => {
- const res = await axios.get('/api/status')
- isRunning.value = res.data.is_running
- isRunning.value ? setTimeout(checkStatus, 2000) : fetchLogs()
-}
-const handleManualRefresh = async () => { await axios.post('/api/run'); isRunning.value = true; checkStatus() }
-onMounted(() => { checkStatus(); fetchLogs() })
+
+onMounted(() => {
+ fetchLogs()
+})