You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

616 lines
18 KiB

4 months ago
<template>
<div class="flex-col gap-16 wh-full">
<el-button type="primary" @click="onBack" class="w-150px">
<i class="i-line-md:arrow-left"></i>返回站点数据
</el-button>
<EdfsWrap title="设备列表" class="flex-1" useScrollBar>
<template #title-right v-if="isTransfer">
<template v-if="isBatchTransfer || isBatchUpgrade">
<el-button type="primary" @click="onBatchSave"> 确定{{ batchText }} </el-button>
4 months ago
<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>
4 months ago
</template>
</template>
<div class="device-list-wrap">
<el-checkbox-group v-model="onlineDeviceCheckList">
<div class="device-item" v-for="item in devices">
<div class="device-item-header">
<div class="flex items-center">
<el-checkbox :value="item.sn" v-if="isBatchTransfer || isBatchUpgrade">
4 months ago
<div>设备ID: {{ item.sn }}</div>
</el-checkbox>
<div v-else>
<div>设备ID: {{ item.sn }}</div>
</div>
</div>
<div class="flex items-center gap-col-2">
<el-tooltip
content="数据迁移"
v-if="isTransfer && item.status === '在线'"
>
<i
class="i-mdi:database-arrow-right-outline :hover:color-[#8ACE6A] color-[#4B9E5F] cursor-pointer text-20px"
@click="onTransfer(item)"
></i>
</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])"
4 months ago
></i>
</el-tooltip>
<el-tooltip content="详情">
<div
class="i-material-symbols:info-outline :hover:color-[#8ACE6A] color-[#4B9E5F] cursor-pointer text-20px"
@click="onDeviceDetails(item)"
></div>
</el-tooltip>
</div>
</div>
<div class="device-item-body relative">
4 months ago
<template v-if="isTransfer">
<template v-for="key in Object.keys(onlineDeviceMap)">
<div class="info-item" v-if="isTransfer && key === 'status'">
<div>{{ onlineDeviceMap.status }}:</div>
<el-tag :type="item.status === '在线' ? 'success' : 'danger'">
{{ item.status }}
</el-tag>
</div>
<div class="info-item" v-else>
<div>{{ onlineDeviceMap[key as keyof typeof onlineDeviceMap] }}:</div>
<div>{{ item[key] }}</div>
</div>
</template>
</template>
<template v-else>
<template v-for="key in Object.keys(offlineDeviceMap)">
<div class="info-item" v-if="key === 'create_time'">
<div>
{{ offlineDeviceMap[key as keyof typeof offlineDeviceMap] }}:
</div>
<div>{{ dayjs(item[key]).format('YYYY-MM-DD HH:mm:ss') }}</div>
</div>
<div class="info-item" v-else>
<div>
{{ offlineDeviceMap[key as keyof typeof offlineDeviceMap] }}:
</div>
<div>{{ item[key] }}</div>
</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>
4 months ago
</div>
</div>
</el-checkbox-group>
</div>
</EdfsWrap>
<EdfsWrap title="迁移进度" class="transfer-wrap h-[42%]" v-if="isShowTransfer">
<div class="flex-col gap-col-10 wh-full">
<div class="flex items-center gap-col-1">
<div class="flex-1 flex items-center">
<el-progress
:percentage="100"
class="flex-1"
:stroke-width="18"
:text-inside="true"
:status="
['progress', 'success', undefined].includes(transferStatus)
? '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">
<div class="text-16px font-500">迁移日志</div>
<el-scrollbar class="h-full">
<div
v-for="i in curTransferLog"
:class="i.status === 'failed' ? 'text-red-500' : ''"
class="text-gray-600"
4 months ago
>
{{ i.msg }}
</div>
</el-scrollbar>
</div>
</div>
</EdfsWrap>
</div>
<TransferDlg
ref="transferDlgRef"
@on-save="onSave"
:is-batch-transfer="isBatchTransfer"
/>
<DeviceDrawer
v-model="isShowDetails"
ref="deviceDrawerRef"
:siteInfo="siteInfo"
:is-transfer="isTransfer"
/>
</template>
<script setup lang="ts">
import dayjs from 'dayjs'
import TransferDlg from './components/transferDlg.vue'
import ZMQWorker from '@/composables/useZMQJsonWorker'
import {
getPubInitData,
type PublishMsg,
type PubMsgData,
type TimeoutMsg,
} from '@/utils/zmq'
import { useTransferDataStore } from '@/stores/transferData'
import { storeToRefs } from 'pinia'
import type {
IOfflineDevice,
IOnlineDevice,
IUpFirmwareStatus,
UpFirmwarsDevice,
} from './type'
4 months ago
import { useMessage } from '@/composables/useMessage'
import { getDeviceList, type ISite } from '@/api/module/transfer'
4 months ago
import DeviceDrawer from './components/deviceDrawer.vue'
import {
getFirmwareUpTopic,
getTransferTopic,
postFirmwareUpTopic,
postTransferTopic,
upgradeProgressStatusMap,
} from './utils'
import { getFirmwarePath } from '@/api/module/firmware'
4 months ago
const transferDlgRef = ref<typeof TransferDlg>()
const router = useRouter()
const route = useRoute()
const siteInfo = ref<ISite>(
4 months ago
route.query.site ? JSON.parse(route.query.site as string) : null
)
const type = ref<'export' | 'details'>(route.query.type as 'export' | 'details')
const isTransfer = computed(() => type.value === 'export')
const isShowTransfer = computed(() => isTransfer.value && !!curTransferLog.value.length)
const message = useMessage()
const worker = ZMQWorker.getInstance()
const transferDataStore = useTransferDataStore()
const { upFirmwarePending, upFirmwareReset, upFirmwareStatus, upFirmwareSucceed } =
transferDataStore
4 months ago
const { devicesMap } = storeToRefs(transferDataStore)
const transferStatusMap = {
progress: '迁移中',
success: '迁移成功',
failed: '迁移失败',
timeout: '迁移超时',
}
const exportPubDeviceMap = new Map<string, { device: IOnlineDevice; action: 'export' }>()
4 months ago
const curTransferLog = ref<
{ msg: string; host: string; status: 'success' | 'padding' | 'failed' }[]
>([])
const transferStatus = ref<'progress' | 'success' | 'failed' | 'timeout' | undefined>()
const devices = computed(() => {
return isTransfer.value ? Array.from(devicesMap.value.values()) : deviceList.value
}) as Ref<any[]>
function onSave(msg: PublishMsg<'export'>, device: IOnlineDevice) {
curTransferLog.value = []
worker.publish(postTransferTopic, msg, true, zmqTimeoutCb)
exportPubDeviceMap.set(msg.id, { device, action: 'export' })
4 months ago
worker.subscribe(getTransferTopic, zmqExportCb, msg.id)
if (isBatchTransfer.value) {
onBatchCancel()
}
}
const statusMap = {
200: 'success',
1002: 'padding',
1003: 'failed',
}
function zmqExportCb(msg: PubMsgData) {
if (!isTransfer.value) return
4 months ago
const { feedback, result, id } = msg
if (feedback && feedback[0]) {
const status = feedback[1]
? (statusMap[feedback[1] as keyof typeof statusMap] as
| 'success'
| 'padding'
| 'failed')
: 'failed'
4 months ago
curTransferLog.value.push({
msg: `主机【${feedback[0]}】: ${feedback[2]}`,
4 months ago
host: feedback[0],
status,
4 months ago
})
}
// 找到 status 为 failed 的
transferStatus.value = 'progress'
if (result !== 'progress') {
const curMsgInfo = exportPubDeviceMap.get(id)!
4 months ago
if (!curMsgInfo) return
const { device, action } = curMsgInfo
if (device && action === 'export') {
4 months ago
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)
4 months ago
}
}
}
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('迁移已取消')
clearTransferData()
})
}
function clearTransferData() {
curTransferLog.value = []
transferStatus.value = undefined
exportPubDeviceMap.clear()
4 months ago
}
function zmqTimeoutCb(msg: TimeoutMsg) {
const { device, action } = exportPubDeviceMap.get(msg.timeoutId)!
4 months ago
if (device && action === 'export') {
message.error(`迁移超时,请重新稍后尝试`)
exportPubDeviceMap.delete(msg.timeoutId)
4 months ago
}
}
const onlineDeviceMap: Record<
keyof Omit<
IOnlineDevice,
'lastUpdated' | 'sn' | 'isChecked' | 'upFirmware' | 'upFirmwareStatus'
>,
4 months ago
string
> = {
status: '状态',
stationName: '站点名称',
clientIp: '客户端IP',
footprint: '数据占用空间',
}
const offlineDeviceMap: Record<
keyof Pick<IOfflineDevice, 'stationName' | 'db' | 'create_time'>,
string
> = {
stationName: '站点名称',
db: '数据库',
create_time: '创建时间',
}
const onlineDeviceCheckList = ref<string[]>([])
const isBatchTransfer = ref(false)
function onBatchTransfer() {
onlineDeviceCheckList.value = []
4 months ago
isBatchTransfer.value = true
}
function onBatchTransferSave(checkList: IOnlineDevice[]) {
const clientIpList = checkList.map(item => item?.clientIp).join(',')
4 months ago
const pathList = checkList
.map(item => `${item?.stationName}/${item?.sn}`)
.filter(Boolean)
.join(',')
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)
}
}
4 months ago
function onBatchCancel() {
isBatchTransfer.value = false
isBatchUpgrade.value = false
4 months ago
onlineDeviceCheckList.value = []
}
function onTransfer(item: IOnlineDevice) {
transferDlgRef.value?.open(item)
}
function onBack() {
router.push('/station')
}
// 监听页面刷新
window.onbeforeunload = function () {
stop()
}
onBeforeRouteLeave(async (to, from, next) => {
if (transferStatus.value === 'progress') {
try {
await message.confirm('当前迁移尚未完成,是否确认离开?')
window.location.href = to.fullPath
} catch (error) {
next(false)
}
} else {
next()
}
})
const deviceList = ref<IOfflineDevice[]>([])
4 months ago
async function loadDeviceList() {
const res = await getDeviceList(siteInfo.value.id)
if (res.code === 200 || res.code === 0) {
deviceList.value = res.data
}
}
const firmwarePath = ref('')
onMounted(async () => {
4 months ago
if (!isTransfer.value) {
loadDeviceList()
} else {
const res = await getFirmwarePath()
if (res.code === 200 || res.code === 0) {
firmwarePath.value = res.data
}
4 months ago
}
})
const isShowDetails = ref(false)
const deviceDrawerRef = ref<typeof DeviceDrawer>()
function onDeviceDetails(item: IOfflineDevice) {
deviceDrawerRef.value?.openFullScreen()
4 months ago
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)
}
}
}
4 months ago
</script>
<style scoped lang="scss">
.transfer-log-wrap {
margin-top: 10px;
height: calc(100% - 30px);
4 months ago
@apply border-radius-8px bg-[#F9FAFB] p-10;
4 months ago
:deep(.el-scrollbar) {
height: calc(100% - 20px);
}
}
4 months ago
.device-list-wrap {
@apply wh-full;
4 months ago
:deep(.el-checkbox-group) {
@apply wh-full flex flex-wrap gap-col-6 gap-row-4;
}
4 months ago
:deep(.el-checkbox__inner) {
width: 18px;
height: 18px;
4 months ago
&::after {
left: 6px;
top: 3px;
}
}
4 months ago
:deep(.el-checkbox__label) {
@apply text-14px font-500 text-[#313131];
}
4 months ago
:deep(.el-checkbox__input.is-checked + .el-checkbox__label) {
color: var(--el-color-primary);
}
4 months ago
.device-item {
@apply w-289 h-160 border border-solid border-[#E0E0E0] rounded-8px p-x-14 p-y-10 flex-col;
4 months ago
.device-item-header {
@apply w-full text-black text-18px font-500 flex items-center justify-between;
4 months ago
.info {
font-size: 14px;
color: #f1bf63;
cursor: pointer;
text-decoration: underline;
4 months ago
&:hover {
color: #8ace6a;
}
}
}
4 months ago
.device-item-body {
@apply flex-col m-t-10 text-sm gap-6;
4 months ago
.info-item {
@apply flex items-center gap-col-2;
4 months ago
}
}
}
}
</style>