19 changed files with 1020 additions and 147 deletions
@ -1,7 +1,8 @@
@@ -1,7 +1,8 @@
|
||||
/// <reference types="vite/client" />
|
||||
declare module '*.vue' { |
||||
import { ComponentOptions } from 'vue' |
||||
const componentOptions: ComponentOptions |
||||
export default componentOptions |
||||
import type { DefineComponent } from 'vue' |
||||
const component: DefineComponent<{}, {}, any> |
||||
export default component |
||||
} |
||||
declare module "element-plus/dist/locale/zh-cn.mjs"; |
||||
|
||||
|
@ -0,0 +1,103 @@
@@ -0,0 +1,103 @@
|
||||
import { merge } from "lodash-es"; |
||||
import type { IUseG6Options } from "./useG6"; |
||||
import { VueNode } from "g6-extension-vue"; |
||||
import { GenerateGraphData, MyLineEdge } from "./utils"; |
||||
import { |
||||
ExtensionCategory, |
||||
Graph, |
||||
type GraphOptions, |
||||
register, |
||||
type PluginOptions |
||||
} from "@antv/g6"; |
||||
|
||||
export function useG6<T = any>({ |
||||
tree, |
||||
nodeComponent, |
||||
nodeSize, |
||||
layoutType = 'TB' |
||||
}: IUseG6Options<T>, |
||||
plugins?: PluginOptions |
||||
) { |
||||
|
||||
register(ExtensionCategory.NODE, 'vue-node', VueNode); |
||||
register(ExtensionCategory.EDGE, 'my-line-edge', MyLineEdge); |
||||
|
||||
const container = ref<HTMLElement>() |
||||
const graphData = new GenerateGraphData(tree) |
||||
|
||||
const LR = () => ({ |
||||
layout: { |
||||
type: 'compact-box', |
||||
direction: 'LR', |
||||
getHeight: function getHeight() { |
||||
return 32; |
||||
}, |
||||
getWidth: function getWidth() { |
||||
return 32; |
||||
}, |
||||
getVGap: function getVGap() { |
||||
return 10; |
||||
}, |
||||
getHGap: function getHGap() { |
||||
return 100; |
||||
}, |
||||
}, |
||||
node: { |
||||
style: { |
||||
ports: [{ placement: 'right' }, { placement: 'left' }], |
||||
} |
||||
} |
||||
}) |
||||
|
||||
const TB = () => ({ |
||||
layout: { |
||||
type: 'antv-dagre', |
||||
}, |
||||
node: { |
||||
style: { |
||||
size: nodeSize, |
||||
dx: -nodeSize[0] / 2, |
||||
dy: -nodeSize[1] / 2, |
||||
ports: [{ placement: 'top' }, { placement: 'bottom' }], |
||||
} |
||||
} |
||||
}) |
||||
|
||||
const options: GraphOptions = { |
||||
container: container.value!, |
||||
padding: 50, |
||||
autoFit: 'view', |
||||
node: { |
||||
type: 'vue-node', |
||||
style: { |
||||
component: (data: any) => { |
||||
return h(nodeComponent, { data: data }) |
||||
}, |
||||
}, |
||||
}, |
||||
edge: { |
||||
type: 'my-line-edge', |
||||
style: { |
||||
radius: 10, |
||||
router: { |
||||
type: 'orth', |
||||
}, |
||||
}, |
||||
}, |
||||
data: { |
||||
nodes: graphData.getNodes(), |
||||
edges: graphData.getEdges() |
||||
}, |
||||
plugins, |
||||
behaviors: ['drag-canvas', 'zoom-canvas'], |
||||
} |
||||
|
||||
const mergeOption = merge({}, options, layoutType === 'LR' ? LR() : TB()) |
||||
|
||||
const graph = new Graph(Object.assign(options, mergeOption)); |
||||
|
||||
return { |
||||
container, |
||||
graph |
||||
} |
||||
} |
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
import type { Component } from "vue"; |
||||
|
||||
|
||||
interface IUseG6Options<T> { |
||||
tree: T[], |
||||
nodeComponent: Component, |
||||
nodeSize: [number, number] |
||||
layoutType?: 'LR' | 'TB' |
||||
} |
@ -0,0 +1,49 @@
@@ -0,0 +1,49 @@
|
||||
import type { EdgeData, NodeData } from "@antv/g6"; |
||||
import type { Device, MyNodeData } from "@/types/device"; |
||||
|
||||
export class GenerateGraphData { |
||||
private readonly devices: Device[] = []; |
||||
private nodes: MyNodeData[] = []; |
||||
private edges: EdgeData[] = []; |
||||
|
||||
constructor(devices: Device[]) { |
||||
this.devices = devices; |
||||
this.generateNodeData() |
||||
this.generateEdgeData(); |
||||
} |
||||
|
||||
private generateNodeData() { |
||||
for (const device of this.devices) { |
||||
this.nodes.push({ |
||||
id: String(device.name), |
||||
label: device.name, |
||||
data: Object.assign(device, { |
||||
// typeString: DeviceType[device.type],
|
||||
}), |
||||
}) |
||||
} |
||||
} |
||||
|
||||
private generateEdgeData() { |
||||
for (const node of this.nodes) { |
||||
const customData: Device = node.data |
||||
const findParentNode: Device = this.nodes.some((n: MyNodeData) => n.id === String(customData.parentName)); |
||||
|
||||
if (findParentNode) { |
||||
this.edges.push({ |
||||
source: String(customData.parentName), |
||||
target: String(node.id), |
||||
data: customData, |
||||
}) |
||||
} |
||||
} |
||||
} |
||||
|
||||
getNodes(): NodeData[] { |
||||
return this.nodes as NodeData[]; |
||||
} |
||||
|
||||
getEdges(): EdgeData[] { |
||||
return this.edges; |
||||
} |
||||
} |
@ -0,0 +1,64 @@
@@ -0,0 +1,64 @@
|
||||
import { Polyline } from '@antv/g6'; |
||||
import { Circle } from '@antv/g'; |
||||
import { subStyleProps } from '@antv/g6'; |
||||
import type { Device } from "@/types/device"; |
||||
|
||||
export class MyLineEdge extends Polyline { |
||||
getMarkerStyle(attributes: object) { |
||||
return { |
||||
r: 4, |
||||
fill: '#58C448', |
||||
offsetPath: this.shapeMap.key, ...subStyleProps(attributes, 'marker') |
||||
}; |
||||
} |
||||
|
||||
onCreate() { |
||||
const marker = this.upsert('marker', Circle, this.getMarkerStyle(this.attributes), this)!; |
||||
|
||||
const prev = this.context.model.getRelatedEdgesData(this.sourceNode.id) // 获取源节点的相关边数据
|
||||
.find((edge) => edge.target === this.targetNode.id) as Device; |
||||
|
||||
|
||||
const depth = prev!.data!.depth; |
||||
const delay = depth * 3000; // 每级延迟 3 秒
|
||||
marker.animate( |
||||
[{ offsetDistance: 0 }, { offsetDistance: 1 }], |
||||
{ |
||||
duration: 3000, |
||||
iterations: Infinity, |
||||
delay, |
||||
} |
||||
); |
||||
|
||||
// 涟漪效果:在 marker 外套一个圆
|
||||
const ripple = this.upsert( |
||||
'ripple', |
||||
Circle, |
||||
{ |
||||
r: 4, // 初始半径和 marker 一样
|
||||
stroke: '#58C448', |
||||
lineWidth: 2, |
||||
fill: 'none', |
||||
offsetPath: this.shapeMap.key, |
||||
}, |
||||
this |
||||
)!; |
||||
|
||||
// 涟漪动画:半径变大 + 透明度变小
|
||||
ripple.animate( |
||||
[ |
||||
{ offsetDistance: 0, r: 4, opacity: 0.7 }, |
||||
{ offsetDistance: 1, r: 6, opacity: 0 }, |
||||
], |
||||
{ |
||||
duration: 3000, |
||||
iterations: Infinity, |
||||
delay, |
||||
} |
||||
); |
||||
// marker.animate([{ offsetDistance: 0 }, { offsetDistance: 1 }], {
|
||||
// duration: 3000,
|
||||
// iterations: Infinity,
|
||||
// });
|
||||
} |
||||
} |
@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
|
||||
export * from './GenerateGraphData' |
||||
export * from './MyLineEdge' |
||||
|
||||
|
@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
|
||||
import type { NodeData } from "@antv/g6"; |
||||
export type Device = any |
||||
export interface MyNodeData extends Omit<NodeData, 'data'> { |
||||
data?: Device; |
||||
} |
@ -1,8 +1,13 @@
@@ -1,8 +1,13 @@
|
||||
<template> |
||||
<div class="device-data-chart"> |
||||
<v-chart class="chart" :option="chartOption" :autoresize="autoresize" |
||||
<v-chart |
||||
class="chart" |
||||
ref="chartRef" |
||||
:option="chartOption" |
||||
:autoresize="autoresize" |
||||
:loading-options="loadingOpt" |
||||
:loading="loading" ref="chartRef" @legendselectchanged="changeLegend"/> |
||||
:loading="loading" |
||||
@legendselectchanged="changeLegend"/> |
||||
</div> |
||||
</template> |
||||
|
@ -0,0 +1,87 @@
@@ -0,0 +1,87 @@
|
||||
<template> |
||||
<div class="w-340 h-full" v-loading="loadingPoints" element-loading-text="数据加载中..."> |
||||
<template v-if="!pointList.length"> |
||||
<el-empty></el-empty> |
||||
</template> |
||||
<el-scrollbar class="scroll" v-else> |
||||
<el-checkbox-group v-model="checkPointList" class="point-checks" @change="changePoints"> |
||||
<template v-for="(item, index) in pointList" :key="index"> |
||||
<el-checkbox :label="item.label" :value="item"/> |
||||
</template> |
||||
</el-checkbox-group> |
||||
</el-scrollbar> |
||||
</div> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
import type { IMyPoint } from "@/views/stationData/type"; |
||||
|
||||
const emit = defineEmits<{ |
||||
'onchange-points': [IMyPoint[]] |
||||
}>() |
||||
|
||||
interface Props { |
||||
pointList: Array<{ |
||||
label: string |
||||
addr: string |
||||
}> |
||||
loadingPoints: boolean |
||||
} |
||||
|
||||
const props = withDefaults(defineProps<Props>(), { |
||||
pointList: () => [] |
||||
}) |
||||
|
||||
const checkPointList = ref<any>([]) |
||||
|
||||
function changePoints() { |
||||
emit('onchange-points', checkPointList.value) |
||||
} |
||||
|
||||
function clearCheck() { |
||||
checkPointList.value = [] |
||||
} |
||||
|
||||
function getCheckList() { |
||||
return checkPointList.value |
||||
} |
||||
|
||||
defineExpose({ |
||||
clearCheck, |
||||
getCheckList |
||||
}) |
||||
|
||||
</script> |
||||
|
||||
|
||||
<style lang="scss"> |
||||
.point-checks { |
||||
display: flex; |
||||
flex-direction: column; |
||||
align-items: start; |
||||
|
||||
|
||||
:deep(.el-checkbox__inner) { |
||||
width: 20px; |
||||
height: 20px; |
||||
} |
||||
|
||||
:deep(.el-checkbox__inner::after) { |
||||
width: 4px; |
||||
height: 8px; |
||||
left: 50%; |
||||
top: 50%; |
||||
transform: translate(-50%, -50%) rotate(45deg); |
||||
border-width: 3px; |
||||
// border: 1px solid #ffffff; |
||||
} |
||||
} |
||||
</style> |
||||
|
||||
<style lang="scss" scoped> |
||||
|
||||
.scroll { |
||||
height: 100%; |
||||
padding: 10px; |
||||
} |
||||
</style> |
@ -0,0 +1,114 @@
@@ -0,0 +1,114 @@
|
||||
<template> |
||||
<div :style="{ |
||||
width: NODE_SIZE[0] + 'px', |
||||
height: NODE_SIZE[1] + 'px', |
||||
borderRadius: '6px', |
||||
marginTop: '6px', |
||||
}" class="box-border rounded-md overflow-hidden flex"> |
||||
<div |
||||
class="p-4px" |
||||
:style="{ |
||||
background: Device?.bgColor || '#000', |
||||
width: NODE_SIZE[1] + 'px', |
||||
height: NODE_SIZE[1] + 'px'}"> |
||||
<img :src="Device.icon" style="height: 100%;" alt=""> |
||||
</div> |
||||
<div class="bg-white flex-1 flex items-center justify-center overflow-hidden"> |
||||
<span class="text-12px overflow-hidden text-ellipsis whitespace-nowrap">{{ Device.name }}</span> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
import type { MyNodeData } from "@/types/device"; |
||||
import { DeviceType, NODE_SIZE } from "../utils/index"; |
||||
import EmsIcon from '@/assets/images/device/ems.png'; |
||||
import EcuIcon from '@/assets/images/device/ecu.png'; |
||||
import EmIcon from '@/assets/images/device/em.png'; |
||||
import EmMeasureIcon from '@/assets/images/device/em.png'; |
||||
import BmsIcon from '@/assets/images/device/bms.png'; |
||||
import CacIcon from '@/assets/images/device/cac.png'; |
||||
import DgsIcon from '@/assets/images/device/dgs.png'; |
||||
import FfsIcon from '@/assets/images/device/ffs.png'; |
||||
import MpptIcon from '@/assets/images/device/mppt.png'; |
||||
import PcsIcon from '@/assets/images/device/pcs.png'; |
||||
import ThsIcon from '@/assets/images/device/ths.png'; |
||||
import TmsIcon from '@/assets/images/device/tms.png'; |
||||
import UnitIcon from '@/assets/images/device/unit.png'; |
||||
import WppIcon from '@/assets/images/device/wpp.png'; |
||||
import { computed } from "vue"; |
||||
|
||||
const typeToIconMap: Record<string, Object> = { |
||||
[DeviceType.emu]: { |
||||
icon: EmsIcon, |
||||
bgColor: '#0769FF' |
||||
}, |
||||
[DeviceType.mce]: { |
||||
icon: EcuIcon, |
||||
bgColor: '#2CA02C' |
||||
}, |
||||
[DeviceType.bms]: { |
||||
icon: BmsIcon, |
||||
bgColor: '#FF7F50' |
||||
}, |
||||
[DeviceType.bms_stack]: { |
||||
icon: BmsIcon, |
||||
bgColor: '#9467BD' |
||||
}, |
||||
[DeviceType.bms_cluster]: { |
||||
icon: BmsIcon, |
||||
bgColor: '#9467BD' |
||||
}, |
||||
[DeviceType.bms_cell]: { |
||||
icon: BmsIcon, |
||||
bgColor: '#9467BD' |
||||
}, |
||||
[DeviceType.pcs]: { |
||||
icon: PcsIcon, |
||||
bgColor: '#17BECF' |
||||
}, |
||||
// [DeviceType.Em]: { |
||||
// icon: EmIcon, |
||||
// bgColor: '#2CA02C' |
||||
// }, |
||||
// [DeviceType.Cac]: { |
||||
// icon: CacIcon, |
||||
// bgColor: '#17BECF' |
||||
// }, |
||||
// [DeviceType.Dgs]: { |
||||
// icon: DgsIcon, |
||||
// bgColor: '#9467BD' |
||||
// }, |
||||
// [DeviceType.Ffs]: { |
||||
// icon: FfsIcon, |
||||
// bgColor: '#17BECF' |
||||
// }, |
||||
// [DeviceType.Mppt]: { |
||||
// icon: MpptIcon, |
||||
// bgColor: '#9467BD' |
||||
// }, |
||||
|
||||
// [DeviceType.Wpp]: { |
||||
// icon: WppIcon, |
||||
// bgColor: '#9467BD' |
||||
// }, |
||||
// [DeviceType.Tms]: { |
||||
// icon: TmsIcon, |
||||
// bgColor: '#17BECF' |
||||
// }, |
||||
} |
||||
|
||||
const props = defineProps<{ |
||||
data: MyNodeData; |
||||
}>() |
||||
|
||||
const Device = computed(() => { |
||||
const style = typeToIconMap[props.data.data.type?.toLowerCase()]; |
||||
return Object.assign(props.data.data, style); |
||||
}) |
||||
|
||||
</script> |
||||
|
||||
<style scoped lang="scss"> |
||||
|
||||
</style> |
@ -0,0 +1,336 @@
@@ -0,0 +1,336 @@
|
||||
<template> |
||||
<div class="fault-rule-drawer"> |
||||
<el-drawer |
||||
v-model="isShowDrawer" |
||||
:title="title" |
||||
direction="rtl" |
||||
size="90%" |
||||
modal-class="model-dev-opn" |
||||
:before-close="handleBeforeClose" |
||||
@opened="onDrawerOpened"> |
||||
<main class="wh-full flex"> |
||||
<EdfsWrap title="选择" class="p-r-4 h-full w-340 border-r-1 border-solid border-r-#e4e7ed"> |
||||
<PointCheckbox |
||||
:pointList="pointList" |
||||
:loadingPoints="loadingPoints" |
||||
@onchange-points="onchangePoints" |
||||
ref="PointCheckboxRef" |
||||
> |
||||
</PointCheckbox> |
||||
</EdfsWrap> |
||||
<div class="flex-1 p-4 h-full overflow-hidden relative" |
||||
> |
||||
<div class="absolute w-full h-full bg-[#ffffffe5] z-99" v-if="loadingChart"> |
||||
<div class="w-full h-full flex flex-col justify-center items-center"> |
||||
<el-progress |
||||
style="width: 70%" |
||||
:text-inside="true" |
||||
:stroke-width="18" |
||||
:percentage="progress" |
||||
status="success" |
||||
/> |
||||
<div class="text-xl"> |
||||
<div>当前加载数据量较大请稍等。。。</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<el-button class="mb-4" type="primary" @click="loadChardData">查询数据</el-button> |
||||
<NewDataChart |
||||
v-if="isShowChart" |
||||
:chart-datas="chartData" |
||||
:legends="legends" |
||||
:axis-data="Array.from(axisData)" |
||||
ref="chartRef"/> |
||||
</div> |
||||
</main> |
||||
</el-drawer> |
||||
</div> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
import NewDataChart from '../../component/newDataChart.vue' |
||||
import type { IDevice, IMyPoint, IOfflineDevice, IOnlineDevice } from '../../type' |
||||
import { |
||||
getDeviceDetails, |
||||
getPoints, |
||||
type IGetDeviceDataParams, |
||||
type IPointGroupOV, |
||||
type IPointsParams, |
||||
} from '@/api/module/transfer' |
||||
import { useMessage } from '@/composables/useMessage' |
||||
import dayjs from 'dayjs' |
||||
import { nextTick } from "vue"; |
||||
import EdfsWrap from "@/components/Edfs-wrap.vue"; |
||||
import PointCheckbox from "@/views/stationData/component/pointCheckbox.vue"; |
||||
|
||||
const env = import.meta.env |
||||
const isShowDrawer = defineModel<boolean>() |
||||
const title = computed(() => (curDevice.value?.isonLine ? '数据详情' : `已迁移数据详情`)) |
||||
const message = useMessage() |
||||
const PointCheckboxRef = ref<InstanceType<typeof PointCheckbox>>() |
||||
|
||||
const emit = defineEmits(['on-save']) |
||||
const pointList = ref<IMyPoint[]>([]) |
||||
const curDevice = ref<IDevice & { isonLine: boolean, siteName: string }>() |
||||
|
||||
async function open(device: IDevice & { |
||||
isonLine: boolean, |
||||
siteName: string |
||||
}, group: IPointGroupOV) { |
||||
curDevice.value = device |
||||
currentGroup.value = group |
||||
PointCheckboxRef.value?.clearCheck() |
||||
await loadPoints() |
||||
.then(async () => { |
||||
await loadDeviceDetails() |
||||
}) |
||||
.catch(() => { |
||||
message.error('获取点位数据失败') |
||||
fullscreenLoading.value?.close() |
||||
}) |
||||
} |
||||
|
||||
|
||||
const loadingPoints = ref(false) |
||||
const columParams = computed(() => ['ts', ...checkPointList.value.map(i => i.addr)]) |
||||
|
||||
const currentGroup = ref<IPointGroupOV>() |
||||
|
||||
async function loadPoints() { |
||||
if (!currentGroup.value) { |
||||
message.error('请先选择点位组') |
||||
return |
||||
} |
||||
const params: IPointsParams = { |
||||
type: currentGroup.value.type, |
||||
} |
||||
if (curDevice.value?.isonLine) { |
||||
const onlineDevice = curDevice.value as IOnlineDevice |
||||
params.isLocal = false |
||||
params.host = onlineDevice.clientIp |
||||
} else { |
||||
const offlineDevice = curDevice.value as IOfflineDevice |
||||
params.sn = offlineDevice.sn |
||||
params.site = curDevice.value!.siteName |
||||
params.isLocal = true |
||||
} |
||||
loadingPoints.value = true |
||||
const res = await getPoints(params) |
||||
if (res.code === 0) { |
||||
const data = Array.isArray(res?.data) ? res.data : [] |
||||
pointList.value = data.map((i: any) => ({ |
||||
label: i.cnName, |
||||
addr: i.addr, |
||||
unit: i.unit || '', |
||||
})) |
||||
} |
||||
loadingPoints.value = false |
||||
return res |
||||
} |
||||
|
||||
async function loadDeviceDetails() { |
||||
if (!fullscreenLoading.value) { |
||||
openFullScreen() |
||||
} |
||||
chartData.clear() |
||||
axisData.clear() |
||||
legends.value = [] |
||||
const pointsRes = await loadPoints() |
||||
if (!pointsRes || pointsRes.code !== 0) { |
||||
message.error('获取点位数据失败') |
||||
pointList.value = [] |
||||
chartData.clear() |
||||
axisData.clear() |
||||
fullscreenLoading.value?.close() |
||||
return |
||||
} |
||||
isShowDrawer.value = true |
||||
fullscreenLoading.value?.close() |
||||
} |
||||
|
||||
const chartData = reactive(new Map<string, any[]>()) |
||||
const axisData = new Set<string>() |
||||
const legends = ref<{ addr: string; label: string, unit: string }[]>([]) |
||||
|
||||
const loadingChart = ref(false) |
||||
const chartAllTotal = ref(0) |
||||
const chartLimit = ref(1000) |
||||
const chartOffset = ref(0) |
||||
const progress = ref(0) |
||||
|
||||
async function loadChardData() { |
||||
if (!currentGroup.value) return |
||||
if (!columParams.value.filter(i => i !== 'ts').length) { |
||||
message.error('请选择点位') |
||||
return |
||||
} |
||||
clearData() |
||||
const limit = chartLimit.value |
||||
const offset = chartOffset.value |
||||
const options = { |
||||
columns: columParams.value, |
||||
isLocal: !curDevice.value?.isonLine, |
||||
host: curDevice.value?.isonLine ? (curDevice.value as IOnlineDevice).clientIp : '', |
||||
name: currentGroup.value.name as string, |
||||
} |
||||
|
||||
const params: IGetDeviceDataParams = { |
||||
...options, |
||||
limit, |
||||
offset |
||||
} |
||||
|
||||
if (env.VITE_APP_ENV !== 'local' || !curDevice.value?.isonLine) { |
||||
params.site_id = curDevice.value!.siteName || '' |
||||
params.device_id = curDevice.value?.sn || '' |
||||
} |
||||
loadingChart.value = true |
||||
isShowChart.value = false |
||||
|
||||
const res = await getDeviceDetails(params) |
||||
|
||||
if (res.code !== 0) { |
||||
resetChartStatus() |
||||
message.error('获取设备数据失败') |
||||
return |
||||
} |
||||
|
||||
chartAllTotal.value = res?.data?.total ?? 0 |
||||
if (chartAllTotal.value === 0) { |
||||
resetChartStatus() |
||||
message.info('暂无数据') |
||||
return |
||||
} |
||||
|
||||
const pointData = Array.isArray(res.data.results) ? res.data.results : [] |
||||
setChartData(pointData) |
||||
const pageCount = Math.ceil(chartAllTotal.value / Number(limit)) |
||||
if (pageCount <= 1) { |
||||
progress.value = 100 |
||||
setTimeout(() => { |
||||
resetChartStatus() |
||||
}) |
||||
return |
||||
} |
||||
|
||||
for (let i = 1; i < pageCount; i++) { |
||||
const params: IGetDeviceDataParams = { |
||||
...options, |
||||
limit, |
||||
offset: i * Number(limit) |
||||
} |
||||
const res = await getDeviceDetails(params) |
||||
if (res.code !== 0) { |
||||
clearData() |
||||
resetChartStatus() |
||||
message.error('获取设备数据失败') |
||||
return |
||||
} |
||||
const pointData = Array.isArray(res.data.results) ? res.data.results : [] |
||||
setChartData(pointData) |
||||
progress.value = Math.min(100, Math.floor(((i + 1) / pageCount) * 100)) |
||||
} |
||||
|
||||
resetChartStatus() |
||||
} |
||||
|
||||
|
||||
function setChartData(pointData: any[]) { |
||||
for (const addr of columParams.value.filter(i => i !== 'ts')) { |
||||
const label = pointList.value.find(i => i.addr === addr)?.label || addr |
||||
const find = legends.value.find(i => i.addr === addr) |
||||
const unit = pointList.value.find(i => i.addr === addr)?.unit || '' |
||||
if (!find) { |
||||
legends.value.push({ addr, label, unit }) |
||||
} |
||||
} |
||||
pointData.forEach((data: any[]) => { |
||||
const [ts, val, addr] = data |
||||
if (checkPointList.value.some(i => i.addr === addr)) { |
||||
const time = dayjs(Number(ts)).format('YYYY-MM-DD HH:mm:ss') |
||||
if (addr) { |
||||
const colData = chartData.get(addr) |
||||
if (colData) { |
||||
colData.push([time, val]) |
||||
} else { |
||||
chartData.set(addr, [[time, val]]) |
||||
} |
||||
} |
||||
|
||||
if (ts) { |
||||
axisData.add(time) |
||||
} |
||||
} |
||||
}) |
||||
|
||||
} |
||||
|
||||
|
||||
function handleBeforeClose(done: () => void) { |
||||
isShowDrawer.value = false |
||||
clearData() |
||||
fullscreenLoading.value = null |
||||
done() |
||||
} |
||||
|
||||
function clearData() { |
||||
chartLimit.value = 1000 |
||||
chartOffset.value = 0 |
||||
chartAllTotal.value = 0 |
||||
progress.value = 0 |
||||
legends.value = [] |
||||
chartData.clear() |
||||
axisData.clear() |
||||
} |
||||
|
||||
function resetChartStatus() { |
||||
loadingChart.value = false |
||||
nextTick(() => { |
||||
isShowChart.value = true |
||||
}) |
||||
} |
||||
|
||||
const chartRef = ref<InstanceType<typeof NewDataChart>>() |
||||
const isShowChart = ref(false) |
||||
|
||||
function onDrawerOpened() { |
||||
nextTick(() => { |
||||
isShowChart.value = true |
||||
}) |
||||
} |
||||
|
||||
const fullscreenLoading = ref<any>(null) |
||||
const openFullScreen = () => { |
||||
fullscreenLoading.value = ElLoading.service({ |
||||
lock: true, |
||||
text: '数据加载中,请稍后...', |
||||
background: 'rgba(255, 255, 255, 0.8)', |
||||
}) |
||||
} |
||||
|
||||
const checkPointList = ref<IMyPoint[]>([]) |
||||
|
||||
function onchangePoints(checkPoints: IMyPoint[]) { |
||||
checkPointList.value = [] |
||||
checkPointList.value = checkPoints |
||||
} |
||||
|
||||
defineExpose({ |
||||
open, |
||||
openFullScreen, |
||||
}) |
||||
</script> |
||||
|
||||
<style scoped lang="scss"> |
||||
.fault-rule-drawer { |
||||
font-size: 16px; |
||||
|
||||
:deep(.edfs-wrap) { |
||||
width: auto; |
||||
} |
||||
|
||||
:deep(.el-drawer__header) { |
||||
color: var(--text-color); |
||||
} |
||||
} |
||||
</style> |
@ -0,0 +1,145 @@
@@ -0,0 +1,145 @@
|
||||
<template> |
||||
<div class="relative h-full w-full"> |
||||
<el-button type="primary" @click="onBack" class="absolute top-0 z-99"> |
||||
<i class="i-line-md:arrow-left"></i>返回站点数据 |
||||
</el-button> |
||||
<el-empty v-if="!topologyTree && !loading" class="w-full h-full"></el-empty> |
||||
<div |
||||
id="container" |
||||
class="w-full h-full" |
||||
v-loading="loading" |
||||
element-loading-text="设备加载中请稍等..." |
||||
style="width: 100%;height: 100%"> |
||||
</div> |
||||
</div> |
||||
|
||||
<detail-drawer ref="detailDrawerRef"/> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
import { NODE_SIZE } from "./utils"; |
||||
import { useG6 } from "@/hooks/useG6"; |
||||
import Node from './components/Node.vue' |
||||
import { flattenTree } from "@/views/testG6/utils"; |
||||
import { type PluginOptions } from "@antv/g6" |
||||
import type { IDevice, IOfflineDevice, IOnlineDevice } from "@/views/stationData/type"; |
||||
import { getPointGroup, type IPointGroupParams, type IPointGroupOV } from "@/api/module/transfer"; |
||||
import DetailDrawer from "./components/detailDrawer.vue"; |
||||
|
||||
const router = useRouter() |
||||
|
||||
const route = useRoute() |
||||
const deviceInfo = route.query?.deviceInfo |
||||
? JSON.parse(route.query?.deviceInfo as string) |
||||
: {} as unknown as IDevice & { isonLine: boolean, siteName: string } |
||||
|
||||
const detailDrawerRef = ref<InstanceType<typeof DetailDrawer>>() |
||||
const topologyTree = ref<IPointGroupOV>() |
||||
const loading = ref(true) |
||||
|
||||
async function loadDeviceTopology() { |
||||
const params: IPointGroupParams = {} |
||||
if (deviceInfo.isonLine) { |
||||
const onlineDevice = deviceInfo as IOnlineDevice |
||||
params.isLocal = false |
||||
params.host = onlineDevice.clientIp |
||||
} else { |
||||
const offlineDevice = deviceInfo as unknown as IOfflineDevice & { siteName: string } |
||||
params.sn = offlineDevice.sn |
||||
params.site = offlineDevice!.siteName |
||||
params.isLocal = true |
||||
} |
||||
|
||||
const res = await getPointGroup(params) |
||||
if (res.code === 0) { |
||||
topologyTree.value = { |
||||
id: deviceInfo.sn, |
||||
ip: deviceInfo.ip, |
||||
name: deviceInfo?.sn, |
||||
port: deviceInfo.port, |
||||
slave_addr: 'root', |
||||
type: deviceInfo?.type ?? 'emu', |
||||
children: [] |
||||
} |
||||
topologyTree.value.children = Array.isArray(res?.data) ? res.data : [] |
||||
} |
||||
} |
||||
|
||||
const canvas = ref<HTMLElement | undefined>(undefined) |
||||
|
||||
const plugins: PluginOptions = [ |
||||
{ |
||||
type: 'contextmenu', |
||||
enable: (e: any) => e.targetType === 'node', |
||||
getItems: () => { |
||||
return [{ name: '查看详情', value: 'detail' }]; |
||||
}, |
||||
onClick: onDetail, |
||||
}, |
||||
{ |
||||
type: 'tooltip', |
||||
enable: (e: any) => e.targetType === 'node', |
||||
getContent: (e: any, items: any) => { |
||||
console.log(items) |
||||
return `<div>节点: ${items[0].data.name}</div>`; |
||||
}, |
||||
}, |
||||
] |
||||
|
||||
function init() { |
||||
if (!topologyTree.value) return |
||||
const filteredTree = filterTree(topologyTree.value) |
||||
const device = flattenTree([filteredTree]) |
||||
|
||||
const { container, graph } = useG6({ |
||||
tree: device, |
||||
nodeComponent: Node, |
||||
nodeSize: NODE_SIZE |
||||
}, plugins) |
||||
canvas.value = container.value |
||||
graph.render(); |
||||
} |
||||
|
||||
function onDetail(value: string, item: HTMLElement, current: any) { |
||||
const nodeData = current.config.context.model.getNodeData(current.config.id) |
||||
const device = deviceInfo as unknown as IDevice & { |
||||
isonLine: boolean |
||||
siteName: string |
||||
} |
||||
detailDrawerRef.value?.open(device, nodeData[nodeData.length - 1].data) |
||||
} |
||||
|
||||
onMounted(async () => { |
||||
await loadDeviceTopology() |
||||
loading.value = false |
||||
init() |
||||
}) |
||||
|
||||
function filterTree(node: IPointGroupOV) { |
||||
if (node.type === 'bms_cluster' && Array.isArray(node.children)) { |
||||
if (node.children.length > 2) { |
||||
const customNode = node.children[node.children.length - 1] as any |
||||
customNode.name = `${node.children[0].name} ~ ${node.children[node.children.length - 2].name}` |
||||
customNode.customNode = true |
||||
node.children = [customNode] |
||||
} |
||||
} |
||||
|
||||
if (Array.isArray(node.children)) { |
||||
node!.children = node.children.map((child: any) => { |
||||
child.parentName = node.name |
||||
return filterTree(child) |
||||
}) |
||||
} |
||||
|
||||
return node |
||||
} |
||||
|
||||
function onBack() { |
||||
router.push('/station') |
||||
} |
||||
</script> |
||||
|
||||
<style scoped lang="scss"> |
||||
|
||||
</style> |
@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
|
||||
import { flatMap } from 'lodash-es' |
||||
import type { Device } from "@/types/device"; |
||||
|
||||
export const NODE_SIZE: [number, number] = [130, 36] |
||||
|
||||
export enum DeviceType { |
||||
emu = "emu",// 前端默认跟类型
|
||||
mce = "mce", |
||||
bms = "bms", |
||||
bms_stack = "bms_stack", |
||||
pcs = "pcs", |
||||
bms_cluster = "bms_cluster", |
||||
bms_cell = "bms_cell" |
||||
} |
||||
|
||||
export function flattenTree(tree: Device[], depth = 0): Device[] { |
||||
return flatMap(tree, (node: Device,) => { |
||||
const { children, ...rest } = node |
||||
const current = { ...rest, depth } |
||||
return [current, ...(children ? flattenTree(children, depth + 1) : [])] |
||||
}) |
||||
} |
Loading…
Reference in new issue