Browse Source

feat: topo结构

main
betaqi 1 month ago
parent
commit
0a0a0e02e7
  1. 9
      global.types/env.d.ts
  2. 4
      src/api/module/transfer/index.ts
  3. 103
      src/hooks/useG6/index.ts
  4. 9
      src/hooks/useG6/useG6.d.ts
  5. 49
      src/hooks/useG6/utils/GenerateGraphData.ts
  6. 64
      src/hooks/useG6/utils/MyLineEdge.ts
  7. 4
      src/hooks/useG6/utils/index.ts
  8. 20
      src/router/index.ts
  9. 5
      src/types/device.ts
  10. 2
      src/views/layout/index.vue
  11. 11
      src/views/stationData/component/newDataChart.vue
  12. 87
      src/views/stationData/component/pointCheckbox.vue
  13. 114
      src/views/stationData/topology/components/Node.vue
  14. 336
      src/views/stationData/topology/components/detailDrawer.vue
  15. 145
      src/views/stationData/topology/index.vue
  16. 22
      src/views/stationData/topology/utils/index.ts
  17. 68
      src/views/stationData/transfer/components/PointGroupTree.vue
  18. 102
      src/views/stationData/transfer/components/deviceDrawer.vue
  19. 13
      src/views/stationData/transfer/index.vue

9
global.types/env.d.ts vendored

@ -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";
declare module "element-plus/dist/locale/zh-cn.mjs";

4
src/api/module/transfer/index.ts

@ -63,11 +63,11 @@ export interface IPointGroupParams { @@ -63,11 +63,11 @@ export interface IPointGroupParams {
export interface IPointGroupOV {
id: number,
id: number | string,
ip: string,
name: string,
port: number,
slave_addr: number,
slave_addr: number | string,
type: string,
children?: IPointGroupOV[]
}

103
src/hooks/useG6/index.ts

@ -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
}
}

9
src/hooks/useG6/useG6.d.ts vendored

@ -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'
}

49
src/hooks/useG6/utils/GenerateGraphData.ts

@ -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;
}
}

64
src/hooks/useG6/utils/MyLineEdge.ts

@ -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,
// });
}
}

4
src/hooks/useG6/utils/index.ts

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
export * from './GenerateGraphData'
export * from './MyLineEdge'

20
src/router/index.ts

@ -32,16 +32,16 @@ export const defaultRouter = [ @@ -32,16 +32,16 @@ export const defaultRouter = [
icon: '',
},
},
// {
// path: '/station/topology',
// name: 'data-topology',
// component: () => import('@/views/stationData/transfer/index.vue'),
// meta: {
// title: '数据拓扑',
// isShow: false,
// icon: '',
// },
// },
{
path: '/station/topology',
name: 'data-topology',
component: () => import('@/views/stationData/topology/index.vue'),
meta: {
title: '数据拓扑',
isShow: false,
icon: '',
},
},
// 固件上传
{
path: '/firmware-upload',

5
src/types/device.ts

@ -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;
}

2
src/views/layout/index.vue

@ -94,7 +94,7 @@ const getIconClass = (icon: string) => { @@ -94,7 +94,7 @@ const getIconClass = (icon: string) => {
const { push, currentRoute } = useRouter()
const activeMenu = computed(() => {
const { meta, path } = unref(currentRoute)
if (path.includes('/station/data-transfer')) {
if (['/station/topology', '/station/data-transfer'].includes(path)) {
return '/station'
}
return path

11
src/views/stationData/transfer/components/newDataChart.vue → src/views/stationData/component/newDataChart.vue

@ -1,8 +1,13 @@ @@ -1,8 +1,13 @@
<template>
<div class="device-data-chart">
<v-chart class="chart" :option="chartOption" :autoresize="autoresize"
:loading-options="loadingOpt"
:loading="loading" ref="chartRef" @legendselectchanged="changeLegend"/>
<v-chart
class="chart"
ref="chartRef"
:option="chartOption"
:autoresize="autoresize"
:loading-options="loadingOpt"
:loading="loading"
@legendselectchanged="changeLegend"/>
</div>
</template>

87
src/views/stationData/component/pointCheckbox.vue

@ -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>

114
src/views/stationData/topology/components/Node.vue

@ -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>

336
src/views/stationData/topology/components/detailDrawer.vue

@ -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>

145
src/views/stationData/topology/index.vue

@ -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>

22
src/views/stationData/topology/utils/index.ts

@ -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) : [])]
})
}

68
src/views/stationData/transfer/components/PointGroupTree.vue

@ -2,8 +2,10 @@ @@ -2,8 +2,10 @@
<div class=" h-full flex">
<div class="w-280">
<el-scrollbar class="fault-device-tree">
<el-tree :data="data" node-key="name" ref="treeRef" :expand-on-click-node="false" :props="defaultProps"
highlight-current @node-click="handleNodeClick" :default-expanded-keys="checkDefault">
<el-tree :data="data" node-key="name" ref="treeRef" :expand-on-click-node="false"
:props="defaultProps"
highlight-current @node-click="handleNodeClick"
:default-expanded-keys="checkDefault">
<template #default="scope">
<div class="item">
<span class="label">{{ scope.data?.cnName ?? '--' }}</span>
@ -12,29 +14,24 @@ @@ -12,29 +14,24 @@
</el-tree>
</el-scrollbar>
</div>
<el-divider direction="vertical" class="h-full" />
<div class="w-340" v-loading="loadingPoints" element-loading-text="数据加载中...">
<template v-if="!devicePoints.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 devicePoints" :key="index">
<el-checkbox :label="item.label" :value="item" />
</template>
</el-checkbox-group>
</el-scrollbar>
</div>
<el-divider direction="vertical" class="h-full"/>
<PointCheckbox
:pointList="pointList"
:loadingPoints="loadingPoints"
@onchange-points="changePoints"
ref="PointCheckboxRef"
/>
</div>
</template>
<script lang="ts" setup>
import type { ElTree } from 'element-plus'
import type { IPointGroupOV } from '@/api/module/transfer'
import type { IMyPoint } from '../../type'
import PointCheckbox from '../../component/pointCheckbox.vue'
interface Props {
data: IPointGroupOV[]
devicePoints: Array<{
pointList: Array<{
label: string
addr: string
}>
@ -52,10 +49,12 @@ const emit = defineEmits<{ @@ -52,10 +49,12 @@ const emit = defineEmits<{
'onchange-points': [IMyPoint[]]
}>()
const PointCheckboxRef = ref<InstanceType<typeof PointCheckbox>>()
const treeRef = ref<InstanceType<typeof ElTree>>()
const checkDefault = ref<string[]>([])
const handleNodeClick = (data: IPointGroupOV) => {
checkPointList.value = []
PointCheckboxRef.value?.clearCheck()
emit('device-select', data)
nextTick(() => {
changePoints()
@ -68,11 +67,8 @@ const defaultProps = { @@ -68,11 +67,8 @@ const defaultProps = {
label: 'name',
}
const checkPointList = ref<any>([])
function changePoints() {
emit('onchange-points', checkPointList.value)
emit('onchange-points', PointCheckboxRef.value?.getCheckList() || [])
}
onMounted(() => {
@ -86,29 +82,6 @@ onMounted(() => { @@ -86,29 +82,6 @@ onMounted(() => {
})
</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>
.fault-device-tree {
width: 100%;
@ -126,17 +99,10 @@ onMounted(() => { @@ -126,17 +99,10 @@ onMounted(() => {
background-color: rgba(174, 230, 114, 0.12);
border-left: 2px solid #619925;
}
}
:deep(.el-divider--vertical) {
height: 100%;
margin: 0;
}
.scroll {
height: 100%;
padding: 10px;
}
</style>

102
src/views/stationData/transfer/components/deviceDrawer.vue

@ -5,12 +5,17 @@ @@ -5,12 +5,17 @@
:before-close="handleBeforeClose" @opened="onDrawerOpened">
<main class="wh-full flex">
<EdfsWrap title="点位组" class="p-r-4 h-full border-r-1 border-solid border-r-#e4e7ed">
<PointGroupTree v-if="isShowDrawer" :data="pointGroup" @device-select="onGroupChange"
:groupChangeLoading="groupChangeLoading" @onchangePoints="onchangePoints"
:devicePoints="pointList"
:loadingPoints="loadingPoints" :isTransfer="props.isTransfer"
:siteInfo="props.siteInfo"
ref="pointGroupTreeRef"/>
<PointGroupTree
v-if="isShowDrawer"
:data="pointGroup"
@device-select="onGroupChange"
:groupChangeLoading="groupChangeLoading"
@onchangePoints="onchangePoints"
:pointList="pointList"
:loadingPoints="loadingPoints"
:isTransfer="props.isTransfer"
:siteInfo="props.siteInfo"
ref="pointGroupTreeRef"/>
</EdfsWrap>
<div class="flex-1 p-4 h-full overflow-hidden relative"
>
@ -29,7 +34,7 @@ @@ -29,7 +34,7 @@
</div>
</div>
<el-button class="mb-4" type="primary" @click="loadChardData">查询数据</el-button>
<NewDataChart v-if="isShowChart" :chart-datas="chartDatas" :legends="legends"
<NewDataChart v-if="isShowChart" :chart-datas="chartData" :legends="legends"
:axis-data="Array.from(axisData)" ref="chartRef"/>
</div>
</main>
@ -38,10 +43,8 @@ @@ -38,10 +43,8 @@
</template>
<script setup lang="ts">
import NewDataChart from './newDataChart.vue'
import NewDataChart from '../../component/newDataChart.vue'
import PointGroupTree from './PointGroupTree.vue'
import { type ManualAction, } from '@/utils/zmq'
import ZMQWorker from '@/composables/useZMQJsonWorker'
import type { IMyPoint, IOfflineDevice, IOnlineDevice } from '../../type'
import {
getDeviceDetails,
@ -56,11 +59,11 @@ import { @@ -56,11 +59,11 @@ import {
import { useMessage } from '@/composables/useMessage'
import dayjs from 'dayjs'
import { nextTick } from "vue";
import EdfsWrap from "@/components/Edfs-wrap.vue";
const env = import.meta.env
const worker = ZMQWorker.getInstance()
const isShowDrawer = defineModel<boolean>()
const title = computed(() => (props.isTransfer ? '数据详情' : `已迁移数据详情`))
const message = useMessage()
@ -72,10 +75,6 @@ const props = defineProps<{ @@ -72,10 +75,6 @@ const props = defineProps<{
}>()
const emit = defineEmits(['on-save'])
const pubIdWithDevice = new Map<
string,
{ device: IOfflineDevice | IOnlineDevice; action: ManualAction }
>()
const pointList = ref<IMyPoint[]>([])
@ -85,11 +84,7 @@ async function open(device: IOfflineDevice | IOnlineDevice) { @@ -85,11 +84,7 @@ async function open(device: IOfflineDevice | IOnlineDevice) {
curDevice.value = device
await loadPointGroup()
.then(async () => {
// if (env.VITE_APP_ENV === 'local') {
// props.isTransfer ? await loadDeviceDetails() : zmqImport(device as IOfflineDevice)
// } else {
await loadDeviceDetails()
// }
})
.catch(() => {
message.error('获取点位组数据失败')
@ -99,7 +94,7 @@ async function open(device: IOfflineDevice | IOnlineDevice) { @@ -99,7 +94,7 @@ async function open(device: IOfflineDevice | IOnlineDevice) {
const loadingPoints = ref(false)
const columsParams = computed(() => ['ts', ...checkPointList.value.map(i => i.addr)])
const columParams = computed(() => ['ts', ...checkPointList.value.map(i => i.addr)])
async function loadPoints() {
if (!curGroup.value) {
@ -133,21 +128,18 @@ async function loadPoints() { @@ -133,21 +128,18 @@ async function loadPoints() {
return res
}
const pointData = ref<any[]>([])
async function loadDeviceDetails() {
if (!fullscreenLoading.value) {
openFullScreen()
}
chartDatas.clear()
chartData.clear()
axisData.clear()
legends.value = []
const poinsRes = await loadPoints()
if (!poinsRes || poinsRes.code !== 0) {
const pointsRes = await loadPoints()
if (!pointsRes || pointsRes.code !== 0) {
message.error('获取点位数据失败')
pointList.value = []
chartDatas.clear()
chartData.clear()
axisData.clear()
fullscreenLoading.value?.close()
return
@ -157,7 +149,7 @@ async function loadDeviceDetails() { @@ -157,7 +149,7 @@ async function loadDeviceDetails() {
fullscreenLoading.value?.close()
}
const chartDatas = reactive(new Map<string, any[]>())
const chartData = reactive(new Map<string, any[]>())
const axisData = new Set<string>()
const legends = ref<{ addr: string; label: string, unit: string }[]>([])
@ -168,7 +160,7 @@ const chartOffset = ref(0) @@ -168,7 +160,7 @@ const chartOffset = ref(0)
const progress = ref(0)
async function loadChardData() {
if (!columsParams.value.filter(i => i !== 'ts').length) {
if (!columParams.value.filter(i => i !== 'ts').length) {
message.error('请选择点位')
return
}
@ -176,8 +168,8 @@ async function loadChardData() { @@ -176,8 +168,8 @@ async function loadChardData() {
const limit = chartLimit.value
const offset = chartOffset.value
const options = {
columns: columsParams.value,
isLocal: props.isTransfer ? false : true,
columns: columParams.value,
isLocal: !props.isTransfer,
host: props.isTransfer ? (curDevice.value as IOnlineDevice).clientIp : '',
name: curGroupName.value as string,
}
@ -244,7 +236,7 @@ async function loadChardData() { @@ -244,7 +236,7 @@ async function loadChardData() {
function setChartData(pointData: any[]) {
for (const addr of columsParams.value.filter(i => i !== 'ts')) {
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 || ''
@ -257,11 +249,11 @@ function setChartData(pointData: any[]) { @@ -257,11 +249,11 @@ function setChartData(pointData: any[]) {
if (checkPointList.value.some(i => i.addr === addr)) {
const time = dayjs(Number(ts)).format('YYYY-MM-DD HH:mm:ss')
if (addr) {
const colData = chartDatas.get(addr)
const colData = chartData.get(addr)
if (colData) {
colData.push([time, val])
} else {
chartDatas.set(addr, [[time, val]])
chartData.set(addr, [[time, val]])
}
}
@ -270,48 +262,8 @@ function setChartData(pointData: any[]) { @@ -270,48 +262,8 @@ function setChartData(pointData: any[]) {
}
}
})
}
// function zmqImport(device: IOfflineDevice) {
// if (!device.sn || !props.siteInfo!.name) {
// message.error('')
// return
// }
// const msg = getPubInitData<'import'>(
// 'import',
// ['', '', '', '', '', '', `${props.siteInfo!.name}/${device.sn}`],
// 'yes'
// )
// pubIdWithDevice.set(msg.id, { device, action: 'import' })
// worker.publish(postTransferTopic, msg, true, zmqTimeoutCb)
// worker.subscribe(getTransferTopic, zmqImportCb, msg.id)
// }
// function zmqImportCb(msg: PubMsgData) {
// const { id, result } = msg
// if (result !== 'progress') {
// const { device, action } = pubIdWithDevice.get(id)!
// if (result === 'success' && device && action === 'import') {
// loadDeviceDetails()
// pubIdWithDevice.delete(id)
// } else {
// message.error(``)
// fullscreenLoading.value?.close()
// pubIdWithDevice.delete(id)
// }
// }
// }
// function zmqTimeoutCb(msg: TimeoutMsg) {
// const { device, action } = pubIdWithDevice.get(msg.timeoutId)!
// if (device && action === 'import') {
// message.error(`${device.sn},,`)
// pubIdWithDevice.delete(msg.timeoutId)
// fullscreenLoading.value?.close()
// }
// }
function handleBeforeClose(done: () => void) {
isShowDrawer.value = false
clearData()
@ -325,7 +277,7 @@ function clearData() { @@ -325,7 +277,7 @@ function clearData() {
chartAllTotal.value = 0
progress.value = 0
legends.value = []
chartDatas.clear()
chartData.clear()
axisData.clear()
}

13
src/views/stationData/transfer/index.vue

@ -359,7 +359,18 @@ function onDeviceDetails(item: IOfflineDevice) { @@ -359,7 +359,18 @@ function onDeviceDetails(item: IOfflineDevice) {
}
function onTopology(item: IDevice) {
console.log(item)
if (typeof item !== undefined) {
const obj = {} as any
siteInfo?.value?.name && (obj.siteName = siteInfo.value.name)
router.push({
path: '/station/topology',
query: {
deviceInfo: JSON.stringify(Object.assign(obj, item, {
isonLine: isonLineTransfer.value,
}))
}
})
}
}
const isCanFirmwareUpload = computed(() => !!firmwarePath.value && isonLineTransfer.value)

Loading…
Cancel
Save