19 changed files with 1020 additions and 147 deletions
@ -1,7 +1,8 @@ |
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
declare module '*.vue' { |
declare module '*.vue' { |
||||||
import { ComponentOptions } from 'vue' |
import type { DefineComponent } from 'vue' |
||||||
const componentOptions: ComponentOptions |
const component: DefineComponent<{}, {}, any> |
||||||
export default componentOptions |
export default component |
||||||
} |
} |
||||||
declare module "element-plus/dist/locale/zh-cn.mjs"; |
declare module "element-plus/dist/locale/zh-cn.mjs"; |
||||||
|
|
||||||
|
@ -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 @@ |
|||||||
|
import type { Component } from "vue"; |
||||||
|
|
||||||
|
|
||||||
|
interface IUseG6Options<T> { |
||||||
|
tree: T[], |
||||||
|
nodeComponent: Component, |
||||||
|
nodeSize: [number, number] |
||||||
|
layoutType?: 'LR' | 'TB' |
||||||
|
} |
@ -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 @@ |
|||||||
|
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 @@ |
|||||||
|
export * from './GenerateGraphData' |
||||||
|
export * from './MyLineEdge' |
||||||
|
|
||||||
|
|
@ -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 @@ |
|||||||
<template> |
<template> |
||||||
<div class="device-data-chart"> |
<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-options="loadingOpt" |
||||||
:loading="loading" ref="chartRef" @legendselectchanged="changeLegend"/> |
:loading="loading" |
||||||
|
@legendselectchanged="changeLegend"/> |
||||||
</div> |
</div> |
||||||
</template> |
</template> |
||||||
|
|
@ -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 @@ |
|||||||
|
<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 @@ |
|||||||
|
<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 @@ |
|||||||
|
<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 @@ |
|||||||
|
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