Browse Source

feat: 功能添加

main
betaqi 4 months ago
parent
commit
9abd809062
  1. 2
      .env
  2. 1
      .env.local
  3. 1
      .env.prod
  4. 3
      global.types/components.d.ts
  5. 4
      package.json
  6. 11
      src/api/module/firmware/index.ts
  7. 1
      src/api/module/transfer/index.ts
  8. 16
      src/composables/useZMQJsonWorker.ts
  9. 62
      src/stores/transferData.ts
  10. 4
      src/utils/zmq.ts
  11. 14
      src/utils/zmqJsonWorker.ts
  12. 40
      src/views/firmwareUpload/index.vue
  13. 138
      src/views/stationData/components/PointGroupTree.vue
  14. 169
      src/views/stationData/components/deviceDrawer.vue
  15. 19
      src/views/stationData/components/newDataChart.vue
  16. 35
      src/views/stationData/components/offTransferDlg.vue
  17. 39
      src/views/stationData/components/transferMask.vue
  18. 179
      src/views/stationData/index.vue
  19. 309
      src/views/stationData/transferData.vue
  20. 8
      src/views/stationData/type.ts
  21. 4
      vite.config.ts

2
.env

@ -1 +1,3 @@ @@ -1 +1,3 @@
VITE_BASE_API = '/remoteServer'
VITE_SHOW_ONLINE_DEVICE = true
VITE_APP_ENV = local

1
.env.local

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

1
.env.prod

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

3
global.types/components.d.ts vendored

@ -16,13 +16,13 @@ declare module 'vue' { @@ -16,13 +16,13 @@ declare module 'vue' {
EdfsWrap: typeof import('./../src/components/Edfs-wrap.vue')['default']
ElAside: typeof import('element-plus/es')['ElAside']
ElButton: typeof import('element-plus/es')['ElButton']
ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
ElContainer: typeof import('element-plus/es')['ElContainer']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElDivider: typeof import('element-plus/es')['ElDivider']
ElDrawer: typeof import('element-plus/es')['ElDrawer']
ElHeader: typeof import('element-plus/es')['ElHeader']
ElInput: typeof import('element-plus/es')['ElInput']
@ -34,6 +34,7 @@ declare module 'vue' { @@ -34,6 +34,7 @@ declare module 'vue' {
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
ElTag: typeof import('element-plus/es')['ElTag']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
ElTree: typeof import('element-plus/es')['ElTree']
ElUpload: typeof import('element-plus/es')['ElUpload']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']

4
package.json

@ -8,6 +8,8 @@ @@ -8,6 +8,8 @@
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"build:local": "vite build --mode local",
"build:prod": "vite build --mode prod",
"type-check": "vue-tsc --build"
},
"dependencies": {
@ -48,4 +50,4 @@ @@ -48,4 +50,4 @@
"vite-plugin-vue-devtools": "^7.7.2",
"vue-tsc": "^2.2.2"
}
}
}

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

@ -3,19 +3,20 @@ import { globalServer } from '../index' @@ -3,19 +3,20 @@ import { globalServer } from '../index'
export const uploadFirmwareFile = (params: File, abort: AbortController) =>
export const uploadFirmwareFile = (params: FormData, abort: AbortController) =>
globalServer({
url: 'api/upload',
method: 'POST',
data: params,
signal: abort.signal,
headers: {
'Content-Type': 'multipart/form-data',
},
headers: { 'Content-Type': 'multipart/form-data' },
timeout: 0,
})
export const getFirmwarePath = () => globalServer<string>({
export const getFirmwarePath = () => globalServer<{
path: string
}>({
url: 'api/package-path',
method: 'GET',
})

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

@ -6,6 +6,7 @@ interface IGetDeviceDataParams { @@ -6,6 +6,7 @@ interface IGetDeviceDataParams {
limit?: number
offset?: number
host?: string
name: string
}

16
src/composables/useZMQJsonWorker.ts

@ -2,7 +2,7 @@ import { WorkerCMD, ZmqCMD, } from '@/utils/zmq' @@ -2,7 +2,7 @@ import { WorkerCMD, ZmqCMD, } from '@/utils/zmq'
import type { ManualAction, PublishMsg, PubMsgData, SubMsgData, TimeoutMsg, ZmqMessage } from '@/utils/zmq'
import webWorker from '@/utils/zmqJsonWorker?worker'
const defaultHost = import.meta.env.PROD ? window.location.hostname : '192.168.1.115'
const defaultHost = import.meta.env.PROD ? window.location.hostname : '192.168.1.199'
class ZMQJsonWorker {
private static instance: ZMQJsonWorker | null = null; // ➤ 单例实例
@ -11,6 +11,7 @@ class ZMQJsonWorker { @@ -11,6 +11,7 @@ class ZMQJsonWorker {
private pubTimeoutHandlers: Map<string, (msg: TimeoutMsg) => void> = new Map();
private host: string;
private statusCallback: ((status: string) => void) | null = null;
private isAlwaysListenMsgMap: Map<string, PublishMsg<any>> = new Map();
private constructor(host: string = defaultHost) {
this.host = host;
@ -51,7 +52,7 @@ class ZMQJsonWorker { @@ -51,7 +52,7 @@ class ZMQJsonWorker {
this.worker.postMessage({ cmd: WorkerCMD.UNSUBSCRIBE, topic });
}
publish<T extends ManualAction>(topic: string, msg: PublishMsg<T>, isTimeout: boolean = false, handler?: (msg: TimeoutMsg) => void) {
publish<T extends ManualAction>(topic: string, msg: PublishMsg<T>, isTimeout: boolean = false, handler?: (msg: TimeoutMsg) => void, isAlwaysListen: boolean = false) {
if (isTimeout) {
const timeoutId = msg.id
if (typeof handler !== 'function') {
@ -60,7 +61,10 @@ class ZMQJsonWorker { @@ -60,7 +61,10 @@ class ZMQJsonWorker {
}
this.pubTimeoutHandlers.set(timeoutId, handler)
}
this.worker.postMessage({ cmd: WorkerCMD.PUBLISH, topic, msg: JSON.stringify(msg), isTimeout });
if (isAlwaysListen) {
this.isAlwaysListenMsgMap.set(`${msg.id}`, msg)
}
this.worker.postMessage({ cmd: WorkerCMD.PUBLISH, topic, msg: JSON.stringify(msg), isTimeout, isAlwaysListen });
}
setStatusCallback(callback: (status: string) => void) {
@ -117,8 +121,10 @@ class ZMQJsonWorker { @@ -117,8 +121,10 @@ class ZMQJsonWorker {
this.handleSubscribeMessage(`${topic}-${json.id}`, json);
// 删除发布消息的回调
if (Object.keys(json).includes('result') && json.result !== 'progress') {
this.GC_pubReleaseSub(`${topic}-${json.id}`)
this.GC_pubReleaseTimeout(`${json.id}`)
if (!this.isAlwaysListenMsgMap.has(`${json.id}`)) {
this.GC_pubReleaseSub(`${topic}-${json.id}`)
this.GC_pubReleaseTimeout(`${json.id}`)
}
}
}

62
src/stores/transferData.ts

@ -35,22 +35,27 @@ export const useTransferDataStore = defineStore('transfer', () => { @@ -35,22 +35,27 @@ export const useTransferDataStore = defineStore('transfer', () => {
function getSubDevicesCb(msg: SubMsgData) {
const { feedback } = msg
const sn = feedback[1]
const device: IOnlineDevice = {
clientIp: feedback[0],
sn: sn,
stationName: feedback[2],
footprint: feedback[3] ?? '--',
lastUpdated: Date.now(),
status: '在线', // 初始状态为在线
isChecked: false,
}
const hasDevice = devicesMap.get(sn)
if (hasDevice) {
hasDevice.lastUpdated = Date.now()
hasDevice.status = '在线'
} else {
const device: IOnlineDevice = {
clientIp: feedback[0],
sn: sn,
stationName: feedback[2],
footprint: feedback[3] ?? '--',
lastUpdated: Date.now(),
status: '在线', // 初始状态为在线
isChecked: false,
}
devicesMap.set(sn, device)
}
devicesMap.set(sn, device)
isConnected.value = devicesMap.size > 0
}
const onlineCount = computed(() => {
return Array.from(devicesMap.values()).filter(item => item.status === '在线')
.length
@ -61,9 +66,6 @@ export const useTransferDataStore = defineStore('transfer', () => { @@ -61,9 +66,6 @@ export const useTransferDataStore = defineStore('transfer', () => {
.length
})
onMounted(() => {
worker.subscribe(getDeviceTopic, getSubDevicesCb)
checkDeviceStatus()
@ -84,14 +86,30 @@ export const useTransferDataStore = defineStore('transfer', () => { @@ -84,14 +86,30 @@ export const useTransferDataStore = defineStore('transfer', () => {
const device = devicesMap.get(sn)
if (device) {
device.upFirmware = 'updating'
const step = feedback[0]
const step = feedback[1]
const progress = feedback[2] || undefined
const errMsg = feedback[3] || undefined
if (step < (device.upFirmwareStatus?.step ?? -100)) return
device.upFirmwareStatus = {
step,
progress: progress === -1 ? 100 : progress,
errMsg,
}
}
}
function upFirmwareStatusReject(sn: string, feedback: any[]) {
const device = devicesMap.get(sn)
if (device) {
device.upFirmware = 'rejected'
const step = feedback[1]
const progress = feedback[2] || undefined
const errMsg = feedback[3] || undefined
if (step < (device.upFirmwareStatus?.step ?? -100)) return
device.upFirmwareStatus = {
step,
progress,
errMsg
progress: progress === -1 ? 100 : progress,
errMsg,
}
}
}
@ -127,6 +145,14 @@ export const useTransferDataStore = defineStore('transfer', () => { @@ -127,6 +145,14 @@ export const useTransferDataStore = defineStore('transfer', () => {
}
}
function upFirmwareTimeout(sn: string) {
const device = devicesMap.get(sn)
if (device) {
device.upFirmware = 'timeout'
device.upFirmwareStatus = undefined
}
}
return {
isConnected,
@ -141,5 +167,7 @@ export const useTransferDataStore = defineStore('transfer', () => { @@ -141,5 +167,7 @@ export const useTransferDataStore = defineStore('transfer', () => {
upFirmwareReset,
upFirmwareStatus,
upFirmwareSucceed,
upFirmwareStatusReject,
upFirmwareTimeout
}
})

4
src/utils/zmq.ts

@ -69,11 +69,11 @@ export interface SubMsgData { @@ -69,11 +69,11 @@ export interface SubMsgData {
*/
export function getPubTopic(type: TopicType, module: string,) {
return `web/${type}/${module}`
return `web/${type}/${module}/`
}
export function getSubTopic(server: string, type: TopicType, module: string,) {
return `${server}/${type}/${module}`
return `${server}/${type}/${module}/`
}
// 获取随机id

14
src/utils/zmqJsonWorker.ts

@ -5,7 +5,7 @@ import { WorkerCMD, ZmqCMD, type PublishMsg, type PubMsgData, } from './zmq' @@ -5,7 +5,7 @@ import { WorkerCMD, ZmqCMD, type PublishMsg, type PubMsgData, } from './zmq'
const HEARTBEAT_TOPIC = 'HEARTBEAT'
const HEARTBEAT_INTERVAL = 3000
const STATUS_CHECK_INTERVAL = 1000
let messageTimeout = 10000
let messageTimeout = 5000
let heartClient: ZmqClient | null, subClient: ZmqClient | null, pubClient: ZmqClient | null
let subHost = '', pubHost = ''
@ -78,8 +78,10 @@ function handleZmqMessage(topic: Uint8Array, msg: Uint8Array) { @@ -78,8 +78,10 @@ function handleZmqMessage(topic: Uint8Array, msg: Uint8Array) {
msg: jsonMessage
})
const parsedMessage = JSON.parse(jsonMessage) as PubMsgData
if (parsedMessage.id && traceMessages.has(parsedMessage.id)) {
if (parsedMessage.result === 'progress') {
// traceMessages.get(parsedMessage.id)
const curTraceMessages = traceMessages.get(parsedMessage.id)
if (parsedMessage.id && !!curTraceMessages) {
if (curTraceMessages.isAlwaysListen || parsedMessage.result === 'progress') {
// 重置消息超时时间
const val = traceMessages.get(parsedMessage.id)
if (val) {
@ -106,7 +108,6 @@ function connect(host: string) { @@ -106,7 +108,6 @@ function connect(host: string) {
heartClient = new ZmqClient('sub')
heartClient.zmqSub(subHost, updateHeartbeat)
heartClient.subscribe(HEARTBEAT_TOPIC)
monitorConnection()
pubHost = `ws://${host}:15556`
@ -131,7 +132,7 @@ function connect(host: string) { @@ -131,7 +132,7 @@ function connect(host: string) {
const traceMessages = new Map<string, any>()
self.onmessage = function (event) {
const { cmd, topic, msg, isTimeout = false } = event.data
const { cmd, topic, msg, isTimeout = false, isAlwaysListen } = event.data
switch (cmd) {
case WorkerCMD.START:
@ -152,7 +153,8 @@ self.onmessage = function (event) { @@ -152,7 +153,8 @@ self.onmessage = function (event) {
const parseMsg = JSON.parse(msg) as PublishMsg<string>
traceMessages.set(parseMsg.id, {
timestamp: Date.now(),
topic: topic
topic: topic,
isAlwaysListen,
})
}
pubClient?.publishStr(topic, msg)

40
src/views/firmwareUpload/index.vue

@ -1,19 +1,9 @@ @@ -1,19 +1,9 @@
<template>
<div class="flex justify-center items-center size-full">
<EdfsWrap title="固件上传" style="width: 50%; height: 50%">
<el-upload
v-model:fileList="fileList"
v-loading="loading"
element-loading-text="上传中..."
drag
action=""
accept=".tar.gz"
:limit="1"
:on-exceed="handleExceed"
:auto-upload="false"
ref="upload"
class="h-[calc(100%-30px)] w-full"
>
<el-upload v-model:fileList="fileList" v-loading="loading" element-loading-text="上传中..." drag action=""
accept=".tar.gz" :limit="1" :on-exceed="handleExceed" :auto-upload="false" ref="uploadRef"
class="h-[calc(100%-30px)] w-full">
<div class="i-line-md:cloud-alt-upload-loop text-20px mx-auto"></div>
<div class="text">拖拽文件或者 <em>点击上传</em></div>
<template #tip v-if="!fileList.length">
@ -21,9 +11,7 @@ @@ -21,9 +11,7 @@
</template>
</el-upload>
<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>
</div>
</EdfsWrap>
@ -52,11 +40,15 @@ const handleExceed: UploadProps['onExceed'] = files => { @@ -52,11 +40,15 @@ const handleExceed: UploadProps['onExceed'] = files => {
upload.value!.handleStart(file)
}
const uploadRef = ref<UploadInstance>()
const beforeAvatarUpload: UploadProps['beforeUpload'] = rawFile => {
const accept = ['.zip', '.tar', '.tar.gz']
const fileTypes = rawFile.name.substring(rawFile.name.lastIndexOf('.'))
const fileTypes = rawFile.name.substring(rawFile.name.indexOf('.'))
if (!accept.includes(fileTypes)) {
message.error('请上传 tar.gz 文件')
uploadRef.value?.clearFiles?.()
return false
}
return true
@ -66,11 +58,8 @@ function validate() { @@ -66,11 +58,8 @@ function validate() {
message.error('请上传文件')
return true
}
if (beforeAvatarUpload(fileList.value[0].raw!)) {
message.error('请上传 tar.gz 文件')
return true
}
return false
return beforeAvatarUpload(fileList.value[0].raw!)
}
const loading = ref(false)
const abortController = ref<AbortController>()
@ -83,9 +72,11 @@ async function onSave() { @@ -83,9 +72,11 @@ async function onSave() {
if (!validate()) return
loading.value = true
abortController.value = new AbortController()
const data = new FormData()
data.append('file', fileList.value[0].raw as File)
const userFile = fileList.value[0] as UploadUserFile
const res = await uploadFirmwareFile(userFile.raw as File, abortController.value)
if (res.code === 200) {
const res = await uploadFirmwareFile(data, abortController.value)
if (res.code === 0) {
message.success('上传成功')
} else {
message.error('上传失败')
@ -175,6 +166,7 @@ function onClone() { @@ -175,6 +166,7 @@ function onClone() {
height: calc(100% - 50px);
width: 100%;
}
:deep(.el-upload-dragger) {
height: 100%;
width: 100%;

138
src/views/stationData/components/PointGroupTree.vue

@ -0,0 +1,138 @@ @@ -0,0 +1,138 @@
<template>
<div class=" h-full flex">
<div class="w-530">
<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">
<template #default="scope">
<div class="item">
<span class="label">{{ scope.data.name }}</span>
</div>
</template>
</el-tree>
</el-scrollbar>
</div>
<el-divider direction="vertical" class="h-full" />
<div class="w-440">
<el-scrollbar class="scroll">
<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>
</div>
</template>
<script lang="ts" setup>
import type { ElTree } from 'element-plus'
import type { IPointGroupOV } from '@/api/module/transfer'
import type { IMyPoint } from '../type'
interface Props {
data: IPointGroupOV[]
devicePoints: Array<{
label: string
addr: string
}>
groupChangeLoading: boolean
}
const props = withDefaults(defineProps<Props>(), {
data: () => [],
groupChangeLoading: false,
})
const emit = defineEmits<{
'device-select': [IPointGroupOV]
'onchange-points': [IMyPoint[]]
}>()
const treeRef = ref<InstanceType<typeof ElTree>>()
const checkDefault = ref<string[]>([])
const handleNodeClick = (data: IPointGroupOV) => {
checkPointList.value = []
emit('device-select', data)
nextTick(() => {
changePoints()
})
}
const defaultProps = {
children: 'children',
label: 'name',
}
const checkPointList = ref<any>([])
function changePoints() {
emit('onchange-points', checkPointList.value)
}
onMounted(() => {
if (props.data.length) {
let defaultSelectDevice = props.data[0]
checkDefault.value = [defaultSelectDevice.type]
nextTick(() => {
treeRef.value?.setCurrentKey(defaultSelectDevice.name)
})
}
})
</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%;
height: 100%;
overflow: auto;
:deep(.el-tree-node__content) {
height: 40px;
font-size: 16px;
color: var(--label-text);
border-left: 2px solid transparent;
}
:deep(.el-tree-node.is-current > .el-tree-node__content) {
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>

169
src/views/stationData/components/deviceDrawer.vue

@ -1,27 +1,15 @@ @@ -1,27 +1,15 @@
<template>
<div class="fault-rule-drawer">
<el-drawer
v-model="isShowDrawer"
:title="title"
direction="rtl"
size="60%"
modal-class="model-dev-opn"
:before-close="handleBeforeClose"
>
<main class="drawer-box">
<el-button-group class="ml-4">
<template v-for="item in pointGroup" :key="item.module">
<el-button class="ml-4" @click="onGroupChange(item)">
{{ item.type }}
</el-button>
</template>
</el-button-group>
<NewDataChart
:chart-datas="chartDatas"
:legends="tableCol"
:axis-data="Array.from(axisData)"
v-if="isShowDrawer"
/>
<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 border-r-1 border-solid border-r-#e4e7ed">
<PointGroupTree v-if="isShowDrawer" :data="pointGroup" @device-select="onGroupChange"
:groupChangeLoading="groupChangeLoading" @onchangePoints="onchangePoints" :device-points="pointList"
:isTransfer="props.isTransfer" :siteInfo="props.siteInfo" ref="pointGroupTreeRef" />
</EdfsWrap>
<NewDataChart v-if="isShowChart" :chart-datas="chartDatas" :legends="checkPointList"
:axis-data="Array.from(axisData)" ref="chartRef" />
</main>
</el-drawer>
</div>
@ -29,6 +17,7 @@ @@ -29,6 +17,7 @@
<script setup lang="ts">
import NewDataChart from './newDataChart.vue'
import PointGroupTree from './PointGroupTree.vue'
import {
getPubInitData,
type ManualAction,
@ -36,7 +25,7 @@ import { @@ -36,7 +25,7 @@ import {
type TimeoutMsg,
} from '@/utils/zmq'
import ZMQWorker from '@/composables/useZMQJsonWorker'
import type { IOfflineDevice, IOnlineDevice } from '../type'
import type { IMyPoint, IOfflineDevice, IOnlineDevice } from '../type'
import {
getDeviceDetails,
getPointGroup,
@ -48,12 +37,14 @@ import { @@ -48,12 +37,14 @@ import {
} from '@/api/module/transfer'
import { useMessage } from '@/composables/useMessage'
import { getTransferTopic, postTransferTopic } from '../utils'
import dayjs from 'dayjs'
const worker = ZMQWorker.getInstance()
const isShowDrawer = defineModel<boolean>()
const title = computed(() => (props.isTransfer ? '数据详情' : `已迁移数据详情`))
const message = useMessage()
const pointGroupTreeRef = ref<InstanceType<typeof PointGroupTree>>()
const props = defineProps<{
siteInfo: ISite | null
isTransfer: boolean
@ -65,17 +56,22 @@ const pubIdWithDevice = new Map< @@ -65,17 +56,22 @@ const pubIdWithDevice = new Map<
{ device: IOfflineDevice | IOnlineDevice; action: ManualAction }
>()
const tableCol = ref<any[]>([])
const pointList = ref<IMyPoint[]>([])
const curDevice = ref<IOfflineDevice | IOnlineDevice>()
function open(device: IOfflineDevice | IOnlineDevice) {
async function open(device: IOfflineDevice | IOnlineDevice) {
curDevice.value = device
props.isTransfer ? loadDeviceDetails() : zmqImport(device as IOfflineDevice)
await loadPointGroup()
.then(async () => {
props.isTransfer ? await loadDeviceDetails() : zmqImport(device as IOfflineDevice)
})
.catch(() => {
message.error('获取点位组数据失败')
fullscreenLoading.value?.close()
})
}
const pointsData = ref<{ addr: string; label: string }[]>([])
const columsParams = computed(() => ['ts', ...tableCol.value.map(i => i.addr)])
const columsParams = computed(() => ['ts', ...pointList.value.map(i => i.addr)])
async function loadPoints() {
if (!curGroup.value) {
message.error('请先选择点位组')
@ -96,58 +92,48 @@ async function loadPoints() { @@ -96,58 +92,48 @@ async function loadPoints() {
}
const res = await getPoints(params)
if (res.code === 0) {
pointsData.value = res.data
tableCol.value = res.data.map((i: any) => ({
label: i.point_name,
addr: i.point_id,
pointList.value = res.data.map((i: any) => ({
label: i.cnName,
addr: i.addr,
}))
tableCol.value.push({
pointList.value.push({
label: '时间',
addr: 'ts',
})
}
return res
}
const chartDatas = new Map<string, any[]>()
const axisData = new Set<string>()
const pointData = ref<any[]>([])
async function loadDeviceDetails() {
if (!fullscreenLoading.value) {
openFullScreen()
}
const poinsRes = await loadPoints()
if (!poinsRes || poinsRes.code !== 0) {
message.error('获取点位数据失败')
fullscreenLoading.value?.close()
return
}
const res = await getDeviceDetails({
columns: columsParams.value,
isLocal: props.isTransfer ? false : true,
host: props.isTransfer ? (curDevice.value as IOnlineDevice).clientIp : '',
name: curGroupName.value as string
})
if (res.code === 0) {
res.data.results.forEach((data: any[]) => {
columsParams.value.forEach((col, idx) => {
if (idx === 0) {
axisData.add(data[idx])
}
const ts = data[0]
if (col !== 'ts') {
const colData = chartDatas.get(col)
if (colData) {
colData.push([ts, data[idx]])
} else {
chartDatas.set(col, [[ts, data[idx]]])
}
}
})
})
pointData.value = Array.isArray(res.data.results) ? res.data.results : []
isShowDrawer.value = true
} else {
message.error('获取设备数据失败')
}
groupChangeLoading.value = false
fullscreenLoading.value?.close()
}
@ -191,12 +177,21 @@ function zmqTimeoutCb(msg: TimeoutMsg) { @@ -191,12 +177,21 @@ function zmqTimeoutCb(msg: TimeoutMsg) {
function handleBeforeClose(done: () => void) {
isShowDrawer.value = false
tableCol.value = []
pointList.value = []
chartDatas.clear()
axisData.clear()
fullscreenLoading.value = null
done()
}
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({
@ -208,9 +203,9 @@ const openFullScreen = () => { @@ -208,9 +203,9 @@ const openFullScreen = () => {
const pointGroup = ref<IPointGroupOV[]>([])
const curGroup = ref<string>()
const curGroupName = ref<string>()
async function loadPointGroup() {
if (!props.siteInfo) return
const params: IPointGroupParams = {}
if (props.isTransfer) {
const onlineDevice = curDevice.value as IOnlineDevice
@ -225,23 +220,54 @@ async function loadPointGroup() { @@ -225,23 +220,54 @@ async function loadPointGroup() {
const res = await getPointGroup(params)
if (res.code === 0) {
curGroup.value = res.data[0]?.type
curGroupName.value = res.data[0]?.name
pointGroup.value = Array.isArray(res?.data) ? res.data : []
if (res.data.length > 0) {
curGroup.value = res.data[0].type
}
return Promise.resolve()
} else {
message.error('获取点位组数据失败')
return Promise.reject()
}
}
function onGroupChange(item: IPointGroupOV) {}
onMounted(async () => {
await loadPointGroup()
if (props.isTransfer) {
await loadDeviceDetails()
const groupChangeLoading = ref<boolean>(false)
function onGroupChange(item: IPointGroupOV) {
if (!item?.type) {
return
}
})
groupChangeLoading.value = true
curGroup.value = item.type
curGroupName.value = item.name
loadDeviceDetails()
}
const chartDatas = reactive(new Map<string, any[]>())
const axisData = new Set<string>()
const checkPointList = ref<IMyPoint[]>([])
function onchangePoints(checkPoints: IMyPoint[]) {
checkPointList.value = checkPoints
chartDatas.clear()
axisData.clear()
pointData.value.forEach((data: any[]) => {
const [ts, val, addr] = data
if (checkPoints.some(i => i.addr === addr)) {
const time = dayjs(Number(ts)).format('YYYY-MM-DD HH:mm:ss')
if (addr) {
const colData = chartDatas.get(addr)
if (colData) {
colData.push([time, val])
} else {
chartDatas.set(addr, [[time, val]])
}
}
if (ts) {
axisData.add(time)
}
}
})
}
defineExpose({
open,
@ -253,13 +279,12 @@ defineExpose({ @@ -253,13 +279,12 @@ defineExpose({
.fault-rule-drawer {
font-size: 16px;
:deep(.el-drawer__header) {
color: var(--text-color);
:deep(.edfs-wrap) {
width: auto;
}
.drawer-box {
width: 100%;
height: 100%;
:deep(.el-drawer__header) {
color: var(--text-color);
}
}
</style>

19
src/views/stationData/components/newDataChart.vue

@ -1,14 +1,7 @@ @@ -1,14 +1,7 @@
<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" :option="chartOption" :autoresize="autoresize" :loading-options="loadingOpt"
:loading="loading" ref="chartRef" @legendselectchanged="changeLegend" />
</div>
</template>
@ -65,7 +58,7 @@ const props = defineProps({ @@ -65,7 +58,7 @@ const props = defineProps({
const loading = ref(true)
const loadingOpt = {
type: 'default',
text: '暂无数据',
text: '请选择点位',
color: '#c23531',
textColor: '#666',
maskColor: 'rgba(0, 0, 0, 0)',
@ -79,7 +72,6 @@ const chartOption = computed<EChartsOption>(() => { @@ -79,7 +72,6 @@ const chartOption = computed<EChartsOption>(() => {
return {}
}
loading.value = false
const tmpSeries: SeriesOption[] = []
for (const legend of props.legends.filter(item => item.addr !== 'ts')) {
const entry = props.chartDatas.get(legend.addr)
@ -97,6 +89,8 @@ const chartOption = computed<EChartsOption>(() => { @@ -97,6 +89,8 @@ const chartOption = computed<EChartsOption>(() => {
}
tmpSeries.push(lineData)
}
const option: EChartsOption = {
grid: {
left: 60,
@ -211,10 +205,7 @@ function changeLegend(data: { name: string; selected: Record<string, boolean> }) @@ -211,10 +205,7 @@ function changeLegend(data: { name: string; selected: Record<string, boolean> })
} else {
unCheckArr.value = unCheckArr.value.filter(item => item !== name)
}
console.log(unCheckArr.value)
}
// useWindowResize(handleResize)
</script>
<style scoped lang="scss">

35
src/views/stationData/components/siteTransferDlg.vue → src/views/stationData/components/offTransferDlg.vue

@ -13,17 +13,21 @@ @@ -13,17 +13,21 @@
</template>
<script setup lang="ts">
import dayjs from 'dayjs'
import { getPubInitData, type PublishMsg } from '@/utils/zmq'
import { cloneDeep } from 'lodash-es'
import { useMessage } from '@/composables/useMessage'
import type { IOfflineDevice } from '../type'
import type { ISite } from '@/api/module/transfer'
const message = useMessage()
const emit = defineEmits<{
'on-save': [msg: PublishMsg<'import'>, site: ISite]
'on-save': [msg: PublishMsg<'import'>, site: IOfflineDevice]
}>()
const props = defineProps<{
siteInfo: ISite | null
}>()
@ -32,20 +36,20 @@ const fromData = { @@ -32,20 +36,20 @@ const fromData = {
clientIp: '',
}
const curSite = ref<ISite>()
const curOffDeive = ref<IOfflineDevice>()
const form = ref(cloneDeep(fromData))
function open(item: ISite) {
curSite.value = item
function open(item: IOfflineDevice) {
curOffDeive.value = item
visible.value = true
}
function onSave() {
if (!verifyData()) return
if (!curSite.value) {
if (verifyData()) return
if (!curOffDeive.value) {
message.error('请选择设备')
return
}
@ -56,26 +60,33 @@ function onSave() { @@ -56,26 +60,33 @@ function onSave() {
'',
'',
'',
`${curSite.value.export_root_path}`,
`${props.siteInfo!.name}/${curOffDeive.value.sn}`,
]
const msg = getPubInitData<'import'>('import', params)
emit('on-save', msg, curSite.value)
emit('on-save', msg, curOffDeive.value)
close()
}
function close() {
form.value = cloneDeep(fromData)
curSite.value = undefined
curOffDeive.value = undefined
visible.value = false
}
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]?)$/
function verifyData() {
if (!form.value.clientIp) {
message.error('请输入客户端IP')
return false
return true
}
if (!ipPattern.test(form.value.clientIp)) {
message.error('请输入正确的IP地址')
return true
}
return true
return false
}
defineExpose({

39
src/views/stationData/components/transferMask.vue

@ -0,0 +1,39 @@ @@ -0,0 +1,39 @@
<template>
<div class="transfer-mask absolute left-0 top-0 wh-full z-10 bg-#FFF-90 h-full w-full" v-if="isShowMask">
<el-button class="absolute r-4 t-4" type="info" @click="onclose" v-show="!transferLoading">关闭</el-button>
<div class="wh-full flex-col justify-center items-center">
<span class="absolute top-0 left-1/2 -translate-x-1/2 mt-2 text-red">数据传输中请不要刷新或关闭页面!!!</span>
<div class="w-300px h-300px flex-col justify-center items-center" v-if="transferLoading">
<div class="el-loading-spinner">
<svg class="circular" viewBox="0 0 50 50">
<circle class="path" cx="25" cy="25" r="20" fill="none"></circle>
</svg>
</div>
<div class="mt-28">
<div class="text-14px text-#999999">指令下发中请稍后... </div>
</div>
</div>
<template v-else>
<slot></slot>
</template>
</div>
</div>
</template>
<script setup lang="ts">
const emit = defineEmits<{
'close': []
}>()
defineProps<{
transferLoading: boolean
}>()
const isShowMask = defineModel<boolean>()
function onclose() {
emit('close')
}
</script>
<style lang="scss" scoped></style>

179
src/views/stationData/index.vue

@ -1,48 +1,47 @@ @@ -1,48 +1,47 @@
<template>
<div class="flex-col gap-16 wh-full">
<EdfsWrap title="当前连接站点" v-if="connectSite?.title" shape="circle" shapeColor="#4B9E5F" class="h-auto">
<div class="station-list-flow">
<div class="station-item">
<div class="title bg-[#4B9E5F]">
<div>{{ connectSite.title }}</div>
</div>
<div class="body">
<div class="info">
<div class="info-item">
<div class="info-item-label">在线设备</div>
<div class="info-item-value">
<div class="i-octicon:cloud-16 color-[#4B9E5F] text-16px"></div>
{{ onlineCount }}
<template v-if="env.VITE_APP_ENV == 'local'">
<EdfsWrap title="当前连接站点" v-if="connectSite?.title" shape="circle" shapeColor="#4B9E5F" class="h-auto">
<div class="station-list-flow">
<div class="station-item">
<div class="title bg-[#4B9E5F]">
<div>{{ connectSite.title }}</div>
</div>
<div class="body">
<div class="info">
<div class="info-item">
<div class="info-item-label">在线设备</div>
<div class="info-item-value">
<div class="i-octicon:cloud-16 color-[#4B9E5F] text-16px"></div>
{{ onlineCount }}
</div>
</div>
</div>
<div class="info-item">
<div class="info-item-label">离线设备</div>
<div class="info-item-value">
<div class="i-octicon:cloud-offline-16 color-[#F44336] text-16px"></div>
{{ offlineCount }}
<div class="info-item">
<div class="info-item-label">离线设备</div>
<div class="info-item-value">
<div class="i-octicon:cloud-offline-16 color-[#F44336] text-16px"></div>
{{ offlineCount }}
</div>
</div>
</div>
</div>
</div>
<div class="footer row-end-0">
<div class="m-l-auto p-b-8">
<el-button type="primary" style="height: 28px; padding: 0 12px" color="#4B9E5F"
@click="onTransferData">迁移数据</el-button>
<div class="footer row-end-0">
<div class="m-l-auto p-b-8">
<el-button type="primary" style="height: 28px; padding: 0 12px" color="#4B9E5F"
@click="onTransferData">迁移数据</el-button>
</div>
</div>
</div>
</div>
</div>
</EdfsWrap>
</EdfsWrap>
</template>
<EdfsWrap title="迁移历史" shape="circle" shapeColor="#F1BF63" class="flex-1" useScrollBar>
<div class="station-list-flow">
<div class="station-item" v-for="item in siteList" :key="item.id">
<div class="title bg-[#F1BF63]">
{{ item.name }}
<div class="flex items-center gap-col-2">
<el-tooltip content="数据迁移">
<i class="i-mdi:database-arrow-right-outline :hover:color-[#ddd] color-[#FFFFFF] cursor-pointer text-20px"
@click="openTransferDlg(item)"></i>
</el-tooltip>
<el-tooltip content="详情">
<div
class="i-material-symbols:info-outline :hover:color-[#ddd] color-[#FFFFFF] cursor-pointer text-20px"
@ -70,38 +69,7 @@ @@ -70,38 +69,7 @@
</div>
</EdfsWrap>
</div>
<div class="transfer-mask absolute left-0 top-0 wh-full z-10 bg-#FFF-90 h-full w-full"
v-if="isSiteTransfer || transferLoading">
<div class="wh-full flex-col justify-center items-center">
<span class="mt-2 text-red">导出中请不要刷新或关闭页面!!!</span>
<div class="w-300px h-300px flex-col justify-center items-center" v-if="transferLoading">
<div class="el-loading-spinner"><svg class="circular" viewBox="0 0 50 50">
<circle class="path" cx="25" cy="25" r="20" fill="none"></circle>
</svg></div>
<div class="mt-28">
<div class="text-14px text-#999999">指令下发中请稍后...</div>
</div>
</div>
<div class="w-56% h-full flex justify-center items-center" v-if="isSiteTransfer && !transferLoading">
<div class="flex-col gap-row-4 w-full">
<div class="flex items-center gap-col-1">
<div class="flex-1 items-center">
<el-progress :percentage="100" class="flex-1" :stroke-width="12" striped striped-flow :duration="20">
<div class="text-16px font-500">数据迁移中...</div>
</el-progress>
</div>
</div>
<div class="h-420 border-radius-8px bg-[#F9FAFB] p-10">
<div class="text-16px font-500">迁移日志</div>
<el-scrollbar class="h-full">
<div v-for="i in siteTransferLogList"> {{ i }} </div>
</el-scrollbar>
</div>
</div>
</div>
</div>
</div>
<SiteTransferDlg ref="siteTransferDlgRef" :isBatchTransfer="false" @on-save="onSiteTransfer"></SiteTransferDlg>
</template>
<script setup lang="ts">
@ -109,15 +77,8 @@ import dayjs from 'dayjs' @@ -109,15 +77,8 @@ import dayjs from 'dayjs'
import { useTransferDataStore } from '@/stores/transferData'
import { storeToRefs } from 'pinia'
import { getSiteList, type ISite } from '@/api/module/transfer'
import SiteTransferDlg from './components/siteTransferDlg.vue'
import type { PublishMsg, PubMsgData, TimeoutMsg } from '@/utils/zmq'
import { getTransferTopic, postTransferTopic } from './utils'
import ZMQWorker from '@/composables/useZMQJsonWorker'
import { useMessage } from '@/composables/useMessage'
const message = useMessage()
const env = import.meta.env
const worker = ZMQWorker.getInstance()
const router = useRouter()
const transferDataStore = useTransferDataStore()
@ -152,23 +113,7 @@ function onSiteDetails(site: ISite) { @@ -152,23 +113,7 @@ function onSiteDetails(site: ISite) {
const isSiteTransfer = ref(false)
const transferLoading = ref(false)
const siteList = ref<ISite[]>([
{
id: '1',
name: '站点1',
export_root_path: '/data/transfer/site112312312312',
export_time: '2023-10-01T12:00:00Z',
last_modify_time: '2023-10-01T12:00:00Z',
},
{
id: '2',
name: '站点2',
export_root_path: '/data/transfer/site2',
export_time: '2023-10-02T12:00:00Z',
last_modify_time: '2023-10-02T12:00:00Z',
}
])
const siteList = ref<ISite[]>([])
async function loadSiteList() {
const res = await getSiteList()
if (res.code === 200 || res.code === 0) {
@ -176,63 +121,6 @@ async function loadSiteList() { @@ -176,63 +121,6 @@ async function loadSiteList() {
}
}
const siteTransferDlgRef = ref<typeof SiteTransferDlg>()
function openTransferDlg(item: ISite) {
siteTransferDlgRef.value?.open(item)
}
const pubIdWithSite = new Map<
string,
{ site: ISite; action: 'import' }
>()
function onSiteTransfer(msg: PublishMsg<'import'>, site: ISite) {
pubIdWithSite.set(msg.id, { site, action: 'import' })
worker.publish(postTransferTopic, msg, true, zmqTimeoutCb)
worker.subscribe(getTransferTopic, zmqImportCb, msg.id)
transferLoading.value = true
isSiteTransfer.value = true
}
const siteTransferLogList = ref<string[]>([])
function zmqImportCb(msg: PubMsgData) {
const { id, result, feedback } = msg
const { site, action } = pubIdWithSite.get(id)!
if (action !== 'import' || !site) return
transferLoading.value = false
if (result === 'progress') {
const log: string = Array.isArray(feedback) ? feedback[0] || '' : ''
siteTransferLogList.value.push(log)
}
if (result !== 'progress') {
if (result === 'success') {
isSiteTransfer.value = false
message.error(`导出数据成功`)
} else {
isSiteTransfer.value = false
message.error(`导出数据失败`)
}
}
}
function zmqTimeoutCb(msg: TimeoutMsg) {
const { site, action } = pubIdWithSite.get(msg.timeoutId)!
if (site && action === 'import') {
message.error(`站点:${site.name}数据导出超时,请稍后重试`)
pubIdWithSite.delete(msg.timeoutId)
isSiteTransfer.value = false
transferLoading.value = false
}
}
onMounted(() => {
initConnectSite()
loadSiteList()
@ -254,7 +142,6 @@ onMounted(() => { @@ -254,7 +142,6 @@ onMounted(() => {
column-gap: 20px;
row-gap: 24px;
.station-item {
width: 280px;
height: 180px;
@ -285,8 +172,6 @@ onMounted(() => { @@ -285,8 +172,6 @@ onMounted(() => {
}
}
.body {
background-color: var(--station-card-bg);
display: flex;

309
src/views/stationData/transferData.vue

@ -19,36 +19,26 @@ @@ -19,36 +19,26 @@
<div class="device-item" v-for="item in devices">
<div class="device-item-header">
<div class="flex items-center">
<el-checkbox :value="item.sn" v-if="isBatchTransfer || isBatchUpgrade">
<el-checkbox :value="item.sn" v-if="(isBatchTransfer || isBatchUpgrade) && item.status !== '离线'">
<div>设备ID: {{ item.sn }}</div>
</el-checkbox>
<div v-else>
<div v-else class="h-32 leading-32px">
<div>设备ID: {{ item.sn }}</div>
</div>
</div>
<div class="flex items-center gap-col-2">
<el-tooltip
content="数据迁移"
v-if="isTransfer && item.status === '在线'"
>
<i
class="i-mdi:database-arrow-right-outline :hover:color-[#8ACE6A] color-[#4B9E5F] cursor-pointer text-20px"
@click="onTransfer(item)"
></i>
<div class="flex items-center gap-col-2" v-if="!(isBatchTransfer || isBatchUpgrade)">
<el-tooltip content="数据迁移" v-if="isTransfer ? item.status === '在线' : true">
<i class="i-mdi:database-arrow-right-outline :hover:color-[#8ACE6A] color-[#4B9E5F] cursor-pointer text-20px"
@click="onTransfer(item)"></i>
</el-tooltip>
<!-- v-if="isTransfer && item.status === '在线'" -->
<el-tooltip content="固件升级">
<i
class="i-codicon:chip :hover:color-[#8ACE6A] color-[#4B9E5F] cursor-pointer text-20px"
@click="onFirmwareUpload([item])"
></i>
<el-tooltip content="固件升级" v-if="isTransfer && item.status === '在线'">
<i class="i-codicon:chip :hover:color-[#8ACE6A] color-[#4B9E5F] cursor-pointer text-20px"
@click="onFirmwareUpload([item])"></i>
</el-tooltip>
<el-tooltip content="详情">
<div
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>
</div>
</div>
@ -84,15 +74,10 @@ @@ -84,15 +74,10 @@
</div>
</template>
</template>
<div
class="absolute l-0 t-0 w-full h-full z-10 bg-#FFF-90"
v-if="['updating', 'pending', 'rejected'].includes(item.upFirmware)"
>
<div
class="i-material-symbols-light:close absolute-rt text-base text-gray-950 cursor-pointer"
v-if="item.upFirmware === 'rejected'"
@click="upFirmwareSucceed(item.sn)"
></div>
<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'].includes(item.upFirmware)" @click="upFirmwareSucceed(item.sn)"></div>
<template v-if="item.upFirmware === 'updating'">
<div class="device-item-body">
<div class="info-item">
@ -101,105 +86,118 @@ @@ -101,105 +86,118 @@
</div>
<div class="info-item">
<div>当前进度:</div>
<el-progress
class="flex-1"
:stroke-width="12"
:show-text="true"
:percentage="item.upFirmwareStatus?.progress || 0"
/>
<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">等待升级中...</div>
<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>
<div>错误发生步骤:</div>
<div class="text-red">
{{
upgradeProgressStatusMap.find(
r => r.status === item.upFirmwareStatus?.step
r => r.status == item.upFirmwareStatus?.step
)?.text ?? '--'
}}
}}失败
</div>
</div>
<div class="info-item">
<div>升级错误信息:</div>
<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>
</el-checkbox-group>
</div>
</EdfsWrap>
<EdfsWrap title="迁移进度" class="transfer-wrap h-[42%]" v-if="isShowTransfer">
<div class="flex-col gap-col-10 wh-full">
<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"
:status="
['progress', 'success', undefined].includes(transferStatus)
<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>
">
{{
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">
<div v-for="i in curTransferLog" :class="i.status === 'failed' ? 'text-red-500' : ''"
class="text-gray-600">
{{ i.msg }}
</div>
</el-scrollbar>
</div>
<el-button
v-if="transferStatus === 'progress'"
type="primary"
@click="onStopTransfer"
>停止迁移</el-button
>
</div>
<div class="transfer-log-wrap">
<div class="text-16px font-500">迁移日志</div>
<el-scrollbar class="h-full">
<div
v-for="i in curTransferLog"
:class="i.status === 'failed' ? 'text-red-500' : ''"
class="text-gray-600"
>
{{ i.msg }}
</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'"
:striped-flow="onOffDeviceTransferStatus === 'progress'" :duration="20">
<div class="text-16px font-500">{{ transferStatusMap[onOffDeviceTransferStatus as keyof typeof
transferStatusMap]
??
'' }}</div>
</el-progress>
</div>
</el-scrollbar>
</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">
<div v-for="i in siteTransferLogList"> {{ i }} </div>
</el-scrollbar>
</div>
</div>
</div>
</EdfsWrap>
</template>
</TransferMask>
</div>
<TransferDlg
ref="transferDlgRef"
@on-save="onSave"
:is-batch-transfer="isBatchTransfer"
/>
<DeviceDrawer
v-model="isShowDetails"
ref="deviceDrawerRef"
:siteInfo="siteInfo"
:is-transfer="isTransfer"
/>
<TransferDlg ref="transferDlgRef" @on-save="onLineDeviceTransfer" :is-batch-transfer="isBatchTransfer" />
<OffTransferDlg ref="offTransferDlg" :isBatchTransfer="false" :siteInfo="siteInfo" @on-save="onOffDeviceTransfer" />
<DeviceDrawer v-model="isShowDetails" ref="deviceDrawerRef" :siteInfo="siteInfo" :is-transfer="isTransfer" />
</template>
<script setup lang="ts">
import dayjs from 'dayjs'
import TransferMask from './components/transferMask.vue'
import TransferDlg from './components/transferDlg.vue'
import ZMQWorker from '@/composables/useZMQJsonWorker'
import OffTransferDlg from './components/offTransferDlg.vue'
import {
getPubInitData,
type PublishMsg,
@ -217,6 +215,7 @@ import type { @@ -217,6 +215,7 @@ import type {
import { useMessage } from '@/composables/useMessage'
import { getDeviceList, type ISite } from '@/api/module/transfer'
import DeviceDrawer from './components/deviceDrawer.vue'
import {
getFirmwareUpTopic,
getTransferTopic,
@ -235,14 +234,14 @@ const type = ref<'export' | 'details'>(route.query.type as 'export' | 'details') @@ -235,14 +234,14 @@ const type = ref<'export' | 'details'>(route.query.type as 'export' | 'details')
const isTransfer = computed(() => type.value === 'export')
const isShowTransfer = computed(() => isTransfer.value && !!curTransferLog.value.length)
const isShowTransferMask = ref(false)
const message = useMessage()
const worker = ZMQWorker.getInstance()
const transferDataStore = useTransferDataStore()
const { upFirmwarePending, upFirmwareReset, upFirmwareStatus, upFirmwareSucceed } =
const { upFirmwarePending, upFirmwareReset, upFirmwareStatus, upFirmwareSucceed, upFirmwareStatusReject, upFirmwareTimeout } =
transferDataStore
const { devicesMap } = storeToRefs(transferDataStore)
@ -265,11 +264,33 @@ const devices = computed(() => { @@ -265,11 +264,33 @@ const devices = computed(() => {
return isTransfer.value ? Array.from(devicesMap.value.values()) : deviceList.value
}) as Ref<any[]>
function onSave(msg: PublishMsg<'export'>, device: IOnlineDevice) {
const transferLoading = ref(false)
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) {
onBatchCancel()
}
@ -281,15 +302,17 @@ const statusMap = { @@ -281,15 +302,17 @@ const statusMap = {
1003: 'failed',
}
function zmqExportCb(msg: PubMsgData) {
if (!isTransfer.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')
| 'success'
| 'padding'
| 'failed')
: 'failed'
curTransferLog.value.push({
msg: `主机【${feedback[0]}】: ${feedback[2]}`,
@ -297,9 +320,9 @@ function zmqExportCb(msg: PubMsgData) { @@ -297,9 +320,9 @@ function zmqExportCb(msg: PubMsgData) {
status,
})
}
// status failed
transferStatus.value = 'progress'
if (result !== 'progress') {
const curMsgInfo = exportPubDeviceMap.get(id)!
if (!curMsgInfo) return
const { device, action } = curMsgInfo
@ -314,6 +337,7 @@ function zmqExportCb(msg: PubMsgData) { @@ -314,6 +337,7 @@ function zmqExportCb(msg: PubMsgData) {
transferStatus.value = 'failed'
}
} else if (['failed', 'failure'].includes(result)) {
message.error(`迁移失败`)
transferStatus.value = 'failed'
}
@ -339,21 +363,17 @@ function onStopTransfer() { @@ -339,21 +363,17 @@ function onStopTransfer() {
const msg = getPubInitData<'cancel'>('cancel', [], 'no')
worker.publish(postTransferTopic, msg)
message.success('迁移已取消')
clearTransferData()
exportPubDeviceMap.clear()
})
}
function clearTransferData() {
curTransferLog.value = []
transferStatus.value = undefined
exportPubDeviceMap.clear()
}
function zmqTimeoutCb(msg: TimeoutMsg) {
const { device, action } = exportPubDeviceMap.get(msg.timeoutId)!
if (device && action === 'export') {
message.error(`迁移超时,请重新稍后尝试`)
exportPubDeviceMap.delete(msg.timeoutId)
closeTransferMask()
}
}
@ -430,7 +450,11 @@ function onBatchCancel() { @@ -430,7 +450,11 @@ function onBatchCancel() {
onlineDeviceCheckList.value = []
}
function onTransfer(item: IOnlineDevice) {
transferDlgRef.value?.open(item)
if (isTransfer.value) {
transferDlgRef.value?.open(item)
} else {
offTransferDlg.value?.open(item)
}
}
function onBack() {
@ -458,7 +482,7 @@ onBeforeRouteLeave(async (to, from, next) => { @@ -458,7 +482,7 @@ onBeforeRouteLeave(async (to, from, next) => {
const deviceList = ref<IOfflineDevice[]>([])
async function loadDeviceList() {
const res = await getDeviceList(siteInfo.value.id)
const res = await getDeviceList(siteInfo.value.name)
if (res.code === 200 || res.code === 0) {
deviceList.value = res.data
}
@ -472,7 +496,7 @@ onMounted(async () => { @@ -472,7 +496,7 @@ onMounted(async () => {
} else {
const res = await getFirmwarePath()
if (res.code === 200 || res.code === 0) {
firmwarePath.value = res.data
firmwarePath.value = res.data.path
}
}
})
@ -507,7 +531,7 @@ function onFirmwareUpload(devices: IOnlineDevice[]) { @@ -507,7 +531,7 @@ function onFirmwareUpload(devices: IOnlineDevice[]) {
action: 'upgrade',
})
}
worker.publish(postFirmwareUpTopic, msg, true, firmwareUpTimeoutCb)
worker.publish(postFirmwareUpTopic, msg, true, firmwareUpTimeoutCb, true)
worker.subscribe(getFirmwareUpTopic, zmqUpgradeCb, msg.id)
upgradeSnList.value = deviceSn.split(',')
@ -517,9 +541,18 @@ function onFirmwareUpload(devices: IOnlineDevice[]) { @@ -517,9 +541,18 @@ function onFirmwareUpload(devices: IOnlineDevice[]) {
function firmwareUpTimeoutCb(msg: TimeoutMsg) {
const { device, action } = upgradePubDeviceMap.get(msg.timeoutId)!
if (device && action === 'upgrade') {
message.error(`固件升级超时,请重新稍后尝试`)
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)
upFirmwareReset(upgradeSnList.value)
message.warning(`固件升级超时,请稍后重试`)
}
}
@ -529,8 +562,7 @@ function zmqUpgradeCb(msg: PubMsgData) { @@ -529,8 +562,7 @@ function zmqUpgradeCb(msg: PubMsgData) {
const deviceSn = msg.feedback[0]
const progressStatus = msg.feedback[1] as number
const progress = msg.feedback[2] || undefined
const errMsg = msg.feedback[3] || undefined
const curentDevice = upgradePubDeviceMap.get(deviceSn)
const curentDevice = upgradePubDeviceMap.get(msg.id)
if (curentDevice && curentDevice.action === 'upgrade') {
const { device } = curentDevice
if (device) {
@ -542,17 +574,70 @@ function zmqUpgradeCb(msg: PubMsgData) { @@ -542,17 +574,70 @@ function zmqUpgradeCb(msg: PubMsgData) {
}
}
if (status === 'success' || status === 'error') {
message.success(`固件升级${status === 'success' ? '完成' : '失败'}`)
upgradePubDeviceMap.delete(deviceSn)
upFirmwareStatus(deviceSn, msg.feedback)
if (status === 'error') {
upFirmwareStatusReject(deviceSn, msg.feedback)
}
upgradeSnList.value = upgradeSnList.value.filter(item => item !== deviceSn)
// upgradePubDeviceMap.delete(deviceSn)
}
}
}
// ================线=========
const offTransferDlg = ref<typeof OffTransferDlg>()
const pubIdWithOffDevice = new Map<string, { offDevice: IOfflineDevice; action: 'import' }>()
function onOffDeviceTransfer(msg: PublishMsg<'import'>, offDevice: IOfflineDevice) {
pubIdWithOffDevice.set(msg.id, { offDevice, action: 'import' })
worker.publish(postTransferTopic, msg, true, zmqImportTimeoutCb)
worker.subscribe(getTransferTopic, zmqImportCb, msg.id)
openTransferMask('import')
}
const siteTransferLogList = ref<string[]>([])
const onOffDeviceTransferStatus = ref<'progress' | 'success' | 'failed' | 'timeout' | undefined>()
function zmqImportCb(msg: PubMsgData) {
const { id, result, feedback } = msg
const { offDevice, action } = pubIdWithOffDevice.get(id)!
if (action !== 'import' || !offDevice) return
transferLoading.value = false
if (result === 'progress') {
onOffDeviceTransferStatus.value = 'progress'
const log: string = Array.isArray(feedback) ? feedback[0] || '' : ''
siteTransferLogList.value.push(log)
}
if (result !== 'progress') {
if (result === 'success') {
onOffDeviceTransferStatus.value = 'success'
message.success(`导出数据成功`)
} else {
onOffDeviceTransferStatus.value = 'failed'
message.error(`导出数据失败`)
}
}
}
function zmqImportTimeoutCb(msg: TimeoutMsg) {
const { offDevice, action } = pubIdWithOffDevice.get(msg.timeoutId)!
if (offDevice && action === 'import') {
message.error(`站点:${offDevice.sn}数据导出超时,请稍后重试`)
pubIdWithOffDevice.delete(msg.timeoutId)
onOffDeviceTransferStatus.value = 'timeout'
closeTransferMask()
}
}
// ================ 线 end =========
</script>
<style scoped lang="scss">
.transfer-log-wrap {
margin-top: 10px;
height: calc(100% - 30px);
@apply border-radius-8px bg-[#F9FAFB] p-10;
:deep(.el-scrollbar) {

8
src/views/stationData/type.ts

@ -6,7 +6,7 @@ export interface IOnlineDevice { @@ -6,7 +6,7 @@ export interface IOnlineDevice {
lastUpdated: number // 新增字段,记录最后更新时间
status?: string
isChecked?: boolean
upFirmware?: 'updating' | 'pending' | 'fulfilled' | 'rejected'
upFirmware?: 'updating' | 'pending' | 'fulfilled' | 'rejected' | 'timeout'
upFirmwareStatus?: IUpFirmwareStatus
}
@ -35,4 +35,10 @@ export interface IUpFirmwareStatus { @@ -35,4 +35,10 @@ export interface IUpFirmwareStatus {
step: number
progress: number | undefined
errMsg: string | undefined
}
export interface IMyPoint {
addr: string
label: string
}

4
vite.config.ts

@ -65,14 +65,12 @@ export default defineConfig({ @@ -65,14 +65,12 @@ export default defineConfig({
host: '0.0.0.0',
proxy: {
'/remoteServer': {
target: 'http://192.168.1.115:8080/',
target: 'http://192.168.1.199:8080/',
changeOrigin: true,
secure: false,
ws: true,
rewrite: path => path.replace(/^\/remoteServer/, ''),
},
}
},
})
Loading…
Cancel
Save