Browse Source

feat: 功能添加

main
betaqi 2 months ago
parent
commit
b6289756a0
  1. 6
      global.types/components.d.ts
  2. 21
      src/api/module/firmware/index.ts
  3. 35
      src/api/module/transfer/index.ts
  4. 13
      src/router/index.ts
  5. 55
      src/stores/transferData.ts
  6. 2
      src/utils/zmq.ts
  7. 186
      src/views/firmwareUpload/index.vue
  8. 77
      src/views/stationData/components/deviceDrawer.vue
  9. 102
      src/views/stationData/components/siteTransferDlg.vue
  10. 2
      src/views/stationData/components/transferDlg.vue
  11. 194
      src/views/stationData/index.vue
  12. 257
      src/views/stationData/transferData.vue
  13. 14
      src/views/stationData/type.ts
  14. 29
      src/views/stationData/utils.ts

6
global.types/components.d.ts vendored

@ -16,6 +16,7 @@ declare module 'vue' { @@ -16,6 +16,7 @@ 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']
@ -24,6 +25,7 @@ declare module 'vue' { @@ -24,6 +25,7 @@ declare module 'vue' {
ElDialog: typeof import('element-plus/es')['ElDialog']
ElDrawer: typeof import('element-plus/es')['ElDrawer']
ElHeader: typeof import('element-plus/es')['ElHeader']
ElInput: typeof import('element-plus/es')['ElInput']
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElProgress: typeof import('element-plus/es')['ElProgress']
@ -32,7 +34,11 @@ declare module 'vue' { @@ -32,7 +34,11 @@ declare module 'vue' {
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
ElTag: typeof import('element-plus/es')['ElTag']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
ElUpload: typeof import('element-plus/es')['ElUpload']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
export interface ComponentCustomProperties {
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
}
}

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

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

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

@ -16,10 +16,7 @@ export const getDeviceDetails = (params: IGetDeviceDataParams) => @@ -16,10 +16,7 @@ export const getDeviceDetails = (params: IGetDeviceDataParams) =>
data: params,
})
export interface ISiteList {
export interface ISite {
id: string
name: string
export_time: string
@ -28,7 +25,7 @@ export interface ISiteList { @@ -28,7 +25,7 @@ export interface ISiteList {
}
export const getSiteList = () =>
globalServer<ISiteList[]>({
globalServer<ISite[]>({
url: `/api/sites`,
method: 'get',
})
@ -43,11 +40,39 @@ export interface IPointsParams { @@ -43,11 +40,39 @@ export interface IPointsParams {
site?: string
sn?: string
isLocal?: boolean
type: string
host?: string
}
export const getPoints = (params: IPointsParams) =>
globalServer({
url: `/api/points`,
method: 'get',
params,
})
export interface IPointGroupParams {
site?: string
sn?: string
isLocal?: boolean
host?: string
}
export interface IPointGroupOV {
id: number,
ip: string,
name: string,
port: number,
slave_addr: number,
type: string,
children?: IPointGroupOV[]
}
export const getPointGroup = (params: IPointGroupParams) => {
return globalServer<IPointGroupOV[]>({
url: `/api/modules`,
method: 'get',
params,
})
}

13
src/router/index.ts

@ -31,7 +31,18 @@ export const defaultRouter = [ @@ -31,7 +31,18 @@ export const defaultRouter = [
isShow: false,
icon: 'i-mingcute:transfer-2-line',
},
}
},
// 固件上传
{
path: '/firmware-upload',
name: 'firmware-upload',
component: () => import('@/views/firmwareUpload/index.vue'),
meta: {
title: '固件上传',
isShow: true,
icon: 'i-mingcute:transfer-2-line',
},
},
],
},
]

55
src/stores/transferData.ts

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
import type { IOnlineDevice } from '@/views/stationData/type'
import type { IOnlineDevice, IUpFirmwareStatus } from '@/views/stationData/type'
import ZMQWorker from '@/composables/useZMQJsonWorker'
import { getSubTopic, type SubMsgData } from '@/utils/zmq'
import { getDeviceTopic } from '@/views/stationData/utils'
@ -79,6 +79,55 @@ export const useTransferDataStore = defineStore('transfer', () => { @@ -79,6 +79,55 @@ export const useTransferDataStore = defineStore('transfer', () => {
})
function upFirmwareStatus(sn: string, feedback: any[]) {
const device = devicesMap.get(sn)
if (device) {
device.upFirmware = 'updating'
const step = feedback[0]
const progress = feedback[2] || undefined
const errMsg = feedback[3] || undefined
if (step < (device.upFirmwareStatus?.step ?? -100)) return
device.upFirmwareStatus = {
step,
progress,
errMsg
}
}
}
function upFirmwarePending(snList: string[]) {
for (const sn of snList) {
const device = devicesMap.get(sn)
if (device) {
device.upFirmware = 'pending'
device.upFirmwareStatus = {
step: 0,
progress: undefined,
errMsg: undefined,
}
}
}
}
function upFirmwareReset(snList: string[]) {
for (const sn of snList) {
const device = devicesMap.get(sn)
if (device) {
upFirmwareSucceed(sn)
}
}
}
function upFirmwareSucceed(sn: string) {
const device = devicesMap.get(sn)
if (device) {
device.upFirmware = undefined
device.upFirmwareStatus = undefined
}
}
return {
isConnected,
devicesMap,
@ -88,5 +137,9 @@ export const useTransferDataStore = defineStore('transfer', () => { @@ -88,5 +137,9 @@ export const useTransferDataStore = defineStore('transfer', () => {
offlineCount,
checkDeviceStatus,
initConnectSite,
upFirmwarePending,
upFirmwareReset,
upFirmwareStatus,
upFirmwareSucceed,
}
})

2
src/utils/zmq.ts

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
import { v4 as uuidv4 } from 'uuid';
export type ManualAction =
'init' | 'release' | 'write' | 'report' | 'lock' | 'unlock' |
'export' | 'cancel' | 'import'
'export' | 'cancel' | 'import' | 'upgrade'
export type ZmqStatus = 'disconnected' | 'connected'

186
src/views/firmwareUpload/index.vue

@ -0,0 +1,186 @@ @@ -0,0 +1,186 @@
<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"
>
<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">
<div class="el-upload__tip">上传限制一个文件新文件会覆盖旧文件</div>
</template>
</el-upload>
<div class="flex justify-center">
<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>
</div>
</template>
<script setup lang="ts">
import { uploadFirmwareFile } from '@/api/module/firmware'
import { useMessage } from '@/composables/useMessage'
import type {
UploadInstance,
UploadProps,
UploadRawFile,
UploadUserFile,
} from 'element-plus'
const message = useMessage()
const fileList = ref<UploadUserFile[]>([])
const upload = ref<UploadInstance>()
const handleExceed: UploadProps['onExceed'] = files => {
upload.value!.clearFiles()
const file = files[0] as UploadRawFile
upload.value!.handleStart(file)
}
const beforeAvatarUpload: UploadProps['beforeUpload'] = rawFile => {
const accept = ['.zip', '.tar', '.tar.gz']
const fileTypes = rawFile.name.substring(rawFile.name.lastIndexOf('.'))
if (!accept.includes(fileTypes)) {
message.error('请上传 tar.gz 文件')
return false
}
return true
}
function validate() {
if (!fileList.value.length) {
message.error('请上传文件')
return true
}
if (beforeAvatarUpload(fileList.value[0].raw!)) {
message.error('请上传 tar.gz 文件')
return true
}
return false
}
const loading = ref(false)
const abortController = ref<AbortController>()
async function onSave() {
if (loading.value) {
message.error('文件正在上传中,请稍后')
return
}
if (!validate()) return
loading.value = true
abortController.value = new AbortController()
const userFile = fileList.value[0] as UploadUserFile
const res = await uploadFirmwareFile(userFile.raw as File, abortController.value)
if (res.code === 200) {
message.success('上传成功')
} else {
message.error('上传失败')
}
// const file = userFile.raw as File
// const fileName = userFile.name
// const chunkList = await createChunksFromFileData(file, 1024)
// try {
// for (const chunk of chunkList) {
// const { chunkData, offset } = chunk
// const res = await uploadSftpFile(
// {
// fileName,
// chunk: chunkData,
// targetPath: props.curPath,
// offset,
// },
// abortController.value
// )
// if (res.code === -777) {
// return
// }
// if (![200, 0].includes(res.code)) {
// //
// message.error(`,${res.msg}`)
// return
// }
// }
// } catch (error) {
// message.error('')
// }
abortController.value = undefined
loading.value = false
onClone()
}
async function readFileAsArrayBuffer(file: File): Promise<ArrayBuffer> {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.readAsArrayBuffer(file)
reader.onload = () => resolve(reader.result as ArrayBuffer)
reader.onerror = reject
})
}
async function createChunksFromFileData(file: File, chunkSize: number) {
const fileData = await readFileAsArrayBuffer(file)
const chunkList: { offset: number; chunkIndex: number; chunkData: Uint8Array }[] = []
let offset = 0
let chunkIndex = 0
while (offset < fileData.byteLength) {
const chunkData = new Uint8Array(fileData.slice(offset, offset + chunkSize))
chunkList.push({
offset,
chunkIndex,
chunkData,
})
offset += chunkSize
chunkIndex++
}
return chunkList
}
function clearData() {
fileList.value = []
}
function onClone() {
if (loading.value) {
abortController.value?.abort()
loading.value = false
}
clearData()
}
</script>
<style lang="scss" scoped>
:deep(.el-upload) {
height: calc(100% - 50px);
width: 100%;
}
:deep(.el-upload-dragger) {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
</style>

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

@ -9,6 +9,13 @@ @@ -9,6 +9,13 @@
: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"
@ -21,7 +28,6 @@ @@ -21,7 +28,6 @@
</template>
<script setup lang="ts">
import dayjs from 'dayjs'
import NewDataChart from './newDataChart.vue'
import {
getPubInitData,
@ -33,9 +39,12 @@ import ZMQWorker from '@/composables/useZMQJsonWorker' @@ -33,9 +39,12 @@ import ZMQWorker from '@/composables/useZMQJsonWorker'
import type { IOfflineDevice, IOnlineDevice } from '../type'
import {
getDeviceDetails,
getPointGroup,
getPoints,
type IPointGroupOV,
type IPointGroupParams,
type IPointsParams,
type ISiteList,
type ISite,
} from '@/api/module/transfer'
import { useMessage } from '@/composables/useMessage'
import { getTransferTopic, postTransferTopic } from '../utils'
@ -46,7 +55,7 @@ const title = computed(() => (props.isTransfer ? '数据详情' : `已迁移数 @@ -46,7 +55,7 @@ const title = computed(() => (props.isTransfer ? '数据详情' : `已迁移数
const message = useMessage()
const props = defineProps<{
siteInfo: ISiteList
siteInfo: ISite | null
isTransfer: boolean
}>()
@ -68,7 +77,13 @@ const pointsData = ref<{ addr: string; label: string }[]>([]) @@ -68,7 +77,13 @@ const pointsData = ref<{ addr: string; label: string }[]>([])
const columsParams = computed(() => ['ts', ...tableCol.value.map(i => i.addr)])
async function loadPoints() {
const params: IPointsParams = {}
if (!curGroup.value) {
message.error('请先选择点位组')
return
}
const params: IPointsParams = {
type: curGroup.value,
}
if (props.isTransfer) {
const onlineDevice = curDevice.value as IOnlineDevice
params.isLocal = false
@ -76,7 +91,7 @@ async function loadPoints() { @@ -76,7 +91,7 @@ async function loadPoints() {
} else {
const offlineDevice = curDevice.value as IOfflineDevice
params.sn = offlineDevice.sn
params.site = props.siteInfo.name
params.site = props.siteInfo!.name
params.isLocal = true
}
@ -131,22 +146,19 @@ async function loadDeviceDetails() { @@ -131,22 +146,19 @@ async function loadDeviceDetails() {
}
})
})
console.log(chartDatas)
console.log(axisData)
console.log(tableCol.value)
isShowDrawer.value = true
}
fullscreenLoading.value?.close()
}
function zmqImport(device: IOfflineDevice) {
if (!device.sn || !props.siteInfo.name) {
if (!device.sn || !props.siteInfo!.name) {
message.error('未找到站点或设备')
return
}
const msg = getPubInitData<'import'>(
'import',
['', '', '', '', '', '', `${props.siteInfo.name}/${device.sn}`],
['', '', '', '', '', '', `${props.siteInfo!.name}/${device.sn}`],
'yes'
)
pubIdWithDevice.set(msg.id, { device, action: 'import' })
@ -157,13 +169,15 @@ function zmqImport(device: IOfflineDevice) { @@ -157,13 +169,15 @@ function zmqImport(device: IOfflineDevice) {
function zmqImportCb(msg: PubMsgData) {
const { id, result } = msg
if (result !== 'progress') {
if (result === 'success') {
const { device, action } = pubIdWithDevice.get(id)!
if (result === 'success' && device && action === 'import') {
loadDeviceDetails()
pubIdWithDevice.delete(id)
} else {
message.error(`设备数据获取失败`)
fullscreenLoading.value?.close()
pubIdWithDevice.delete(id)
}
pubIdWithDevice.delete(id)
}
}
@ -192,6 +206,43 @@ const openFullScreen = () => { @@ -192,6 +206,43 @@ const openFullScreen = () => {
})
}
const pointGroup = ref<IPointGroupOV[]>([])
const curGroup = ref<string>()
async function loadPointGroup() {
if (!props.siteInfo) return
const params: IPointGroupParams = {}
if (props.isTransfer) {
const onlineDevice = curDevice.value as IOnlineDevice
params.isLocal = false
params.host = onlineDevice.clientIp
} else {
const offlineDevice = curDevice.value as IOfflineDevice
params.sn = offlineDevice.sn
params.site = props.siteInfo!.name
params.isLocal = true
}
const res = await getPointGroup(params)
if (res.code === 0) {
pointGroup.value = Array.isArray(res?.data) ? res.data : []
if (res.data.length > 0) {
curGroup.value = res.data[0].type
}
} else {
message.error('获取点位组数据失败')
}
}
function onGroupChange(item: IPointGroupOV) {}
onMounted(async () => {
await loadPointGroup()
if (props.isTransfer) {
await loadDeviceDetails()
}
})
defineExpose({
open,
openFullScreen,
@ -201,9 +252,11 @@ defineExpose({ @@ -201,9 +252,11 @@ defineExpose({
<style scoped lang="scss">
.fault-rule-drawer {
font-size: 16px;
:deep(.el-drawer__header) {
color: var(--text-color);
}
.drawer-box {
width: 100%;
height: 100%;

102
src/views/stationData/components/siteTransferDlg.vue

@ -0,0 +1,102 @@ @@ -0,0 +1,102 @@
<template>
<EdfsDialog :title="'数据迁移'" :is-show="visible" width="40%" @on-close="close" @on-save="onSave">
<div class="flex-col gap-10 w-80% m-x-30px">
<el-row>
<div class="label">
<span class="require">*</span>
客户端IP:
</div>
<el-input v-model="form.clientIp" class="flex-1" placeholder="请输入客户端IP" />
</el-row>
</div>
</EdfsDialog>
</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 { ISite } from '@/api/module/transfer'
const message = useMessage()
const emit = defineEmits<{
'on-save': [msg: PublishMsg<'import'>, site: ISite]
}>()
const visible = ref(false)
const fromData = {
clientIp: '',
}
const curSite = ref<ISite>()
const form = ref(cloneDeep(fromData))
function open(item: ISite) {
curSite.value = item
visible.value = true
}
function onSave() {
if (!verifyData()) return
if (!curSite.value) {
message.error('请选择设备')
return
}
const params = [
`${form.value.clientIp}`,
'',
'',
'',
'',
'',
`${curSite.value.export_root_path}`,
]
const msg = getPubInitData<'import'>('import', params)
emit('on-save', msg, curSite.value)
close()
}
function close() {
form.value = cloneDeep(fromData)
curSite.value = undefined
visible.value = false
}
function verifyData() {
if (!form.value.clientIp) {
message.error('请输入客户端IP')
return false
}
return true
}
defineExpose({
open,
})
</script>
<style lang="scss" scoped>
.el-row {
@apply h-32px;
column-gap: 8px;
.label {
color: var(--label-color);
line-height: 33px;
text-align: right;
width: 110px;
.require {
color: red;
}
}
}
</style>

2
src/views/stationData/components/transferDlg.vue

@ -18,7 +18,7 @@ @@ -18,7 +18,7 @@
class="flex-1"
start-placeholder="开始时间"
end-placeholder="结束时间"
:default-value="defaultDateRange"
:default-value="(defaultDateRange as any)"
:disabled-date="disabledAfterToday"
/></el-row>
</div>

194
src/views/stationData/index.vue

@ -1,12 +1,6 @@ @@ -1,12 +1,6 @@
<template>
<div class="flex-col gap-16 wh-full">
<EdfsWrap
title="当前连接站点"
v-if="connectSite?.title"
shape="circle"
shapeColor="#4B9E5F"
class="h-auto"
>
<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]">
@ -32,29 +26,29 @@ @@ -32,29 +26,29 @@
</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
>
<el-button type="primary" style="height: 28px; padding: 0 12px" color="#4B9E5F"
@click="onTransferData">迁移数据</el-button>
</div>
</div>
</div>
</div>
</EdfsWrap>
<EdfsWrap
title="迁移历史"
shape="circle"
shapeColor="#F1BF63"
class="flex-1"
useScrollBar
>
<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"
@click="onSiteDetails(item)"></div>
</el-tooltip>
</div>
</div>
<div class="body">
<div class="info">
@ -62,10 +56,6 @@ @@ -62,10 +56,6 @@
<div class="info-item-label">导出路径</div>
<div class="info-item-value">{{ item.export_root_path }}</div>
</div>
<!-- <div class="info-item">
<div class="info-item-label">数据大小</div>
<div class="info-item-value">2.5GB</div>
</div> -->
</div>
</div>
<div class="footer">
@ -75,20 +65,59 @@ @@ -75,20 +65,59 @@
{{ dayjs(item.export_time).format('YYYY-MM-DD HH:mm:ss') }}
</div>
</div>
<div class="info-details" @click="onSiteDetails(item)">详情</div>
</div>
</div>
</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">
import dayjs from 'dayjs'
import { useTransferDataStore } from '@/stores/transferData'
import { storeToRefs } from 'pinia'
import { getSiteList, type ISiteList } from '@/api/module/transfer'
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 worker = ZMQWorker.getInstance()
const router = useRouter()
const transferDataStore = useTransferDataStore()
@ -110,7 +139,7 @@ function onTransferData() { @@ -110,7 +139,7 @@ function onTransferData() {
})
}
function onSiteDetails(site: ISiteList) {
function onSiteDetails(site: ISite) {
router.push({
path: '/data-transfer',
query: {
@ -120,7 +149,26 @@ function onSiteDetails(site: ISiteList) { @@ -120,7 +149,26 @@ function onSiteDetails(site: ISiteList) {
})
}
const siteList = ref<ISiteList[]>([])
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',
}
])
async function loadSiteList() {
const res = await getSiteList()
if (res.code === 200 || res.code === 0) {
@ -128,12 +176,76 @@ async function loadSiteList() { @@ -128,12 +176,76 @@ 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()
})
</script>
<style lang="scss" scoped>
<style lang="scss">
.transfer-mask {
:deep(.el-loading-spinner) {
position: relative !important;
}
}
</style>
<style lang="scss">
.station-list-flow {
width: 100%;
height: 100%;
@ -142,6 +254,7 @@ onMounted(() => { @@ -142,6 +254,7 @@ onMounted(() => {
column-gap: 20px;
row-gap: 24px;
.station-item {
width: 280px;
height: 180px;
@ -152,6 +265,7 @@ onMounted(() => { @@ -152,6 +265,7 @@ onMounted(() => {
flex-direction: column;
box-sizing: border-box;
}
.title {
display: flex;
align-items: center;
@ -164,29 +278,40 @@ onMounted(() => { @@ -164,29 +278,40 @@ onMounted(() => {
justify-content: space-between;
padding: 0 12px;
user-select: none;
.title-edit-btns {
display: flex;
margin-left: 12px;
}
}
.body {
background-color: var(--station-card-bg);
display: flex;
flex-direction: column;
justify-content: center;
width: 100%;
flex: 1;
overflow: hidden;
.info {
display: flex;
width: 100%;
justify-content: space-around;
.info-item {
display: flex;
flex-direction: column;
width: 100%;
align-items: center;
.info-item-label {
font-size: 14px;
color: var(--label-color);
}
.info-item-value {
font-size: 20px;
font-weight: 500;
@ -196,11 +321,17 @@ onMounted(() => { @@ -196,11 +321,17 @@ onMounted(() => {
align-items: center;
justify-content: center;
column-gap: 4px;
width: 90%;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
padding: 0 8px;
color: var(--station-info-val-text);
}
}
}
}
.footer {
display: flex;
justify-content: space-around;
@ -213,15 +344,18 @@ onMounted(() => { @@ -213,15 +344,18 @@ onMounted(() => {
font-size: 14px;
color: #999;
font-weight: 400;
.value {
color: #999;
}
}
.info-details {
font-size: 14px;
color: #f1bf63;
cursor: pointer;
text-decoration: underline;
&:hover {
color: #8ace6a;
}

257
src/views/stationData/transferData.vue

@ -5,12 +5,13 @@ @@ -5,12 +5,13 @@
</el-button>
<EdfsWrap title="设备列表" class="flex-1" useScrollBar>
<template #title-right v-if="isTransfer">
<template v-if="isBatchTransfer">
<el-button type="primary" @click="onBatchSave"> 确定迁移 </el-button>
<template v-if="isBatchTransfer || isBatchUpgrade">
<el-button type="primary" @click="onBatchSave"> 确定{{ batchText }} </el-button>
<el-button type="info" @click="onBatchCancel"> 取消 </el-button>
</template>
<template v-else>
<el-button type="primary" @click="onBatchTransfer"> 批量迁移 </el-button>
<el-button type="primary" @click="onBatchUpgrade"> 批量升级 </el-button>
</template>
</template>
<div class="device-list-wrap">
@ -18,7 +19,7 @@ @@ -18,7 +19,7 @@
<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">
<el-checkbox :value="item.sn" v-if="isBatchTransfer || isBatchUpgrade">
<div>设备ID: {{ item.sn }}</div>
</el-checkbox>
<div v-else>
@ -32,8 +33,15 @@ @@ -32,8 +33,15 @@
v-if="isTransfer && item.status === '在线'"
>
<i
class="i-line-md:cloud-alt-upload-loop :hover:color-[#8ACE6A] color-[#4B9E5F] cursor-pointer text-20px"
@click="onTransfer(item as IOnlineDevice)"
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>
<el-tooltip content="详情">
@ -45,7 +53,7 @@ @@ -45,7 +53,7 @@
</div>
</div>
<div class="device-item-body">
<div class="device-item-body relative">
<template v-if="isTransfer">
<template v-for="key in Object.keys(onlineDeviceMap)">
<div class="info-item" v-if="isTransfer && key === 'status'">
@ -76,6 +84,56 @@ @@ -76,6 +84,56 @@
</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>
<template v-if="item.upFirmware === 'updating'">
<div class="device-item-body">
<div class="info-item">
<div>当前步骤:</div>
<div>{{ item.upFirmwareStatus?.step ?? '--' }}</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">等待升级中...</div>
</div>
</template>
<template v-if="item.upFirmware === 'rejected'">
<div class="device-item-body">
<div class="info-item">
<div>当前步骤:</div>
<div>
{{
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>
</div>
</div>
</div>
</el-checkbox-group>
@ -144,23 +202,33 @@ import TransferDlg from './components/transferDlg.vue' @@ -144,23 +202,33 @@ import TransferDlg from './components/transferDlg.vue'
import ZMQWorker from '@/composables/useZMQJsonWorker'
import {
getPubInitData,
type ManualAction,
type PublishMsg,
type PubMsgData,
type TimeoutMsg,
type ZmqStatus,
} from '@/utils/zmq'
import { useTransferDataStore } from '@/stores/transferData'
import { storeToRefs } from 'pinia'
import type { IOfflineDevice, IOnlineDevice } from './type'
import type {
IOfflineDevice,
IOnlineDevice,
IUpFirmwareStatus,
UpFirmwarsDevice,
} from './type'
import { useMessage } from '@/composables/useMessage'
import { getDeviceList, type ISiteList } from '@/api/module/transfer'
import { getDeviceList, type ISite } from '@/api/module/transfer'
import DeviceDrawer from './components/deviceDrawer.vue'
import { getTransferTopic, postTransferTopic } from './utils'
import {
getFirmwareUpTopic,
getTransferTopic,
postFirmwareUpTopic,
postTransferTopic,
upgradeProgressStatusMap,
} from './utils'
import { getFirmwarePath } from '@/api/module/firmware'
const transferDlgRef = ref<typeof TransferDlg>()
const router = useRouter()
const route = useRoute()
const siteInfo = ref<ISiteList>(
const siteInfo = ref<ISite>(
route.query.site ? JSON.parse(route.query.site as string) : null
)
const type = ref<'export' | 'details'>(route.query.type as 'export' | 'details')
@ -173,8 +241,9 @@ const message = useMessage() @@ -173,8 +241,9 @@ const message = useMessage()
const worker = ZMQWorker.getInstance()
const zmqStatus = inject<Ref<ZmqStatus>>('zmqStatus')!
const transferDataStore = useTransferDataStore()
const { upFirmwarePending, upFirmwareReset, upFirmwareStatus, upFirmwareSucceed } =
transferDataStore
const { devicesMap } = storeToRefs(transferDataStore)
const transferStatusMap = {
@ -184,7 +253,7 @@ const transferStatusMap = { @@ -184,7 +253,7 @@ const transferStatusMap = {
timeout: '迁移超时',
}
const pubIdWithDevice = new Map<string, { device: IOnlineDevice; action: ManualAction }>()
const exportPubDeviceMap = new Map<string, { device: IOnlineDevice; action: 'export' }>()
const curTransferLog = ref<
{ msg: string; host: string; status: 'success' | 'padding' | 'failed' }[]
@ -199,7 +268,7 @@ const devices = computed(() => { @@ -199,7 +268,7 @@ const devices = computed(() => {
function onSave(msg: PublishMsg<'export'>, device: IOnlineDevice) {
curTransferLog.value = []
worker.publish(postTransferTopic, msg, true, zmqTimeoutCb)
pubIdWithDevice.set(msg.id, { device, action: 'export' })
exportPubDeviceMap.set(msg.id, { device, action: 'export' })
worker.subscribe(getTransferTopic, zmqExportCb, msg.id)
if (isBatchTransfer.value) {
onBatchCancel()
@ -213,6 +282,7 @@ const statusMap = { @@ -213,6 +282,7 @@ const statusMap = {
}
function zmqExportCb(msg: PubMsgData) {
if (!isTransfer.value) return
const { feedback, result, id } = msg
if (feedback && feedback[0]) {
const status = feedback[1]
@ -230,11 +300,10 @@ function zmqExportCb(msg: PubMsgData) { @@ -230,11 +300,10 @@ function zmqExportCb(msg: PubMsgData) {
// status failed
transferStatus.value = 'progress'
if (result !== 'progress') {
const curMsgInfo = pubIdWithDevice.get(id)!
const curMsgInfo = exportPubDeviceMap.get(id)!
if (!curMsgInfo) return
const { device, action } = curMsgInfo
if (device) {
if (device && action === 'export') {
if (result === 'success') {
const res = setTransferStatus()
if (res === 0) {
@ -248,7 +317,7 @@ function zmqExportCb(msg: PubMsgData) { @@ -248,7 +317,7 @@ function zmqExportCb(msg: PubMsgData) {
message.error(`迁移失败`)
transferStatus.value = 'failed'
}
pubIdWithDevice.delete(msg.id)
exportPubDeviceMap.delete(msg.id)
}
}
}
@ -277,19 +346,22 @@ function onStopTransfer() { @@ -277,19 +346,22 @@ function onStopTransfer() {
function clearTransferData() {
curTransferLog.value = []
transferStatus.value = undefined
pubIdWithDevice.clear()
exportPubDeviceMap.clear()
}
function zmqTimeoutCb(msg: TimeoutMsg) {
const { device, action } = pubIdWithDevice.get(msg.timeoutId)!
const { device, action } = exportPubDeviceMap.get(msg.timeoutId)!
if (device && action === 'export') {
message.error(`迁移超时,请重新稍后尝试`)
pubIdWithDevice.delete(msg.timeoutId)
exportPubDeviceMap.delete(msg.timeoutId)
}
}
const onlineDeviceMap: Record<
keyof Omit<IOnlineDevice, 'lastUpdated' | 'sn' | 'isChecked'>,
keyof Omit<
IOnlineDevice,
'lastUpdated' | 'sn' | 'isChecked' | 'upFirmware' | 'upFirmwareStatus'
>,
string
> = {
status: '状态',
@ -310,22 +382,12 @@ const onlineDeviceCheckList = ref<string[]>([]) @@ -310,22 +382,12 @@ const onlineDeviceCheckList = ref<string[]>([])
const isBatchTransfer = ref(false)
function onBatchTransfer() {
onlineDeviceCheckList.value = []
isBatchTransfer.value = true
}
function onBatchSave() {
if (!onlineDeviceCheckList.value.length) {
message.error('请选择要迁移的设备')
return
}
const checkList = onlineDeviceCheckList.value.map(sn => {
return devicesMap.value.get(sn)
})
const clientIpList = checkList
.map(item => item?.clientIp)
.filter(Boolean)
.join(',')
function onBatchTransferSave(checkList: IOnlineDevice[]) {
const clientIpList = checkList.map(item => item?.clientIp).join(',')
const pathList = checkList
.map(item => `${item?.stationName}/${item?.sn}`)
.filter(Boolean)
@ -334,8 +396,37 @@ function onBatchSave() { @@ -334,8 +396,37 @@ function onBatchSave() {
transferDlgRef.value?.open(checkList[0], clientIpList, pathList)
}
const isBatchUpgrade = ref(false)
function onBatchUpgrade() {
onlineDeviceCheckList.value = []
isBatchUpgrade.value = true
}
const batchText = computed(() => {
return isBatchTransfer.value ? '迁移' : '升级'
})
function onBatchSave() {
if (!onlineDeviceCheckList.value.length) {
message.error(`请选择要${batchText.value}的设备`)
return
}
const checkList = onlineDeviceCheckList.value
.map(sn => {
return devicesMap.value.get(sn) ?? undefined
})
.filter(Boolean) as IOnlineDevice[]
if (isBatchTransfer.value) {
onBatchTransferSave(checkList)
}
if (isBatchUpgrade.value) {
onFirmwareUpload(checkList)
}
}
function onBatchCancel() {
isBatchTransfer.value = false
isBatchUpgrade.value = false
onlineDeviceCheckList.value = []
}
function onTransfer(item: IOnlineDevice) {
@ -364,7 +455,7 @@ onBeforeRouteLeave(async (to, from, next) => { @@ -364,7 +455,7 @@ onBeforeRouteLeave(async (to, from, next) => {
}
})
const deviceList = ref<IOfflineDevice[]>()
const deviceList = ref<IOfflineDevice[]>([])
async function loadDeviceList() {
const res = await getDeviceList(siteInfo.value.id)
@ -373,9 +464,16 @@ async function loadDeviceList() { @@ -373,9 +464,16 @@ async function loadDeviceList() {
}
}
onMounted(() => {
const firmwarePath = ref('')
onMounted(async () => {
if (!isTransfer.value) {
loadDeviceList()
} else {
const res = await getFirmwarePath()
if (res.code === 200 || res.code === 0) {
firmwarePath.value = res.data
}
}
})
@ -385,6 +483,70 @@ function onDeviceDetails(item: IOfflineDevice) { @@ -385,6 +483,70 @@ function onDeviceDetails(item: IOfflineDevice) {
deviceDrawerRef.value?.openFullScreen()
deviceDrawerRef.value?.open(item)
}
const isCanFirmwareUpload = computed(() => !!firmwarePath.value && isTransfer.value)
const upgradeSnList = ref<string[]>([])
const upgradePubDeviceMap = new Map<string, UpFirmwarsDevice>()
function onFirmwareUpload(devices: IOnlineDevice[]) {
upgradeSnList.value = []
if (!isCanFirmwareUpload.value) {
message.error('升级失败,请检查固件路径')
onBatchCancel()
return
}
let deviceSn = devices[0].sn
if (isBatchUpgrade.value) {
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)
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') {
message.error(`固件升级超时,请重新稍后尝试`)
upgradePubDeviceMap.delete(msg.timeoutId)
upFirmwareReset(upgradeSnList.value)
}
}
function zmqUpgradeCb(msg: PubMsgData) {
if (!isTransfer.value) return
const status = msg.result
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)
if (curentDevice && curentDevice.action === 'upgrade') {
const { device } = curentDevice
if (device) {
if (status === 'progress') {
upFirmwareStatus(deviceSn, msg.feedback)
if (progressStatus === 4 && progress === 100) {
upFirmwareSucceed(deviceSn)
}
}
}
if (status === 'success' || status === 'error') {
message.success(`固件升级${status === 'success' ? '完成' : '失败'}`)
upgradePubDeviceMap.delete(deviceSn)
}
}
}
</script>
<style scoped lang="scss">
@ -392,47 +554,60 @@ function onDeviceDetails(item: IOfflineDevice) { @@ -392,47 +554,60 @@ function onDeviceDetails(item: IOfflineDevice) {
margin-top: 10px;
height: calc(100% - 30px);
@apply border-radius-8px bg-[#F9FAFB] p-10;
:deep(.el-scrollbar) {
height: calc(100% - 20px);
}
}
.device-list-wrap {
@apply wh-full;
:deep(.el-checkbox-group) {
@apply wh-full flex flex-wrap gap-col-6 gap-row-4;
}
:deep(.el-checkbox__inner) {
width: 18px;
height: 18px;
&::after {
left: 6px;
top: 3px;
}
}
:deep(.el-checkbox__label) {
@apply text-14px font-500 text-[#313131];
}
:deep(.el-checkbox__input.is-checked + .el-checkbox__label) {
color: var(--el-color-primary);
}
.device-item {
@apply w-289 h-160 border border-solid border-[#E0E0E0] rounded-8px p-x-14 p-y-10 flex-col;
.device-item-header {
@apply w-full text-black text-16px font-500 flex items-center justify-between;
@apply w-full text-black text-18px font-500 flex items-center justify-between;
.info {
font-size: 14px;
color: #f1bf63;
cursor: pointer;
text-decoration: underline;
&:hover {
color: #8ace6a;
}
}
}
.device-item-body {
@apply flex-1 flex-col m-t-10 color-[#6C727F] text-14px;
@apply flex-col m-t-10 text-sm gap-6;
.info-item {
@apply flex-1 flex items-center gap-col-2;
@apply flex items-center gap-col-2;
}
}
}

14
src/views/stationData/type.ts

@ -6,6 +6,8 @@ export interface IOnlineDevice { @@ -6,6 +6,8 @@ export interface IOnlineDevice {
lastUpdated: number // 新增字段,记录最后更新时间
status?: string
isChecked?: boolean
upFirmware?: 'updating' | 'pending' | 'fulfilled' | 'rejected'
upFirmwareStatus?: IUpFirmwareStatus
}
export interface IOfflineDevice {
@ -22,3 +24,15 @@ export interface IOfflineDevice { @@ -22,3 +24,15 @@ export interface IOfflineDevice {
}
export type IDevice = IOnlineDevice | IOfflineDevice
export interface UpFirmwarsDevice {
device: IOnlineDevice;
action: 'upgrade';
}
export interface IUpFirmwareStatus {
step: number
progress: number | undefined
errMsg: string | undefined
}

29
src/views/stationData/utils.ts

@ -8,3 +8,32 @@ export const postTransferTopic = getPubTopic('event', 'transfer') @@ -8,3 +8,32 @@ export const postTransferTopic = getPubTopic('event', 'transfer')
// 获取迁移设备信息主题 // action: export => 迁移进度,
export const getTransferTopic = getSubTopic('server', 'event', 'transfer')
// 获取设备固件升级主题
export const postFirmwareUpTopic = getPubTopic('event', 'upgrade')
// 获取设备固件升级进度主题
export const getFirmwareUpTopic = getSubTopic('client', 'event', 'upgrade')
export const upgradeProgressStatusMap = [
{
status: 0,
text: '准备升级',
},
{
status: 1,
text: '获取安装包',
},
{
status: 2,
text: '校验安装包',
},
{
status: 3,
text: '解析安装文件',
},
{
status: 4,
text: '安装中',
},
]
Loading…
Cancel
Save