长城汽车软件包管理平台
whychdw
2025-05-06 4a867727d81b9513e675ad396903368c6a293dca
内容提交
7个文件已修改
3个文件已添加
459 ■■■■ 已修改文件
src/api/software.ts 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/menu.api.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/hooks/useWebSocket.ts 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/admin.routes.ts 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/getLabelByValue.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/getWsUrl.ts 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/dashboard/compoents/uploadSoftware.vue 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/dashboard/index.vue 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/device/index.vue 142 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/user/index.vue 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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 {
}
src/api/system/menu.api.ts
@@ -179,7 +179,7 @@
/** RouteVO,路由对象 */
export interface RouteVO {
  /** 子路由列表 */
  children: RouteVO[];
  children?: RouteVO[];
  /** 组件路径 */
  component?: string;
  /** 路由属性 */
src/hooks/useWebSocket.ts
New file
@@ -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 };
}
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: [],
      },
    ],
  },
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) {
src/utils/getWsUrl.ts
New file
@@ -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;
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: "请输入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,
@@ -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;
  }
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();
});
src/views/device/index.vue
New file
@@ -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="错误代码"
          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>
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()"