| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493 |
- <script setup lang="ts">
- import * as echarts from 'echarts'
- import { Odometer, CircleCheckFilled, CircleCloseFilled } from '@element-plus/icons-vue'
- import { rangeShortcuts } from '@/utils/formatTime'
- import dayjs from 'dayjs'
- import quarterOfYear from 'dayjs/plugin/quarterOfYear'
- import { IotDeviceApi, cancelAllRequests } from '@/api/pms/device'
- import { IotStatApi } from '@/api/pms/stat'
- defineOptions({ name: 'TdDeviceDetail' })
- dayjs.extend(quarterOfYear)
- const { params } = useRoute()
- const data = ref({
- deviceCode: params.code || '',
- deviceName: params.name || '',
- lastInlineTime: params.time || '',
- ifInline: params.ifInline || '',
- dept: params.dept || '',
- vehicle: params.vehicle || '',
- carOnline: params.carOnline || ''
- })
- const disabledIdentifier = ref<string[]>(['online', 'vehicle_name', 'touchScreenDataAccumulate'])
- const specs = ref<any[]>([])
- const gatewayspecs = ref<any[]>([])
- const zhbdspecs = ref<any[]>([])
- const selectSpec = ref<Record<string, boolean>>({})
- const chartMap = ref<Record<string, { name: string; value: any[] }>>({})
- const specsLoading = ref(false)
- const lastTsMap = ref<Record<string, number>>({})
- // 每 10s 刷新定时器
- const timer = ref<NodeJS.Timeout | null>(null)
- const defaultDate = rangeShortcuts[0].value()
- const date = ref([
- dayjs(defaultDate[0]).format('YYYY-MM-DD HH:mm:ss'),
- dayjs(defaultDate[1]).format('YYYY-MM-DD HH:mm:ss')
- ])
- const reset = () => {
- cancelAllRequests().then(() => {
- const def = rangeShortcuts[0].value()
- date.value = [
- dayjs(def[0]).format('YYYY-MM-DD HH:mm:ss'),
- dayjs(def[1]).format('YYYY-MM-DD HH:mm:ss')
- ]
- stopAutoFetch()
- if (chart) chart.clear()
- render()
- initLoad()
- })
- }
- const handleDateChange = () => {
- cancelAllRequests().then(() => {
- stopAutoFetch()
- if (chart) chart.clear()
- render()
- initLoad(false)
- })
- }
- const handleClickSpec = (modelName: string) => {
- selectSpec.value[modelName] = !selectSpec.value[modelName]
- chart?.setOption({
- legend: {
- selected: selectSpec.value
- }
- })
- }
- const chartRef = ref<HTMLDivElement | null>(null)
- let chart: echarts.ECharts | null = null
- const chartInit = () => {
- if (!chart) return
- chart.on('legendselectchanged', (params: any) => {
- selectSpec.value = params.selected
- })
- window.addEventListener('resize', () => {
- if (chart) chart.resize()
- })
- }
- // 映射区间相关
- let intervalArr: number[] = []
- let maxInterval = 0
- let minInterval = 0
- // 1. 加载 specs
- const loadSpecs = async () => {
- if (!params.id) return
- specsLoading.value = true
- const res = await IotDeviceApi.getIotDeviceTds(Number(params.id))
- const zhbdres = await IotDeviceApi.getIotDeviceZHBDTds(Number(params.id))
- zhbdspecs.value = zhbdres.sort((a, b) => b.modelOrder - a.modelOrder)
- gatewayspecs.value = res.sort((a, b) => b.modelOrder - a.modelOrder)
- specs.value = [
- ...JSON.parse(JSON.stringify(gatewayspecs.value)),
- ...JSON.parse(JSON.stringify(zhbdspecs.value))
- ]
- selectSpec.value = specs.value.reduce(
- (acc, spec) => {
- acc[spec.modelName] =
- gatewayspecs.value.some((item) => item.modelName === spec.modelName) &&
- !disabledIdentifier.value.includes(spec.identifier)
- return acc
- },
- {} as Record<string, boolean>
- )
- specsLoading.value = false
- chartMap.value = specs.value
- .filter((spec) => !disabledIdentifier.value.includes(spec.identifier))
- .reduce(
- (acc, spec) => {
- acc[spec.identifier] = { name: spec.modelName, value: [] }
- return acc
- },
- {} as Record<string, { name: string; value: any[] }>
- )
- }
- const chartLoading = ref(false)
- const initLoad = async (real_time: boolean = true) => {
- if (!specs.value.length) return
- Object.keys(chartMap.value).forEach((identifier) => {
- chartMap.value[identifier].value = []
- lastTsMap.value[identifier] = 0
- })
- chartLoading.value = true
- for (const identifier of Object.keys(chartMap.value)) {
- const res = await IotStatApi.getDeviceInfoChart(
- data.value.deviceCode,
- identifier,
- date.value[0],
- date.value[1]
- )
- const sorted = res.sort((a, b) => a.ts - b.ts)
- chartMap.value[identifier].value = sorted
- lastTsMap.value[identifier] = sorted.at(-1)?.ts ?? 0
- updateSingleSeries(identifier)
- chartLoading.value = false
- }
- if (real_time) startAutoFetch()
- }
- const startAutoFetch = () => {
- timer.value = setInterval(fetchIncrementData, 10000)
- }
- const stopAutoFetch = () => {
- if (timer.value) clearInterval(timer.value)
- timer.value = null
- }
- const fetchIncrementData = () => {
- for (const identifier of Object.keys(chartMap.value)) {
- const lastTs = lastTsMap.value[identifier]
- if (!lastTs) continue
- IotStatApi.getDeviceInfoChart(
- data.value.deviceCode,
- identifier,
- dayjs(lastTs).format('YYYY-MM-DD HH:mm:ss'),
- dayjs().format('YYYY-MM-DD HH:mm:ss')
- ).then((res) => {
- if (!res.length) return
- const sorted = res.sort((a, b) => a.ts - b.ts)
- // push 到本地
- chartMap.value[identifier].value.push(...sorted)
- // 更新 lastTs
- lastTsMap.value[identifier] = sorted.at(-1).ts
- appendToSeries(identifier, chartMap.value[identifier].value)
- })
- }
- }
- const render = () => {
- if (!chartRef.value) return
- if (!chart) chart = echarts.init(chartRef.value)
- chartInit()
- const values = Object.values(chartMap.value).flatMap((item) => item.value.map((d) => d.value))
- const maxVal = Math.max(...values)
- const minVal = Math.min(...values, -100)
- const maxDigits = (Math.floor(maxVal) + '').length
- const minDigits = (Math.floor(Math.abs(minVal)) + '').length
- const interval = Math.max(maxDigits, minDigits)
- maxInterval = interval
- minInterval = minDigits
- intervalArr = [0]
- for (let i = 1; i <= interval; i++) {
- intervalArr.push(Math.pow(10, i))
- }
- chart.setOption({
- tooltip: {
- trigger: 'axis',
- formatter: (params) => {
- let d = `${params[0].axisValueLabel}<br>`
- let item = params.map((el) => `${el.marker} ${el.seriesName}: ${el.value[2]}<br>`)
- return d + item.join('')
- }
- },
- dataZoom: [
- { type: 'inside', xAxisIndex: 0 },
- { type: 'slider', xAxisIndex: 0 }
- ],
- xAxis: {
- type: 'time',
- axisLabel: {
- formatter: (v) => dayjs(v).format('YYYY-MM-DD HH:mm:ss'),
- rotate: 20
- },
- inverse: true
- },
- yAxis: {
- type: 'value',
- min: -minInterval,
- max: maxInterval,
- interval: 1,
- axisLabel: {
- formatter: (v) => (v === 0 ? '0' : v > 0 ? Math.pow(10, v) : -Math.pow(10, -v))
- }
- },
- legend: {
- data: Object.values(chartMap.value).map((i) => i.name),
- selected: selectSpec.value
- },
- series: Object.keys(chartMap.value).map((identifier) => ({
- name: chartMap.value[identifier].name,
- type: 'line',
- smooth: true,
- showSymbol: false,
- data: [] // 占位数组
- }))
- })
- }
- const updateSingleSeries = (identifier: string) => {
- if (!chart) render()
- if (!chart) return
- const idx = Object.keys(chartMap.value).indexOf(identifier)
- if (idx === -1) return
- const data = chartMap.value[identifier].value.map((v) => mapData(v))
- chart.setOption({
- series: [
- {
- name: chartMap.value[identifier].name,
- data
- }
- ]
- })
- }
- const appendToSeries = (identifier, list) => {
- if (!chart) return
- const idx = Object.keys(chartMap.value).indexOf(identifier)
- if (idx === -1) return
- const data = list.map(mapData)
- chart.setOption({
- series: [
- {
- name: chartMap.value[identifier].name,
- data
- }
- ]
- })
- }
- const mapData = ({ value, ts }) => {
- if (value === 0) return [ts, 0, 0]
- const isPositive = value > 0
- const absItem = Math.abs(value)
- const min_value = Math.max(...intervalArr.filter((v) => v <= absItem))
- const min_index = intervalArr.findIndex((v) => v === min_value)
- const new_value =
- (absItem - min_value) / (intervalArr[min_index + 1] - intervalArr[min_index]) + min_index
- return [ts, isPositive ? new_value : -new_value, value]
- }
- onMounted(async () => {
- await loadSpecs()
- render()
- initLoad()
- })
- onUnmounted(() => {
- stopAutoFetch()
- window.removeEventListener('resize', () => {
- if (chart) chart.resize()
- })
- })
- </script>
- <template>
- <div
- class="w-full bg-gradient-to-r from-blue-100 to-white rounded-lg p-6 shadow"
- id="td-device-info"
- >
- <h2 class="flex items-center gap-2">
- <el-icon class="text-sky!"><Odometer /></el-icon> 设备基础信息
- </h2>
- <el-form size="large" label-position="top" class="mt-4 grid grid-cols-3 gap-4">
- <el-form-item label="资产编码"> {{ data.deviceCode }} </el-form-item>
- <el-form-item label="设备类别"> {{ data.deviceName }} </el-form-item>
- <el-form-item label="所在部门"> {{ data.dept }} </el-form-item>
- <el-form-item label="网关状态" class="online" type="plain">
- <el-tag
- v-if="data.ifInline === '3'"
- type="success"
- size="default"
- class="flex items-center"
- >
- <el-icon class="text-emerald!"><CircleCheckFilled /></el-icon>
- 在线
- </el-tag>
- <el-tag v-if="data.ifInline === '4'" type="danger" size="default" class="flex items-center">
- <el-icon class="text-rose"><CircleCloseFilled /></el-icon>
- 离线
- </el-tag>
- </el-form-item>
- <el-form-item label="中航北斗" class="online" type="plain">
- <el-tag
- v-if="data.carOnline === 'true'"
- type="success"
- size="default"
- class="flex items-center"
- >
- <el-icon class="text-emerald!"><CircleCheckFilled /></el-icon>
- 在线
- </el-tag>
- <el-tag
- v-if="data.carOnline === 'false'"
- type="danger"
- size="default"
- class="flex items-center"
- >
- <el-icon class="text-rose"><CircleCloseFilled /></el-icon>
- 离线
- </el-tag>
- </el-form-item>
- <el-form-item label="最后数据时间"> {{ data.lastInlineTime }} </el-form-item>
- <el-form-item v-if="data.vehicle" label="车牌号码"> {{ data.vehicle }} </el-form-item>
- </el-form>
- </div>
- <div class="mt-4 w-full rounded-lg bg-gradient-to-r from-blue-100 to-white p-4 shadow min-h-50">
- <header class="font-medium text-center w-full">网关数采</header>
- <div
- v-loading="specsLoading"
- element-loading-background="transparent"
- class="w-full mt-4 grid grid-cols-4 gap-4 min-h-30"
- id="dimension"
- >
- <div
- v-for="item in gatewayspecs"
- :key="item.productId"
- class="h-12 bg-white dark:bg-[#1d1e1f] rounded-lg px-6 shadow flex items-center hover:scale-103 transition-all cursor-pointer"
- :class="{
- 'bg-blue-200': selectSpec[item.modelName]
- }"
- @click="!disabledIdentifier.includes(item.identifier) && handleClickSpec(item.modelName)"
- >
- <span class="text-sm text-[var(--el-text-color-regular)]">{{ item.modelName }}</span>
- <span class="text-lg font-medium ms-a">{{ item.value }}</span>
- </div>
- </div>
- </div>
- <div
- v-if="zhbdspecs.length"
- class="mt-4 w-full rounded-lg bg-gradient-to-r from-blue-100 to-white p-4 shadow"
- >
- <header class="font-medium text-center w-full">中航北斗</header>
- <div class="w-full mt-4 grid grid-cols-4 gap-4" id="dimension">
- <div
- v-for="item in zhbdspecs"
- :key="item.productId"
- class="h-12 bg-white dark:bg-[#1d1e1f] rounded-lg px-6 shadow flex items-center hover:scale-103 transition-all cursor-pointer"
- :class="{
- 'bg-blue-200': selectSpec[item.modelName]
- }"
- @click="!disabledIdentifier.includes(item.identifier) && handleClickSpec(item.modelName)"
- >
- <span class="text-sm text-[var(--el-text-color-regular)]">{{ item.modelName }}</span>
- <span class="text-lg font-medium ms-a">{{ item.value }}</span>
- </div>
- </div>
- </div>
- <div class="w-full mt-4 bg-gradient-to-r from-blue-100 to-white min-h-175 rounded-lg p-6 shadow">
- <header class="flex items-center justify-between">
- <h3 class="flex items-center gap-2">
- <div class="i-material-symbols:area-chart-outline-rounded text-sky size-6" text-sky></div>
- 数据趋势
- </h3>
- <div class="flex gap-4">
- <el-button type="primary" size="default" @click="reset">重置</el-button>
- <el-date-picker
- v-model="date"
- value-format="YYYY-MM-DD HH:mm:ss"
- type="datetimerange"
- unlink-panels
- start-placeholder="开始日期"
- end-placeholder="结束日期"
- :shortcuts="rangeShortcuts"
- size="default"
- class="w-100!"
- placement="bottom-end"
- @change="handleDateChange"
- />
- </div>
- </header>
- <div
- v-loading="specsLoading"
- element-loading-background="transparent"
- ref="chartRef"
- class="w-full h-158 mt-4 mb-4"
- ></div>
- </div>
- </template>
- <style lang="scss" scoped>
- :deep(.el-form-item) {
- margin-bottom: 0;
- .el-form-item__label {
- margin-bottom: 0;
- }
- .el-form-item__content {
- font-size: 1rem;
- font-weight: 500;
- }
- &.online {
- .el-form-item__content {
- height: 2.5rem;
- .el-tag__content {
- display: flex;
- align-items: center;
- gap: 2px;
- }
- }
- }
- }
- </style>
|