106网站图像展示,未完成版
This commit is contained in:
@ -18,6 +18,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<div class="status-summary">
|
||||||
|
<el-tag type="danger">红色:离线/无数据/滞后>7天</el-tag>
|
||||||
|
<el-tag type="warning" style="--el-tag-bg-color: #ff8c00; border-color: #ff8c00; color: #fff;">橘色:滞后2-7天</el-tag>
|
||||||
|
<el-tag type="warning">黄色:滞后1-2天 或 跨天未更新</el-tag>
|
||||||
|
<el-tag type="success">绿色:今日已同步</el-tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<div class="filter-section">
|
<div class="filter-section">
|
||||||
<el-radio-group v-model="filters.site">
|
<el-radio-group v-model="filters.site">
|
||||||
@ -39,23 +46,27 @@
|
|||||||
<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; font-size: 15px;">
|
<el-link type="primary" underline="hover" @click="showDetails(row)" style="font-weight: bold; font-size: 15px;">
|
||||||
{{ row.name }}
|
{{ formatDisplayName(row.name) }}
|
||||||
</el-link>
|
</el-link>
|
||||||
<el-tag v-if="isHidden(row.name)" type="info" size="small" style="margin-left:5px">已隐藏</el-tag>
|
<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="140" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag :type="getStatusType(row)" effect="dark">{{ getStatusLabel(row) }}</el-tag>
|
<el-tag :style="getStatusTagStyle(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), fontWeight: getLevel(row) > 1 ? 'bold' : 'normal' }">
|
||||||
|
{{ formatReason(row) }}
|
||||||
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="offset" label="时效" width="100" />
|
<el-table-column prop="offset" label="时效" width="100" />
|
||||||
<el-table-column prop="latest_time" label="同步日期" width="160" />
|
<el-table-column prop="latest_time" label="同步日期" width="180" />
|
||||||
<el-table-column label="管理" width="80" v-if="showHidden">
|
<el-table-column label="管理" width="80" v-if="showHidden">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button type="primary" link @click="restoreDevice(row.name)">恢复</el-button>
|
<el-button type="primary" link @click="restoreDevice(row.name)">恢复</el-button>
|
||||||
@ -68,30 +79,31 @@
|
|||||||
<div v-if="activeDevice" class="drawer-content">
|
<div v-if="activeDevice" class="drawer-content">
|
||||||
<div class="info-banner">
|
<div class="info-banner">
|
||||||
<el-descriptions :column="3" border size="small">
|
<el-descriptions :column="3" border size="small">
|
||||||
<el-descriptions-item label="设备名称">{{ activeDevice.name }}</el-descriptions-item>
|
<el-descriptions-item label="设备名称">{{ formatDisplayName(activeDevice.name) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="所属来源">{{ activeDevice.source }}</el-descriptions-item>
|
<el-descriptions-item label="所属来源">{{ activeDevice.source }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="最后同步时间">{{ activeDevice.latest_time }}</el-descriptions-item>
|
<el-descriptions-item label="最后同步时间">{{ activeDevice.latest_time }}</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="is106Site" class="visual-section">
|
<div class="visual-section">
|
||||||
<h3 class="section-title"><el-icon><DataLine /></el-icon> 光谱能量分布分析</h3>
|
<h3 class="section-title">
|
||||||
<div v-if="chartModules.length === 0" class="empty-hint">未检测到可解析的光谱数据模块</div>
|
<el-icon><DataLine /></el-icon>
|
||||||
<div v-for="(module, index) in chartModules" :key="index" class="chart-container">
|
{{ is106Site ? '光谱能量分布分析 (自动滤除饱和噪点)' : '气象站光谱数据分析 (Up/Down Spec)' }}
|
||||||
<div class="chart-header">
|
</h3>
|
||||||
<span class="module-tag">{{ module.title }}</span>
|
|
||||||
|
<div v-if="currentChartModules.length === 0" class="empty-hint">
|
||||||
|
{{ isContentEmpty(activeDevice) ? '暂无有效的数据内容' : '未检测到符合格式的光谱数据' }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-for="(module, index) in currentChartModules" :key="index" class="chart-container">
|
||||||
|
<div class="chart-header" v-if="is106Site">
|
||||||
|
<span class="module-tag">型号: {{ module.model }}</span>
|
||||||
<span class="sn-tag">序列号: {{ module.sn }}</span>
|
<span class="sn-tag">序列号: {{ module.sn }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div :id="'chart-' + index" class="echart-box"></div>
|
<div :id="'chart-' + index" class="echart-box" :class="{ 'no-header': !is106Site }"></div>
|
||||||
</div>
|
</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>
|
||||||
</el-drawer>
|
</el-drawer>
|
||||||
</div>
|
</div>
|
||||||
@ -102,22 +114,110 @@ import { ref, reactive, computed, onMounted, nextTick } from 'vue'
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import * as echarts from 'echarts'
|
import * as echarts from 'echarts'
|
||||||
|
|
||||||
// 基础状态与原有逻辑保持一致
|
// 1. 基础响应式变量
|
||||||
const rawData = ref([]); const isRunning = ref(false); const lastUpdateTime = ref('-')
|
const rawData = ref([])
|
||||||
const selectedRows = ref([]); const showHidden = ref(false); const drawerVisible = ref(false); const activeDevice = ref(null)
|
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 filters = reactive({ site: 'all', keyword: '' })
|
||||||
const ignoredList = ref(JSON.parse(localStorage.getItem('hide_list') || '[]'))
|
const ignoredList = ref(JSON.parse(localStorage.getItem('hide_list') || '[]'))
|
||||||
|
|
||||||
const getLevel = (row) => {
|
// --- UI 格式化工具 ---
|
||||||
if (row.reason.includes('离线') || row.reason.includes('失败')) return 3
|
const formatDisplayName = (name) => {
|
||||||
try {
|
if (!name) return ''
|
||||||
const last = new Date(row.latest_time.replace(/_/g, '-'))
|
return name.split('_').map(part => {
|
||||||
return (new Date() - last) / (1000 * 3600 * 24) > 3 ? 3 : 1
|
const lower = part.toLowerCase()
|
||||||
} catch { return 3 }
|
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 isHidden = (name) => ignoredList.value.includes(name)
|
||||||
|
|
||||||
const sortedData = computed(() => {
|
const sortedData = computed(() => {
|
||||||
@ -128,168 +228,211 @@ const sortedData = computed(() => {
|
|||||||
}).sort((a, b) => getLevel(b) - getLevel(a))
|
}).sort((a, b) => getLevel(b) - getLevel(a))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// --- 图表数据解析逻辑 (修正版) ---
|
||||||
|
|
||||||
const is106Site = computed(() => activeDevice.value?.source.includes('106'))
|
const is106Site = computed(() => activeDevice.value?.source.includes('106'))
|
||||||
|
|
||||||
// 106 数据解析逻辑完善
|
// 106 数据解析逻辑:增加对 65534/65535 的饱和值过滤
|
||||||
const chartModules = computed(() => {
|
const chartModules106 = computed(() => {
|
||||||
if (!is106Site.value || !activeDevice.value?.content) return []
|
if (!is106Site.value || isContentEmpty(activeDevice.value)) return []
|
||||||
const content = activeDevice.value.content
|
const content = activeDevice.value.content
|
||||||
const modules = []
|
const modules = []
|
||||||
const blocks = content.match(/(FS\d_Info.*?)(?=FS\d_Info|$)/gs)
|
const infoRegex = /FS\d_Info,Model,([^,]+),SN,([^,]+).*?Wavelength,([\d\.,\s]+)/gs
|
||||||
|
let infoMap = []
|
||||||
if (blocks) {
|
let match
|
||||||
blocks.forEach(block => {
|
while ((match = infoRegex.exec(content)) !== null) {
|
||||||
const snMatch = block.match(/SN,(\d+)/)
|
infoMap.push({
|
||||||
const modelMatch = block.match(/Model,([^,]+)/)
|
model: match[1].trim(),
|
||||||
const wavelengthMatch = block.match(/Wavelength,([\d\.,]+)/)
|
sn: match[2].trim(),
|
||||||
|
wavelengths: match[3].split(',').map(v => v.trim()).filter(v => v && !isNaN(v)).map(Number)
|
||||||
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 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
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
|
return modules
|
||||||
})
|
})
|
||||||
|
|
||||||
// 图表美化与极简坐标轴
|
// 82 数据解析逻辑 (保持逻辑不变)
|
||||||
const initCharts = () => {
|
const chartModules82 = computed(() => {
|
||||||
if (!is106Site.value) return
|
if (is106Site.value || isContentEmpty(activeDevice.value)) return []
|
||||||
nextTick(() => {
|
try {
|
||||||
chartModules.value.forEach((module, index) => {
|
const data = JSON.parse(activeDevice.value.content)
|
||||||
const chartDom = document.getElementById(`chart-${index}`)
|
if (!Array.isArray(data.wavelenth) || !Array.isArray(data.downspec) || !Array.isArray(data.upspec)) {
|
||||||
if (!chartDom) return
|
return []
|
||||||
const myChart = echarts.init(chartDom)
|
}
|
||||||
|
return [{
|
||||||
const allValues = module.series.flatMap(s => s.data)
|
title: formatDisplayName(activeDevice.value.name),
|
||||||
const maxV = Math.max(...allValues); const minV = Math.min(...allValues)
|
xAxis: data.wavelenth,
|
||||||
const midV = ((maxV + minV) / 2).toFixed(2)
|
series: [
|
||||||
|
{ name: 'DownSpec (下行光谱)', data: data.downspec, color: '#409EFF' },
|
||||||
const waveMax = Math.max(...module.xAxis); const waveMin = Math.min(...module.xAxis)
|
{ name: 'UpSpec (上行光谱)', data: data.upspec, color: '#67C23A' }
|
||||||
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)']
|
|
||||||
]
|
]
|
||||||
|
}]
|
||||||
|
} 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 = {
|
const option = {
|
||||||
backgroundColor: '#fff',
|
title: {
|
||||||
tooltip: { trigger: 'axis', backgroundColor: 'rgba(255, 255, 255, 0.9)', textStyle: { color: '#333' } },
|
text: is106Site.value ? `SN: ${module.sn}` : module.title,
|
||||||
grid: { left: '8%', right: '8%', top: '15%', bottom: '15%', containLabel: false },
|
left: 'center',
|
||||||
xAxis: {
|
top: is106Site.value ? 5 : 15,
|
||||||
type: 'category',
|
textStyle: { fontSize: 14, color: '#606266' }
|
||||||
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
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
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: {
|
yAxis: {
|
||||||
type: 'value',
|
type: 'value',
|
||||||
splitLine: { show: false },
|
scale: true,
|
||||||
axisLine: { show: true, lineStyle: { color: '#DCDFE6' } },
|
// 核心点:紧贴 dataMin 和 dataMax
|
||||||
axisLabel: {
|
// 由于上面已经把 65535 变成了 null,这里的 dataMax 将是 20000 左右
|
||||||
formatter: (value) => {
|
min: 'dataMin',
|
||||||
if (value === 0) return '0'
|
max: 'dataMax',
|
||||||
const v = Number(value).toFixed(2)
|
splitLine: { lineStyle: { type: 'dashed' } }
|
||||||
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) => ({
|
series: module.series.map((s, i) => {
|
||||||
name: s.name,
|
const lineColor = s.color || defaultColors[i % 4]
|
||||||
type: 'line',
|
return {
|
||||||
data: s.data,
|
name: s.name,
|
||||||
smooth: true,
|
type: 'line',
|
||||||
showSymbol: false,
|
data: s.data,
|
||||||
lineStyle: { width: 3, color: colors[i % 4][0] },
|
// 连线策略:spanGaps 为 false 表示遇到 null 断开,这样能清楚看到哪里数据过曝了
|
||||||
areaStyle: {
|
// 如果想连起来,可以改为 true
|
||||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
connectNulls: false,
|
||||||
{ offset: 0, color: colors[i % 4][1] },
|
smooth: true,
|
||||||
{ offset: 1, color: 'transparent' }
|
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())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 通用方法
|
// --- API 交互 (保持不变) ---
|
||||||
const showDetails = (row) => { activeDevice.value = row; drawerVisible.value = true }
|
const fetchLogs = async () => {
|
||||||
const parsedData = computed(() => {
|
try {
|
||||||
if (!activeDevice.value?.content) return null
|
const res = await axios.get('/api/logs')
|
||||||
try { return JSON.parse(activeDevice.value.content) } catch { return { raw: activeDevice.value.content } }
|
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 = () => {
|
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))
|
||||||
|
selectedRows.value = []
|
||||||
}
|
}
|
||||||
|
|
||||||
const restoreDevice = (name) => {
|
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 res = await axios.get('/api/logs')
|
onMounted(() => {
|
||||||
rawData.value = res.data; if (res.data.length) lastUpdateTime.value = res.data[0].check_time
|
fetchLogs()
|
||||||
}
|
})
|
||||||
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() })
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.container { padding: 20px; max-width: 1400px; margin: 0 auto; }
|
.container { padding: 20px; max-width: 1400px; margin: 0 auto; background-color: #f5f7fa; min-height: 100vh; }
|
||||||
.header-row { display: flex; justify-content: space-between; align-items: center; }
|
.header-row { display: flex; justify-content: space-between; align-items: center; }
|
||||||
.sys-title { margin: 0; font-size: 22px; color: #303133; }
|
.sys-title { margin: 0; font-size: 22px; color: #303133; font-weight: 600; }
|
||||||
.sys-status { font-size: 13px; color: #909399; margin-top: 5px; }
|
.sys-status { font-size: 13px; color: #909399; margin-top: 5px; }
|
||||||
.status-running { color: #409EFF; font-weight: bold; }
|
.status-running { color: #409EFF; font-weight: bold; display: flex; align-items: center; gap: 5px; }
|
||||||
.toolbar { background: #fff; padding: 15px 20px; border-radius: 8px; display: flex; justify-content: space-between; margin: 20px 0; border: 1px solid #ebeef5; }
|
.status-idle { display: flex; align-items: center; gap: 5px; }
|
||||||
|
.status-summary { margin: 15px 0; display: flex; gap: 10px; flex-wrap: wrap; }
|
||||||
|
.toolbar { background: #fff; padding: 15px 20px; border-radius: 8px; display: flex; justify-content: space-between; margin-bottom: 20px; border: 1px solid #ebeef5; align-items: center; }
|
||||||
.drawer-content { padding: 0 20px 20px; }
|
.drawer-content { padding: 0 20px 20px; }
|
||||||
.info-banner { margin-bottom: 25px; }
|
.info-banner { margin-bottom: 20px; }
|
||||||
.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; }
|
.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 #e4e7ed; border-radius: 8px; overflow: hidden; background: #fff; }
|
||||||
.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: 10px 20px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #e4e7ed; }
|
||||||
.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; font-size: 15px; }
|
||||||
.module-tag { font-weight: bold; color: #409EFF; }
|
.sn-tag { font-size: 12px; color: #606266; background: #ecf5ff; padding: 3px 8px; border-radius: 4px; border: 1px solid #d9ecff; }
|
||||||
.sn-tag { font-size: 12px; color: #909399; background: #eee; padding: 2px 8px; border-radius: 4px; }
|
.echart-box { width: 100%; height: 380px; }
|
||||||
.echart-box { width: 100%; height: 450px; }
|
.echart-box.no-header { margin-top: 15px; }
|
||||||
|
.empty-hint { text-align: center; padding: 50px; color: #909399; font-style: italic; background: #fff; border-radius: 8px; }
|
||||||
.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; }
|
:deep(.el-drawer__header) { margin-bottom: 0; padding: 15px 20px; background: #303133; color: #fff !important; }
|
||||||
|
:deep(.el-drawer__title) { color: #fff; font-weight: bold; }
|
||||||
|
:deep(.el-drawer__close-btn) { color: #fff; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user