| | |
| | | }); |
| | | }, |
| | | |
| | | uploadSoftware(data: FormData) { |
| | | uploadSoftware(id: string, description: string, file: File) { |
| | | const data = new FormData(); |
| | | data.append("id", id); |
| | | data.append("file", file); |
| | | data.append("description", description); |
| | | return request<any, UploadResult>({ |
| | | method: "POST", |
| | | url: "software/upload", |
| | | headers: { |
| | | "Content-Type": "multipart/form-data" |
| | | "Content-Type": "multipart/form-data", |
| | | }, |
| | | data, |
| | | }); |
| | |
| | | fileUrl: string; |
| | | uploadUserId: string; |
| | | uploadUserName: string; |
| | | version?: string; |
| | | description?: string; |
| | | createTime: string; |
| | | } |
| | | |
| | |
| | | code: number; |
| | | msg: string; |
| | | } |
| | | |
| | | export interface UpdateApplyParams { |
| | | |
| | | } |
| | |
| | | /** RouteVO,路由对象 */ |
| | | export interface RouteVO { |
| | | /** 子路由列表 */ |
| | | children: RouteVO[]; |
| | | children?: RouteVO[]; |
| | | /** 组件路径 */ |
| | | component?: string; |
| | | /** 路由属性 */ |
New file |
| | |
| | | // useWebSocket.js |
| | | import { ref, onMounted, onUnmounted } from "vue"; |
| | | import getWsUrl from "@/utils/getWsUrl"; |
| | | import mitt from "mitt"; |
| | | |
| | | /** |
| | | * |
| | | * @param url socket的名称 |
| | | * @returns {{socket: Ref<UnwrapRef<null>>, isConnected: Ref<UnwrapRef<boolean>>, message: Ref<UnwrapRef<string>>, sendData: sendData, eventBus: Emitter<Record<EventType, unknown>>}} |
| | | */ |
| | | export default function (url: string) { |
| | | url = getWsUrl(url); |
| | | const socket = ref<WebSocket>(); |
| | | const isConnected = ref(false); |
| | | const message = ref(""); |
| | | |
| | | const eventBus = mitt(); |
| | | let isManualClose = ref(false); // 是否手动关闭 |
| | | let reconnectTimer: any; // 重连计时器 |
| | | const reconnectInterval = 1000; // 初始重连间隔(ms) |
| | | const maxInterval = 30000; // 最大重连间隔(ms) |
| | | let retryCount = 0; // 重连计数 |
| | | |
| | | const connect = () => { |
| | | if (socket.value) { |
| | | socket.value.close(); |
| | | } |
| | | socket.value = new WebSocket(url); |
| | | socket.value.onopen = () => { |
| | | isConnected.value = true; |
| | | retryCount = 0; |
| | | console.log("WebSocket Connected, url: ", url); |
| | | eventBus.emit("onopen", true); |
| | | }; |
| | | |
| | | socket.value.onmessage = (event) => { |
| | | // 处理接收到的消息 |
| | | // console.log("Received:", event.data); |
| | | // 可以在这里通过 emit 发送消息到组件 |
| | | message.value = event.data; |
| | | eventBus.emit("message", event.data); |
| | | }; |
| | | |
| | | socket.value.onerror = (error) => { |
| | | console.error("WebSocket Error:", error, url); |
| | | eventBus.emit("error", error); |
| | | socket.value?.close(); |
| | | }; |
| | | |
| | | socket.value.onclose = () => { |
| | | isConnected.value = false; |
| | | // 手动关闭就不需要再重连了 |
| | | if (isManualClose.value) return; |
| | | console.log("WebSocket 连接已关闭, url: ", url); |
| | | scheduleReconnect(); |
| | | }; |
| | | }; |
| | | |
| | | // 发送数据 |
| | | const sendData = (data: any) => { |
| | | console.log(socket.value?.readyState); |
| | | if (socket.value && socket.value.readyState === socket.value.OPEN) { |
| | | console.log("send", data, "============="); |
| | | socket.value.send(data); |
| | | } |
| | | }; |
| | | |
| | | // 定时重连(指数退避) |
| | | const scheduleReconnect = () => { |
| | | const delay = Math.min(reconnectInterval * 2 ** retryCount, maxInterval); |
| | | console.log(`第${retryCount + 1}次重试,${delay}ms后重连${url}`); |
| | | reconnectTimer = setTimeout(() => { |
| | | retryCount++; |
| | | connect(); |
| | | }, delay); |
| | | }; |
| | | |
| | | const close = () => { |
| | | isManualClose.value = true; // 手动关闭 |
| | | if (socket.value) { |
| | | socket.value.close(); |
| | | } |
| | | clearTimeout(reconnectTimer); |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | connect(); |
| | | }); |
| | | |
| | | onUnmounted(() => { |
| | | close(); |
| | | }); |
| | | |
| | | // 返回 socket 对象和状态 |
| | | return { socket, isConnected, message, sendData, eventBus }; |
| | | } |
| | |
| | | |
| | | const routes: RouteVO[] = [ |
| | | { |
| | | path: "/device", |
| | | component: "Layout", |
| | | redirect: "/device/real", |
| | | name: "/device", |
| | | meta: { |
| | | title: "设备管理", |
| | | icon: "system", |
| | | hidden: false, |
| | | alwaysShow: false, |
| | | }, |
| | | children: [ |
| | | { |
| | | path: "real", |
| | | component: "device/index", |
| | | name: "DeviceReal", |
| | | meta: { |
| | | title: "设备实时管理", |
| | | icon: "el-icon-User", |
| | | hidden: false, |
| | | keepAlive: true, |
| | | alwaysShow: false, |
| | | }, |
| | | }, |
| | | ], |
| | | }, |
| | | { |
| | | path: "/system", |
| | | component: "Layout", |
| | | redirect: "/system/user", |
| | |
| | | keepAlive: true, |
| | | alwaysShow: false, |
| | | }, |
| | | children: [], |
| | | }, |
| | | ], |
| | | }, |
| | |
| | | } |
| | | |
| | | function getLabelByValue<T extends LabelValue>(value: number | string, list: T[], msg: T | null) { |
| | | let result = msg ? msg : "未知"; |
| | | let result = msg ? msg.label : "未知"; |
| | | for (let i = 0; i < list.length; i++) { |
| | | let item = list[i]; |
| | | if (item.value === value) { |
New file |
| | |
| | | /** |
| | | * 获取Websocket的连接 |
| | | * @param action |
| | | * @param port |
| | | * @returns {string} |
| | | */ |
| | | function getWsUrl(action: string, port?: string): string { |
| | | let _port = port ? port : 8080; |
| | | let hostname = window.location.hostname; |
| | | let wsProtocol = "ws://"; |
| | | if (window.location.protocol === "https:") { |
| | | wsProtocol = "wss://"; |
| | | } |
| | | if (process.env.NODE_ENV === "development") { |
| | | hostname = "localhost"; |
| | | } else { |
| | | _port = window.location.port; |
| | | } |
| | | // 处理端口为80 |
| | | _port = _port === 80 ? "" : ":" + _port; |
| | | return wsProtocol + hostname + _port + "/bg/" + action; |
| | | } |
| | | |
| | | export default getWsUrl; |
| | |
| | | <script setup lang="ts"> |
| | | import { UploadFilled } from "@element-plus/icons-vue"; |
| | | import { genFileId } from "element-plus"; |
| | | import type { FormInstance, UploadProps, UploadRawFile } from "element-plus"; |
| | | import type { FormInstance, UploadProps, UploadRawFile, UploadUserFile } from "element-plus"; |
| | | import SoftwareApi from "@/api/software"; |
| | | |
| | | const props = defineProps(["data"]); |
| | | const emit = defineEmits(["update:close", "success"]); |
| | | |
| | | const paramsRef = ref({ |
| | | id: "", |
| | | snCode: "", |
| | | serialNumber: "", |
| | | materialCode: "", |
| | | description: "", |
| | | }); |
| | | const rulesRef = ref({ |
| | | snCode: [{ required: true, message: "请输入SN码", trigger: "blur" }], |
| | | serialNumber: [{ required: true, message: "请输入序列号", trigger: "blur" }], |
| | | materialCode: [{ required: true, message: "请输入物料编码", trigger: "blur" }], |
| | | snCode: [ |
| | | { required: true, message: "请输入SN码", trigger: "blur" }, |
| | | { |
| | | validator: (rule: any, value: any, callback: Function) => { |
| | | if (value !== props.data.snCode) { |
| | | callback(new Error("SN码与当前包SN码不匹配!!!")); |
| | | } else { |
| | | callback(); |
| | | } |
| | | }, |
| | | trigger: "blur", |
| | | }, |
| | | ], |
| | | serialNumber: [ |
| | | { required: true, message: "请输入序列号", trigger: "blur" }, |
| | | { |
| | | validator: (rule: any, value: any, callback: Function) => { |
| | | if (value !== props.data.serialNumber) { |
| | | callback(new Error("序列号与当前包序列号不匹配!!!")); |
| | | } else { |
| | | callback(); |
| | | } |
| | | }, |
| | | trigger: "blur", |
| | | }, |
| | | ], |
| | | description: [{ required: true, message: "请输入版本更新内容", trigger: "blur" }], |
| | | }); |
| | | const layout = reactive({ |
| | | gutter: 16, |
| | |
| | | } |
| | | |
| | | function uploadFile() { |
| | | const params = getParams(); |
| | | SoftwareApi.uploadSoftware(params) |
| | | SoftwareApi.uploadSoftware(paramsRef.value.id, paramsRef.value.description, fileList.value[0].raw) |
| | | .then((result) => { |
| | | const { code } = result; |
| | | if (code === 1) { |
| | |
| | | .finally(() => {}); |
| | | } |
| | | |
| | | function getParams(): FormData { |
| | | const formData = new FormData(); |
| | | formData.append("file", fileList.value[0].raw); |
| | | formData.append("softwareStr", JSON.stringify(paramsRef.value)); |
| | | return formData; |
| | | } |
| | | |
| | | function close() { |
| | | emit("update:close", false); |
| | | } |
| | | |
| | | onMounted(() => { |
| | | paramsRef.value.id = props.data.id; |
| | | paramsRef.value.description = props.data.description; |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | |
| | | label-width="auto" |
| | | > |
| | | <el-row :gutter="layout.gutter"> |
| | | <el-col :span="layout.span"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="序列号" prop="serialNumber"> |
| | | <el-input v-model="paramsRef.serialNumber"></el-input> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="SN码" prop="snCode"> |
| | | <el-input v-model="paramsRef.snCode"></el-input> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="layout.span"> |
| | | <el-form-item label="物料编码" prop="materialCode"> |
| | | <el-input v-model="paramsRef.materialCode"></el-input> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="layout.span"> |
| | | <el-form-item label="序列号" prop="serialNumber"> |
| | | <el-input v-model="paramsRef.serialNumber"></el-input> |
| | | <el-form-item label="版本更新内容" prop="description"> |
| | | <el-input |
| | | v-model="paramsRef.description" |
| | | type="textarea" |
| | | :autosize="{ minRows: 4, maxRows: 10 }" |
| | | ></el-input> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="24"> |
| | |
| | | |
| | | <style scoped lang="scss"> |
| | | .dialog-wrapper { |
| | | width: 580px; |
| | | width: 620px; |
| | | .from-footer { |
| | | text-align: right; |
| | | } |
| | |
| | | <el-col :lg="24" :xs="24"> |
| | | <div class="search-bar"> |
| | | <el-form ref="queryFormRef" :model="queryParams" :inline="true"> |
| | | <el-form-item label="SN码:" prop="status"> |
| | | <el-form-item label="序列号:" prop="status"> |
| | | <el-select |
| | | v-model="queryParams.snCode" |
| | | v-model="queryParams.serialNumber" |
| | | filterable |
| | | placeholder="全部" |
| | | clearable |
| | | class="!w-[200px]" |
| | | @change="handleQuery" |
| | | > |
| | | <el-option label="全部" value="" /> |
| | | <el-option |
| | | v-for="item in snCodeList" |
| | | v-for="item in serialNumberList" |
| | | :key="item" |
| | | :label="item" |
| | | :value="item" |
| | |
| | | <el-form-item> |
| | | <el-button type="primary" icon="search" @click="handleQuery">搜索</el-button> |
| | | <el-button icon="refresh" @click="handleResetQuery">重置</el-button> |
| | | <el-button |
| | | v-hasPerm="['sys:user:upload']" |
| | | type="success" |
| | | icon="upload" |
| | | @click="uploadDialog = true" |
| | | > |
| | | 上传 |
| | | </el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | </div> |
| | |
| | | <el-table v-loading="loading" :data="pageData"> |
| | | <el-table-column |
| | | show-overflow-tooltip |
| | | label="SN码" |
| | | label="序列号" |
| | | min-width="210" |
| | | align="center" |
| | | prop="serialNumber" |
| | | /> |
| | | <el-table-column |
| | | show-overflow-tooltip |
| | | label="SN码" |
| | | min-width="180" |
| | | align="center" |
| | | prop="snCode" |
| | | /> |
| | | <el-table-column |
| | | show-overflow-tooltip |
| | | label="物料编码" |
| | | min-width="210" |
| | | label="版本号" |
| | | min-width="120" |
| | | align="center" |
| | | prop="materialCode" |
| | | prop="version" |
| | | /> |
| | | <el-table-column |
| | | show-overflow-tooltip |
| | | label="序列号" |
| | | label="更新内容" |
| | | min-width="210" |
| | | align="center" |
| | | prop="serialNumber" |
| | | prop="description" |
| | | /> |
| | | <el-table-column |
| | | show-overflow-tooltip |
| | |
| | | <el-table-column align="center" label="操作" fixed="right" width="220"> |
| | | <template #default="{ row }"> |
| | | <el-button |
| | | v-hasPerm="['sys:user:add']" |
| | | type="primary" |
| | | icon="download" |
| | | icon="upload" |
| | | link |
| | | size="small" |
| | | @click="handleDownload(row)" |
| | | @click="handleUpload(row)" |
| | | > |
| | | 下载 |
| | | 上传 |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | |
| | | <upload-software |
| | | v-if="uploadDialog" |
| | | v-model:close="uploadDialog" |
| | | :data="rowRef" |
| | | @success="handleSuccess" |
| | | ></upload-software> |
| | | </el-dialog> |
| | |
| | | } |
| | | }) |
| | | .finally(() => { |
| | | searchSerialNumber(); |
| | | loading.value = false; |
| | | }); |
| | | } |
| | |
| | | handleQuery(); |
| | | } |
| | | |
| | | const snCodeList = ref<string[]>([]); |
| | | function searchSnCode() { |
| | | SoftwareApi.searchSn().then((result) => { |
| | | const { code, data } = result; |
| | | if (code === 1) { |
| | | snCodeList.value = data; |
| | | } else { |
| | | snCodeList.value = []; |
| | | } |
| | | }); |
| | | } |
| | | |
| | | const materialList = ref<string[]>([]); |
| | | function searchMaterial() { |
| | | SoftwareApi.searchMaterial().then((result) => { |
| | | const { code, data } = result; |
| | | if (code === 1) { |
| | | materialList.value = data; |
| | | } else { |
| | | materialList.value = []; |
| | | } |
| | | }); |
| | | } |
| | | |
| | | const serialNumberList = ref<string[]>([]); |
| | | function searchSerialNumber() { |
| | | SoftwareApi.searchSerial().then((result) => { |
| | | const { code, data } = result; |
| | | if (code === 1) { |
| | | serialNumberList.value = data; |
| | | serialNumberList.value = [...new Set(data)]; |
| | | } else { |
| | | serialNumberList.value = []; |
| | | } |
| | |
| | | const downloadDialog = ref(false); |
| | | const rowRef = ref<SoftwareInfo>(); |
| | | |
| | | function handleDownload(data: SoftwareInfo) { |
| | | function handleUpload(data: SoftwareInfo) { |
| | | rowRef.value = data; |
| | | downloadDialog.value = true; |
| | | uploadDialog.value = true; |
| | | } |
| | | |
| | | onMounted(() => { |
| | | searchSnCode(); |
| | | searchMaterial(); |
| | | searchSerialNumber(); |
| | | handleQuery(); |
| | | }); |
New file |
| | |
| | | <script setup lang="ts"> |
| | | import getLabelByValue from "@/utils/getLabelByValue"; |
| | | |
| | | defineOptions({ |
| | | name: "DeviceReal", |
| | | inheritAttrs: false, |
| | | }); |
| | | import useWebSocket from "@/hooks/useWebSocket"; |
| | | import { SearchParams } from "@/api/software"; |
| | | |
| | | const { eventBus, sendData } = useWebSocket("devState"); |
| | | const loading = ref(false); |
| | | const queryParams = reactive<SearchParams>({ |
| | | pageNum: 1, |
| | | pageSize: 20, |
| | | }); |
| | | const total = ref(0); |
| | | const listRef = ref(); |
| | | |
| | | eventBus.on("onopen", () => { |
| | | handleQuery(); |
| | | }); |
| | | const errorList = [ |
| | | { |
| | | label: "---", |
| | | value: 0, |
| | | }, |
| | | { |
| | | label: "文件未找到", |
| | | value: 1, |
| | | }, |
| | | { |
| | | label: "参数错误", |
| | | value: 2, |
| | | }, |
| | | { |
| | | label: "文件发送超时", |
| | | value: 3, |
| | | }, |
| | | { |
| | | label: "远程停止", |
| | | value: 4, |
| | | }, |
| | | ]; |
| | | eventBus.on("message", (res) => { |
| | | let rs = JSON.parse(res as string); |
| | | if (rs.code === 1) { |
| | | total.value = rs.data.total; |
| | | listRef.value = rs.data.list.map((item: any) => { |
| | | item.fullStationName = |
| | | item.stationProvince + |
| | | "-" + |
| | | item.stationCity + |
| | | "-" + |
| | | item.stationCounty + |
| | | "-" + |
| | | item.stationName + |
| | | "-" + |
| | | item.devName; |
| | | item.updatePercent = |
| | | item.dfuDataBlockLen === 0 ? 0 : (item.dfuDataBlockNum / item.dfuDataBlockLen) * 100; |
| | | item.updatePercent = item.updatePercent.toFixed(2); |
| | | item.errorCodeText = getLabelByValue(item.errorCode, errorList, { label: "未知", value: -1 }); |
| | | return item; |
| | | }); |
| | | } else { |
| | | queryParams.pageNum = 1; |
| | | total.value = 0; |
| | | } |
| | | }); |
| | | |
| | | function handleQuery() { |
| | | sendData(JSON.stringify(queryParams)); |
| | | } |
| | | |
| | | const format = (percentage: number) => (percentage === 100 ? "升级完成" : `${percentage}%`); |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="app-container"> |
| | | <el-card shadow="never"> |
| | | <el-table v-loading="loading" :data="listRef"> |
| | | <el-table-column |
| | | show-overflow-tooltip |
| | | label="机房名称" |
| | | min-width="320" |
| | | align="center" |
| | | prop="fullStationName" |
| | | /> |
| | | <el-table-column |
| | | show-overflow-tooltip |
| | | label="序列号" |
| | | min-width="180" |
| | | align="center" |
| | | prop="serialNumber" |
| | | /> |
| | | <el-table-column |
| | | show-overflow-tooltip |
| | | label="SN码" |
| | | min-width="180" |
| | | align="center" |
| | | prop="snCode" |
| | | /> |
| | | <el-table-column |
| | | show-overflow-tooltip |
| | | label="版本号" |
| | | min-width="180" |
| | | align="center" |
| | | prop="version" |
| | | /> |
| | | <el-table-column label="升级进度" min-width="180" align="center" prop="updatePercent"> |
| | | <template #default="{ row }"> |
| | | <el-progress :percentage="row.updatePercent" :format="format" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column |
| | | show-overflow-tooltip |
| | | label="错误代码" |
| | | min-width="180" |
| | | align="center" |
| | | prop="errorCodeText" |
| | | /> |
| | | <el-table-column |
| | | show-overflow-tooltip |
| | | label="数据更新时间" |
| | | min-width="180" |
| | | align="center" |
| | | prop="recordTime" |
| | | /> |
| | | </el-table> |
| | | <pagination |
| | | v-if="total > 0" |
| | | v-model:total="total" |
| | | v-model:page="queryParams.pageNum" |
| | | v-model:limit="queryParams.pageSize" |
| | | @pagination="handleQuery" |
| | | /> |
| | | </el-card> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped lang="scss"></style> |
| | |
| | | <el-card shadow="never"> |
| | | <div class="flex-x-between mb-10px"> |
| | | <div> |
| | | <el-button type="primary" icon="search" @click="handleQuery">搜索</el-button> |
| | | <el-button |
| | | v-hasPerm="['sys:user:add']" |
| | | v-hasPerm="['sys:user:upload']" |
| | | type="success" |
| | | icon="plus" |
| | | @click="handleOpenDialog()" |