Compare commits

...

14 Commits

  1. 3
      .env.cloud
  2. 2
      .env.local
  3. 1
      .env.prod
  4. 2
      global.types/auto-imports.d.ts
  5. 6
      global.types/components.d.ts
  6. 4883
      package-lock.json
  7. 15
      package.json
  8. 3
      src/api/module/firmware/index.ts
  9. 69
      src/api/module/taks/index.ts
  10. BIN
      src/assets/images/device/bms.png
  11. BIN
      src/assets/images/device/cac.png
  12. BIN
      src/assets/images/device/dgs.png
  13. BIN
      src/assets/images/device/ecg.png
  14. BIN
      src/assets/images/device/ecu.png
  15. BIN
      src/assets/images/device/em.png
  16. BIN
      src/assets/images/device/ems.png
  17. BIN
      src/assets/images/device/ffs.png
  18. BIN
      src/assets/images/device/group.png
  19. BIN
      src/assets/images/device/mppt.png
  20. BIN
      src/assets/images/device/pcs.png
  21. BIN
      src/assets/images/device/ths.png
  22. BIN
      src/assets/images/device/tms.png
  23. BIN
      src/assets/images/device/unit.png
  24. BIN
      src/assets/images/device/wpp.png
  25. 1
      src/components/Edfs-button.vue
  26. 47
      src/components/Edfs-table/index.vue
  27. 1
      src/components/Edfs-wrap.vue
  28. 27
      src/composables/useZMQJsonWorker.ts
  29. 20
      src/router/index.ts
  30. 37
      src/views/firmwareUpload/index.vue
  31. 30
      src/views/layout/index.vue
  32. 52
      src/views/stationData/components/offTransferDlg.vue
  33. 94
      src/views/stationData/components/onLineTransferDlg.vue
  34. 3
      src/views/stationData/index.vue
  35. 647
      src/views/stationData/transferData.vue
  36. 154
      src/views/taskList/index.vue
  37. 137
      src/views/taskList/infoDrawer.vue
  38. 100
      src/views/testG6/components/Node.vue
  39. 93
      src/views/testG6/index.vue
  40. 5
      src/views/testG6/type/index.ts
  41. 114
      src/views/testG6/utils/GenerateGraphData.ts
  42. 146
      src/views/testG6/utils/MyLineEdge.ts
  43. 72
      src/views/testG6/utils/MyNode.ts
  44. 239
      src/views/testG6/utils/data.ts
  45. 32
      src/views/testG6/utils/index.ts
  46. 10
      tsconfig.json
  47. 43
      uno.config.js
  48. 122
      vite.config.ts

3
.env.cloud

@ -0,0 +1,3 @@
VITE_APP_ENV = cloud
VITE_BASE_URL = 'http://192.168.1.99:8080'
VITE_ZMQ_BASE_URL = '192.168.1.99'

2
.env.local

@ -1 +1,3 @@
VITE_APP_ENV = local VITE_APP_ENV = local
VITE_BASE_URL = 'http://192.168.1.99:8080'
VITE_ZMQ_BASE_URL = '192.168.1.99'

1
.env.prod

@ -1 +0,0 @@
VITE_APP_ENV = prod

2
global.types/auto-imports.d.ts vendored

@ -72,6 +72,6 @@ declare global {
// for type re-export // for type re-export
declare global { declare global {
// @ts-ignore // @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue' export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue') import('vue')
} }

6
global.types/components.d.ts vendored

@ -21,6 +21,8 @@ declare module 'vue' {
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider'] ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
ElContainer: typeof import('element-plus/es')['ElContainer'] ElContainer: typeof import('element-plus/es')['ElContainer']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
ElDialog: typeof import('element-plus/es')['ElDialog'] ElDialog: typeof import('element-plus/es')['ElDialog']
ElDivider: typeof import('element-plus/es')['ElDivider'] ElDivider: typeof import('element-plus/es')['ElDivider']
ElDrawer: typeof import('element-plus/es')['ElDrawer'] ElDrawer: typeof import('element-plus/es')['ElDrawer']
@ -28,10 +30,12 @@ declare module 'vue' {
ElInput: typeof import('element-plus/es')['ElInput'] ElInput: typeof import('element-plus/es')['ElInput']
ElMenu: typeof import('element-plus/es')['ElMenu'] ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElPagination: typeof import('element-plus/es')['ElPagination']
ElProgress: typeof import('element-plus/es')['ElProgress'] ElProgress: typeof import('element-plus/es')['ElProgress']
ElRow: typeof import('element-plus/es')['ElRow'] ElRow: typeof import('element-plus/es')['ElRow']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
ElSubMenu: typeof import('element-plus/es')['ElSubMenu'] ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTag: typeof import('element-plus/es')['ElTag'] ElTag: typeof import('element-plus/es')['ElTag']
ElTooltip: typeof import('element-plus/es')['ElTooltip'] ElTooltip: typeof import('element-plus/es')['ElTooltip']
ElTree: typeof import('element-plus/es')['ElTree'] ElTree: typeof import('element-plus/es')['ElTree']
@ -39,7 +43,7 @@ declare module 'vue' {
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
} }
export interface ComponentCustomProperties { export interface GlobalDirectives {
vLoading: typeof import('element-plus/es')['ElLoadingDirective'] vLoading: typeof import('element-plus/es')['ElLoadingDirective']
} }
} }

4883
package-lock.json generated

File diff suppressed because it is too large Load Diff

15
package.json

@ -8,11 +8,13 @@
"build": "run-p type-check \"build-only {@}\" --", "build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview", "preview": "vite preview",
"build-only": "vite build", "build-only": "vite build",
"build:local": "vite build --mode local", "build:dev": "vite build --mode development",
"build:prod": "vite build --mode prod", "build:cloud": "vite build --mode cloud",
"type-check": "vue-tsc --build" "type-check": "vue-tsc --build"
}, },
"dependencies": { "dependencies": {
"@antv/g-svg": "^2.0.42",
"@antv/g6": "^5.0.49",
"@types/qs": "^6.9.18", "@types/qs": "^6.9.18",
"@unocss/reset": "^66.0.0", "@unocss/reset": "^66.0.0",
"axios": "^1.8.4", "axios": "^1.8.4",
@ -20,6 +22,7 @@
"dexie": "^4.0.11", "dexie": "^4.0.11",
"echarts": "^5.6.0", "echarts": "^5.6.0",
"element-plus": "^2.9.5", "element-plus": "^2.9.5",
"g6-extension-vue": "^0.1.0",
"jszmq": "^0.1.2", "jszmq": "^0.1.2",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"pinia": "^3.0.1", "pinia": "^3.0.1",
@ -32,11 +35,11 @@
"devDependencies": { "devDependencies": {
"@iconify/json": "^2.2.310", "@iconify/json": "^2.2.310",
"@tsconfig/node22": "^22.0.0", "@tsconfig/node22": "^22.0.0",
"@types/node": "^22.13.5", "@types/node": "^24.2.1",
"@unocss/preset-icons": "^66.0.0", "@unocss/preset-icons": "^66.0.0",
"@unocss/preset-rem-to-px": "^66.0.0", "@unocss/preset-rem-to-px": "^66.0.0",
"@vitejs/plugin-vue": "^5.2.1", "@vitejs/plugin-vue": "^6.0.1",
"@vitejs/plugin-vue-jsx": "^4.1.1", "@vitejs/plugin-vue-jsx": "^4.2.0",
"@vue/tsconfig": "^0.7.0", "@vue/tsconfig": "^0.7.0",
"npm-run-all2": "^7.0.2", "npm-run-all2": "^7.0.2",
"sass": "^1.85.0", "sass": "^1.85.0",
@ -46,7 +49,7 @@
"unplugin-auto-import": "^19.1.0", "unplugin-auto-import": "^19.1.0",
"unplugin-icons": "^22.1.0", "unplugin-icons": "^22.1.0",
"unplugin-vue-components": "^28.4.0", "unplugin-vue-components": "^28.4.0",
"vite": "^6.2.2", "vite": "^6.3.5",
"vite-plugin-vue-devtools": "^7.7.2", "vite-plugin-vue-devtools": "^7.7.2",
"vue-tsc": "^2.2.2" "vue-tsc": "^2.2.2"
} }

3
src/api/module/firmware/index.ts

@ -1,8 +1,5 @@
import { globalServer } from '../index' import { globalServer } from '../index'
export const uploadFirmwareFile = (params: FormData, abort: AbortController) => export const uploadFirmwareFile = (params: FormData, abort: AbortController) =>
globalServer({ globalServer({
url: 'api/upload', url: 'api/upload',

69
src/api/module/taks/index.ts

@ -0,0 +1,69 @@
import { globalServer } from '../index'
interface Param {
page: number
size: number
}
export const getTaskList = (params: Param) =>
globalServer<{
tasks: TaskList[]
total: number
}>({
url: 'api/task/summary',
method: 'get',
params,
})
export const getTaskInfo = (taskId: string) => globalServer<{ details: TaskInfo[] }>({
url: 'api/task/detail',
method: 'GET',
params: { task: taskId },
})
export interface TaskCreateParams {
site: string
devices: {
sn: string
disk?: string
host?: string
}[],
mode: 'import' | 'export' | 'update'
startTime?: string
endTime?: string
}
export const createTask = (params: TaskCreateParams) => globalServer({
url: 'api/task/apply',
method: 'POST',
data: params,
})
export const cancelTask = (taskId: string) => globalServer({
url: 'api/task/cancel',
method: 'POST',
data: { task: taskId },
})
export type TaskInfo = {
finish: number;
id: string;
info: string;
site: string;
sn: string;
status: -1 | 0 | 1 | 2; // -1 失败, 0 未开始, 1 进行中, 2 成功
task_id: string;
total: number;
};
export interface TaskList {
endTime: string; // 结束时间(可能是空字符串)
id: string; // "task759956"
info: string; // ""
mode: string; // "export"
site: string; // "test1"
startTime: string; // 开始时间(可能是空字符串)
status: -1 | 0 | 1 | 2 | 3; // -1 失败, 0 未开始, 1 进行中, 2 取消, 3 成功
loading?: boolean
}

BIN
src/assets/images/device/bms.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
src/assets/images/device/cac.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
src/assets/images/device/dgs.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
src/assets/images/device/ecg.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 878 B

BIN
src/assets/images/device/ecu.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
src/assets/images/device/em.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
src/assets/images/device/ems.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
src/assets/images/device/ffs.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
src/assets/images/device/group.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 878 B

BIN
src/assets/images/device/mppt.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
src/assets/images/device/pcs.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
src/assets/images/device/ths.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
src/assets/images/device/tms.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
src/assets/images/device/unit.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
src/assets/images/device/wpp.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

1
src/components/Edfs-button.vue

@ -13,6 +13,7 @@
:color="color" :color="color"
:class="className" :class="className"
@click="onClick" @click="onClick"
:link="link"
:text="text" :text="text"
class="edfs-button" class="edfs-button"
> >

47
src/components/Edfs-table/index.vue

@ -1,36 +1,16 @@
<template> <template>
<div class="edfs-table-components"> <div class="edfs-table-components">
<el-table <el-table :data="data" :fit="fit" :stripe="stripe" :border="border" v-loading="loading" :show-header="showHeader"
:data="data" :max-height="maxHeight" :highlight-current-row="highlightCurrentRow" :row-class-name="rowClassName"
:fit="fit" @current-change="onCurrentChange" ref="ELTableRef" @row-click="onRowClick" @row-dblclick="onRowDblclick"
:stripe="stripe" @selection-change="handleSelectionChange" @expand-change="expandChange" class="edfs-table"
:border="border" :span-method="spanMethod">
v-loading="loading"
:show-header="showHeader"
:max-height="maxHeight"
:highlight-current-row="highlightCurrentRow"
:row-class-name="rowClassName"
@current-change="onCurrentChange"
ref="ELTableRef"
@row-click="onRowClick"
@row-dblclick="onRowDblclick"
@selection-change="handleSelectionChange"
class="edfs-table"
:span-method="spanMethod"
>
<slot></slot> <slot></slot>
</el-table> </el-table>
<template v-if="usePaging"> <template v-if="usePaging">
<div class="pagination-block"> <div class="pagination-block">
<el-pagination <el-pagination v-model:current-page="currentPage" v-model:page-size="pageSize" :width-type="1"
v-model:current-page="currentPage" layout="prev, pager, next, jumper" :total="pageTotal" background @current-change="onPageCurrentChange" />
v-model:page-size="pageSize"
:width-type="1"
layout="prev, pager, next, jumper"
:total="pageTotal"
background
@current-change="onPageCurrentChange"
/>
<!-- @size-change="onPageSizeChange" --> <!-- @size-change="onPageSizeChange" -->
</div> </div>
@ -59,6 +39,7 @@ const emit = defineEmits<{
// 'page-size-change': [pageSize: number] // 'page-size-change': [pageSize: number]
'page-current-change': [currentPage: number] 'page-current-change': [currentPage: number]
'selection-change': [selection: any[]] 'selection-change': [selection: any[]]
'expand-change': [row: any, expandedRows: any[]]
}>() }>()
function onCurrentChange(currentRow: any, oldCurrentRow: any) { function onCurrentChange(currentRow: any, oldCurrentRow: any) {
@ -74,6 +55,9 @@ function onRowDblclick(row: any, column: any, event: Event) {
function handleSelectionChange(selection: any[]) { function handleSelectionChange(selection: any[]) {
emit('selection-change', selection) emit('selection-change', selection)
} }
function expandChange(row: any, expandedRows: any[]) {
emit('expand-change', row, expandedRows)
}
// //
const pageSize = ref() const pageSize = ref()
@ -125,6 +109,7 @@ defineExpose({
display: flex; display: flex;
flex-direction: column; flex-direction: column;
box-sizing: border-box; box-sizing: border-box;
// background-color: #fff; // background-color: #fff;
.edfs-table { .edfs-table {
width: 100%; width: 100%;
@ -155,21 +140,26 @@ defineExpose({
align-items: center; align-items: center;
justify-content: end; justify-content: end;
height: 60px; height: 60px;
:deep(.el-pagination__editor.el-input) { :deep(.el-pagination__editor.el-input) {
width: 66px; width: 66px;
} }
:deep(.el-input__wrapper) { :deep(.el-input__wrapper) {
height: 30px; height: 30px;
margin-left: 8px; margin-left: 8px;
padding: 0 10px; padding: 0 10px;
.el-input__inner { .el-input__inner {
font-size: 14px; font-size: 14px;
height: 100%; height: 100%;
} }
} }
:deep(.el-pagination) { :deep(.el-pagination) {
// width: cvw(200); // width: cvw(200);
} }
:deep(.el-pagination), :deep(.el-pagination),
:deep(.el-pagination .el-icon), :deep(.el-pagination .el-icon),
:deep(.el-pagination li) { :deep(.el-pagination li) {
@ -181,6 +171,7 @@ defineExpose({
:deep(.el-pagination li) { :deep(.el-pagination li) {
background-color: transparent; background-color: transparent;
} }
:deep(.el-pager) { :deep(.el-pager) {
height: 28px; height: 28px;
} }
@ -196,12 +187,14 @@ defineExpose({
color: #619925; color: #619925;
} }
} }
:deep(.is-active) { :deep(.is-active) {
color: #619925 !important; color: #619925 !important;
background: rgba(97, 153, 37, 0.06) !important; background: rgba(97, 153, 37, 0.06) !important;
border: 1px solid rgba(97, 153, 37, 1) !important; border: 1px solid rgba(97, 153, 37, 1) !important;
border-radius: 2px; border-radius: 2px;
} }
:deep(.el-input__wrapper) { :deep(.el-input__wrapper) {
background-color: var(--pagination-bg); background-color: var(--pagination-bg);
} }

1
src/components/Edfs-wrap.vue

@ -1,7 +1,6 @@
<template> <template>
<div class="edfs-wrap" :class="{ 'edfs-wrap-cvh': useCvh }"> <div class="edfs-wrap" :class="{ 'edfs-wrap-cvh': useCvh }">
<div class="wrap-title" v-if="title || customLeft"> <div class="wrap-title" v-if="title || customLeft">
{{}}
<div class="title-left" v-if="!customLeft"> <div class="title-left" v-if="!customLeft">
<template v-if="shape === 'rect'"> <template v-if="shape === 'rect'">
<div <div

27
src/composables/useZMQJsonWorker.ts

@ -1,15 +1,26 @@
import { WorkerCMD, ZmqCMD, } from '@/utils/zmq' import { WorkerCMD, ZmqCMD, } from '@/utils/zmq'
import type { ManualAction, PublishMsg, PubMsgData, SubMsgData, TimeoutMsg, ZmqMessage } from '@/utils/zmq' import type {
ManualAction,
PublishMsg,
PubMsgData,
SubMsgData,
TimeoutMsg,
ZmqMessage
} from '@/utils/zmq'
import webWorker from '@/utils/zmqJsonWorker?worker' import webWorker from '@/utils/zmqJsonWorker?worker'
const env = import.meta.env const env = import.meta.env
const defaultHost = import.meta.env.PROD ? window.location.hostname : '192.168.1.199' let defaultHost = env.VITE_ZMQ_BASE_URL
if (env.VITE_APP_ENV === 'local') {
defaultHost = window.location.hostname === 'localhost' ? env.VITE_ZMQ_BASE_URL : window.location.hostname
}
class ZMQJsonWorker { class ZMQJsonWorker {
private static instance: ZMQJsonWorker | null = null; // ➤ 单例实例 private static instance: ZMQJsonWorker | null = null; // ➤ 单例实例
private worker: Worker; private worker: Worker;
private scribeHandlers: Map<string, (msg: SubMsgData | PubMsgData) => void> = new Map(); private scribeHandlers: Map<string, (msg: SubMsgData | PubMsgData) => void> = new Map();
private pubTimeoutHandlers: Map<string, (msg: TimeoutMsg) => void> = new Map(); private pubTimeoutHandlers: Map<string, (msg: TimeoutMsg) => void> = new Map();
private host: string; private readonly host: string;
private statusCallback: ((status: string) => void) | null = null; private statusCallback: ((status: string) => void) | null = null;
private isAlwaysListenMsgMap: Map<string, PublishMsg<any>> = new Map(); private isAlwaysListenMsgMap: Map<string, PublishMsg<any>> = new Map();
@ -64,7 +75,13 @@ class ZMQJsonWorker {
if (isAlwaysListen) { if (isAlwaysListen) {
this.isAlwaysListenMsgMap.set(`${msg.id}`, msg) this.isAlwaysListenMsgMap.set(`${msg.id}`, msg)
} }
this.worker.postMessage({ cmd: WorkerCMD.PUBLISH, topic, msg: JSON.stringify(msg), isTimeout, isAlwaysListen }); this.worker.postMessage({
cmd: WorkerCMD.PUBLISH,
topic,
msg: JSON.stringify(msg),
isTimeout,
isAlwaysListen
});
} }
setStatusCallback(callback: (status: string) => void) { setStatusCallback(callback: (status: string) => void) {
@ -104,7 +121,7 @@ class ZMQJsonWorker {
private handleMessage(e: MessageEvent<ZmqMessage>) { private handleMessage(e: MessageEvent<ZmqMessage>) {
const { cmd, msg, topic, community } = e.data; const { cmd, msg, topic, community } = e.data;
console.log(e.data)
if (cmd === ZmqCMD.STATUS) { if (cmd === ZmqCMD.STATUS) {
const status = community ? 'disconnected' : 'connected'; const status = community ? 'disconnected' : 'connected';
if (this.statusCallback) { if (this.statusCallback) {

20
src/router/index.ts

@ -43,6 +43,26 @@ export const defaultRouter = [
icon: 'i-mingcute:transfer-2-line', icon: 'i-mingcute:transfer-2-line',
}, },
}, },
{
path: '/task',
name: 'task',
component: () => import('@/views/taskList/index.vue'),
meta: {
title: '任务列表',
isShow: true,
icon: 'i-mingcute:task-line',
}
},
// {
// path: '/testG6',
// name: 'testG6',
// component: () => import('@/views/testG6/index.vue'),
// meta: {
// title: '测试G6',
// isShow: true,
// icon: 'i-mingcute:task-line',
// }
// }
], ],
}, },
] ]

37
src/views/firmwareUpload/index.vue

@ -1,17 +1,23 @@
<template> <template>
<div class="flex justify-center items-center size-full"> <div class="flex justify-center items-center size-full">
<EdfsWrap :title="title" style="width: 50%; height: 50%"> <EdfsWrap :title="title" style="width: 50%; height: 50%">
<el-upload v-model:fileList="fileList" v-loading="loading" element-loading-text="上传中..." drag action="" <el-upload v-model:fileList="fileList" v-loading="loading" element-loading-text="上传中..."
accept=".tar.gz" :limit="1" :on-exceed="handleExceed" :auto-upload="false" ref="uploadRef" drag action=""
class="h-[calc(100%-30px)] w-full"> accept=".zip,.tar,.tar,.gz" :limit="1" :on-exceed="handleExceed"
:auto-upload="false"
ref="uploadRef"
:before-upload="beforeAvatarUpload" class="h-[calc(100%-30px)] w-full">
<div class="i-line-md:cloud-alt-upload-loop text-20px mx-auto"></div> <div class="i-line-md:cloud-alt-upload-loop text-20px mx-auto"></div>
<div class="text">拖拽文件或者 <em>点击上传</em></div> <div class="text">拖拽文件或者 <em>点击上传</em></div>
<template #tip v-if="!fileList.length"> <template #tip v-if="!fileList.length">
<div class="el-upload__tip">上传限制一个文件新文件会覆盖旧文件</div> <div class="el-upload__tip">上传限制一个文件新文件会覆盖旧文件仅支持 .zip.tar
.tar.gz 格式
</div>
</template> </template>
</el-upload> </el-upload>
<div class="flex justify-center"> <div class="flex justify-center">
<el-button type="primary" @click="onSave" v-show="fileList.length && !loading">确定上传</el-button> <el-button type="primary" @click="onSave" v-show="fileList.length && !loading">确定上传
</el-button>
<el-button type="info" v-show="loading" @click="onClone">取消上传</el-button> <el-button type="info" v-show="loading" @click="onClone">取消上传</el-button>
</div> </div>
</EdfsWrap> </EdfsWrap>
@ -43,16 +49,26 @@ const handleExceed: UploadProps['onExceed'] = files => {
const uploadRef = ref<UploadInstance>() const uploadRef = ref<UploadInstance>()
const beforeAvatarUpload: UploadProps['beforeUpload'] = rawFile => { const beforeAvatarUpload: UploadProps['beforeUpload'] = rawFile => {
const accept = ['.zip', '.tar', '.tar.gz'] const fileName = rawFile.name.toLowerCase()
const fileTypes = rawFile.name.substring(rawFile.name.indexOf('.')) const supportedFormats = ['.zip', '.tar', '.tar.gz']
const isValidFormat = supportedFormats.some(format => fileName.endsWith(format))
if (!accept.includes(fileTypes)) { if (!isValidFormat) {
message.error('请上传 tar.gz 文件') message.error('请上传 .zip、.tar 或 .tar.gz 格式的文件')
uploadRef.value?.clearFiles?.() uploadRef.value?.clearFiles?.()
return false return false
} }
// const maxSize = 100 * 1024 * 1024
// if (rawFile.size > maxSize) {
// message.error('100MB')
// uploadRef.value?.clearFiles?.()
// return false
// }
return true return true
} }
function validate() { function validate() {
if (!fileList.value.length) { if (!fileList.value.length) {
message.error('请上传文件') message.error('请上传文件')
@ -61,6 +77,7 @@ function validate() {
return beforeAvatarUpload(fileList.value[0].raw!) return beforeAvatarUpload(fileList.value[0].raw!)
} }
const loading = ref(false) const loading = ref(false)
const abortController = ref<AbortController>() const abortController = ref<AbortController>()
@ -154,6 +171,7 @@ async function createChunksFromFileData(file: File, chunkSize: number) {
function clearData() { function clearData() {
fileList.value = [] fileList.value = []
} }
function onClone() { function onClone() {
if (loading.value) { if (loading.value) {
abortController.value?.abort() abortController.value?.abort()
@ -161,6 +179,7 @@ function onClone() {
} }
clearData() clearData()
} }
const firmwarePath = ref<string>() const firmwarePath = ref<string>()
const title = computed(() => { const title = computed(() => {

30
src/views/layout/index.vue

@ -4,7 +4,7 @@
<el-aside class="aside-wrap"> <el-aside class="aside-wrap">
<RouterLink to="/" class="layout-logo" :class="{ 'layout-logo-collapse': isCollapse }"> <RouterLink to="/" class="layout-logo" :class="{ 'layout-logo-collapse': isCollapse }">
<svg class="inline-block text-32px" width="1em" height="1em" viewBox="0 0 160 160" <svg class="inline-block text-32px" width="1em" height="1em" viewBox="0 0 160 160"
xmlns="http://www.w3.org/2000/svg"> xmlns="http://www.w3.org/2000/svg">
<path <path
d="M81.28 55.9c-.1-11.67-2.93-22.55-9.37-32.38-1-1.5-2.14-2.86-2.5-4.71a8.1 8.1 0 014-8.61 7.89 7.89 0 019.3 1.23 35.999 35.999 0 015.9 8.83 75.18 75.18 0 018.44 28.58 83.211 83.211 0 01-5.23 36.74 102.983 102.983 0 01-3 7.28 1.2 1.2 0 000 1.41c9.58 13.3 21.76 23 37.85 27.24a54.37 54.37 0 0019.68 1.57 7.72 7.72 0 018.36 6.9 7.903 7.903 0 01-6.7 9 64.744 64.744 0 01-23-1.33 77.68 77.68 0 01-36.93-19.88 93.628 93.628 0 01-11.91-13.71 2.18 2.18 0 00-2.3-1.06 72.744 72.744 0 00-27.38 7.55c-11.6 6-20.67 14.58-26.4 26.45a10.134 10.134 0 01-3.7 4.7 8 8 0 01-9.19-.7 7.86 7.86 0 01-2.36-9.28 60.324 60.324 0 018.72-14.52c12.2-15.43 28.21-24.59 47.32-28.57A85.085 85.085 0 0173.07 87c.524.015 1-.307 1.18-.8a76.06 76.06 0 006.53-22.3c.351-2.652.518-5.325.5-8z" d="M81.28 55.9c-.1-11.67-2.93-22.55-9.37-32.38-1-1.5-2.14-2.86-2.5-4.71a8.1 8.1 0 014-8.61 7.89 7.89 0 019.3 1.23 35.999 35.999 0 015.9 8.83 75.18 75.18 0 018.44 28.58 83.211 83.211 0 01-5.23 36.74 102.983 102.983 0 01-3 7.28 1.2 1.2 0 000 1.41c9.58 13.3 21.76 23 37.85 27.24a54.37 54.37 0 0019.68 1.57 7.72 7.72 0 018.36 6.9 7.903 7.903 0 01-6.7 9 64.744 64.744 0 01-23-1.33 77.68 77.68 0 01-36.93-19.88 93.628 93.628 0 01-11.91-13.71 2.18 2.18 0 00-2.3-1.06 72.744 72.744 0 00-27.38 7.55c-11.6 6-20.67 14.58-26.4 26.45a10.134 10.134 0 01-3.7 4.7 8 8 0 01-9.19-.7 7.86 7.86 0 01-2.36-9.28 60.324 60.324 0 018.72-14.52c12.2-15.43 28.21-24.59 47.32-28.57A85.085 85.085 0 0173.07 87c.524.015 1-.307 1.18-.8a76.06 76.06 0 006.53-22.3c.351-2.652.518-5.325.5-8z"
fill="currentColor"></path> fill="currentColor"></path>
@ -16,17 +16,20 @@
EMU-主机运维平台 EMU-主机运维平台
</h2> </h2>
</RouterLink> </RouterLink>
<el-menu class="layout-menu" :default-active="activeMenu" @select="menuSelect" router :collapse="isCollapse"> <el-menu class="layout-menu" :default-active="activeMenu" @select="menuSelect" router
:collapse="isCollapse">
<template v-for="router in menuList"> <template v-for="router in menuList">
<template v-if="router.meta?.isShow"> <template v-if="router.meta?.isShow">
<el-sub-menu v-if="router?.children?.filter((item: any) => item.meta?.isShow).length" :index="router.path" <el-sub-menu v-if="router?.children?.filter((item: any) => item.meta?.isShow).length"
:key="router.path"> :index="router.path"
:key="router.path">
<template #title> <template #title>
<div :class="router.meta.icon" class="menu-icon"></div> <div :class="router.meta.icon" class="menu-icon"></div>
<span>{{ router.meta.title }}</span> <span>{{ router.meta.title }}</span>
</template> </template>
<template v-for="child in router?.children"> <template v-for="child in router?.children">
<el-menu-item v-if="child?.meta?.isShow" :key="child.path" :index="`${router.path}/${child.path}`"> <el-menu-item v-if="child?.meta?.isShow" :key="child.path"
:index="`${router.path}/${child.path}`">
<div :class="child.meta.icon" class="menu-icon"></div> <div :class="child.meta.icon" class="menu-icon"></div>
<span>{{ child.meta.title }}</span> <span>{{ child.meta.title }}</span>
</el-menu-item> </el-menu-item>
@ -50,13 +53,9 @@
{{ currentTime }} {{ currentTime }}
</div> </div>
</div> </div>
<!-- <div class="flex items-center gap-col-2 p-r-20">
<el-avatar :src="circleUrl" class="avatar" />
<span class="username">John Doe</span>
</div> -->
</el-header> </el-header>
<main class="main-wrap"> <main class="main-wrap">
<RouterView /> <RouterView/>
</main> </main>
</el-container> </el-container>
</el-container> </el-container>
@ -67,6 +66,7 @@
import { useTheme } from '@/composables/useTheme' import { useTheme } from '@/composables/useTheme'
import { defaultRouter } from '@/router' import { defaultRouter } from '@/router'
import dayjs from 'dayjs' import dayjs from 'dayjs'
const env = import.meta.env const env = import.meta.env
const unfold = 'i-icon-park-outline:menu-unfold' const unfold = 'i-icon-park-outline:menu-unfold'
const fold = 'i-icon-park-outline:menu-fold' const fold = 'i-icon-park-outline:menu-fold'
@ -74,12 +74,12 @@ const fold = 'i-icon-park-outline:menu-fold'
const { theme } = useTheme() const { theme } = useTheme()
const menuList = computed<any[]>(() => { const menuList = computed<any[]>(() => {
let data = defaultRouter[0].children let data = defaultRouter[0].children
if (env.VITE_APP_ENV !== 'local') { if (env.VITE_APP_ENV !== 'local') {
data = data.filter(item => item.name != 'firmware-upload') data = data.filter(item => !['firmware-upload', 'task'].includes(item.name))
}
return data
} }
return data
}
) )
const circleUrl = ref( const circleUrl = ref(

52
src/views/stationData/components/offTransferDlg.vue

@ -6,8 +6,7 @@
<span class="require">*</span> <span class="require">*</span>
云端IP: 云端IP:
</div> </div>
<el-input v-model="form.clientIp" class="flex-1" placeholder="请输入云端IP: <el-input v-model="form.clientIp" class="flex-1" placeholder="请输入云端IP:" />
" />
</el-row> </el-row>
</div> </div>
</EdfsDialog> </EdfsDialog>
@ -20,11 +19,12 @@ import { cloneDeep } from 'lodash-es'
import { useMessage } from '@/composables/useMessage' import { useMessage } from '@/composables/useMessage'
import type { IOfflineDevice } from '../type' import type { IOfflineDevice } from '../type'
import type { ISite } from '@/api/module/transfer' import type { ISite } from '@/api/module/transfer'
import { createTask, type TaskCreateParams } from '@/api/module/taks'
const message = useMessage() const message = useMessage()
const emit = defineEmits<{ const emit = defineEmits<{
'on-save': [msg: PublishMsg<'import'>[], site: IOfflineDevice[]] 'on-save': []
}>() }>()
const props = defineProps<{ const props = defineProps<{
@ -37,48 +37,40 @@ const fromData = {
clientIp: '', clientIp: '',
} }
const curOffDeive = ref<IOfflineDevice[]>([])
const form = ref(cloneDeep(fromData)) const form = ref(cloneDeep(fromData))
const paramsData = ref<TaskCreateParams>()
function open(parmas: TaskCreateParams) {
const isBatchTransfer = ref(false) paramsData.value = parmas
function open(item: IOfflineDevice[], isBatch: boolean = false) {
curOffDeive.value = item
visible.value = true visible.value = true
isBatchTransfer.value = isBatch
} }
function onSave() { async function onSave() {
if (verifyData()) return if (verifyData()) return
if (!curOffDeive.value.length) { if (!paramsData.value) {
message.error('请选择设备') message.error('参数错误')
return return
} }
const msgList: PublishMsg<"import">[] = []
for (const device of curOffDeive.value) { paramsData.value.devices.forEach((r) => {
const params = [ r.host = form.value.clientIp
`${form.value.clientIp}`, })
'',
'', const res = await createTask(paramsData.value)
'', if (res.code !== 0) {
'', message.error(`任务创建失败`)
'', } else {
`${props.siteInfo!.name}/${device.sn}`, message.success('任务创建成功,请在任务列表中查看')
]
const msg = getPubInitData<'import'>('import', params)
msgList.push(msg)
} }
emit('on-save', msgList, curOffDeive.value) emit('on-save')
close() close()
} }
function close() { function close() {
form.value = cloneDeep(fromData)
curOffDeive.value = []
visible.value = false visible.value = false
isBatchTransfer.value = false form.value = cloneDeep(fromData)
paramsData.value = undefined
} }
const ipPattern = const ipPattern =
/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/ /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/

94
src/views/stationData/components/onLineTransferDlg.vue

@ -1,28 +1,15 @@
<template> <template>
<EdfsDialog <EdfsDialog :title="isBatchTransfer ? '批量迁移' : '数据迁移'" :is-show="visible" width="580px" @on-close="close"
:title="isBatchTransfer ? '批量迁移' : '数据迁移'" @on-save="onSave">
:is-show="visible"
width="580px"
@on-close="close"
@on-save="onSave"
>
<div class="flex-col gap-10 w-80% m-x-30px"> <div class="flex-col gap-10 w-80% m-x-30px">
<el-row> <el-row>
<div class="label"> <div class="label">
<span class="require">*</span> <span class="require">*</span>
数据开始时间: 数据开始时间:
</div> </div>
<el-date-picker <el-date-picker v-model="startTime" value-format="YYYY-MM-DD HH:mm" format="YYYY-MM-DD HH:mm" class="flex-1"
v-model="startTime" type="datetime" placeholder="请选择开始时间" :disabled-date="disabledDate" :disabled-time="disabledStartTime"
value-format="YYYY-MM-DD HH:mm" @change="handleStartTimeChange" />
format="YYYY-MM-DD HH:mm"
class="flex-1"
type="datetime"
placeholder="请选择开始时间"
:disabled-date="disabledDate"
:disabled-time="disabledStartTime"
@change="handleStartTimeChange"
/>
</el-row> </el-row>
<el-row> <el-row>
@ -30,17 +17,9 @@
<span class="require">*</span> <span class="require">*</span>
数据结束时间: 数据结束时间:
</div> </div>
<el-date-picker <el-date-picker v-model="endTime" class="flex-1" value-format="YYYY-MM-DD HH:mm" format="YYYY-MM-DD HH:mm"
v-model="endTime" type="datetime" placeholder="请选择结束时间" :disabled-date="disabledDate" :disabled-time="disabledEndTime"
class="flex-1" @change="handleEndTimeChange" />
value-format="YYYY-MM-DD HH:mm"
format="YYYY-MM-DD HH:mm"
type="datetime"
placeholder="请选择结束时间"
:disabled-date="disabledDate"
:disabled-time="disabledEndTime"
@change="handleEndTimeChange"
/>
</el-row> </el-row>
</div> </div>
</EdfsDialog> </EdfsDialog>
@ -51,10 +30,11 @@ import dayjs from 'dayjs'
import { getPubInitData, type PublishMsg } from '@/utils/zmq' import { getPubInitData, type PublishMsg } from '@/utils/zmq'
import type { IOnlineDevice } from '../type' import type { IOnlineDevice } from '../type'
import { useMessage } from '@/composables/useMessage' import { useMessage } from '@/composables/useMessage'
import { createTask, type TaskCreateParams } from '@/api/module/taks'
const message = useMessage() const message = useMessage()
const emit = defineEmits<{ const emit = defineEmits<{
'on-save': [PublishMsg<'export'>, IOnlineDevice] 'on-save': []
}>() }>()
const props = defineProps<{ const props = defineProps<{
@ -115,40 +95,31 @@ function handleEndTimeChange(val: string) {
} }
} }
const curDevice = ref<IOnlineDevice>() const paramsData = ref<TaskCreateParams>()
const batchClientIp = ref('') function open(parmas: TaskCreateParams) {
const batchPath = ref('') paramsData.value = parmas
function open(item: IOnlineDevice, clientIps: string, paths: string) {
curDevice.value = item
visible.value = true visible.value = true
batchClientIp.value = clientIps
batchPath.value = paths
} }
function onSave() { async function onSave() {
if (!verifyData()) return if (!verifyData()) return
if (!curDevice.value) { if (!paramsData.value) {
message.error('请选择设备') message.error('任务参数未定义')
return return
} }
const params = [ const params = paramsData.value
`${props.isBatchTransfer ? batchClientIp.value : curDevice.value.clientIp}`, params.startTime = dayjs(startTime.value).format('YYYY-MM-DD HH:mm:ss')
'', params.endTime = dayjs(endTime.value).format('YYYY-MM-DD HH:mm:ss')
'', const res = await createTask(params)
'', if (res.code !== 0) {
'', message.error(`任务创建失败`)
'', } else {
`${ message.success('任务创建成功,请在任务列表中查看')
props.isBatchTransfer }
? batchPath.value
: `${curDevice.value.site_id}/${curDevice.value.sn}`
}`, emit('on-save')
`${dayjs(startTime.value).valueOf()},${dayjs(endTime.value).valueOf()}`,
]
const msg = getPubInitData<'export'>('export', params)
emit('on-save', msg, curDevice.value as IOnlineDevice)
close() close()
} }
@ -156,18 +127,16 @@ function close() {
startTime.value = '' startTime.value = ''
endTime.value = '' endTime.value = ''
visible.value = false visible.value = false
curDevice.value = undefined paramsData.value = undefined
batchClientIp.value = ''
batchPath.value = ''
} }
function verifyData() { function verifyData() {
if(!startTime.value) { if (!startTime.value) {
message.error('请选择开始时间') message.error('请选择开始时间')
return false return false
} }
if(!endTime.value) { if (!endTime.value) {
message.error('请选择结束时间') message.error('请选择结束时间')
return false return false
} }
@ -190,6 +159,7 @@ defineExpose({
line-height: 33px; line-height: 33px;
text-align: right; text-align: right;
width: 110px; width: 110px;
.require { .require {
color: red; color: red;
} }

3
src/views/stationData/index.vue

@ -78,7 +78,6 @@
</div> </div>
</div> </div>
</template> </template>
</div> </div>
</div> </div>
</EdfsWrap> </EdfsWrap>
@ -87,10 +86,10 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import dayjs from 'dayjs'
import { useTransferDataStore } from '@/stores/transferData' import { useTransferDataStore } from '@/stores/transferData'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { getSiteList, type ISite } from '@/api/module/transfer' import { getSiteList, type ISite } from '@/api/module/transfer'
import EdfsWrap from "@/components/Edfs-wrap.vue";
const env = import.meta.env const env = import.meta.env
const router = useRouter() const router = useRouter()

647
src/views/stationData/transferData.vue

@ -3,18 +3,27 @@
<el-button type="primary" @click="onBack" class="w-150px"> <el-button type="primary" @click="onBack" class="w-150px">
<i class="i-line-md:arrow-left"></i>返回站点数据 <i class="i-line-md:arrow-left"></i>返回站点数据
</el-button> </el-button>
<EdfsWrap title="设备列表" class="flex-1" useScrollBar> <EdfsWrap
:title="isonLineTransfer ? `在线设备列表` : `历史设备列表`"
class="flex-1"
useScrollBar
:shapeColor="isonLineTransfer ? '#4B9E5F' : '#F1BF63'"
>
<template #title-right> <template #title-right>
<template v-if="env.VITE_APP_ENV == 'local'"> <template v-if="env.VITE_APP_ENV == 'local'">
<template v-if="isBatchTransfer || isBatchUpgrade"> <template v-if="isBatchTransfer || isBatchUpgrade">
<el-button type="primary" @click="onBatchSave"> 确定{{ batchText }} </el-button> <el-button type="primary" @click="onBatchSave">
确定{{ batchText }}
</el-button>
<el-button type="info" @click="onBatchCancel"> 取消 </el-button> <el-button type="info" @click="onBatchCancel"> 取消 </el-button>
</template> </template>
<template v-else> <template v-else>
<el-button type="primary" @click="onBatchTransfer"> <i class="i-mdi:database-arrow-right-outline mr-1" /> <el-button type="primary" @click="onBatchTransfer">
{{ isonLineTransfer ? '数据迁移' : '数据导出' }} </el-button> <i class="i-mdi:database-arrow-right-outline mr-1" />
<el-button v-if="isonLineTransfer" type="primary" @click="onBatchUpgrade"> <i {{ isonLineTransfer ? '数据迁移' : '数据导出' }}
class="i-codicon:chip mr-1" />批量升级 </el-button>
<el-button v-if="isonLineTransfer" type="primary" @click="onBatchUpgrade">
<i class="i-codicon:chip mr-1" />批量升级
</el-button> </el-button>
</template> </template>
</template> </template>
@ -24,7 +33,10 @@
<div class="device-item" v-for="item in devices"> <div class="device-item" v-for="item in devices">
<div class="device-item-header"> <div class="device-item-header">
<div class="flex items-center"> <div class="flex items-center">
<el-checkbox :value="item.sn" v-if="(isBatchTransfer || isBatchUpgrade) && item.status !== '离线'"> <el-checkbox
:value="item.sn"
v-if="(isBatchTransfer || isBatchUpgrade) && item.status !== '离线'"
>
<div>设备ID: {{ item.sn }}</div> <div>设备ID: {{ item.sn }}</div>
</el-checkbox> </el-checkbox>
<div v-else class="h-32 leading-32px"> <div v-else class="h-32 leading-32px">
@ -32,26 +44,38 @@
</div> </div>
</div> </div>
<div class="flex items-center gap-col-2" v-if="!(isBatchTransfer || isBatchUpgrade)"> <div
class="flex items-center gap-col-2"
v-if="!(isBatchTransfer || isBatchUpgrade)"
>
<template v-if="env.VITE_APP_ENV == 'local'"> <template v-if="env.VITE_APP_ENV == 'local'">
<el-tooltip :content="isonLineTransfer ? '数据迁移' : '数据导出'" <el-tooltip
v-if="isonLineTransfer ? item.status === '在线' : true"> :content="isonLineTransfer ? '数据迁移' : '数据导出'"
<i class="i-mdi:database-arrow-right-outline :hover:color-[#8ACE6A] color-[#4B9E5F] cursor-pointer text-20px" v-if="isonLineTransfer ? item.status === '在线' : true"
@click="onTransfer(item)"></i> >
<i
class="i-mdi:database-arrow-right-outline :hover:color-[#8ACE6A] color-[#4B9E5F] cursor-pointer text-20px"
@click="onBatchTransferSave([item])"
></i>
</el-tooltip> </el-tooltip>
<el-tooltip content="固件升级" v-if="isonLineTransfer && item.status === '在线'"> <el-tooltip
<i class="i-codicon:chip :hover:color-[#8ACE6A] color-[#4B9E5F] cursor-pointer text-20px" content="固件升级"
@click="onFirmwareUpload([item])"></i> v-if="isonLineTransfer && item.status === '在线'"
>
<i
class="i-codicon:chip :hover:color-[#8ACE6A] color-[#4B9E5F] cursor-pointer text-20px"
@click="onFirmwareUpload([item])"
></i>
</el-tooltip> </el-tooltip>
</template> </template>
<el-tooltip content="详情"> <el-tooltip content="详情">
<div <div
class="i-material-symbols:info-outline :hover:color-[#8ACE6A] color-[#4B9E5F] cursor-pointer text-20px" class="i-material-symbols:info-outline :hover:color-[#8ACE6A] color-[#4B9E5F] cursor-pointer text-20px"
@click="onDeviceDetails(item)"></div> @click="onDeviceDetails(item)"
></div>
</el-tooltip> </el-tooltip>
</div> </div>
</div> </div>
<div class="device-item-body relative"> <div class="device-item-body relative">
@ -85,168 +109,46 @@
</div> </div>
</template> </template>
</template> </template>
<div class="absolute l-0 t-0 w-full h-full z-10 bg-#FFF-90"
v-if="['updating', 'pending', 'rejected', 'timeout'].includes(item.upFirmware)">
<div class="i-material-symbols-light:close absolute-rt text-base text-gray-950 cursor-pointer" v-if="
['timeout', 'rejected', 'sc'].includes(item.upFirmware) ||
(item.upFirmwareStatus?.step === 4 && item.upFirmwareStatus?.progress === 100)"
@click="upFirmwareSucceed(item.sn)">
</div>
<template v-if="item.upFirmware === 'updating'">
<div class="device-item-body">
<div class="info-item">
<div>当前步骤:</div>
<div>{{item.upFirmwareStatus?.step === 4 && item.upFirmwareStatus?.progress === 100 ? '安装完成' :
upgradeProgressStatusMap.find(r => r.status === item.upFirmwareStatus?.step)?.text ??
'--'}}
</div>
</div>
<div class="info-item">
<div>当前进度:</div>
<el-progress class="flex-1" :stroke-width="12" :show-text="true"
:percentage="item.upFirmwareStatus?.progress || 0" />
</div>
</div>
</template>
<template v-else-if="item.upFirmware === 'pending'">
<div class="w-full h-full flex items-center justify-center">
<div class="font-400 text-base text-[#4B9E5F]">等待升级中...</div>
</div>
</template>
<template v-if="item.upFirmware === 'rejected'">
<div class="device-item-body">
<div class="info-item">
<div>错误发生步骤:</div>
<div class="text-red">
{{
upgradeProgressStatusMap.find(
r => r.status == item.upFirmwareStatus?.step
)?.text ?? '--'
}}失败
</div>
</div>
<div class="info-item">
<div>错误信息:</div>
<div>{{ item.upFirmwareStatus?.errMsg ?? '--' }}</div>
</div>
</div>
</template>
<template v-if="item.upFirmware === 'timeout'">
<div class="w-full h-full flex items-center justify-center">
<div class="font-400 text-base text-[#E6A23C]">升级超时</div>
</div>
</template>
</div>
</div> </div>
</div> </div>
</el-checkbox-group> </el-checkbox-group>
</div> </div>
</EdfsWrap> </EdfsWrap>
<TransferMask v-model="isShowTransferMask" :transferLoading="transferLoading" @close="closeTransferMask">
<template v-if="curTransfer === 'export'">
<div class="flex-col gap-col-10 h-full w-56% justify-center">
<div class="flex items-center gap-col-1">
<div class="flex-1 flex items-center">
<el-progress :percentage="100" class="flex-1" :stroke-width="18" :text-inside="true"
:striped="transferStatus === 'progress'" :striped-flow="transferStatus === 'progress'" :duration="20"
:status="['progress', 'success', undefined].includes(transferStatus)
? 'success'
: 'exception'
">
{{
transferStatusMap[transferStatus as keyof typeof transferStatusMap] ?? ''
}}
</el-progress>
</div>
<el-button v-if="transferStatus === 'progress'" type="primary" @click="onStopTransfer">停止迁移</el-button>
</div>
<div class="transfer-log-wrap h-490 flex-col">
<div class="text-16px font-500">迁移日志</div>
<el-scrollbar class="flex-1" ref="transferLogScrollbar">
<div v-for="i in curTransferLog" :class="i.status === 'failed' ? 'text-red-500' : ''"
class="text-gray-600">
{{ i.msg }}
</div>
</el-scrollbar>
</div>
</div>
</template>
<template v-else-if="curTransfer === 'import'">
<div class="flex-col gap-col-10 h-full w-56% justify-center">
<div class="flex items-center gap-col-1">
<div class="flex-1 items-center">
<el-progress :percentage="100" :status="['progress', 'success', undefined].includes(onOffDeviceTransferStatus)
? 'success'
: 'exception'
" class="flex-1" :stroke-width="18" :text-inside="true"
:striped="onOffDeviceTransferStatus === 'progress' || !onOffDeviceTransferStatus"
:striped-flow="onOffDeviceTransferStatus === 'progress' || !onOffDeviceTransferStatus" :duration="20">
<div class="text-16px font-500"> {{ !!onOffDeviceTransferStatus ? onOffDeviceTransferStatus ===
'progress'
? '数据导入中' : `数据导入完成 (失败:${offLineTransferRes().error}个 超时:${offLineTransferRes().timeout}个) ` :
'数据导入中'
}}</div>
</el-progress>
</div>
</div>
<div class="h-490 border-radius-8px bg-[#F9FAFB] p-10 flex-col">
<div class="text-16px font-500">迁移日志</div>
<el-scrollbar class="flex-1" ref="transferLogScrollbar">
<div v-for="i in siteTransferLogList"
:class="['error', 'timeout'].includes(i.status) ? 'text-red-500' : ''" class="text-gray-600">
{{ i.device.sn }}{{ i.msg }}
</div>
</el-scrollbar>
</div>
</div>
</template>
</TransferMask>
</div> </div>
<OnLineTransferDlg ref="onLineTransferDlgRef" @on-save="onLineDeviceTransfer" :is-batch-transfer="isBatchTransfer" /> <OnLineTransferDlg
<OffTransferDlg ref="offTransferDlg" :isBatchTransfer="false" :siteInfo="siteInfo" @on-save="onOffDeviceTransfer" /> ref="onLineTransferDlgRef"
<DeviceDrawer v-model="isShowDetails" ref="deviceDrawerRef" :siteInfo="siteInfo" :is-transfer="isonLineTransfer" /> @on-save="onLineDeviceTransfer"
:is-batch-transfer="isBatchTransfer"
/>
<OffTransferDlg
ref="offTransferDlgRef"
:isBatchTransfer="false"
:siteInfo="siteInfo"
@on-save="onOffDeviceTransfer"
/>
<DeviceDrawer
v-model="isShowDetails"
ref="deviceDrawerRef"
:siteInfo="siteInfo"
:is-transfer="isonLineTransfer"
/>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import dayjs from 'dayjs' import dayjs from 'dayjs'
import TransferMask from './components/transferMask.vue'
import OnLineTransferDlg from './components/onLineTransferDlg.vue' import OnLineTransferDlg from './components/onLineTransferDlg.vue'
import ZMQWorker from '@/composables/useZMQJsonWorker'
import OffTransferDlg from './components/offTransferDlg.vue' import OffTransferDlg from './components/offTransferDlg.vue'
import {
getPubInitData,
ZmqMsgResultType,
type PublishMsg,
type PubMsgData,
type TimeoutMsg,
} from '@/utils/zmq'
import { useTransferDataStore } from '@/stores/transferData' import { useTransferDataStore } from '@/stores/transferData'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import type { import type { IOfflineDevice, IOnlineDevice } from './type'
IOfflineDevice,
IOnlineDevice,
IUpFirmwareStatus,
UpFirmwarsDevice,
} from './type'
import { useMessage } from '@/composables/useMessage' import { useMessage } from '@/composables/useMessage'
import { getDeviceList, type ISite } from '@/api/module/transfer' import { getDeviceList, type ISite } from '@/api/module/transfer'
import DeviceDrawer from './components/deviceDrawer.vue' import DeviceDrawer from './components/deviceDrawer.vue'
import {
getFirmwareUpTopic,
getTransferTopic,
postFirmwareUpTopic,
postTransferTopic,
upgradeProgressStatusMap,
} from './utils'
import { getFirmwarePath } from '@/api/module/firmware' import { getFirmwarePath } from '@/api/module/firmware'
import { createTask, type TaskCreateParams } from '@/api/module/taks'
import EdfsWrap from "@/components/Edfs-wrap.vue";
const env = import.meta.env const env = import.meta.env
const onLineTransferDlgRef = ref<typeof OnLineTransferDlg>() const onLineTransferDlgRef = ref<typeof OnLineTransferDlg>()
const router = useRouter() const router = useRouter()
@ -254,181 +156,30 @@ const route = useRoute()
const siteInfo = ref<ISite>( const siteInfo = ref<ISite>(
route.query.site ? JSON.parse(route.query.site as string) : null route.query.site ? JSON.parse(route.query.site as string) : null
) )
const type = ref<'export' | 'details'>(route.query.type as 'export' | 'details')
const type = ref<'export' | 'details'>(route.query.type as 'export' | 'details')
const isonLineTransfer = computed(() => type.value === 'export') const isonLineTransfer = computed(() => type.value === 'export')
const isShowTransferMask = ref(false)
const message = useMessage() const message = useMessage()
const worker = ZMQWorker.getInstance()
const transferDataStore = useTransferDataStore() const transferDataStore = useTransferDataStore()
const { upFirmwarePending, upFirmwareReset, upFirmwareStatus, upFirmwareSucceed, upFirmwareStatusReject, upFirmwareTimeout } =
transferDataStore
const { devicesMap } = storeToRefs(transferDataStore) const { devicesMap } = storeToRefs(transferDataStore)
const transferStatusMap = { const transferStatus = ref<
progress: '迁移中', 'progress' | 'success' | 'failed' | 'timeout' | 'stop' | undefined
success: '迁移成功', >()
failed: '迁移失败',
timeout: '迁移超时',
stop: '迁移已终止',
}
const exportPubDeviceMap = new Map<string, { device: IOnlineDevice; action: 'export' }>()
const curTransferLog = ref<
{ msg: string; host: string; status: 'success' | 'padding' | 'failed' }[]
>([])
const transferStatus = ref<'progress' | 'success' | 'failed' | 'timeout' | 'stop' | undefined>()
const devices = computed(() => { const devices = computed(() => {
return isonLineTransfer.value ? Array.from(devicesMap.value.values()) : offLineDeviceList.value return isonLineTransfer.value
? Array.from(devicesMap.value.values())
: offLineDeviceList.value
}) as Ref<any[]> }) as Ref<any[]>
const transferLoading = ref(false) // 线
function onLineDeviceTransfer() {
const curTransfer = ref<'import' | 'export'>()
function openTransferMask(status: 'import' | 'export') {
isShowTransferMask.value = true
curTransfer.value = status
transferLoading.value = true
}
function closeTransferMask() {
curTransfer.value = undefined
isShowTransferMask.value = false
transferLoading.value = false
curTransferLog.value = []
transferStatus.value = undefined
siteTransferLogList.value = []
pubIdWithOffDevice.clear()
exportPubDeviceMap.clear()
}
function onLineDeviceTransfer(msg: PublishMsg<'export'>, device: IOnlineDevice) {
curTransferLog.value = []
openTransferMask('export')
worker.publish(postTransferTopic, msg, true, zmqTimeoutCb)
exportPubDeviceMap.set(msg.id, { device, action: 'export' })
worker.subscribe(getTransferTopic, zmqExportCb, msg.id)
if (isBatchTransfer.value) { if (isBatchTransfer.value) {
onBatchCancel() onBatchCancel()
} }
} }
const statusMap = {
200: 'success',
1002: 'padding',
1003: 'failed',
}
const zmqExportMSG = ref('')
function zmqExportCb(msg: PubMsgData) {
if (!isonLineTransfer.value) return
const { feedback, result, id } = msg
transferLoading.value = false
if (feedback && feedback[0]) {
const status = feedback[1]
? (statusMap[feedback[1] as keyof typeof statusMap] as
| 'success'
| 'padding'
| 'failed')
: 'failed'
const isLineFeed = feedback[2] && feedback[2].includes('\n')
if (isLineFeed) {
if (zmqExportMSG.value) {
curTransferLog.value.at(-1)!.msg += feedback[2]
} else {
curTransferLog.value.push({
msg: `主机【${feedback[0]}】: ${feedback[2]}`,
host: feedback[0],
status,
})
}
zmqExportMSG.value = ''
} else {
if (!zmqExportMSG.value) {
curTransferLog.value.push({
msg: `主机【${feedback[0]}】: ${zmqExportMSG.value + feedback[2]}`,
host: feedback[0],
status,
})
}
zmqExportMSG.value += typeof feedback[2] === 'string' ? feedback : ''
curTransferLog.value.at(-1)!.msg = zmqExportMSG.value
}
}
transferStatus.value = 'progress'
if (result !== 'progress') {
const curMsgInfo = exportPubDeviceMap.get(id)!
if (!curMsgInfo) return
const { device, action } = curMsgInfo
if (device && action === 'export') {
if (result === 'success') {
const res = setTransferStatus()
if (res === 0) {
message.success(`迁移成功`)
transferStatus.value = 'success'
} else {
message.error(`迁移失败,请检查迁移日志`)
transferStatus.value = 'failed'
}
} else if (['failed', 'failure'].includes(result)) {
message.error(`迁移失败`)
transferStatus.value = 'failed'
}
exportPubDeviceMap.delete(msg.id)
zmqExportMSG.value = ''
}
}
}
function setTransferStatus() {
const failed = curTransferLog.value.filter(i => i.status === 'failed')
for (const f of failed) {
curTransferLog.value.forEach(j => {
if (f.host === j.host) {
j.status = 'failed'
}
})
}
return failed.length
}
function onStopTransfer() {
message.confirm('是否确认停止迁移?').then(() => {
const msg = getPubInitData<'cancel'>('cancel', [], 'no')
worker.publish(postTransferTopic, msg)
message.success('迁移已取消')
exportPubDeviceMap.clear()
transferStatus.value = 'stop'
})
}
function zmqTimeoutCb(msg: TimeoutMsg) {
const { device, action } = exportPubDeviceMap.get(msg.timeoutId)!
if (device && action === 'export') {
message.error(`迁移超时,请重新稍后尝试`)
exportPubDeviceMap.delete(msg.timeoutId)
closeTransferMask()
transferStatus.value === 'failed'
zmqExportMSG.value = ''
}
}
const onlineDeviceMap: Record< const onlineDeviceMap: Record<
keyof Omit< keyof Omit<
IOnlineDevice, IOnlineDevice,
@ -460,16 +211,38 @@ function onBatchTransfer() {
function onBatchTransferSave(checkDeviceList: IOnlineDevice[] | IOfflineDevice[]) { function onBatchTransferSave(checkDeviceList: IOnlineDevice[] | IOfflineDevice[]) {
if (isonLineTransfer.value) { if (isonLineTransfer.value) {
const checkList = checkDeviceList as IOnlineDevice[] const lineDevices = checkDeviceList as IOnlineDevice[]
const clientIpList = checkList.map(item => item?.clientIp).join(',') const site = lineDevices[0]?.site_id
const pathList = checkList if (!site) {
.map(item => `${item?.site_id}/${item?.sn}`) message.error('请选择站点')
.filter(Boolean) return
.join(',') }
const deivcesParms = lineDevices.map(item => ({
onLineTransferDlgRef.value?.open(checkList[0], clientIpList, pathList) sn: item.sn,
host: item.clientIp,
disk: item.footprint,
}))
const parmas: TaskCreateParams = {
site,
devices: deivcesParms,
mode: 'export',
}
onLineTransferDlgRef.value?.open(parmas)
} else { } else {
offTransferDlg.value?.open(checkDeviceList, true) const offlineDevices = checkDeviceList as IOfflineDevice[]
const site = offlineDevices[0]?.site_id
const deivcesParms = offlineDevices.map(item => ({
sn: item.sn,
}))
const parmas: TaskCreateParams = {
site,
devices: deivcesParms,
mode: 'import',
}
offTransferDlgRef.value?.open(parmas)
} }
} }
@ -511,9 +284,7 @@ function getOnlineDeviceList(): IOnlineDevice[] {
} }
function getOfflineDeviceList(): IOfflineDevice[] { function getOfflineDeviceList(): IOfflineDevice[] {
return offLineDeviceList.value.filter(item => return offLineDeviceList.value.filter(item => checkDeviceList.value.includes(item.sn))
checkDeviceList.value.includes(item.sn)
)
} }
function onBatchCancel() { function onBatchCancel() {
@ -521,13 +292,6 @@ function onBatchCancel() {
isBatchUpgrade.value = false isBatchUpgrade.value = false
checkDeviceList.value = [] checkDeviceList.value = []
} }
function onTransfer(item: IOnlineDevice) {
if (isonLineTransfer.value) {
onLineTransferDlgRef.value?.open(item)
} else {
offTransferDlg.value?.open([item])
}
}
function onBack() { function onBack() {
router.push('/station') router.push('/station')
@ -583,202 +347,43 @@ function onDeviceDetails(item: IOfflineDevice) {
const isCanFirmwareUpload = computed(() => !!firmwarePath.value && isonLineTransfer.value) const isCanFirmwareUpload = computed(() => !!firmwarePath.value && isonLineTransfer.value)
const upgradeSnList = ref<string[]>([]) const upgradeSnList = ref<string[]>([])
const upgradePubDeviceMap = new Map<string, UpFirmwarsDevice>() async function onFirmwareUpload(devices: IOnlineDevice[]) {
function onFirmwareUpload(devices: IOnlineDevice[]) {
upgradeSnList.value = [] upgradeSnList.value = []
if (!isCanFirmwareUpload.value) { if (!isCanFirmwareUpload.value) {
message.error('升级失败,请检查固件路径') message.error('升级失败,请检查固件路径')
onBatchCancel() onBatchCancel()
return return
} }
let deviceSn = devices[0].sn const site = devices[0]?.site_id
if (!site) {
if (isBatchUpgrade.value) { message.error('请选择站点')
deviceSn = devices.map(item => item.sn).join(',')
}
const msg = getPubInitData<'upgrade'>('upgrade', [deviceSn, firmwarePath.value])
for (const device of devices) {
upgradePubDeviceMap.set(msg.id, {
device: device,
action: 'upgrade',
})
}
worker.publish(postFirmwareUpTopic, msg, true, firmwareUpTimeoutCb, true)
worker.subscribe(getFirmwareUpTopic, zmqUpgradeCb, msg.id)
upgradeSnList.value = deviceSn.split(',')
upFirmwarePending(upgradeSnList.value)
onBatchCancel()
}
function firmwareUpTimeoutCb(msg: TimeoutMsg) {
const { device, action } = upgradePubDeviceMap.get(msg.timeoutId)!
if (device && action === 'upgrade') {
const timeoutUpgradeSnList = upgradeSnList.value
if (timeoutUpgradeSnList.length === 0) {
upFirmwareReset(upgradeSnList.value)
upgradePubDeviceMap.delete(msg.timeoutId)
return
}
for (const deviec of timeoutUpgradeSnList) {
upFirmwareTimeout(deviec)
}
upgradePubDeviceMap.delete(msg.timeoutId)
message.warning(`固件升级超时,请稍后重试`)
}
}
function zmqUpgradeCb(msg: PubMsgData) {
if (!isonLineTransfer.value) return
const status = msg.code
const deviceSn = msg.feedback[0]
const progressStatus = msg.feedback[1] as number
const progress = msg.feedback[2] || undefined
const curentDevice = upgradePubDeviceMap.get(msg.id)
if (curentDevice && curentDevice.action === 'upgrade') {
const { device } = curentDevice
if (device) {
if (status === ZmqMsgResultType.PROGRESS) {
upFirmwareStatus(deviceSn, msg.feedback)
if (progressStatus === 4 && progress === 100) {
upFirmwareSucceed(deviceSn)
}
}
}
if (status === ZmqMsgResultType.SUCCESS || status === ZmqMsgResultType.ERROR) {
upFirmwareStatus(deviceSn, msg.feedback)
if (status === ZmqMsgResultType.ERROR) {
upFirmwareStatusReject(deviceSn, msg.feedback)
}
upgradeSnList.value = upgradeSnList.value.filter(item => item !== deviceSn)
}
}
}
// ================线=========
const offTransferDlg = ref<typeof OffTransferDlg>()
const pubIdWithOffDevice = new Map<string, { offDevice: IOfflineDevice; action: 'import' }>()
const importQueue = ref<{ msg: PublishMsg<'import'>; offDevice: IOfflineDevice }[]>([])
const isImporting = ref(false)
async function processNextImport() {
if (importQueue.value.length === 0 || isImporting.value) {
onOffDeviceTransferStatus.value = 'success'
return return
} }
const deivcesParms = devices.map(item => ({
isImporting.value = true sn: item.sn,
const { msg, offDevice } = importQueue.value[0] host: item.clientIp,
disk: item.footprint,
pubIdWithOffDevice.set(msg.id, { offDevice, action: 'import' }) }))
worker.publish(postTransferTopic, msg, true, zmqImportTimeoutCb)
worker.subscribe(getTransferTopic, zmqImportCb, msg.id) const parmas: TaskCreateParams = {
site,
} devices: deivcesParms,
mode: 'update',
function onOffDeviceTransfer(msg: PublishMsg<'import'>[], offDevice: IOfflineDevice[]) {
msg.forEach((m, index) => {
importQueue.value.push({ msg: m, offDevice: offDevice[index] })
})
processNextImport()
onBatchCancel()
openTransferMask('import')
}
const siteTransferLogList = ref<Array<{
msg: string
device: IOfflineDevice
status: 'success' | 'timeout' | 'error'
}>>([])
const offLineTransferRes = () => {
let timeoutNum = 0
let errorNum = 0
const findTimeout = siteTransferLogList.value.filter(i => i.status === 'timeout')
const uniqueTimeoutSn = new Set(findTimeout.map(i => i.device.sn))
timeoutNum = uniqueTimeoutSn.size
const findError = siteTransferLogList.value.filter(i => i.status === 'error')
errorNum = new Set(findError.map(i => i.device.sn)).size
return {
timeout: timeoutNum,
error: errorNum,
} }
} const res = await createTask(parmas)
if (res.code !== 0) {
const importMsg = ref('') message.error(`任务创建失败`)
const onOffDeviceTransferStatus = ref<'progress' | 'success' | undefined>() } else {
function zmqImportCb(msg: PubMsgData) { message.success('任务创建成功,请在任务列表中查看')
const { id, feedback, result, code } = msg
transferLoading.value = false
const { offDevice, action } = pubIdWithOffDevice.get(id)!
if (action !== 'import' || !offDevice) return
if (code === ZmqMsgResultType.PROGRESS) {
onOffDeviceTransferStatus.value = 'progress'
const log: string = Array.isArray(feedback) ? feedback[0] || '' : ''
const isLineFeed = log && log.includes('\n')
if (isLineFeed) {
if (importMsg.value) {
siteTransferLogList.value.at(-1)!.msg += log
} else {
siteTransferLogList.value.push({
msg: log,
device: offDevice,
status: 'success'
})
}
importMsg.value = ''
} else {
if (!importMsg.value) {
siteTransferLogList.value.push({
msg: log,
device: offDevice,
status: 'success'
})
}
importMsg.value += typeof log === 'string' ? log : ''
siteTransferLogList.value.at(-1)!.msg = importMsg.value
}
} }
if (code !== ZmqMsgResultType.PROGRESS) { onBatchCancel()
importQueue.value.shift()
isImporting.value = false
if (code === ZmqMsgResultType.ERROR) {
siteTransferLogList.value.push({
msg: `数据导入失败,请稍后重试消息结果:${result}`,
device: offDevice,
status: 'error'
})
}
importMsg.value = ''
processNextImport()
}
} }
function zmqImportTimeoutCb(msg: TimeoutMsg) { // ================ 线 =========
const { offDevice, action } = pubIdWithOffDevice.get(msg.timeoutId)! const offTransferDlgRef = ref<typeof OffTransferDlg>()
if (offDevice && action === 'import') { function onOffDeviceTransfer() {
message.error(`站点:${offDevice.sn}数据导出超时,请稍后重试`) onBatchCancel()
transferLoading.value = false
pubIdWithOffDevice.delete(msg.timeoutId)
siteTransferLogList.value.push({
msg: `数据导入超时,请稍后重试`,
device: offDevice,
status: 'timeout'
})
importQueue.value.shift()
isImporting.value = false
importMsg.value = ''
processNextImport()
}
} }
// ================ 线 end ========= // ================ 线 end =========

154
src/views/taskList/index.vue

@ -0,0 +1,154 @@
<template>
<div class="task-list wh-full">
<EdfsWrap title="任务列表" class="h-full">
<EdfsTable class="wh-full" v-loading="loading" :data="list" ref="tableRef"
:highlight-current-row="true"
:page-total="total" :current-page="queryParams.page" :page-size="queryParams.size"
row-class-name="row"
@pageCurrentChange="handleJump">
<template v-for="(col, idx) in tableCol" :key="idx">
<el-table-column v-if="col.prop.endsWith('Time')" :label="col.label"
:min-width="col.minWidth">
<template #default="scope">
{{ dayjs(scope.row.createTime).format('YYYY-MM-DD HH:mm:ss') }}
</template>
</el-table-column>
<el-table-column v-else-if="col.prop === 'status'" :prop="col.prop" :label="col.label"
:min-width="col.minWidth">
<template #default="scope">
{{ taskStatus.find(r => r.value === scope.row.status)?.label || '--' }}
</template>
</el-table-column>
<el-table-column v-else-if="col.prop === 'mode'" :prop="col.prop" :label="col.label"
:min-width="col.minWidth">
<template #default="scope">
{{ taskMode.find(r => r.value === scope.row.mode)?.label || '--' }}
</template>
</el-table-column>
<el-table-column v-else :prop="col.prop" :label="col.label" :min-width="col.minWidth"/>
</template>
<el-table-column label="操作" width="210" align="center">
<template #default="scope">
<EdfsButton size="small" link type="primary" inner-text="详情"
@click="onDetail(scope.row)"/>
<EdfsButton size="small" link type="danger" v-if="[0, 1].includes(scope.row.mode)"
inner-text="取消任务"
@click="onCancel(scope.row.id)"/>
</template>
</el-table-column>
</EdfsTable>
</EdfsWrap>
</div>
<InfoDrawer ref="InfoDrawerRef" @task-finish="onTaskFinish"/>
</template>
<script setup lang="ts">
import {
cancelTask,
getTaskInfo,
getTaskList,
type TaskInfo,
type TaskList
} from '@/api/module/taks'
import { useMessage } from '@/composables/useMessage'
import EdfsWrap from "@/components/Edfs-wrap.vue";
import EdfsTable from "@/components/Edfs-table/index.vue";
import EdfsButton from "@/components/Edfs-button.vue";
import dayjs from 'dayjs'
import InfoDrawer from './infoDrawer.vue'
const message = useMessage()
const taskStatus = [{
value: -1,
label: '执行失败'
}, {
label: '等待执行',
value: 0
}, {
label: '执行中',
value: 1
}, {
label: '已取消',
value: 2
}, {
label: '执行成功',
value: 3
}]
const taskMode = [{
value: 'export',
label: '迁移'
}, {
value: 'import',
label: '导入'
}, {
value: 'update',
label: '升级'
}]
const tableCol = [
{ label: '任务ID', prop: 'id', minWidth: '10%' },
{ label: '站点', prop: 'site', minWidth: '16%' },
{ label: '任务类型', prop: 'mode', minWidth: '10%' },
{ label: '任务状态', prop: 'status', minWidth: '10%' },
{ label: '创建时间', prop: 'startTime', minWidth: '10%' },
]
const loading = ref(true)
const total = ref(0)
const list = ref<TaskList[]>([])
const queryParams = reactive({
page: 1,
size: undefined,
})
const tableRef = ref()
const getList = async () => {
if (!queryParams.size) {
queryParams.size = tableRef.value.getSize()
}
loading.value = true
const res = await getTaskList(queryParams as any)
if (res !== null && res.code === 0) {
list.value = res.data.tasks
total.value = res.data.total
}
loading.value = false
}
function handleJump(page: number) {
queryParams.page = page
getList()
}
const InfoDrawerRef = ref<typeof InfoDrawer>()
function onDetail(row: TaskList) {
InfoDrawerRef.value?.open(row)
}
const onCancel = async (id: string) => {
await message.delConfirm(`是否确认取消该任务?`)
const res = await cancelTask(id)
if (res.code !== 0) return
await getList()
}
function onTaskFinish(sn: string, status: -1 | 0 | 1 | 2 | 3) {
const curTask = list.value.findIndex(item => item.id === sn)
if (curTask !== -1) {
list.value[curTask].status = status
}
}
onMounted(() => {
getList()
})
</script>
<style scoped></style>

137
src/views/taskList/infoDrawer.vue

@ -0,0 +1,137 @@
<template>
<div class="fault-rule-drawer">
<el-drawer v-model="isShowDrawer" title="任务详情" direction="rtl" size="50%"
modal-class="model-dev-opn"
:before-close="handleBeforeClose">
<main class="wh-full" v-loading="loading">
<template v-for="detail in detailList">
<el-descriptions border :title="`${detail.sn}任务详情`">
<el-descriptions-item :label-width="60" label="状态">
<el-tag size="small">{{
statusList.find(r => r.value ===
detail.status)?.label || '--'
}}
</el-tag>
</el-descriptions-item>
<el-descriptions-item :label-width="100" label="总数任务量">{{
detail.total
}}
</el-descriptions-item>
<el-descriptions-item :label-width="110" label="已完任务量">{{
detail.finish
}}
</el-descriptions-item>
<el-descriptions-item :label-width="60" label="信息">
{{ detail?.info ? detail.info : '暂无信息' }}
</el-descriptions-item>
<el-descriptions-item label="当前进度">
<template v-if="detail.total === 0">
<span>暂无进度</span>
</template>
<el-progress v-else style="width: 95%;"
:percentage="Math.floor((detail.finish / detail.total) * 100)"
:text-inside="true" :stroke-width="20"/>
</el-descriptions-item>
</el-descriptions>
</template>
</main>
</el-drawer>
</div>
</template>
<script setup lang="ts">
import { getSubTopic, type SubMsgData } from '@/utils/zmq'
import ZMQWorker from '@/composables/useZMQJsonWorker'
import { getTaskInfo, type TaskInfo, type TaskList } from '@/api/module/taks'
import { useMessage } from '@/composables/useMessage'
const emit = defineEmits<{ 'task-finish': [string, -1 | 0 | 1 | 2 | 3] }>()
const message = useMessage()
const worker = ZMQWorker.getInstance()
const isShowDrawer = defineModel<boolean>()
const detailList = ref<TaskInfo[]>([])
const loading = ref(false)
async function onDetail(row: TaskList) {
loading.value = true
const res = await getTaskInfo(row.id);
if (res.code === 0) {
detailList.value = res?.data?.details ?? []
} else {
message.error('获取任务详情失败')
}
loading.value = false
}
const currentTaskId = ref('')
async function open(row: TaskList) {
isShowDrawer.value = true
currentTaskId.value = row.id
await onDetail(row)
worker.subscribe(getSubTopic('server', 'event', 'task'), zmqTaskCb)
}
const statusList = [
{
value: -1,
label: '执行失败'
}, {
label: '等待执行',
value: 0
}, {
label: '执行中',
value: 1
}, {
label: '已取消',
value: 2
}, {
label: '执行成功',
value: 3
}
]
function zmqTaskCb(msg: SubMsgData) {
const { feedback } = msg
const taskId = feedback[0]
const deviceSN = feedback[1]
const deviceStatus = feedback[2] || '未知状态'
const finish = feedback[3] || 0
const total = feedback[4] || 0
if (currentTaskId.value !== taskId) return
const detail = detailList.value.find(item => item.sn === deviceSN)
if (!detail) return
detail.status = deviceStatus
detail.finish = finish
detail.total = total
if (detail.finish === detail.finish) {
emit('task-finish', deviceSN, deviceStatus)
}
}
function handleBeforeClose(done: () => void) {
isShowDrawer.value = false
currentTaskId.value = ''
done()
}
defineExpose({
open,
})
</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>

100
src/views/testG6/components/Node.vue

@ -0,0 +1,100 @@
<template>
<div :style="{
width: NODE_SIZE[0] + 'px',
height: NODE_SIZE[1] + 'px',
borderRadius: '6px',
marginLeft: -(NODE_SIZE[0] / 2 - 10) + 'px',
}" class="box-border rounded-md overflow-hidden flex">
<div class="h-100% p-4px" :style="{ background: Device.bgColor}">
<img :src="Device.icon" style="height: 100%;" alt="">
</div>
<div class="bg-white flex-1 flex items-center justify-center">
<span class="text-12px">{{ Device.name }}</span>
</div>
</div>
</template>
<script setup lang="ts">
import type { MyNodeData } from "../type/index.ts";
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.Ems]: {
icon: EmsIcon,
bgColor: '#0769FF'
},
[DeviceType.Ecu]: {
icon: EcuIcon,
bgColor: '#0769FF'
},
[DeviceType.Em]: {
icon: EmIcon,
bgColor: '#2CA02C'
},
[DeviceType['Em-Measure']]: {
icon: EmIcon,
bgColor: '#2CA02C'
},
[DeviceType.Bms]: {
icon: BmsIcon,
bgColor: '#FF7F50'
},
[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.Pcs]: {
icon: PcsIcon,
bgColor: '#FF7F50'
},
[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];
return Object.assign(props.data.data, style);
})
</script>
<style scoped lang="scss">
</style>

93
src/views/testG6/index.vue

@ -0,0 +1,93 @@
<template>
<div id="container" class="w-full h-full"></div>
</template>
<script setup lang="ts">
import DeviceData from './utils/data';
import { EmLineEdge, MyLineEdge } from './utils/MyLineEdge';
import { DeviceType, flattenTree, NODE_SIZE, VIRTUAL_NODE } from "./utils";
import { ExtensionCategory, Graph, register } from '@antv/g6';
import GenerateGraphData from "./utils/GenerateGraphData";
import { VueNode } from 'g6-extension-vue';
import Node from './components/Node.vue'
onMounted(() => {
register(ExtensionCategory.NODE, 'vue-node', VueNode);
register(ExtensionCategory.EDGE, 'em-line-edge', EmLineEdge)
register(ExtensionCategory.EDGE, 'my-line-edge', MyLineEdge);
const device = flattenTree(DeviceData)
const graphData = new GenerateGraphData(device)
const graph = new Graph({
container: document.getElementById('container')!,
padding: 50,
autoFit: {
type: 'center',
},
layout: {
type: 'dagre',
nodesep: 50 + NODE_SIZE[0],
ranksep: 50,
},
animation: false,
node: {
type: 'vue-node',
style: {
component: (data: any) => {
return data.data.type !== VIRTUAL_NODE ? h(Node, { data: data }) : ''
},
},
},
data: {
nodes: graphData.getNodes(),
edges: graphData.getEdges()
},
plugins: [
{
type: 'contextmenu',
enable: (e: any) => e.targetType === 'node',
getItems: () => {
return [{ name: '查看详情', value: 'detail' }];
},
onClick: (value: string, item: HTMLElement, current: any) => {
console.log('展示节点详情', current.config.context.model.getNodeData(current.config.id));
},
},
],
behaviors: ['drag-canvas', 'zoom-canvas'],
});
graph.render();
graph.on('afterlayout', async () => {
const nodes = graph.getNodeData();
const root = nodes.find(n => n.data?.type === DeviceType.Ems);
if (!root) return;
const emNodes = nodes.filter(n =>
n.data?.type === DeviceType.Em || n.data?.type === DeviceType['Em-Measure']
);
const [x, y] = graph.getElementPosition(root.id);
graph.updateNodeData(
emNodes.map((n, idx) => {
//
return {
id: n.id,
style: Object.assign(
{},
n.style,
{ // Em
x: x + NODE_SIZE[0] * 2,
y: -y - (emNodes.length * 10) + (idx * 60),
},
)
}
})
);
await graph.draw();
await graph.fitView();
});
})
;
</script>

5
src/views/testG6/type/index.ts

@ -0,0 +1,5 @@
import type { NodeData } from "@antv/g6";
export type Device = any
export interface MyNodeData extends Omit<NodeData, 'data'> {
data?: Device;
}

114
src/views/testG6/utils/GenerateGraphData.ts

@ -0,0 +1,114 @@
import type { EdgeData, NodeData } from "@antv/g6";
import { DeviceType, VIRTUAL_NODE } from "./index";
import EmsIcon from '@/assets/images/device/ems.png';
import type { Device, MyNodeData } from "../type";
export default 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) {
const deviceChild = this.devices.filter((item: Device) => device.id === item.parentId)
const isNeedPoint = device.type === DeviceType.Ems || device.type === DeviceType['Em-Measure'] || device.type === DeviceType.Em
this.nodes.push({
id: String(device.id),
label: device.name,
data: Object.assign(device, {
typeString: DeviceType[device.type],
}),
style: Object.assign(
{
// labelText: device.name,
opacity: .3,
icon: (d: any) => EmsIcon,
})
})
if (!!deviceChild.length) {
// 在每个有子节点的设备下插入一个虚拟节点
this.nodes.push({
id: `${device.id}-virtual`,
label: '',
type: '',
style: { opacity: 0 },
size: 0,
anchorPoints: [],
data: {
type: VIRTUAL_NODE,
parentId: String(device.id),
depth: device.depth + 1,
},
})
}
}
}
private generateEdgeData() {
for (const node of this.nodes) {
const customData: Device = node.data
const virtualId = `${customData.parentId}-virtual`;
const findParentNode: Device = this.nodes.some((n: MyNodeData) => n.id === String(customData.parentId));
if (this.isVirtualNode(customData.type)) {
this.edges.push({
source: customData.parentId,
target: String(node.id),
type: 'my-line-edge',
data: customData,
})
continue;
}
//
if (findParentNode) {
if (this.isEms(customData.type)) continue;
if (this.isEm(customData.type)) {
// 如果是 电表 类型的节点 target 指向父节点Ems
this.edges.push({
target: String(customData.parentId),
source: String(node.id),
type: 'em-line-edge',
data: customData,
})
continue;
}
this.edges.push({
source: virtualId,
target: String(node.id),
type: 'my-line-edge',
data: customData,
})
}
}
}
isEm(type: number): boolean {
return type == DeviceType["Em-Measure"] || type == DeviceType.Em
}
isEms(type: number): boolean {
return type == DeviceType.Ems
}
isVirtualNode(type: number | string): boolean {
return type === VIRTUAL_NODE;
}
getNodes(): NodeData[] {
return this.nodes as NodeData[];
}
getEdges(): EdgeData[] {
return this.edges;
}
}

146
src/views/testG6/utils/MyLineEdge.ts

@ -0,0 +1,146 @@
import { BaseEdge } from '@antv/g6';
import type { PathArray } from "@antv/util";
import { Circle } from '@antv/g';
import { subStyleProps } from '@antv/g6';
import { NODE_SIZE } from "./index";
import type { Device } from "../type";
export class MyLineEdge extends BaseEdge {
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 - 1;
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,
// });
}
protected getKeyPath(attrs: any): PathArray {
const [sourcePoint, targetPoint] = this.getEndpoints(attrs, false)
const [sx, sy] = [sourcePoint[0], sourcePoint[1]];
const [tx, ty] = [targetPoint[0], targetPoint[1]];
// 固定的主干 X
const busX = attrs.busX ?? tx;
return [
['M', sx, sy],
['L', busX, sy],
['L', busX, ty],
['L', tx, ty],
];
}
}
export class EmLineEdge extends BaseEdge {
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 delay = 0;
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,
}
);
}
getKeyPath(attributes: any): PathArray {
const [sourcePoint, targetPoint] = this.getEndpoints(attributes, false);
return [
['M', sourcePoint[0], sourcePoint[1]],
['L', targetPoint[0] / 2 + (1 / 2) * sourcePoint[0], sourcePoint[1]],
['L', targetPoint[0] / 2 + (1 / 2) * sourcePoint[0], targetPoint[1]],
['L', targetPoint[0], targetPoint[1]],
];
}
}

72
src/views/testG6/utils/MyNode.ts

@ -0,0 +1,72 @@
import { Graph, register, Rect, ExtensionCategory } from '@antv/g6';
import { DeviceType, NODE_SIZE } from "./index";
// const typeToIconMap = Object.entries(DeviceType).filter(([key, value]) => typeof value === 'number')
// console.log(typeToIconMap)
export default class UserCardNode extends Rect {
get nodeData() {
return this.context.graph.getNodeData(this.id);
}
get data() {
return this.nodeData.data || {};
}
// 用户名样式
drawNameShape(attributes: any, container: any) {
if (!this.data.name) return;
const getUsernameStyle = (attributes: any) => {
return {
x: -14,
text: this.data.name || '',
// fontSize: 12,
width: 20,
fill: '#262626',
fontWeight: 'bold',
textAlign: 'left',
textBaseline: 'middle',
maxWidth: 80, // 限制最大宽度
ellipsis: true, // 超出显示 ...
};
}
const usernameStyle = getUsernameStyle(attributes);
this.upsert('username', 'text', usernameStyle, container);
}
// 头像样式
getAvatarStyle(attributes: any) {
const [width, height] = this.getSize(attributes);
const type = this!.data!.type
return {
x: -width / 2 + 4,
y: -height / 2 + 6,
width: 24,
height: 24,
src: typeToIconMap[type as keyof typeof type] || '',
};
}
drawAvatarShape(attributes: any, container: any) {
if (!this.data.typeString) return;
const avatarStyle = this.getAvatarStyle(attributes);
this.upsert('avatar', 'image', avatarStyle, container);
}
render(attributes: any, container: any) {
// 渲染基础矩形
super.render(attributes, container);
this.drawAvatarShape(attributes, container);
this.drawNameShape(attributes, container);
}
}

239
src/views/testG6/utils/data.ts

@ -0,0 +1,239 @@
export default [{
"id": 398,
"sn": "ems1",
"name": "安庆滨江夜市站",
"sort": 0,
"parentId": 0,
"deviceId": "t00510398",
"dictName": "device_one_entity_type",
"level": 0,
"type": 0,
"description": null,
"children": [
{
"id": 400,
"sn": "yiming-EMS1",
"name": "储能单元1",
"sort": 1,
"parentId": 398,
"deviceId": "t00510400",
"dictName": "device_two_entity_type",
"level": 1,
"type": 1,
"description": "",
"children": [
{
"id": 404,
"sn": "B48100B210TF36003E-bms",
"name": "BMS-1",
"sort": 1,
"parentId": 400,
"deviceId": "t00510404",
"dictName": "",
"level": 2,
"type": 2,
"description": "",
"children": null
},
{
"id": 408,
"sn": "B48100B210TF36003E-pcs",
"name": "PCS-1",
"sort": 1,
"parentId": 400,
"deviceId": "t00510408",
"dictName": "",
"level": 2,
"type": 3,
"description": "",
"children": null
}
]
},
{
"id": 401,
"sn": "yiming-EMS2",
"name": "储能单元2",
"sort": 1,
"parentId": 398,
"deviceId": "t00510401",
"dictName": "device_two_entity_type",
"level": 1,
"type": 1,
"description": "",
"children": [
{
"id": 405,
"sn": "B48100B210TF36003K-bms",
"name": "BMS-2",
"sort": 1,
"parentId": 401,
"deviceId": "t00510405",
"dictName": "",
"level": 2,
"type": 2,
"description": "",
"children": null
},
{
"id": 4022,
"sn": "B48100B210TF36003K-bms",
"name": "BMS-3",
"sort": 1,
"parentId": 401,
"deviceId": "t005104205",
"dictName": "",
"level": 2,
"type": 2,
"description": "",
"children": null
},
{
"id": 409,
"sn": "B48100B210TF36003K-pcs",
"name": "PCS-2",
"sort": 1,
"parentId": 401,
"deviceId": "t00510409",
"dictName": "",
"level": 2,
"type": 3,
"description": "",
"children": null
}
]
},
{
"id": 402,
"sn": "yiming-EMS3",
"name": "储能单元3",
"sort": 1,
"parentId": 398,
"deviceId": "t00510402",
"dictName": "device_two_entity_type",
"level": 1,
"type": 1,
"description": "",
"children": [
{
"id": 406,
"sn": "B48100B210TF36002V-bms",
"name": "BMS-3",
"sort": 1,
"parentId": 402,
"deviceId": "t00510406",
"dictName": "",
"level": 2,
"type": 2,
"description": "",
"children": null
},
{
"id": 410,
"sn": "B48100B210TF36002V-pcs",
"name": "PCS-3",
"sort": 1,
"parentId": 402,
"deviceId": "t00510410",
"dictName": "",
"level": 2,
"type": 3,
"description": "",
"children": null
}
]
},
{
"id": 403,
"sn": "yiming-EMS4",
"name": "储能单元4",
"sort": 1,
"parentId": 398,
"deviceId": "t00510403",
"dictName": "device_two_entity_type",
"level": 1,
"type": 1,
"description": "",
"children": [
{
"id": 407,
"sn": "B48100B210TF360029-bms",
"name": "BMS-4",
"sort": 1,
"parentId": 403,
"deviceId": "t00510407",
"dictName": "",
"level": 2,
"type": 2,
"description": "",
"children": null
},
{
"id": 411,
"sn": "B48100B210TF360029-pcs",
"name": "PCS-4",
"sort": 1,
"parentId": 403,
"deviceId": "t00510411",
"dictName": "",
"level": 2,
"type": 3,
"description": "",
"children": null
}
]
},
{
"id": 412,
"sn": "B48100B210TF36003E-em",
"name": "电量信息-1",
"sort": 1,
"parentId": 398,
"deviceId": "t00510412",
"dictName": "device_two_entity_type",
"level": 1,
"type": 4,
"description": "",
"children": null
},
{
"id": 413,
"sn": "B48100B210TF36003K-em",
"name": "电量信息-2",
"sort": 1,
"parentId": 398,
"deviceId": "t00510413",
"dictName": "device_two_entity_type",
"level": 1,
"type": 4,
"description": "",
"children": null
},
{
"id": 414,
"sn": "B48100B210TF36002V-em",
"name": "电量信息-3",
"sort": 1,
"parentId": 398,
"deviceId": "t00510414",
"dictName": "device_two_entity_type",
"level": 1,
"type": 4,
"description": "",
"children": null
},
{
"id": 415,
"sn": "B48100B210TF360029-em",
"name": "电量信息-4",
"sort": 1,
"parentId": 398,
"deviceId": "t00510415",
"dictName": "device_two_entity_type",
"level": 1,
"type": 4,
"description": "",
"children": null
}
]
}]

32
src/views/testG6/utils/index.ts

@ -0,0 +1,32 @@
import { flatMap } from 'lodash-es'
import type { Device } from "../type";
export const NODE_SIZE: [number, number] = [130, 36]
export enum DeviceType {
Ems,
Ecu,
Bms,
Pcs,
Em,
'Em-Measure',
Tms,
Ffs,
Wpp,
Mppt,
Dgs,
Cac,
}
export const VIRTUAL_NODE = 'virtual-node'
export function flattenTree(tree: Device[], depth = 1): Device[] {
return flatMap(tree, (node: Device,) => {
const { children, ...rest } = node
const current = { ...rest, depth }
// 有用虚拟节点的存在 depth + 2
return [current, ...(children ? flattenTree(children, depth + 2) : [])]
})
}

10
tsconfig.json

@ -12,6 +12,12 @@
"types": [ "types": [
"node" "node"
], ],
"moduleResolution": "node" "moduleResolution": "node",
} "typeRoots": [
"node_modules/@types"
]
},
"include": [
"src/**/*"
]
} }

43
uno.config.js

@ -0,0 +1,43 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var preset_rem_to_px_1 = require("@unocss/preset-rem-to-px");
var unocss_preset_scalpel_1 = require("unocss-preset-scalpel");
var index_1 = require("./src/uno-preset/src/index");
var unocss_1 = require("unocss");
exports.default = (0, unocss_1.defineConfig)({
shortcuts: [],
theme: {
colors: {},
},
content: {
pipeline: {
include: [
//参考:https://unocss.dev/guide/extracting#extracting-from-build-tools-pipeline
/\.(vue|svelte|[jt]sx|mdx?|astro|elm|php|phtml|html)($|\?)/,
'src/**/*.{js,ts}',
'src/router/index.ts',
'src/views/home/utils/menuConfig.ts',
],
},
},
rules: [['wh-full', { width: '100%', height: '100%' }]],
presets: [
(0, preset_rem_to_px_1.default)(),
(0, unocss_preset_scalpel_1.presetScalpel)(),
(0, unocss_1.presetWind3)(),
(0, index_1.presetSoybeanAdmin)(),
(0, unocss_1.presetAttributify)({
prefix: 'uno-',
prefixedOnly: true,
}),
(0, unocss_1.presetIcons)({
scale: 1.2,
warn: true,
}),
(0, unocss_1.presetTypography)(),
(0, unocss_1.presetWebFonts)({
fonts: {},
}),
],
transformers: [(0, unocss_1.transformerDirectives)(), (0, unocss_1.transformerVariantGroup)()],
});

122
vite.config.ts

@ -1,6 +1,6 @@
import { fileURLToPath, URL } from 'node:url' import { fileURLToPath, URL } from 'node:url'
import UnoCSS from 'unocss/vite' import UnoCSS from 'unocss/vite'
import { defineConfig } from 'vite' import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools' import vueDevTools from 'vite-plugin-vue-devtools'
import AutoImport from 'unplugin-auto-import/vite' import AutoImport from 'unplugin-auto-import/vite'
@ -9,68 +9,74 @@ import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import vueJsx from '@vitejs/plugin-vue-jsx' import vueJsx from '@vitejs/plugin-vue-jsx'
import Icons from 'unplugin-icons/vite' import Icons from 'unplugin-icons/vite'
// Vite 配置文件 // Vite 配置文件
export default defineConfig({ export default defineConfig(({ mode }) => {
plugins: [ const env = loadEnv(mode, process.cwd())
vue(), console.log(env)
vueJsx(), console.log(env.PROD)
vueDevTools(), return {
UnoCSS(), plugins: [
Icons({ vue(),
autoInstall: true, vueJsx(),
}), vueDevTools(),
AutoImport({ UnoCSS(),
imports: ['vue', 'vue-router'], Icons({
resolvers: [ElementPlusResolver()], autoInstall: true,
dts: 'global.types/auto-imports.d.ts', }),
}), AutoImport({
Components({ imports: ['vue', 'vue-router'],
dirs: ['src/components'], resolvers: [ElementPlusResolver()],
extensions: ['vue'], dts: 'global.types/auto-imports.d.ts',
dts: 'global.types/components.d.ts', }),
resolvers: [ElementPlusResolver()], Components({
}), dirs: ['src/components'],
], extensions: ['vue'],
resolve: { dts: 'global.types/components.d.ts',
alias: { resolvers: [ElementPlusResolver()],
'@': fileURLToPath(new URL('./src', import.meta.url)), }),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
}, },
},
build: { build: {
minify: 'terser', outDir: env.VITE_APP_ENV === 'cloud' ? 'dist-cloud' : 'dist-local',
terserOptions: { minify: 'terser',
compress: { terserOptions: {
drop_console: true, compress: {
drop_debugger: true, drop_console: true,
drop_debugger: true,
},
}, },
}, },
}, css: {
css: { preprocessorOptions: {
preprocessorOptions: { scss: {
scss: { additionalData: '@use "@/assets/styles/mixins.scss" as *;',
additionalData: '@use "@/assets/styles/mixins.scss" as *;', },
}, },
}, },
}, define: {
define: { 'process.env': {}
'process.env': {} },
},
// 开发服务器配置 // 开发服务器配置
server: { server: {
// 启动时自动打开浏览器 // 启动时自动打开浏览器
// 开发服务器端口 // 开发服务器端口
port: 3000, port: 3000,
// 允许局域网访问 // 允许局域网访问
host: '0.0.0.0', host: '0.0.0.0',
proxy: { proxy: {
'/remoteServer': { '/remoteServer': {
target: 'http://192.168.1.199:8080/', target: env.VITE_BASE_URL,
changeOrigin: true, changeOrigin: true,
secure: false, secure: false,
ws: true, ws: true,
rewrite: path => path.replace(/^\/remoteServer/, ''), rewrite: path => path.replace(/^\/remoteServer/, ''),
}, },
} }
}, },
}
}) })
Loading…
Cancel
Save