安灯系统TS接口参数&WebSocket配置

HTTP请求相关

import { http } from "../utils";

/** 返回的数据格式 */
interface ResponseData<T> {
  data: T;
  success: boolean;
  message: string;
}

/** 首页 - start */
/**
 * 获取安灯请求类型列表
 * @returns 返回的是安灯请求类型列表,里面的item是一个对象,包含id、name、imageUrl三个字段
 */
export const getAndonRequestTypeList = (params?: { name: string }) =>
  http.get<ResponseData<{ id: number; name: string; imageUrl: string[] }[]>>(
    "/andon/request/type/list",
    { params }
  );

/**
 * 获取处理人员列表
 * @returns 返回的是处理人员列表,里面的item是一个对象,包含id和name两个字段
 */
export const getAndonHandlerList = (name: string) =>
  http.get<ResponseData<{ id: number; name: string }[]>>("/andon/handler/list", {
    params: { name }
  });

/** 提交记录列表里面的item */
interface andonSubmitListItem {
  /** 安灯请求id */
  id: string;
  /** 安灯工位 */
  station: string;
  /** 安灯类型 */
  requestType: string;
  /** 说明 */
  description: string;
  /** 附件 */
  attachment: string;
  /** 附件的copy变量,attachment与后端固有字段冲突,故添加 */
  attachmentList: string[];
  /** 处理人员 */
  handler: string;
  /** 发起时间 */
  createTime: string;
}

/** 获取提交记录列表 */
export const getAndonSubmitList = (params: { name: string }) =>
  http.get<ResponseData<andonSubmitListItem[]>>("/andon/request/history/list", { params });

/**
 *  获取单号列表
 * @param params 【可能有,也可能没有参数,待定】
 * @returns 返回的是单号列表,里面的item是一个对象,包含id、label、value三个字段
 */
export const getAndonRequestNoList = (params?: any) =>
  http.get<ResponseData<{ id: number; label: string; value: string }[]>>("/andon/request/no/list", {
    params
  });

/** 安灯请求的入参 */
interface andonRequestParams {
  /** 请求发起人的名字 */
  name?: string;
  /** 安灯请求类型 */
  requestType: string;
  /** 处理人员 - 数组里面放的是发起请求时勾选的人员name */
  handlerList: string[];
  /** 安灯请求携带的单号 */
  requestNo: string;
  /** 是否同步到微信 */
  isWechatNotifyEnable: boolean;
  /** 说明 */
  description: string;
  /** 附件 - 数组里面存放的是照片路径对应字符串 */
  attachmentList: string[];
}

/**
 * 上传拍摄的照片
 * @param data 图片二进制流的表单数据
 * @returns 返回的是照片的路径字符串【塞data里面】
 */
export const uploadPhoto = (data: FormData) =>
  http.post<ResponseData<string>>("/andon/request/image/upload", data, {
    headers: { "Content-Type": "multipart/form-data" }
  });

/**
 * 删除上传的照片
 */
export const deletePhoto = (params: { path: string }) =>
  http.delete<ResponseData<void>>("/andon/request/image/delete", { params });

/**
 * 确认发起安灯请求
 * @param data 安灯请求的入参
 * @returns 【这里的void,表明放在data中的数据为null即可】
 */
export const confirmAddAndonRequest = (data: andonRequestParams) =>
  http.post<ResponseData<void>>("/andon/request/add", data);

/**
 * 是否有待处理的安灯请求
 * @param name 传入的是当前操作人员的名称
 * @returns boolean值,true表示有待处理的安灯请求,false表示没有
 */
export const hasPendingAndonRequest = (params: { name: string }) =>
  http.get<ResponseData<boolean>>("/andon/request/has-pending", { params });
/** 首页 - end */

/** 安灯请求处理页 - start */
/** 安灯请求列表里面的item构成 */
interface andonRequestItem {
  /** 安灯请求id */
  id: string;
  /** 工位名称 放在最上面的地方 */
  station: string;
  /** 安灯请求类型 */
  requestType: string;
  /** 发起人员 */
  initiator: string[];
  /** 处理人员 - 数组里面放的是发起请求时勾选的人员id */
  handler: number[];
  /** 安灯请求携带的单号 */
  requestNo: string;
  /** 是否同步到微信 */
  isWechatNotifyEnable: boolean;
  /** 说明 */
  description: string;
  /** 附件 */
  attachment: string;
  /** 附件的copy变量,attachment与后端固有字段冲突,故添加 */
  attachmentList: string[];
  /** 发起时间 */
  createTime: string;
  /** 是否紧急 */
  isUrgent: boolean;
}

/** 历史记录列表里面的item */
interface andonHistoryItem {
  /** 安灯请求id */
  id: string;
  /** 安灯工位 */
  station: string;
  /** 安灯类型 */
  requestType: string;
  /** 说明 */
  description: string;
  /** 附件 */
  attachment: string;
  /** 附件的copy变量,attachment与后端固有字段冲突,故添加 */
  attachmentList: string[];
  /** 发起人员 */
  initiator: string;
  /** 发起时间 */
  createTime: string;
}

/**
 *  获取历史记录列表
 * @param name 传入的是当前操作人员的名称
 * @returns 返回的是历史记录列表,里面的item是一个对象,包含id、station、requestType、description、attachment、initiator、createTime等字段【andonHistoryItem】
 */
export const getAndonHistoryList = (params?: { name: string }) =>
  http.get<ResponseData<andonHistoryItem[]>>("/andon/receive/history/list", { params });

/**
 * 获取安灯请求列表
 * @param name 传入的是当前操作人员的名称,用于筛选出当前操作人员需要处理的安灯请求
 */
export const getAndonRequestList = (params: { name: string }) =>
  http.get<ResponseData<andonRequestItem[]>>("/andon/request/list", { params });

/** 确认他人发来的安灯请求
 * - 会传一个是否保留的参数 【不保留=> 这条安灯请求就移动到这个人的历史记录里面,保留=>无事发生】
 * @param id 安灯请求id
 * @param isHold 是否保留
 * @param name 发起请求的这个人
 */
export const confirmAndonRequest = (params: { id: string; isHold: boolean; name?: string }) =>
  http.post<ResponseData<void>>("/andon/request/confirm", params);
/** 安灯请求处理页 - end */

/** 安灯设置页 - start */
/** 安灯类型设置列表里面的item */
interface andonTypeItem {
  /** 安灯类型id */
  id: number;
  /** 安灯类型名称 */
  name: string;
  /** 编辑时用到的名称 - 【前端用到,他的值默认是name的值】 */
  tempName: string;
  /** 是否启用 */
  enable: boolean;
  /** 顺序 */
  orderId: number;
  /** 是否处于编辑状态 - 【前端用到】 */
  isEdit: boolean;
  /** 说明图片 */
  imageUrl: string[];
}
/** 获取安灯类型设置列表 */
export const getAndonTypeList = () =>
  http.get<ResponseData<andonTypeItem[]>>("/andon/setting/type/list");

/**
 * 新增安灯类型设置
 * @param name 安灯类型名称
 * @param enable 是否启用
 */
export const addAndonType = (data: andonTypeItem) =>
  http.post<ResponseData<void>>("/andon/setting/type/add", data);

/**
 * 修改安灯类型设置的入参
 * @param ?问号表示这个参数不一定传【可选参数】,
 * @param orderId 是修改这个安灯类型设置列表中 item 的顺序,会影响“/andon/request/type/list”这个接口里面返回的 item 顺序
 */
interface editAndonTypeParams {
  /** 安灯类型id */
  id: number;
  /** 安灯类型名称 */
  name?: string;
  /** 是否启用 */
  enable?: boolean;
  /** 原来的顺序 */
  oldIndex?: number;
  /** 新的顺序 */
  newIndex?: number;
}

/**
 * 修改安灯类型设置
 * @param id 安灯类型id
 * @param name 安灯类型名称
 * @param enable 是否启用
 * @param oldIndex 原来的顺序
 * @param newIndex 新的顺序
 */
export const editAndonType = (data: editAndonTypeParams) =>
  http.post<ResponseData<void>>("/andon/setting/type/edit", data);

/**
 * 删除安灯类型设置
 * @param id 安灯类型id
 */
export const deleteAndonType = (params: { id: string }) =>
  http.delete<ResponseData<void>>("/andon/setting/type/delete/" + params.id);

/**
 * 安灯类型说明图片上传
 * 【实际调用由组件进行,仅在此处说明有这个接口】
 * @param data 图片二进制流的表单数据【FormData】
 * @returns 返回的是图片的路径字符串【塞data里面】
 */
export const uploadAndonTypeDescriptionImage = (data: FormData) =>
  http.post<ResponseData<string>>("/andon/setting/type/image/upload", data, {
    headers: { "Content-Type": "multipart/form-data" }
  });

/**
 * 删除安灯类型说明图片
 * @param path 图片路径
 * @returns 返回的是void
 * @description 由于后端没有返回值,所以这里的返回值是void
 */
export const deleteAndonTypeDescriptionImage = (id: string, path: string) =>
  http.delete<ResponseData<void>>("/andon/setting/type/image/delete/" + id, { params: { path } });

/** 安灯工位设置列表里面的item */
interface andonStationItem {
  /** 安灯工位id */
  id: number;
  /** 安灯工位名称 */
  stationName: string;
  /** 编辑时用到的名称 - 【前端用到,他的值默认是name的值】 */
  tempStationName: string;
  /** 安灯工位编号 */
  stationNo: string;
  /** 编辑时用到的工位编号 - 【前端用到,他的值默认是stationNo的值】 */
  tempStationNo: string;
  /** 人员 */
  personnel: string;
  /** 编辑时用到的人员 - 【前端用到,他的值默认是personnel的值】 */
  tempPersonnel: string;
  /** 当前工位 - 还没想好怎么弄 你自己看着办【也许放当前登录人的sessionId,如果说后面有人又点了同一个那么把之前的人挤掉,然后让新的人登录】 */
  currentStation: any;
  /** 是否处于编辑状态 - 【前端用到】 */
  isEdit: boolean;
}

/** 获取安灯工位设置列表
 * @returns 返回的是安灯工位设置列表,里面的item是一个对象,包含id、stationName、tempStationName、stationNo、tempStationNo、personnel、tempPersonnel、currentStation、isEdit等字段
 */
export const getAndonStationList = () =>
  http.get<ResponseData<andonStationItem[]>>("/andon/setting/station/list");

/** 新增安灯工位设置
 * @param name 安灯工位名称
 * @param code 安灯工位编号
 * @param personnel 人员
 */
export const addAndonStation = (data: andonStationItem) =>
  http.post<ResponseData<void>>("/andon/setting/station/add", data);

/** 修改安灯工位设置 */
export const editAndonStation = (data: andonStationItem) =>
  http.post<ResponseData<void>>("/andon/setting/station/edit", data);

/**
 * 点击当前工位设置
 * - 相当于就是登录操作,后端要在响应头中返回一个set-cookie来设置sessionId及其其他参数,
 * - 后续发起的请求都会携带这个参数。以便区分当前操作的是哪个人
 * @param id 安灯工位id
 * @returns 返回这个人的信息给我,然后我就可以根据这个人的信息去请求其他接口了
 */
export const clickCurrentStation = (params: { id: string }) =>
  http.get<ResponseData<andonStationItem>>("/andon/setting/station/current", { params });

/**
 * 删除安灯工位设置
 * @param id 安灯工位id
 */
export const deleteAndonStation = (params: { id: string }) =>
  http.delete<ResponseData<void>>("/andon/setting/station/delete/" + params.id);
/** 安灯设置页 - end */

/** 微信扫码登记相关 - start */
/** 获取公司ID */
export const getCompanyIdForWeChatBinding = () =>
  http.get<ResponseData<string>>("/andon/wechat/company-id");

/**
 * 确认扫码登记参数
 */
interface wechatScanRegisterParams {
  /** 扫码获取的code,由wechat认证返回 */
  code: string;
  /** 昵称 */
  nickName: string;
  /** 真实姓名 */
  realName: string;
  /** 手机号码 */
  phone: string;
  /** 工位名称 */
  stationName: string;
  /** 工位编号 */
  stationNo: string;
}

/** 确认扫码登记*/
export const confirmScanRegister = (params: wechatScanRegisterParams) =>
  http.post<ResponseData<void>>("/andon/wechat/bind", params);
/** 微信扫码登记相关 - end */

WebSocket相关

import { h, ref } from "vue";
import { defineStore } from "pinia";
import { wsApi } from "@/api/utils";
import { useRoute, useRouter } from "vue-router";
import { ElMessage, ElMessageBox } from "element-plus";

/** 封装一下log,简洁一点 */
class LOG {
  private static _(color: string, title: string, message = "") {
    console.log(
      `%c ${title} %c ${message}${message ? " %c" : ""}`,
      `background: ${color}; border: 1px solid ${color}; padding: 4px 3px; border-radius: ${message ? "4px 0 0 4px" : "4px"}; color: #fff;`,
      message
        ? `border: 1px solid ${color}; padding: 4px 3px; border-radius: 0 4px 4px 0; color: ${color}; font-weight: bold;`
        : "",
      message ? "background: transparent;" : ""
    );
  }

  static success(title: string, message = "") {
    LOG._("#4caf50", title, message);
  }

  static warning(title: string, message = "") {
    LOG._("#ff9800", title, message);
  }

  static error(title: string, message = "") {
    LOG._("#f44336", title, message);
  }

  static primary(title: string, message = "") {
    LOG._("#2196f3", title, message);
  }

  static info(title: string, message = "") {
    LOG._("#90a4ae", title, message);
  }
}

// websocket store
export const useWebSocketStore = defineStore("webSocket", () => {
  const ws = ref(null);
  const interval = ref(null);
  const timeout = ref(null);
  const userId = ref(null);
  const reconnectInterval = ref(1000); // 初始重连间隔
  const heartbeatInterval = ref(45000); // 心跳包间隔
  const maxReconnectInterval = ref(30000); // 最大重连间隔
  const serialPort = ref(null);
  const route = useRoute();
  const router = useRouter();

  if (sessionStorage.getItem("AndonCurrentStation")) {
    const userInfo = JSON.parse(sessionStorage.getItem("AndonCurrentStation") as string);
    userId.value = userInfo.id;
  }

  const flags = ref({
    refreshRequestMap: 0, // 用于刷新请求列表,也是一个标识,触发页面的watch,让他重新请求数据(当当前页面处于接收页的时候)
    openSerialPortDevTools: 0, // 打开串口设备调试工具
    webSocketUserIdChanged: false, // webSocket连接的userId发生变化,需要重新连接
    isUnloading: false, // 是否正在卸载页面
    serialPortDevDialogStatus: false, // 串口设备调试工具的弹窗状态
    isSerialConnectionTipsShowed: false, // 是否已经显示过串口连接提示
    isServerDisconnectActivelyTipsShowed: false // 服务端主动断开连接的提示是否已经显示
  });

  /** 清理 */
  window.onbeforeunload = () => {
    flags.value.isUnloading = true;
    ws.value?.close();
    clearInterval(interval.value);
    clearTimeout(timeout.value);
  };

  /** 开始进行webSocket连接 */
  function webSocketConnect(id: string | number) {
    // 如果传来的ID和之前的不一样,先把上次的连接关闭,如果存在的话
    if (id !== userId.value) {
      flags.value.isUnloading = true;
      ws.value?.close();
      flags.value.webSocketUserIdChanged = true;
    } else {
      flags.value.webSocketUserIdChanged = false;
    }

    ws.value = new WebSocket(wsApi() + id);
    userId.value = id;
    initWebSocket();
  }

  /** 具体webSocket连接逻辑 */
  function initWebSocket() {
    ws.value.onopen = onopen;
    ws.value.onmessage = onmessage;
    ws.value.onclose = onclose;
    ws.value.onerror = onerror;
    keepWebSocketConnectionAlive();
  }

  /** webSocket连接成功 */
  function onopen() {
    LOG.primary("开始WebSocket连接", wsApi() + userId.value);
    LOG.success("WebSocket连接成功");
    ws.value.send(JSON.stringify({ type: "connected", msg: "客户端连接成功" }));
  }

  /** webSocket接收消息 */
  function onmessage(evt) {
    let { type, msg } = (evt.data as string).startsWith("{")
      ? JSON.parse(evt.data)
      : { type: "default", msg: evt.data };

    LOG.primary("接收到消息", `类型:${type} 内容:${msg}`);

    switch (type) {
      case "incomingRequest": // 收到新的请求,那么跳到接收页面
        if (route.name === "receive") {
          ElMessage.success("收到新的请求,已刷新请求列表");
          flags.value.refreshRequestMap++; // 如果当前页面是接收页面,那么直接请求数据
        } else {
          handleIncomingMessage("接收到新的请求,是否跳转到处理页面?"); // 如果当前页面不是接收页面,那么弹出提示框,让用户选择是否跳转到接收页面
        }
        break;
      case "pendingRequest": // 登录以后,如果有待处理的请求,那么跳到接收页面
        if (route.name === "receive") {
          ElMessage.success("收到新的请求,已更新请求列表");
          flags.value.refreshRequestMap++;
        } else {
          handleIncomingMessage("收到新的待处理请求,是否跳转到处理页面?");
        }
        break;
      case "towerLight": // 控制塔灯
        try {
          !Array.isArray(msg) && (msg = msg.split(","));
        } catch (err) {
          LOG.error("towerLight解析指令失败", err.message);
          ElMessage.error("ws消息体异常,请查看console" + err.message);
        }

        sendMessageToSerialPort(msg, "ws");
        break;
      case "serialPort": // 串口连接
        if (msg.includes("未开启") || msg.includes("已关闭")) {
          router.push({ name: "setting" });

          setTimeout(() => {
            flags.value.openSerialPortDevTools++;
          }, 1000);

          ElMessageBox.alert(
            h("p", { class: "text-xl" }, "请先选择串口设备,再点击打开串口按钮"),
            "塔灯未连接",
            {
              confirmButtonText: "确定",
              type: "warning",
              draggable: true,
              showClose: false
            }
          );
        } else {
          LOG.info("串口消息", msg);
        }
        break;
      case "close": // "服务端主动关闭连接"
        LOG.error("服务端主动关闭连接");

        // 只展示一次提示,不然会一直弹窗,页面会黑屏
        if (flags.value.isServerDisconnectActivelyTipsShowed) return;

        flags.value.isServerDisconnectActivelyTipsShowed = true;
        router.push({ name: "home" });
        ElMessageBox.alert(
          h("div", [
            h("p", { class: "text-xl" }, "服务端主动关闭连接,请重新登录"),
            h("p", { class: "text-lg !mt-2" }, "(当前用户可能在其他地方登录,被挤下线)")
          ]),
          "连接中断",
          {
            confirmButtonText: "确认重连",
            type: "warning",
            draggable: true,
            showClose: false
          }
        )
          .then(() => {
            ElMessage.info({
              message: "请重新登录",
              duration: 1000
            });

            sessionStorage.removeItem("AndonCurrentStation");
            router.push({ name: "setting" });
          })
          .finally(() => {
            flags.value.isServerDisconnectActivelyTipsShowed = false;
          });
      default:
        // ElMessage.info("未知消息类型:" + type + "," + msg);
        break;
    }
  }

  /** 处理新消息或者待处理消息的情况 */
  function handleIncomingMessage(msg: string, isAutoJump = true) {
    let time = ref(5);
    let timer = null;

    if (isAutoJump) {
      timer = setInterval(() => {
        time.value--;
        if (time.value === 0) {
          clearInterval(timer);
          router.push({ name: "receive" });
          ElMessageBox.close();
        }
      }, 1000);
    }

    // 让用户选择是否跳转到接收页面,如果没操作,那么5秒后自动跳转
    ElMessageBox.confirm(
      () => {
        return h("div", [
          h("p", { class: "text-xl" }, msg),
          (isAutoJump &&
            h("p", { class: "text-lg !mt-2" }, [h("b", time.value), h("span", "秒后自动跳转")])) ||
            null
        ]);
      },
      "提示",
      {
        confirmButtonText: `确定`,
        cancelButtonText: "取消",
        type: "warning",
        draggable: true
      }
    )
      .then(() => {
        router.push({ name: "receive" });
      })
      .catch(() => {
        clearInterval(timer);
      });
  }

  /** webSocket关闭 */
  function onclose() {
    LOG.error("WebSocket连接关闭");
    clearInterval(interval.value);

    // 暂时不要重连,和用户点击当前工位冲突了
    // /**
    //  * 重连的时机是:用户ID没变,但是由于某些原因断开了连接,那么就重连
    //  * 例如:网络波动,断网重连,或者服务端主动断开连接
    //  * 如果用户ID变了,那么说明切换用户了,那么就不需要重连
    //  */
    // if (!flags.value.webSocketUserIdChanged) {
    //   reconnect();
    // }

    if (flags.value.isUnloading) return;

    ElMessageBox.alert(
      h("div", [
        h("p", { class: "text-xl" }, "与服务器意外断开了连接,点击确认重连"),
        h("p", { class: "text-lg !mt-2" }, "(塔灯也需要重新连接)")
      ]),
      "连接中断",
      {
        confirmButtonText: "确认重连",
        type: "warning",
        draggable: true,
        showClose: false
      }
    ).then(() => {
      location.reload();
    });
  }

  /** webSocket错误 */
  function onerror(err) {
    console.warn("WebSocket连接错误:", err);
    LOG.warning("WebSocket连接错误", JSON.stringify(err));
  }

  /** webSocket重连 */
  function reconnect() {
    if (!userId.value) {
      ElMessage.error("WebSocket重连失败,请重新点击当前工位登录");
      return;
    }

    if (reconnectInterval.value === maxReconnectInterval.value) {
      LOG.warning("WebSocket重连", "已达最大重连次数");
      return;
    }

    LOG.warning("WebSocket重连", `${reconnectInterval.value}ms将再次连接`);
    timeout.value = setTimeout(() => {
      webSocketConnect(userId.value);
      reconnectInterval.value = Math.min(reconnectInterval.value * 2, maxReconnectInterval.value);
    }, reconnectInterval.value);
  }

  /** websocket保活 心跳包 */
  function keepWebSocketConnectionAlive() {
    // tips: websocket.readyState值对应的状态 0:连接尚未建立;1:连接已建立;2:连接正在关闭;3:连接已关闭
    interval.value = setInterval(() => {
      if (ws.value && ws.value.readyState === 1) {
        ws.value.send(JSON.stringify({ type: "heart", msg: "💖" }));
      } else {
        clearInterval(interval.value);
      }
    }, heartbeatInterval.value);
  }

  /** 选择串口设备来连接 */
  async function startSerialPortConnection() {
    if ("serial" in navigator) {
      try {
        // @ts-ignore
        serialPort.value = await navigator.serial.requestPort();
        const info = serialPort.value.getInfo();

        console.log("串口设备对象信息:", serialPort.value);
        LOG.success("串口设备信息", JSON.stringify(info));
        ElMessage.success("串口设备连接成功");
      } catch (err) {
        LOG.error("连接串口设备失败", err.message);
        if (err.message.includes("No port selected by the user")) {
          ElMessage.info("用户取消选择串口设备");
        } else {
          ElMessage.error("连接串口设备失败:" + err.message);
        }
      }
    } else {
      ElMessage.error({
        message: "您的浏览器不支持串口连接,请更换新版Chrome浏览器后重试",
        duration: 3000
      });
    }
  }

  /** 打开串口 */
  async function openSerialPortConnection() {
    if (serialPort.value) {
      if (!serialPort.value.readable && !serialPort.value.writable) {
        try {
          await serialPort.value.open({ baudRate: 9600 });
          ElMessage.success("串口已打开");

          if (ws.value) {
            ws.value.send(JSON.stringify({ type: "serialPort", msg: "串口已打开,可以接收指令" }));
          } else {
            LOG.warning("请先点击当前工位", "以便正常接收指令");
            ElMessage.warning({
              message: "请先点击当前工位登录,以便正常接收指令",
              duration: 3000
            });
          }
        } catch (err) {
          let msg = err.message;
          LOG.error("打开串口失败", msg);
          if (msg.includes("The port is already open")) {
            ElMessage.warning("串口已经打开");
          } else if (msg.includes("Access to the port is denied")) {
            ElMessage.error("串口打开失败!权限被拒绝,请检查串口设备是否正确连接");
          } else if (msg.includes("A call to open() is already in progress")) {
            ElMessage.warning("串口开启中,请稍后...");
          } else if (msg.includes("Failed to open serial port.")) {
            ElMessage.error("串口打开失败!请检查设备是否能打开串口");
          } else {
            ElMessage.error("打开串口失败:" + msg);
          }
        }
      } else {
        ElMessage.warning("串口已开启,可以发送指令");
      }
    } else {
      LOG.warning("打开串口失败", "请先选择串口设备");
      ElMessage.warning("请先选择串口设备");
    }
  }

  /** 断开串口 */
  async function closeSerialPortConnection() {
    if (serialPort.value) {
      try {
        await serialPort.value.close();
        ElMessage.success("串口已关闭");
      } catch (err) {
        LOG.error("关闭串口失败", err.message);
        if (err.message.includes("The port is already closed")) {
          ElMessage.warning("串口未开启或已被关闭");
        } else {
          ElMessage.error("关闭串口失败:" + err.message);
        }
      }
    } else {
      ElMessage.warning("请先选择串口设备");
      LOG.warning("关闭串口失败", "请先选择串口设备");
    }
  }

  /** 发送消息给串口设备 */
  async function sendMessageToSerialPort(msg?: Array<string>, source = "web") {
    // 显示提示框,让用户选择是否前往设置
    const showTips = (title, content, type) => {
      const noTips = JSON.parse(
        sessionStorage.getItem("notShowSerialConnectionTipsFor" + type) || "false"
      );

      // 如果用户选择了不再提示,那么就不显示提示框
      if (noTips) return;

      // 只展示一次提示,不然会一直弹窗,页面会黑屏
      if (flags.value.isSerialConnectionTipsShowed) return;

      // 如果串口设备调试工具弹窗打开了,那么就不再显示让用户去设置串口设备的提示(我都打开设置界面,由于服务器一直发请求,不拦截一下的话就会一直弹很烦)
      if (flags.value.serialPortDevDialogStatus) return;

      flags.value.isSerialConnectionTipsShowed = true;
      ElMessageBox.alert(
        h("div", [
          h("p", { class: "text-xl" }, "接收到服务端指令,给塔灯发送指令失败!"),
          h("b", { class: "text-lg" }, "是否前往设置?"),
          h("div", { style: "display:flex;align-items:center;", class: "!mt-2" }, [
            h("input", {
              type: "checkbox",
              id: "notShowSerialConnectionTips",
              onclick: (e) => {
                if ((e.target as HTMLInputElement).checked) {
                  sessionStorage.setItem("notShowSerialConnectionTipsFor" + type, "true");
                } else {
                  sessionStorage.removeItem("notShowSerialConnectionTipsFor" + type);
                }
              }
            }),
            h("label", { class: "text-lg", for: "notShowSerialConnectionTips" }, "不再提示")
          ])
        ]),
        title,
        {
          confirmButtonText: "去设置",
          cancelButtonText: "取消",
          showCancelButton: true,
          type: "warning",
          draggable: true
        }
      )
        .then(() => {
          router.push({ name: "setting" });
          setTimeout(() => {
            flags.value.openSerialPortDevTools++;
          }, 500);
          ElMessageBox.alert(h("p", { class: "text-xl" }, content), title, {
            confirmButtonText: "确定",
            type: "warning",
            draggable: true,
            showClose: false
          });
        })
        .catch(() => {
          LOG.warning("已取消前往设置");
        })
        .finally(() => {
          flags.value.isSerialConnectionTipsShowed = false;
        });
    };

    // 如果没有传入msg,那么就不发送
    if (!msg) return;

    // 如果没有选择串口设备,那么提示用户先选择串口设备
    if (!serialPort.value) {
      if (source === "ws") {
        showTips("塔灯未连接", "请先【选择串口设备】,再点击【打开串口】按钮", "NotConnected");
      } else {
        ElMessage.warning("请先选择串口设备");
      }
      LOG.warning("发送指令失败", "请先在设置中选择串口设备");
      return;
    }

    // 如果串口没有打开,就打开串口
    if (!serialPort.value.readable || !serialPort.value.writable) {
      if (source === "ws") {
        showTips("串口未开启", "请点击【打开串口】按钮", "NotOpen");
      } else {
        ElMessage.warning("请先打开串口连接");
      }
      LOG.warning("发送指令失败", "请先打开串口连接");
      return;
    }

    const writer = serialPort.value.writable.getWriter();
    await writer.write(new Uint8Array(msg.map((item) => parseInt(item, 16))));
    writer.releaseLock();
    source === "web" &&
      ElMessage.success({
        message: "发送成功",
        duration: 1000
      });
  }

  return {
    ws,
    flags,
    serialPort,
    webSocketConnect,
    sendMessageToSerialPort,
    openSerialPortConnection,
    startSerialPortConnection,
    closeSerialPortConnection,
    handleIncomingMessage
  };
});

posted @ 2024-09-11 15:02  脆皮鸡  阅读(27)  评论(0)    收藏  举报