Compare commits

...

2 Commits

Author SHA1 Message Date
betaqi 1f6437b09b feat: 一些调整 1 month ago
betaqi 83aa271805 feat: 功能调整 1 month ago
  1. 7
      global.types/components.d.ts
  2. 3
      src/api/module/firmware/index.ts
  3. 70
      src/api/module/taks/index.ts
  4. 1
      src/components/Edfs-button.vue
  5. 47
      src/components/Edfs-table/index.vue
  6. 1
      src/components/Edfs-wrap.vue
  7. 10
      src/router/index.ts
  8. 23
      src/views/firmwareUpload/index.vue
  9. 7
      src/views/layout/index.vue
  10. 52
      src/views/stationData/components/offTransferDlg.vue
  11. 94
      src/views/stationData/components/onLineTransferDlg.vue
  12. 1
      src/views/stationData/index.vue
  13. 642
      src/views/stationData/transferData.vue
  14. 130
      src/views/taskList/index.vue
  15. 107
      src/views/taskList/infoDrawer.vue

7
global.types/components.d.ts vendored

@ -21,17 +21,24 @@ declare module 'vue' {
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider'] ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
ElContainer: typeof import('element-plus/es')['ElContainer'] ElContainer: typeof import('element-plus/es')['ElContainer']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
ElDialog: typeof import('element-plus/es')['ElDialog'] ElDialog: typeof import('element-plus/es')['ElDialog']
ElDivider: typeof import('element-plus/es')['ElDivider'] ElDivider: typeof import('element-plus/es')['ElDivider']
ElDrawer: typeof import('element-plus/es')['ElDrawer'] ElDrawer: typeof import('element-plus/es')['ElDrawer']
ElDropdown: typeof import('element-plus/es')['ElDropdown']
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
ElHeader: typeof import('element-plus/es')['ElHeader'] ElHeader: typeof import('element-plus/es')['ElHeader']
ElInput: typeof import('element-plus/es')['ElInput'] ElInput: typeof import('element-plus/es')['ElInput']
ElMenu: typeof import('element-plus/es')['ElMenu'] ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElPagination: typeof import('element-plus/es')['ElPagination']
ElProgress: typeof import('element-plus/es')['ElProgress'] ElProgress: typeof import('element-plus/es')['ElProgress']
ElRow: typeof import('element-plus/es')['ElRow'] ElRow: typeof import('element-plus/es')['ElRow']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
ElSubMenu: typeof import('element-plus/es')['ElSubMenu'] ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTag: typeof import('element-plus/es')['ElTag'] ElTag: typeof import('element-plus/es')['ElTag']
ElTooltip: typeof import('element-plus/es')['ElTooltip'] ElTooltip: typeof import('element-plus/es')['ElTooltip']
ElTree: typeof import('element-plus/es')['ElTree'] ElTree: typeof import('element-plus/es')['ElTree']

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

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

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

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

1
src/components/Edfs-button.vue

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

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

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

1
src/components/Edfs-wrap.vue

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

10
src/router/index.ts

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

23
src/views/firmwareUpload/index.vue

@ -2,12 +2,12 @@
<div class="flex justify-center items-center size-full"> <div class="flex justify-center items-center size-full">
<EdfsWrap :title="title" style="width: 50%; height: 50%"> <EdfsWrap :title="title" style="width: 50%; height: 50%">
<el-upload v-model:fileList="fileList" v-loading="loading" element-loading-text="上传中..." drag action="" <el-upload v-model:fileList="fileList" v-loading="loading" element-loading-text="上传中..." drag action=""
accept=".tar.gz" :limit="1" :on-exceed="handleExceed" :auto-upload="false" ref="uploadRef" accept=".zip,.tar,.tar.gz" :limit="1" :on-exceed="handleExceed" :auto-upload="false" ref="uploadRef"
class="h-[calc(100%-30px)] w-full"> :before-upload="beforeAvatarUpload" class="h-[calc(100%-30px)] w-full">
<div class="i-line-md:cloud-alt-upload-loop text-20px mx-auto"></div> <div class="i-line-md:cloud-alt-upload-loop text-20px mx-auto"></div>
<div class="text">拖拽文件或者 <em>点击上传</em></div> <div class="text">拖拽文件或者 <em>点击上传</em></div>
<template #tip v-if="!fileList.length"> <template #tip v-if="!fileList.length">
<div class="el-upload__tip">上传限制一个文件新文件会覆盖旧文件</div> <div class="el-upload__tip">上传限制一个文件新文件会覆盖旧文件仅支持 .zip.tar .tar.gz 格式</div>
</template> </template>
</el-upload> </el-upload>
<div class="flex justify-center"> <div class="flex justify-center">
@ -43,14 +43,23 @@ const handleExceed: UploadProps['onExceed'] = files => {
const uploadRef = ref<UploadInstance>() const uploadRef = ref<UploadInstance>()
const beforeAvatarUpload: UploadProps['beforeUpload'] = rawFile => { const beforeAvatarUpload: UploadProps['beforeUpload'] = rawFile => {
const accept = ['.zip', '.tar', '.tar.gz'] const fileName = rawFile.name.toLowerCase()
const fileTypes = rawFile.name.substring(rawFile.name.indexOf('.')) const supportedFormats = ['.zip', '.tar', '.tar.gz']
const isValidFormat = supportedFormats.some(format => fileName.endsWith(format))
if (!accept.includes(fileTypes)) { if (!isValidFormat) {
message.error('请上传 tar.gz 文件') message.error('请上传 .zip、.tar 或 .tar.gz 格式的文件')
uploadRef.value?.clearFiles?.() uploadRef.value?.clearFiles?.()
return false return false
} }
// const maxSize = 100 * 1024 * 1024
// if (rawFile.size > maxSize) {
// message.error('100MB')
// uploadRef.value?.clearFiles?.()
// return false
// }
return true return true
} }
function validate() { function validate() {

7
src/views/layout/index.vue

@ -50,10 +50,6 @@
{{ currentTime }} {{ currentTime }}
</div> </div>
</div> </div>
<!-- <div class="flex items-center gap-col-2 p-r-20">
<el-avatar :src="circleUrl" class="avatar" />
<span class="username">John Doe</span>
</div> -->
</el-header> </el-header>
<main class="main-wrap"> <main class="main-wrap">
<RouterView /> <RouterView />
@ -76,8 +72,9 @@ const { theme } = useTheme()
const menuList = computed<any[]>(() => { const menuList = computed<any[]>(() => {
let data = defaultRouter[0].children let data = defaultRouter[0].children
if (env.VITE_APP_ENV !== 'local') { if (env.VITE_APP_ENV !== 'local') {
data = data.filter(item => item.name != 'firmware-upload') data = data.filter(item => !['firmware-upload', 'task'].includes(item.name))
} }
console.log(data)
return data return data
} }
) )

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

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

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

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

1
src/views/stationData/index.vue

@ -78,7 +78,6 @@
</div> </div>
</div> </div>
</template> </template>
</div> </div>
</div> </div>
</EdfsWrap> </EdfsWrap>

642
src/views/stationData/transferData.vue

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

130
src/views/taskList/index.vue

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

107
src/views/taskList/infoDrawer.vue

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