From 4a867727d81b9513e675ad396903368c6a293dca Mon Sep 17 00:00:00 2001 From: whychdw <496960745@qq.com> Date: 星期二, 06 五月 2025 09:42:51 +0800 Subject: [PATCH] 内容提交 --- src/views/system/user/index.vue | 3 src/router/admin.routes.ts | 27 +++ src/utils/getLabelByValue.ts | 2 src/views/dashboard/index.vue | 75 +++------ src/views/device/index.vue | 142 +++++++++++++++++ src/api/software.ts | 14 + src/utils/getWsUrl.ts | 24 +++ src/hooks/useWebSocket.ts | 96 ++++++++++++ src/views/dashboard/compoents/uploadSoftware.vue | 74 ++++++-- src/api/system/menu.api.ts | 2 10 files changed, 380 insertions(+), 79 deletions(-) diff --git a/src/api/software.ts b/src/api/software.ts index 23b93f9..501a836 100644 --- a/src/api/software.ts +++ b/src/api/software.ts @@ -44,12 +44,16 @@ }); }, - 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, }); @@ -66,6 +70,8 @@ fileUrl: string; uploadUserId: string; uploadUserName: string; + version?: string; + description?: string; createTime: string; } @@ -81,3 +87,7 @@ code: number; msg: string; } + +export interface UpdateApplyParams { + +} diff --git a/src/api/system/menu.api.ts b/src/api/system/menu.api.ts index 0a7de38..31ca862 100644 --- a/src/api/system/menu.api.ts +++ b/src/api/system/menu.api.ts @@ -179,7 +179,7 @@ /** RouteVO锛岃矾鐢卞璞� */ export interface RouteVO { /** 瀛愯矾鐢卞垪琛� */ - children: RouteVO[]; + children?: RouteVO[]; /** 缁勪欢璺緞 */ component?: string; /** 璺敱灞炴�� */ diff --git a/src/hooks/useWebSocket.ts b/src/hooks/useWebSocket.ts new file mode 100644 index 0000000..57f28b0 --- /dev/null +++ b/src/hooks/useWebSocket.ts @@ -0,0 +1,96 @@ +// 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 }; +} diff --git a/src/router/admin.routes.ts b/src/router/admin.routes.ts index c5a0906..d9b81ca 100644 --- a/src/router/admin.routes.ts +++ b/src/router/admin.routes.ts @@ -2,6 +2,32 @@ 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", @@ -24,7 +50,6 @@ keepAlive: true, alwaysShow: false, }, - children: [], }, ], }, diff --git a/src/utils/getLabelByValue.ts b/src/utils/getLabelByValue.ts index f6ffc8e..f3f638c 100644 --- a/src/utils/getLabelByValue.ts +++ b/src/utils/getLabelByValue.ts @@ -4,7 +4,7 @@ } 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) { diff --git a/src/utils/getWsUrl.ts b/src/utils/getWsUrl.ts new file mode 100644 index 0000000..2f1542a --- /dev/null +++ b/src/utils/getWsUrl.ts @@ -0,0 +1,24 @@ +/** + * 鑾峰彇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; diff --git a/src/views/dashboard/compoents/uploadSoftware.vue b/src/views/dashboard/compoents/uploadSoftware.vue index f73eeb4..8a1760a 100644 --- a/src/views/dashboard/compoents/uploadSoftware.vue +++ b/src/views/dashboard/compoents/uploadSoftware.vue @@ -1,19 +1,46 @@ <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: "璇疯緭鍏N鐮�", trigger: "blur" }], - serialNumber: [{ required: true, message: "璇疯緭鍏ュ簭鍒楀彿", trigger: "blur" }], - materialCode: [{ required: true, message: "璇疯緭鍏ョ墿鏂欑紪鐮�", trigger: "blur" }], + snCode: [ + { required: true, message: "璇疯緭鍏N鐮�", trigger: "blur" }, + { + validator: (rule: any, value: any, callback: Function) => { + if (value !== props.data.snCode) { + callback(new Error("SN鐮佷笌褰撳墠鍖匰N鐮佷笉鍖归厤锛侊紒锛�")); + } 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, @@ -62,8 +89,7 @@ } 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) { @@ -88,16 +114,14 @@ .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> @@ -110,19 +134,23 @@ 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"> @@ -153,7 +181,7 @@ <style scoped lang="scss"> .dialog-wrapper { - width: 580px; + width: 620px; .from-footer { text-align: right; } diff --git a/src/views/dashboard/index.vue b/src/views/dashboard/index.vue index aedb039..fc8de35 100644 --- a/src/views/dashboard/index.vue +++ b/src/views/dashboard/index.vue @@ -4,18 +4,17 @@ <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" @@ -25,14 +24,6 @@ <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> @@ -41,24 +32,31 @@ <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 @@ -77,13 +75,14 @@ <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> @@ -102,6 +101,7 @@ <upload-software v-if="uploadDialog" v-model:close="uploadDialog" + :data="rowRef" @success="handleSuccess" ></upload-software> </el-dialog> @@ -151,6 +151,7 @@ } }) .finally(() => { + searchSerialNumber(); loading.value = false; }); } @@ -165,36 +166,12 @@ 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 = []; } @@ -211,14 +188,12 @@ 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(); }); diff --git a/src/views/device/index.vue b/src/views/device/index.vue new file mode 100644 index 0000000..b230e26 --- /dev/null +++ b/src/views/device/index.vue @@ -0,0 +1,142 @@ +<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="閿欒浠g爜" + 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> diff --git a/src/views/system/user/index.vue b/src/views/system/user/index.vue index 8cc7bc9..7b630f7 100644 --- a/src/views/system/user/index.vue +++ b/src/views/system/user/index.vue @@ -7,8 +7,9 @@ <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()" -- Gitblit v1.9.1