fanfan 2 anni fa
parent
commit
fb414a9113

+ 131 - 93
src/hooks/web/useWebsocket.ts

@@ -1,145 +1,183 @@
-import { useMessage } from '/@/hooks/web/useMessage';
-import { useWebSocket } from '@vueuse/core';
-import { APP_WEBSOCKET_KEY, LOGINTYPE_KEY, TOKEN_KEY } from '../../enums/cacheEnum';
+// 自己定义的 websocket, 使用时注意
 import { useGlobSetting } from '../setting';
-import { getAuthCache, setAuthCache } from '../../utils/auth';
 import { useUserStoreWithOut } from '/@/store/modules/user';
-import { ResultEnum } from '../../enums/httpEnum';
+import { getAuthCache } from '../../utils/auth';
+import { LOGINTYPE_KEY, TOKEN_KEY } from '../../enums/cacheEnum';
+import { ref } from 'vue';
+import { useIntervalFn } from '@vueuse/core';
+import { useMessage } from './useMessage';
+import { ResultEnum } from '/@/enums/httpEnum';
+
+function resolveNestedOptions<T>(options: T | true): T {
+  if (options === true) return {} as T;
+  return options;
+}
+export type WebSocketStatus = 'OPEN' | 'CONNECTING' | 'CLOSED';
 
 export default function useWebSocketFn() {
   const { createMessage } = useMessage();
   const { websocketUrl } = useGlobSetting();
   const userStore = useUserStoreWithOut();
-  let websocketData = {
-    data: undefined,
-  } as any;
+
   let token = undefined;
   let loginType = undefined;
-  let websocketKey = undefined;
-  const DEFAULT_MESSAGE = {
-    ERROR: 'websocket服务连接失败',
-    SUCCESS: 'websocket服务连接成功',
-    CANCEL: 'websocket服务取消连接',
-  };
-  const WEBSOCKET_VALUE = {
-    CONNECT_SUCCESS: '1',
-    CONNECT_ERROR: '',
-  };
-  const isAutoReconnect = {
-    delay: 5000,
-    onFailed: () => {
-      setAuthCache(APP_WEBSOCKET_KEY, '');
-      console.log(DEFAULT_MESSAGE.ERROR);
-    },
-  };
 
-  const isHeartbeat = {
+  const wsRef = ref<WebSocket | undefined>();
+  const status = ref<WebSocketStatus>('CLOSED');
+  const data = ref<any>(null);
+  const DEFAULT_PING_MESSAGE = 'ping';
+  let pongTimeoutWait: ReturnType<typeof setTimeout> | undefined;
+  let bufferedData: (string | ArrayBuffer | Blob)[] = [];
+  let heartbeatResume: Fn | undefined;
+  let heartbeatPause: Fn | undefined;
+
+  const heartbeat = {
     interval: 1000 * 10,
     message: JSON.stringify({ s: '2' }),
     pongTimeout: 1000 * 30,
   };
+
   // 是否可以连接
   const boolCondition = (): Boolean => {
     token = getAuthCache(TOKEN_KEY);
     loginType = getAuthCache(LOGINTYPE_KEY);
-    websocketKey = getAuthCache(APP_WEBSOCKET_KEY);
     if (token == '' || token == undefined || loginType == undefined || loginType == '') {
       return false;
     } else {
       return true;
     }
   };
-  // 订阅, 默认会订阅所有主题
-  const init = () => {
+
+  const resetHeartbeat = () => {
+    clearTimeout(pongTimeoutWait);
+    pongTimeoutWait = undefined;
+  };
+
+  const _sendBuffer = () => {
+    if (bufferedData.length && wsRef.value && status.value === 'OPEN') {
+      for (const buffer of bufferedData) wsRef.value.send(buffer);
+      bufferedData = [];
+    }
+  };
+
+  const send = (data: string | ArrayBuffer | Blob, useBuffer = true) => {
+    if (!wsRef.value || status.value !== 'OPEN') {
+      if (useBuffer) bufferedData.push(data);
+      return false;
+    }
+    _sendBuffer();
+    wsRef.value.send(data);
+    return true;
+  };
+
+  const close: WebSocket['close'] = (code = 1000, reason) => {
+    if (!wsRef.value) return;
+    heartbeatPause?.();
+    wsRef.value.close(code, reason);
+  };
+
+  const _init = () => {
     const isWebscoket = boolCondition();
     if (!isWebscoket) return;
     const params = `?Authorization=${token}&LoginType=${loginType}`;
     // 设置 websocket 连接地址
     const setWebsocketUrl = `${websocketUrl + params}`;
-    // 存放 websocket 数据
-    if (websocketKey == '' || websocketKey == undefined) {
-      // 创建 websocket
-      websocketData = useWebSocket(setWebsocketUrl, {
-        autoReconnect: isAutoReconnect,
-        onMessage: _ws => {
-          getSub();
-        },
-        onConnected: _ws => {
-          setAuthCache(APP_WEBSOCKET_KEY, WEBSOCKET_VALUE.CONNECT_SUCCESS);
-        },
-        onDisconnected: _ws => {
-          setAuthCache(APP_WEBSOCKET_KEY, WEBSOCKET_VALUE.CONNECT_ERROR);
+    const ws = new WebSocket(setWebsocketUrl);
+    wsRef.value = ws;
+    status.value = 'CONNECTING';
+
+    if (heartbeat) {
+      const {
+        message = DEFAULT_PING_MESSAGE,
+        interval = 1000,
+        pongTimeout = 1000,
+      } = resolveNestedOptions(heartbeat);
+
+      const { pause, resume } = useIntervalFn(
+        () => {
+          send(message, false);
+          if (pongTimeoutWait != null) return;
+          pongTimeoutWait = setTimeout(() => {
+            console.log('是否关闭');
+            // auto-reconnect will be trigger with ws.onclose()
+            close();
+          }, pongTimeout);
         },
-        heartbeat: isHeartbeat,
-        autoClose: false,
-      });
+        interval,
+        { immediate: false },
+      );
+      heartbeatPause = pause;
+      heartbeatResume = resume;
     }
-  };
 
-  // 获取 订阅数据
-  const getSub = () => {
-    const isWebscoket = boolCondition();
-    if (isWebscoket && websocketData.data == undefined) {
-      setAuthCache(APP_WEBSOCKET_KEY, '');
-      init();
-      return;
-    }
-    // 关闭当前连接
-    if (websocketData.data?.value) {
-      const jsonData = JSON.parse(websocketData.data?.value)?.d || {};
-      if (jsonData.code == ResultEnum.NO_LOGIN) {
-        websocketData.close();
-      }
-    }
-    return {
-      data: websocketData.data?.value || '{}',
-      time: Date.now(),
+    // 监听是否连接成功
+    ws.onopen = () => {
+      status.value = 'OPEN';
+      console.log('ws连接状态:' + ws.readyState);
+      heartbeatResume?.();
+      _sendBuffer();
     };
-  };
 
-  // 取消链
-  const closeSub = () => {
-    console.log('WebSocket 取消链接');
-    setAuthCache(APP_WEBSOCKET_KEY, WEBSOCKET_VALUE.CONNECT_ERROR);
-  };
-
-  // 调整数据
-  const setData = async data => {
-    try {
-      const jsonData = JSON.parse(data);
-      if (jsonData.s !== 3 && jsonData.s !== 1 && jsonData.d !== null) {
+    //接听服务器发回的信息并处理展示
+    ws.onmessage = async function (e) {
+      console.log('接收到来自服务器的消息:', e);
+      console.log('status', status.value);
+      if (heartbeat) {
+        resetHeartbeat();
+        const { message = DEFAULT_PING_MESSAGE } = resolveNestedOptions(heartbeat);
+        if (e.data === message) return;
+      }
+      const jsonData = Object.assign(JSON.parse(e.data), { time: Date.now() });
+      if (jsonData.s == 0 && jsonData.d !== null) {
         if (jsonData.d?.extra?.type == 'user_token_expired') {
-          closeSub();
+          data.value = {};
+          close();
           createMessage.error(jsonData.d?.content);
           await userStore.logout();
+          return;
         }
-        return jsonData?.d;
+        data.value = jsonData;
       }
-      if (jsonData.s == 1) {
+      if (jsonData.s == 1 || jsonData.s == 3) {
         // 登录过程中被踢下线或者token过期 || 用户未登录
         if (jsonData.d?.code == ResultEnum.NO_LOGIN) {
-          closeSub();
+          data.value = {};
+          close();
           createMessage.error(jsonData.d?.content);
+        } else {
+          data.value = jsonData;
         }
       }
-    } catch (error) {
-      return {};
-    }
+    };
+
+    //监听连接关闭事件
+    ws.onclose = function (ev) {
+      console.log('🚀 ~ file: useWebsocket.ts:61 ~ useWebSocket ~ ev:', ev);
+      //监听整个过程中websocket的状态
+      console.log('ws连接状态:' + ws.readyState);
+      status.value = 'CLOSED';
+      wsRef.value = undefined;
+    };
+
+    //监听并处理error事件
+    ws.onerror = function (error) {
+      console.log(error);
+    };
   };
 
-  // 筛选数据
-  const filterSub = async (data: any) => {
-    console.log('WebSocket 过滤数据', data);
-    try {
-      setData(data);
-    } catch (error) {
-      return {};
+  const open = () => {
+    console.log('status', status.value);
+    if (status.value == 'CLOSED') {
+      close();
+      _init();
     }
   };
 
   return {
-    getSub,
-    filterSub,
-    closeSub,
+    data,
+    close,
+    status,
+    open,
+    ws: wsRef,
   };
 }

+ 0 - 2
src/layouts/default/header/components/user-dropdown/index.vue

@@ -83,7 +83,6 @@
       const { prefixCls } = useDesign('header-user-dropdown');
       const { getShowDoc, getUseLockPage } = useHeaderSetting();
       const userStore = useUserStore();
-      const { closeSub } = useWebSocketFn();
 
       const getUserInfo = computed(() => {
         const { realName = '', avatar, desc } = userStore.getUserInfo || {};
@@ -100,7 +99,6 @@
 
       //  login out
       function handleLoginOut() {
-        closeSub();
         userStore.confirmLoginOut();
       }
 

+ 4 - 1
src/layouts/default/header/index.vue

@@ -44,7 +44,7 @@
   </Header>
 </template>
 <script lang="ts">
-  import { defineComponent, unref, computed } from 'vue';
+  import { defineComponent, unref, computed, inject } from 'vue';
 
   import { propTypes } from '/@/utils/propTypes';
 
@@ -107,6 +107,9 @@
 
       const { getIsMobile } = useAppInject();
 
+      const socket = inject('useWebSocketFn') as any;
+      console.log('🚀 ~ file: index.vue:101 ~ socket:', socket.open());
+
       const getHeaderClass = computed(() => {
         const theme = unref(getHeaderTheme);
         return [

+ 4 - 0
src/main.ts

@@ -22,6 +22,8 @@ import 'ant-design-vue/dist/antd.variable.min.css';
 import './assets/iconfont/iconfont.js';
 import './assets/iconfont/iconfont.css';
 
+import useWebSocketFn from './hooks/web/useWebsocket';
+
 async function bootstrap() {
   const app = createApp(App);
   // Configure store
@@ -45,6 +47,8 @@ async function bootstrap() {
   // https://next.router.vuejs.org/api/#isready
   // await router.isReady();
 
+  app.provide('useWebSocketFn', useWebSocketFn());
+
   app.mount('#app');
 }
 

+ 2 - 1
src/store/modules/user.ts

@@ -22,7 +22,7 @@ import { usePermissionStore } from '/@/store/modules/permission';
 import { RouteRecordRaw } from 'vue-router';
 import { PAGE_NOT_FOUND_ROUTE } from '/@/router/routes/basic';
 import { isArray } from '/@/utils/is';
-import { h } from 'vue';
+import { h, inject } from 'vue';
 import locales from '/@/utils/locales';
 // import { getWebsocket } from '/@/utils/websocket';
 
@@ -155,6 +155,7 @@ export const useUserStore = defineStore({
         const permissionStore = usePermissionStore();
         if (!permissionStore.isDynamicAddedRoute) {
           const routes = await permissionStore.buildRoutesAction();
+          console.log('🚀 ~ file: user.ts:152 ~ afterLoginAction ~ routes', routes);
           routes.forEach(route => {
             router.addRoute(route as unknown as RouteRecordRaw);
           });

+ 6 - 6
src/utils/filters.ts

@@ -179,18 +179,18 @@ export const ArticleSelect = [
 
 // 文章类型
 export const radioSwitch = [
-  { label: '开启', value: 0 },
-  { label: '关闭', value: 1 },
+  { label: '开启', value: 1 },
+  { label: '关闭', value: 0 },
 ];
 
 // 文章类型
 export const radioBoolean = [
-  { label: '是', value: 0 },
-  { label: '否', value: 1 },
+  { label: '是', value: 1 },
+  { label: '否', value: 0 },
 ];
 
 // 文章类型
 export const radioSuccess = [
-  { label: '成功', value: 0 },
-  { label: '失败', value: 1 },
+  { label: '成功', value: 1 },
+  { label: '失败', value: 0 },
 ];

+ 4 - 4
src/views/sys/sysDict/index.vue

@@ -22,17 +22,17 @@
     if (!value) return;
     dictId.value = value;
   }
-  const { getSub, filterSub } = useWebSocketFn();
+  const { getSub, filterData } = useWebSocketFn();
   getSub();
   // const isGetSub = getSub();
   // console.log('🚀 ~ file: index.vue ~ line 20 ~ isGetSub', isGetSub);
   // // 监听数据
   watch(
-    () => getSub(),
-    async (newValue, _oldValue) => {
+    () => filterData.value,
+    async (_newValue, _oldValue) => {
       // console.log('🚀 ~ file: index.vue ~ line 22 ~ watch ~ _oldValue', _oldValue);
       // console.log('🚀 ~ file: index.vue ~ line 22 ~ watch ~ newValue', newValue);
-      filterSub(newValue?.data);
+      console.log('🚀 ~ file: index.vue:32 ~ filterData.value:', filterData.value);
       // console.log('🚀 ~ file: index.vue ~ line 24 ~ watch ~ res', res);
     },
     { deep: true },

+ 9 - 1
src/views/sys/sysLog/index.vue

@@ -34,7 +34,7 @@
   </div>
 </template>
 <script lang="ts" setup>
-  import { onBeforeMount, ref } from 'vue';
+  import { inject, onBeforeMount, ref, watch } from 'vue';
   import { Tag } from 'ant-design-vue';
   import { BasicTable, useTable, TableAction } from '/@/components/Table';
   import { useDrawer } from '/@/components/Drawer';
@@ -94,4 +94,12 @@
       record,
     });
   }
+
+  const socket = inject('useWebSocketFn') as any;
+  console.log('🚀 ~ file: index.vue:101 ~ socket:', socket);
+  // 监听数据
+  watch(socket.data, (_newValue, _oldValue) => {
+    console.log('🚀 ~ file: index.vue ~ line 22 ~ watch ~ _oldValue', _oldValue);
+    console.log('🚀 ~ file: index.vue ~ line 22 ~ watch ~ newValue', _newValue);
+  });
 </script>

+ 31 - 19
src/views/sys/sysMenu/sysMenuTable/data.ts

@@ -5,7 +5,7 @@ import { radioBoolean, radioSwitch } from '/@/utils/filters';
 const isDir = (menuType: string) => menuType === 'dir';
 const isMenu = (menuType: string) => menuType === 'menu';
 const isButton = (menuType: string) => menuType === 'button';
-const isLinkExternal = (linkExternal: string) => linkExternal === '1';
+const isLinkExternal = (linkExternal: number) => linkExternal === 1;
 export const columns: BasicColumn[] = [
   {
     title: '菜单名称',
@@ -134,9 +134,12 @@ export const dataFormSchema: FormSchema[] = [
     component: 'RadioGroup',
     required: true,
     componentProps: {
-      options: radioSwitch,
+      options: [
+        { label: '开启', value: 0 },
+        { label: '关闭', value: 1 },
+      ],
     },
-    defaultValue: '0',
+    defaultValue: 1,
   },
   {
     field: 'icon',
@@ -149,7 +152,7 @@ export const dataFormSchema: FormSchema[] = [
     label: '排序',
     component: 'InputNumber',
     required: true,
-    defaultValue: '1',
+    defaultValue: 1,
     componentProps: {
       placeholder: '请输入排序',
       min: 1,
@@ -191,31 +194,37 @@ export const dataFormSchema: FormSchema[] = [
     component: 'RadioGroup',
     required: true,
     componentProps: {
-      options: radioBoolean,
+      options: [
+        { label: '是', value: 1 },
+        { label: '否', value: 0 },
+      ],
     },
-    defaultValue: '0',
+    defaultValue: 1,
     ifShow: ({ values }) => isDir(values.menuType) || isMenu(values.menuType),
   },
   {
-    field: 'linkUrl',
-    label: '外',
-    component: 'Input',
+    field: 'linkExternal',
+    label: '是否外链',
+    component: 'ApiRadioGroup',
     required: true,
     componentProps: {
-      placeholder: '请输入外部链接',
+      options: [
+        { label: '是', value: 1 },
+        { label: '否', value: 0 },
+      ],
     },
-    ifShow: ({ values }) => isMenu(values.menuType) && isLinkExternal(values.linkExternal),
+    defaultValue: 0,
+    ifShow: ({ values }) => isMenu(values.menuType),
   },
   {
-    field: 'linkExternal',
-    label: '是否外链',
-    component: 'ApiRadioGroup',
+    field: 'linkUrl',
+    label: '外',
+    component: 'Input',
     required: true,
     componentProps: {
-      options: radioBoolean,
+      placeholder: '请输入外部链接',
     },
-    defaultValue: '0',
-    ifShow: ({ values }) => isMenu(values.menuType),
+    ifShow: ({ values }) => isMenu(values.menuType) && isLinkExternal(values.linkExternal),
   },
   {
     field: 'frame',
@@ -223,9 +232,12 @@ export const dataFormSchema: FormSchema[] = [
     component: 'ApiRadioGroup',
     required: true,
     componentProps: {
-      options: radioBoolean,
+      options: [
+        { label: '是', value: 1 },
+        { label: '否', value: 0 },
+      ],
     },
-    defaultValue: '0',
+    defaultValue: 0,
     ifShow: ({ values }) => isMenu(values.menuType) && isLinkExternal(values.linkExternal),
   },
   {