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