li.shaoyi 7 hónapja
szülő
commit
deaaec0e63

+ 2 - 2
oem/gzcj/config/appconfig.json

@@ -1,8 +1,8 @@
 {
   "appId": "com.muchinfo.gzcj",
   "appName": "贵州茶交",
-  "version": "1.0.9",
-  "versionCode": "100009",
+  "version": "1.0.10",
+  "versionCode": "100010",
   "apiUrl": "http://192.168.31.204:8080/cfg?key=test_204",
   "tradeChannel": "ws",
   "modules": [

+ 3 - 3
oem/sjgj/config/appconfig.json

@@ -1,8 +1,8 @@
 {
   "appId": "com.muchinfo.sjgj",
-  "appName": "纯金网金银报价结算中心订单管理系统",
-  "version": "1.0.7",
-  "versionCode": "100007",
+  "appName": "纯金网结算中心",
+  "version": "1.0.8",
+  "versionCode": "100008",
   "apiUrl": "http://192.168.31.139:8080/cfg?key=test_139",
   "tradeChannel": "ws",
   "modules": [

+ 2 - 2
oem/tss/config/appconfig.json

@@ -1,8 +1,8 @@
 {
   "appId": "com.muchinfo.tss",
   "appName": "TCE",
-  "version": "1.0.48",
-  "versionCode": "100048",
+  "version": "1.0.51",
+  "versionCode": "100051",
   "apiUrl": "http://192.168.31.210:8080/cfg?key=test_210",
   "tradeChannel": "ws",
   "showLoginAlert": true,

+ 7 - 0
script/oem.env.json

@@ -225,5 +225,12 @@
         "VUE_APP_ROOT": "src/packages/pc/",
         "VUE_APP_OEM": "oem/tss/",
         "VUE_APP_TRADE_CHANNEL": "ws"
+    },
+    {
+        "VUE_APP_ENV": "tss-vi",
+        "VUE_APP_NAME": "TCE",
+        "VUE_APP_ROOT": "src/packages/tss-vi/",
+        "VUE_APP_OEM": "oem/tss/",
+        "VUE_APP_TRADE_CHANNEL": "ws"
     }
 ]

+ 72 - 0
src/packages/tss-vi/index.html

@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<html lang="zh-cn">
+
+<head>
+  <meta charset="utf-8">
+  <meta http-equiv="X-UA-Compatible" content="IE=edge">
+  <meta name="viewport"
+    content="width=device-width,initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0,user-scalable=no,viewport-fit=cover">
+  <link rel="icon" href="<%= BASE_URL %>favicon.ico">
+  <link rel="apple-touch-icon-precomposed" href="<%= BASE_URL %>apple-touch-icon-precomposed.png">
+  <title>
+    <%= htmlWebpackPlugin.options.title %>
+  </title>
+  <style>
+    @keyframes app-load {
+      0% {
+        opacity: 1;
+      }
+
+      100% {
+        opacity: 0;
+      }
+    }
+
+    .app-load {
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      gap: 5px;
+      height: 100vh;
+    }
+
+    .app-load span {
+      display: inline-block;
+      width: 8px;
+      height: 8px;
+      border-radius: 50%;
+      background: #ccc;
+      opacity: 0;
+      animation: app-load 600ms ease infinite;
+    }
+
+    .app-load span:nth-child(1) {
+      animation-delay: 100ms;
+    }
+
+    .app-load span:nth-child(2) {
+      animation-delay: 200ms;
+    }
+
+    .app-load span:nth-child(3) {
+      animation-delay: 300ms;
+    }
+  </style>
+</head>
+
+<body>
+  <noscript>
+    <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
+        Please enable it to continue.</strong>
+  </noscript>
+  <div id="app" class="app">
+    <div class="app-load">
+      <span></span>
+      <span></span>
+      <span></span>
+    </div>
+  </div>
+  <!-- built files will be auto injected -->
+</body>
+
+</html>

+ 35 - 0
src/packages/tss-vi/main.ts

@@ -0,0 +1,35 @@
+import 'core-js'
+import 'regenerator-runtime/runtime'
+import { createApp } from 'vue'
+import App from '../tss/App.vue'
+import router from './router'
+import directives from '@/directives' // 自定义指令集
+//import 'default-passive-events'
+import '@/utils/h5plus' // 加载html5+
+import 'hqchart/src/jscommon/umychart.resource/font/iconfont.css'
+import layouts from '@mobile/components/layouts' // 全局布局组件
+import 'vant/lib/index.css'
+import VXETable from 'vxe-table'
+import 'vxe-table/lib/style.css'
+import '../tss/assets/themes/style.less' // 主题样式
+// import { timerInterceptor } from '@/utils/timer'
+import { i18n } from '@/stores'
+// import Vconsole from 'vconsole'
+// new Vconsole()
+
+const app = createApp(App)
+app.use(i18n)
+app.use(router)
+app.use(directives)
+app.use(VXETable)
+app.use(layouts)
+app.mount('#app')
+
+// 等待 html 加载完成
+// document.addEventListener('DOMContentLoaded', () => {
+//     const { screenAdapter } = useGlobalStore()
+//     // 适配客户端
+//     screenAdapter(true)
+//     // 监听窗口大小变化
+//     window.addEventListener('resize', timerInterceptor.setDebounce(() => screenAdapter(true)))
+// }, false)

+ 10 - 0
src/packages/tss-vi/postcss.config.js

@@ -0,0 +1,10 @@
+module.exports = {
+    plugins: {
+        'postcss-px-to-viewport': {
+            viewportWidth: 375,
+            landscape: true, // 是否处理横屏情况
+            landscapeUnit: 'vw', // 横屏时使用的单位
+            landscapeWidth: 844, // 横屏时使用的视口宽度
+        },
+    },
+}

+ 391 - 0
src/packages/tss-vi/router/index.ts

@@ -0,0 +1,391 @@
+import { createWebHashHistory, RouteRecordRaw } from 'vue-router'
+import { useLoginStore } from '@/stores'
+import { clearPending } from '@/services/http/pending'
+import { homeRoutes, pageRoutes } from '@mobile/router/section'
+import service from '@/services'
+import Page from '@mobile/components/layouts/page/index.vue'
+import animateRouter from '@mobile/router/animateRouter'
+
+const loginStore = useLoginStore()
+
+const routes: Array<RouteRecordRaw> = [
+  {
+    path: '/:pathMatch(.*)*',
+    name: 'error',
+    component: () => import('@mobile/views/error/404.vue'),
+    meta: {
+      ignoreAuth: true,
+    },
+  },
+  {
+    path: '/boot',
+    name: 'boot',
+    component: () => import('../../tss/views/boot/Index.vue'),
+    meta: {
+      ignoreAuth: true,
+    },
+  },
+  {
+    path: '/',
+    component: Page,
+    children: [
+      {
+        path: '',
+        name: 'home',
+        component: () => import('../../tss/views/home/index.vue'),
+        children: [
+          {
+            path: '',
+            name: 'home-index',
+            component: () => import('../../tss/views/home/main/index.vue'),
+            meta: {
+              ignoreAuth: true,
+            },
+          },
+          {
+            path: 'mine',
+            name: 'home-mine',
+            component: () => import('../../tss/views/mine/Index.vue'),
+          },
+          {
+            path: 'group',
+            name: 'home-group',
+            component: () => import('../../tss/views/order/position/Index.vue'),
+          },
+          ...homeRoutes
+        ]
+      }
+    ]
+  },
+  {
+    path: '/product',
+    component: Page,
+    children: [
+      {
+        path: 'list',
+        name: 'product-list',
+        component: () => import('../../tss/views/product/list/index.vue'),
+      }
+    ]
+  },
+  {
+    path: '/search',
+    component: Page,
+    children: [
+      {
+        path: '',
+        name: 'search',
+        component: () => import('../../tss/views/search/index.vue'),
+      }
+    ]
+  },
+  {
+    path: '/user',
+    component: Page,
+    children: [
+      {
+        path: 'login',
+        name: 'user-login',
+        component: () => import('../../tss/views/user/login/Index.vue'),
+        meta: {
+          ignoreAuth: true,
+        },
+      },
+      {
+        path: 'register',
+        name: 'user-register',
+        component: () => import('@mobile/views/user/register/Index.vue'),
+        meta: {
+          ignoreAuth: true,
+        },
+        props: {
+          showYhkhfxgzs: false,
+          insetStyle: false
+        }
+      },
+      {
+        path: 'forget',
+        name: 'user-forget',
+        component: () => import('@mobile/views/user/forget/Index.vue'),
+        meta: {
+          ignoreAuth: true,
+        },
+        props: {
+          insetStyle: false
+        }
+      },
+      {
+        path: 'cancel',
+        name: 'user-cancel',
+        component: () => import('@mobile/views/user/cancel/Index.vue'),
+      },
+      {
+        path: 'password',
+        name: 'user-password',
+        component: () => import('@mobile/views/user/password/Index.vue'),
+      },
+      {
+        path: 'avatar',
+        name: 'user-avatar',
+        component: () => import('@mobile/views/user/avatar/Index.vue'),
+      },
+    ],
+  },
+  {
+    path: '/report',
+    component: Page,
+    children: [
+      {
+        path: '',
+        name: 'report',
+        component: () => import('@mobile/views/report/index.vue'),
+      }
+    ]
+  },
+  {
+    path: '/account',
+    component: Page,
+    children: [
+      {
+        path: 'certification',
+        name: 'account-certification',
+        component: () => import('@mobile/views/account/certification/Index.vue'),
+      },
+      {
+        path: 'authresult',
+        name: 'account-authresult',
+        component: () => import('@mobile/views/account/authresult/Index.vue'),
+      }
+    ],
+  },
+  {
+    path: '/news',
+    component: Page,
+    children: [
+      {
+        path: '',
+        name: 'news-list',
+        component: () => import('@mobile/views/news/list/Index.vue'),
+        meta: {
+          ignoreAuth: true,
+        },
+      },
+      {
+        path: 'detail',
+        name: 'news-detail',
+        component: () => import('@mobile/views/news/detail/Index.vue'),
+        meta: {
+          ignoreAuth: true,
+        },
+      },
+    ],
+  },
+  {
+    path: '/bank',
+    component: Page,
+    children: [
+      {
+        path: 'wallet',
+        name: 'bank-wallet',
+        component: () => import('../../tss/views/bank/wallet/Index.vue'),
+      },
+      {
+        path: 'sign',
+        name: 'bank-sign',
+        component: () => import('@mobile/views/bank/sign/Index.vue'),
+      },
+      {
+        path: 'capital',
+        name: 'bank-capital',
+        component: () => import('../../tss/views/bank/capital/index.vue'),
+      }
+    ],
+  },
+  {
+    path: '/order',
+    component: Page,
+    children: [
+      {
+        path: 'list',
+        name: 'order-list',
+        component: () => import('../../tss/views/order/list/Index.vue'),
+      },
+      {
+        path: 'position',
+        name: 'order-position',
+        component: () => import('../../tss/views/order/position/Index.vue'),
+        props: {
+          showBackButton: true
+        }
+      },
+      {
+        path: 'delivery',
+        name: 'order-delivery',
+        component: () => import('../../tss/views/order/delivery/Index.vue'),
+      },
+      {
+        path: 'performance',
+        name: 'order-performance',
+        component: () => import('@mobile/views/order/performance/Index.vue'),
+      }
+    ]
+  },
+  {
+    path: '/mine',
+    component: Page,
+    children: [
+      {
+        path: 'address',
+        name: 'mine-address',
+        component: () => import('@mobile/views/mine/address/Index.vue'),
+      },
+      {
+        path: 'invoice',
+        name: 'mine-invoice',
+        component: () => import('@mobile/views/mine/invoice/Index.vue'),
+      },
+      {
+        path: 'profile',
+        name: 'mine-profile',
+        component: () => import('@mobile/views/mine/profile/Index.vue'),
+      },
+      {
+        path: 'setting',
+        name: 'mine-setting',
+        component: () => import('@mobile/views/mine/setting/Index.vue'),
+      },
+      {
+        path: 'wechat',
+        name: 'mine-wechat',
+        component: () => import('@mobile/views/mine/wechat/Index.vue'),
+      },
+      {
+        path: 'email',
+        name: 'mine-email',
+        component: () => import('@mobile/views/mine/email/Index.vue'),
+      }
+    ],
+  },
+  {
+    path: '/notice',
+    component: Page,
+    children: [
+      {
+        path: '',
+        name: 'notice-list',
+        component: () => import('@mobile/views/notice/list/index.vue'),
+      },
+    ],
+  },
+  {
+    path: '/rules',
+    component: Page,
+    children: [
+      {
+        path: "zcxy",
+        name: "rules-zcxy",
+        component: () => import("@mobile/views/rules/zcxy/Index.vue"),
+        meta: {
+          ignoreAuth: true,
+        },
+      },
+      {
+        path: "yhkhfxgzs",
+        name: "rules-yhkhfxgzs",
+        component: () => import("@mobile/views/rules/fxgzs/Index.vue"),
+        meta: {
+          ignoreAuth: true,
+        },
+      },
+      {
+        path: "yszc",
+        name: "rules-yszc",
+        component: () => import("@mobile/views/rules/yszc/Index.vue"),
+        meta: {
+          ignoreAuth: true,
+        },
+      },
+      {
+        path: "gywm",
+        name: "rules-gywm",
+        component: () => import("../../tss/views/rules/gywm/Index.vue"),
+        meta: {
+          ignoreAuth: true,
+        },
+      },
+      {
+        path: "fwrx",
+        name: "rules-fwrx",
+        component: () => import("../../tss/views/rules/fwrx/Index.vue"),
+        meta: {
+          ignoreAuth: true,
+        },
+      },
+      {
+        path: "benefits",
+        name: "rules-benefits",
+        component: () => import("../../tss/views/rules/benefits/index.vue"),
+        meta: {
+          ignoreAuth: true,
+        },
+      },
+      {
+        path: "malls",
+        name: "rules-malls",
+        component: () => import("../../tss/views/rules/malls/index.vue"),
+        meta: {
+          ignoreAuth: true,
+        },
+      },
+      {
+        path: "logistics",
+        name: "rules-logistics",
+        component: () => import("../../tss/views/rules/logistics/index.vue"),
+        meta: {
+          ignoreAuth: true,
+        },
+      },
+    ]
+  },
+  ...pageRoutes
+]
+
+const router = animateRouter.create({
+  history: createWebHashHistory(),
+  routes,
+})
+
+// 路由跳转拦截
+router.beforeEach((to, from, next) => {
+  clearPending()
+  // 判断服务是否加载完成
+  if (service.isReady) {
+    if (to.meta.ignoreAuth || loginStore.token) {
+      next()
+    } else {
+      if (to.matched.some((e) => e.name === 'home')) {
+        // 如果是主页导航页面,强制跳转到首页
+        next({
+          name: 'home-index',
+          replace: true,
+        })
+      } else {
+        next({
+          name: 'user-login',
+          query: { redirect: to.fullPath },
+        })
+      }
+    }
+  } else {
+    if (to.name === 'boot' || to.name === 'user-login') {
+      next()
+    } else {
+      next({
+        name: 'boot',
+        query: { redirect: to.fullPath },
+      })
+    }
+  }
+})
+
+export default router

+ 40 - 4
src/services/worker/index.ts → src/services/worker/quote/index.ts

@@ -1,6 +1,42 @@
-import { reactive, toRaw } from 'vue'
-import { queryErmcpGoods } from '@/services/api/goods'
-import Worker from 'worker-loader!./quote'
+import { reactive, toRefs, toRaw } from 'vue'
+import { queryErmcpGoods, queryQuoteDay } from '@/services/api/goods'
+import Worker from 'worker-loader!./main'
+import Service from '../service'
+
+const useService = (function () {
+  const state = reactive({
+    loading: false,
+    goodsMap: [] as Model.GoodsRsp[], // 商品列表
+    quoteMap: [] as Model.QuoteDayRsp[], // 盘面列表
+  })
+
+  const worker = new Worker()
+
+  const updateData = () => {
+    queryErmcpGoods().then((res) => {
+      state.goodsMap = res.data
+      queryQuoteDay().then((res) => {
+        state.quoteMap = res.data
+      })
+    })
+  }
+
+  Service.onReady(() => {
+    worker.postMessage({
+      type: 'init',
+      data: {
+        socketUrl: Service.getServiceConfig('quoteUrl'),
+        socketProtocols: []
+      }
+    })
+  })
+
+  return {
+    ...toRefs(state),
+    worker
+  }
+})()
+
 
 export default new (class {
   private readonly worker = new Worker()
@@ -65,7 +101,7 @@ export default new (class {
   async updateData() {
     try {
       this.state.loading = true
-      
+
       const res = await queryErmcpGoods()
       res.data.forEach((item) => {
         this.state.goodsMap.set(item.goodscode, item)

+ 26 - 7
src/services/worker/quote.ts → src/services/worker/quote/main.ts

@@ -1,13 +1,14 @@
 import Long from 'long'
-import MarketService from './market'
-import WebSocketManager from './websocket'
+import MarketData from './market'
+import WebSocketManager from '@/utils/websocket'
 import { Package40 } from '@/services/websocket/package'
 import { subscribeListToByteArrary } from '@/services/websocket/package/package40/encode'
 import { parseReceivePush } from '@/services/websocket/package/package40/decode'
 
-const marketService = new MarketService()
+const marketData = new MarketData()
 
 const webSocket = new WebSocketManager({
+    url: '',
     //heartbeatMessage: () => JSON.stringify({ type: 'ping' }),
     onMessage: (data) => {
         new Response(data as Uint8Array).arrayBuffer().then((res) => {
@@ -16,7 +17,7 @@ const webSocket = new WebSocketManager({
 
             if (raw?.content) {
                 const quotes = parseReceivePush(raw.content)
-                const result = marketService.subscribeToQuotes(quotes)
+                const result = marketData.subscribeToQuotes(quotes)
 
                 result.forEach((item) => {
                     self.postMessage({
@@ -70,25 +71,43 @@ const disposeReceiveDatas = (bytes: Uint8Array) => {
     }
 }
 
+let ws: WebSocketManager
+
 // 监听来自主线程的消息
 self.onmessage = (e) => {
     const message = e.data
 
     if (typeof message === 'object') {
         switch (message.type) {
+            case 'init': {
+                ws = new WebSocketManager({
+                    url: message.data,
+                    onMessage: (data) => {
+                        console.log(data)
+                    }
+                })
+
+                // 消息队列
+                const messageQueue = new Map()
+
+
+
+
+
+                break
+            }
             case 'connect': {
                 const { url, protocols } = message
-                webSocket.connection(url, protocols)
+                webSocket.connection()
                 break
             }
             case 'update': {
-                marketService.updateGoodsList(message.data)
+                marketData.updateGoodsList(message.data)
                 break
             }
             case 'send': {
                 const content = subscribeListToByteArrary(message.data, '2_TOKEN_NEKOT_', Long.fromNumber(2))
                 const package40 = new Package40(32, content)
-
                 webSocket.send(package40.data())
                 break
             }

+ 0 - 0
src/services/worker/market.ts → src/services/worker/quote/market.ts


+ 165 - 0
src/services/worker/service.ts

@@ -0,0 +1,165 @@
+import plus from '@/utils/h5plus'
+
+// 本地配置
+interface AppConfig {
+    appName: string; // 应用名称
+    version: string; // 应用版本
+    versionCode: string; // 应用版本号
+    apiUrl: string; // 接口地址
+    tradeChannel: 'http' | 'ws'; // 交易通道
+    showLoginAlert: boolean; // 登录是否显示弹窗
+    modules: ('register' | 'delivery')[]; // 应用包含的模块
+    quotationProperties: (keyof Model.QuoteDayRsp)[]; // 盘面可显示的属性
+    forcedPasswordChange: boolean; // 首次登录是否强制修改密码
+    registrationCodeRule: -1 | 0 | 1; // 注册编码规则,-1=隐藏,0=非必填,1=必填
+    riskType: 0 | 1 | 2; // 风控类型,1=按单风控,2=按账户风控
+    i18nEnabled: boolean; // 是否启用多语言设置
+    allCloseEnabled: boolean; // 是否启用全部平仓
+    allDeliveryEnabled: boolean; // 是否启用全部交收
+}
+
+// 服务配置
+interface ServiceConfig {
+    commSearchUrl: string;
+    goCommonSearchUrl: string;
+    androidUpdateUrl: string;
+    tradeUrl: string;
+    quoteUrl: string;
+    goAccess: string;
+    mobileOpenUrl: string;
+    openApiUrl: string;
+    uploadUrl: string;
+    quoteWS: string;
+    tradeWS: string;
+    oem: string;
+    shareUrl: string;
+}
+
+export default new (class {
+    constructor() {
+        this.appConfigAsync = this.loadAppConfig()
+
+        // 自动初始化,若断网或其它原因导致初始化失败,需重新初始化
+        // IOS上架审核可能会遇到网络权限的情况,应用可能会在未授权网络权限的情况下发起请求,导致请求等待时间过长,最终审核被拒
+        //this.init()
+    }
+
+    isReady = false
+
+    /** 限制重试次数,0 = 无限制 */
+    private retryLimit = 5
+
+    private readyPromise: Promise<void> | undefined = undefined
+
+    private readyCallbacks = new Map<symbol, () => void>()
+
+    private appConfigAsync
+
+    private appConfig: AppConfig = {
+        appName: document.title,
+        version: '1.0.0',
+        versionCode: '100000',
+        apiUrl: 'http://localhost',
+        tradeChannel: 'ws',
+        showLoginAlert: false,
+        modules: [],
+        quotationProperties: [],
+        forcedPasswordChange: false,
+        registrationCodeRule: 1,
+        riskType: 0,
+        i18nEnabled: true,
+        allCloseEnabled: false,
+        allDeliveryEnabled: false
+    }
+
+    private serviceConfig: ServiceConfig = {
+        commSearchUrl: '',
+        goCommonSearchUrl: '',
+        androidUpdateUrl: '',
+        tradeUrl: '',
+        quoteUrl: '',
+        goAccess: '',
+        mobileOpenUrl: '',
+        openApiUrl: '',
+        uploadUrl: '',
+        quoteWS: '',
+        tradeWS: '',
+        oem: '',
+        shareUrl: '',
+    }
+
+    /** 加载本地配置 */
+    private loadAppConfig() {
+        return plus.getLocalFileContent<AppConfig>('./config/appconfig.json')
+    }
+
+    /** 加载服务配置 */
+    private loadServiceConfig = (url: string, retryCount = 0) => {
+        return new Promise<void>((resolve, reject) => {
+            if (this.retryLimit === 0 || retryCount <= this.retryLimit) {
+                plus.httpRequest({ url }).then((res) => {
+                    this.serviceConfig = res.data.data
+                    this.isReady = true
+
+                    this.readyCallbacks.forEach((callback) => callback())
+                    this.readyCallbacks.clear()
+                    resolve()
+                }).catch(() => {
+                    setTimeout(() => {
+                        retryCount++
+                        this.loadServiceConfig(url, retryCount).then(resolve).catch(reject)
+                    }, 3000)
+                })
+            } else {
+                this.readyPromise = undefined
+                reject('服务加载失败,请稍后再试')
+            }
+        })
+    }
+
+    async init() {
+        if (!this.readyPromise) {
+            try {
+                await this.appConfigAsync
+            } catch {
+                this.appConfigAsync = this.loadAppConfig()
+            }
+            this.readyPromise = new Promise<void>((resolve, reject) => {
+                this.appConfigAsync.then((data) => {
+                    this.appConfig = {
+                        ...this.appConfig,
+                        ...data
+                    }
+                    this.loadServiceConfig(data.apiUrl).then(resolve).catch(reject)
+                }).catch(() => {
+                    this.readyPromise = undefined
+                    reject('配置文件加载失败,请稍后再试')
+                })
+            })
+        }
+        // 确保当前只有一个初始化实例
+        return this.readyPromise
+    }
+
+    removeReadyCallback(key: symbol) {
+        this.readyCallbacks.delete(key)
+    }
+
+    getAppConfig<K extends keyof AppConfig>(key: K) {
+        return this.appConfig[key]
+    }
+
+    getServiceConfig<K extends keyof ServiceConfig>(key: K) {
+        return this.serviceConfig[key]
+    }
+
+    onReady(callback: () => void) {
+        const key = Symbol()
+        if (this.isReady) {
+            callback()
+        } else {
+            this.readyCallbacks.set(key, callback)
+        }
+        return key
+    }
+})

+ 115 - 0
src/services/worker/trade/index.ts

@@ -0,0 +1,115 @@
+import { Package50 } from '@/services/websocket/package'
+import { FunCode } from '@/constants/funcode'
+import { encodeProto, decodeProto } from '@/services/websocket/package/package50/proto'
+import Worker from 'worker-loader!./main'
+import Service from '../service'
+
+interface RequestParams {
+  data: unknown;
+  requestCode: keyof typeof FunCode;
+  responseCode: keyof typeof FunCode;
+}
+
+export default new (class {
+  private readonly worker = new Worker()
+  private responseQueue = new Map()
+  private isTokenValid = false
+
+  constructor() {
+    Service.onReady(() => {
+      this.worker.postMessage({
+        type: 'init',
+        data: {
+          url: Service.getServiceConfig('tradeUrl')
+        }
+      })
+    })
+
+    // 接收工作线程消息
+    this.worker.onmessage = (e) => {
+      const message = e.data
+      if (typeof message === 'object') {
+        switch (message.type) {
+          case 'reconnecting': {
+            this.stopCheckToken()
+            break
+          }
+          case 'reconnected': {
+            this.checkToken()
+            break
+          }
+          case 'response': {
+            console.log(message.data)
+            const { responseCode, content, error } = message.data
+            const queue = this.responseQueue.get(responseCode)
+            if (queue) {
+              const { resolve, reject } = queue
+              error ? reject(error) : resolve(content)
+            }
+            break
+          }
+          case 'push': {
+            console.log(message.data)
+            break
+          }
+        }
+      }
+    }
+  }
+
+  request<T>({ data, requestCode, responseCode }: RequestParams) {
+    return new Promise<T>((resolve, reject) => {
+      const requestId = FunCode[requestCode]
+      const responseId = FunCode[responseCode]
+
+      encodeProto(requestCode, data).then(async (uint8Array) => {
+        const run = () => {
+          const content = new Package50(requestId, uint8Array)
+          const queueId = new Date().getTime()
+
+          content.serialNumber = queueId
+          this.responseQueue.set(queueId, { resolve, reject })
+
+          this.worker.postMessage({
+            type: 'send',
+            data: {
+              messageId: responseId,
+              content // worker 不支持包含函数对象,content.data() 函数会丢失
+            }
+          })
+        }
+
+        if (this.isTokenValid || ['TokenCheckReq', 'LoginReq'].includes(requestCode)) {
+          run()
+        } else {
+          this.checkToken().then(() => run()).catch(reject)
+        }
+      }).catch(() => {
+        reject('构建失败')
+      })
+    })
+  }
+
+  checkToken() {
+    return new Promise<void>((resolve, reject) => {
+      this.request({
+        data: {
+          LoginID: '',
+          Token: ''
+        },
+        requestCode: 'TokenCheckReq',
+        responseCode: 'TokenCheckRsp'
+      }).then(() => {
+        this.isTokenValid = true
+        resolve()
+      }).catch((error) => {
+        this.isTokenValid = false
+        reject(error)
+      })
+    })
+  }
+
+  stopCheckToken() {
+    this.isTokenValid = false
+  }
+})

+ 200 - 0
src/services/worker/trade/main.ts

@@ -0,0 +1,200 @@
+import WebSocketManager from '@/utils/websocket'
+import { Package50 } from '@/services/websocket/package'
+import { encodeProto, decodeProto } from '@/services/websocket/package/package50/proto'
+import { FunCode } from '@/constants/funcode'
+
+interface SendMessageEvent {
+    data: {
+        messageId: number; // 消息Id
+        content: {
+            data: unknown;
+            requestCode: keyof typeof FunCode;
+            responseCode: keyof typeof FunCode;
+        }; // 发送的数据内容
+    };
+    onSuccess?: (res: Package50) => void; // 成功回调
+    onFail?: (err: string) => void; // 失败回调
+}
+
+const disposeReceiveDatas = (bytes: Uint8Array) => {
+    const cache: number[] = [];
+    let cachePackageLength = 0;
+
+    for (let i = 0; i < bytes.length; i++) {
+        const byte = bytes[i];
+        cache.push(byte);
+
+        if (i === 0 && byte !== 0xff) {
+            console.error('接收到首字节不是0xFF');
+            return;
+        } else {
+            // 取报文长度
+            if (cache.length === 5) {
+                const sub = new Uint8Array(cache).subarray(1, 5);
+                const dataView = new DataView(new Uint8Array(sub).buffer); // 注意这里要new一个新的Uint8Array,直接buffer取到的还是原数组的buffer
+                cachePackageLength = dataView.getUint32(0, true);
+            }
+            // 判断是否已经到包尾
+            if (cache.length === cachePackageLength) {
+                if (byte !== 0x0) {
+                    console.error('接收到尾字节不是0x0的错误数据包');
+                    return;
+                }
+
+                const content = new Uint8Array(cache);
+                const result = new Package50(content);
+
+                if (result.packageLength === 0) {
+                    console.error('报文装箱失败');
+                    return;
+                }
+                return result;
+            }
+        }
+    }
+}
+
+let socket: WebSocketManager
+
+// 消息队列
+const messageQueue = new Map<number, SendMessageEvent>()
+// 定时器队列
+const timerQueue = new Map<number, number>()
+
+const initWebSocket = (url: string, protocols?: string | string[]) => {
+    return new WebSocketManager({
+        url,
+        protocols,
+        onMessage: (uint8Array) => {
+            // 接收数据
+            new Response(uint8Array).arrayBuffer().then((res) => {
+                const arr = new Uint8Array(res)
+                const raw = disposeReceiveDatas(arr)
+
+                if (raw) {
+                    const queue = messageQueue.get(raw.serialNumber)
+
+                    if (raw.funCode === 0) {
+                        // 接收到心跳回复
+                    } else if (raw.serialNumber === 0) {
+                        // 推送消息
+                        self.postMessage({
+                            type: 'push',
+                            data: raw
+                        })
+                    } else if (queue) {
+                        // 响应消息
+                        const { data, onSuccess } = queue
+                        const { content } = data
+                        const responseId = FunCode[content.requestCode]
+
+                        // 可能会收到多个流水号相同的数据包,需判断是否正确的回复码
+                        if (responseId === raw.funCode) {
+                            onSuccess?.(raw)
+                        } else {
+                            // 跳过当前收到的数据包,继续等待直到队列超时
+                            //console.log('无效的回复码', raw.funCode)
+                            return
+                        }
+                    }
+
+                    // 清除当前定时器
+                    const timerId = timerQueue.get(raw.serialNumber)
+                    if (timerId) {
+                        clearTimeout(timerId)
+                        timerQueue.delete(raw.serialNumber)
+                    }
+                    // 移除当前队列消息
+                    if (queue) {
+                        messageQueue.delete(raw.serialNumber)
+                    }
+                }
+            })
+        },
+        onBeforeReconnect: () => {
+            self.postMessage({ type: 'reconnecting' })
+        },
+        onReconnect: () => {
+            self.postMessage({ type: 'reconnected' })
+        }
+    })
+}
+
+const sendMessage = (e: SendMessageEvent) => {
+    const { data, onSuccess, onFail } = e
+    const { messageId, content } = data
+    const requestId = FunCode[content.requestCode]
+
+    encodeProto(content.requestCode, data).then(async (unit8) => {
+        const pkg = new Package50(requestId, unit8)
+
+        // 判断是否需要回调
+        if (onSuccess || onFail) {
+            pkg.serialNumber = messageId
+
+            const timerId = setTimeout(() => {
+                cancel() // 取消请求
+                messageQueue.delete(messageId)
+                timerQueue.delete(messageId)
+                onFail?.('请求超时')
+            }, 30 * 1000)
+
+            messageQueue.set(messageId, e)
+            timerQueue.set(messageId, timerId)
+        }
+
+        const cancel = socket.connection({
+            onSuccess: () => socket.send(pkg.data())
+        })
+    }).catch(() => {
+        onFail?.('构建失败')
+    })
+}
+
+// 监听来自主线程的消息
+self.onmessage = (e) => {
+    const message = e.data
+
+    if (typeof message === 'object') {
+        switch (message.type) {
+            case 'init': {
+                const { url, protocols } = message.data
+                socket = initWebSocket(url, protocols)
+                break
+            }
+            case 'send': {
+                sendMessage({
+                    data: message.data,
+                    onSuccess: (content) => {
+                        self.postMessage({
+                            type: 'response',
+                            data: {
+                                messageId: message.data.responseCode,
+                                content
+                            }
+                        })
+                    },
+                    onFail: (error) => {
+                        self.postMessage({
+                            type: 'response',
+                            data: {
+                                messageId: message.data.responseCode,
+                                error
+                            }
+                        })
+                    }
+                })
+                break
+            }
+            case 'close': {
+                socket.disconnect()
+                break
+            }
+        }
+    }
+}
+
+// 错误处理
+self.onerror = (error) => {
+    console.error('工作线程错误:', error)
+}

+ 0 - 177
src/services/worker/websocket.ts

@@ -1,177 +0,0 @@
-export default class {
-    private ws: WebSocket | null = null; // WebSocket 对象
-    private connectionId = 0;
-    private url = '';
-    private protocols?: string | string[];
-    private isReconnecting = false; // 是否正在重连
-    private messageTimer = 0; // 消息超时定时器
-    private messageTimeout = 1000 * 15; // 消息超时时间
-    private heartbeatTimer = 0; // 心跳定时器
-    private heartbeatInterval = 1000 * 30; // 心跳间隔时间
-    private reconnectTimer = 0; // 重连定时器
-    private reconnectCount = 0; // 本次已重连次数
-    private reconnectLimit = 10; // 限制重连次数,0 = 无限制
-
-    private heartbeatMessage?: () => string | ArrayBufferLike | Blob | ArrayBufferView; // 心跳消息
-    private onOpen?: () => void; // 连接成功的回调
-    private onMessage: (data: unknown) => void; // 消息回调
-    private onClose?: () => void; // 连接断开的回调
-    private onError?: (err: Event) => void; // 连接发生错误的回调
-    private onBeforeReconnect?: (count: number) => void; // 重连之前的回调
-    private onReconnect?: () => void; // 重连成功之后的回调
-
-    constructor(options: {
-        heartbeatMessage?: () => string | ArrayBufferLike | Blob | ArrayBufferView;
-        onOpen?: () => void;
-        onMessage: (data: unknown) => void;
-        onClose?: () => void;
-        onError?: (err: Event) => void;
-        onBeforeReconnect?: (count: number) => void;
-        onReconnect?: () => void;
-    }) {
-        this.heartbeatMessage = options.heartbeatMessage
-        this.onOpen = options.onOpen
-        this.onMessage = options.onMessage
-        this.onClose = options.onClose
-        this.onError = options.onError
-        this.onBeforeReconnect = options.onBeforeReconnect
-        this.onReconnect = options.onReconnect
-    }
-
-    // 创建 WebSocket 连接
-    connection(url: string, protocols?: string | string[]) {
-        clearTimeout(this.reconnectTimer)
-        this.stopHeartbeat()
-
-        const currentConnectionId = this.connectionId + 1
-        this.connectionId = currentConnectionId
-        this.url = url
-        this.protocols = protocols
-
-        this.ws?.close()
-        this.ws = this.protocols ? new WebSocket(this.url, this.protocols) : new WebSocket(this.url)
-        console.log(this.url, '正在连接')
-
-        // 连接成功
-        this.ws.onopen = () => {
-            if (this.connectionId === currentConnectionId) {
-                if (this.reconnectCount) {
-                    this.onReconnect?.()
-                } else {
-                    this.onOpen?.()
-                }
-
-                console.log(this.url, '连接成功')
-                this.reconnectCount = 0
-                this.startHeartbeat()
-            } else {
-                this.ws?.close()
-            }
-        }
-
-        // 接收消息
-        this.ws.onmessage = (event) => {
-            this.stopHeartbeat()
-
-            if (this.connectionId === currentConnectionId) {
-                this.startHeartbeat()
-                this.onMessage?.(event.data)
-            }
-        }
-
-        // 连接断开
-        this.ws.onclose = () => {
-            this.stopHeartbeat()
-            this.ws = null
-            this.isReconnecting = false
-
-            // 判断是否当前实例,重连时不处理旧 WebSocket 事件
-            if (this.connectionId === currentConnectionId) {
-                if (this.reconnectCount) {
-                    console.warn(this.url, `第${this.reconnectCount}次重连失败`)
-                } else {
-                    console.warn(this.url, '连接已断开')
-                }
-                this.reconnect() // 重连失败会不断尝试,直到成功为止
-            }
-        }
-
-        // 连接发生错误
-        this.ws.onerror = (error) => {
-            if (this.connectionId === currentConnectionId) {
-                console.error(this.url, '连接发生错误')
-                this.onError?.(error)
-            }
-        }
-    }
-
-    // 主动断开连接,断开后不会自动重连
-    disconnect(forced = false) {
-        return new Promise<void>((resolve) => {
-            clearTimeout(this.reconnectTimer)
-            this.reconnectCount = 0
-            this.connectionId++
-
-            if (!forced && this.ws) {
-                const listener = () => {
-                    console.warn(this.url, '主动断开')
-                    this.ws?.removeEventListener('close', listener)
-                    this.onClose?.()
-                    resolve()
-                }
-                this.ws.addEventListener('close', listener)
-                this.ws.close()
-            } else {
-                if (this.ws) {
-                    console.warn(this.url, '主动断开')
-                    this.ws.close()
-                }
-                this.onClose?.()
-                resolve()
-            }
-        })
-    }
-
-    // 发送消息
-    send(data: string | ArrayBufferLike | Blob | ArrayBufferView) {
-        this.ws?.send(data)
-    }
-
-    // 断开重连
-    private reconnect() {
-        if (!this.isReconnecting && this.reconnectCount < this.reconnectLimit) {
-            this.isReconnecting = true
-            this.reconnectCount++
-            this.onBeforeReconnect?.(this.reconnectCount)
-
-            // 自动计算每次重试的延时,重试次数越多,延时越大
-            const delay = this.reconnectCount * 3000
-            console.log(this.url, `${delay / 1000}秒后将进行第${this.reconnectCount}次重连`)
-
-            this.reconnectTimer = setTimeout(() => {
-                this.connection(this.url, this.protocols)
-            }, delay)
-        }
-    }
-
-    // 发送心跳检测
-    private startHeartbeat() {
-        const message = this.heartbeatMessage?.()
-        if (message) {
-            this.heartbeatTimer = setTimeout(() => {
-                this.send(message)
-                // 如果已经超过或心跳超时时长没有收到心跳回复,则认为网络已经异常,进行断网重连
-                this.messageTimer = setTimeout(() => {
-                    console.warn(this.url, '心跳超时')
-                    this.reconnect()
-                }, this.messageTimeout)
-            }, this.heartbeatInterval)
-        }
-    }
-
-    // 停止心跳检测
-    private stopHeartbeat() {
-        clearTimeout(this.messageTimer)
-        clearTimeout(this.heartbeatTimer)
-    }
-}

+ 2 - 0
src/stores/modules/global.ts

@@ -14,6 +14,7 @@ export interface SystemInfo {
     modules: ('register' | 'delivery')[]; // 应用包含的模块
     quotationProperties: (keyof Model.QuoteDayRsp)[]; // 盘面可显示的属性
     forcedPasswordChange: boolean; // 首次登录是否强制修改密码
+    strongPassword: boolean; // 是否使用强密码规则
     registrationCodeRule: -1 | 0 | 1; // 注册编码规则,-1=隐藏,0=非必填,1=必填
     riskType: 0 | 1 | 2; // 风控类型,1=按单风控,2=按账户风控
     i18nEnabled: boolean; // 是否启用多语言设置
@@ -42,6 +43,7 @@ export const useGlobalStore = defineStore(() => {
             modules: [],
             quotationProperties: [],
             forcedPasswordChange: false,
+            strongPassword: true,
             registrationCodeRule: 1,
             riskType: 0,
             i18nEnabled: true,

+ 239 - 0
src/utils/websocket/index.ts

@@ -0,0 +1,239 @@
+interface WebSocketOptions {
+    url: string,
+    protocols?: string | string[]
+    heartbeatMessage?: () => MessageEvent['data'];
+    onOpen?: () => void;
+    onMessage: (data: MessageEvent['data']) => void;
+    onClose?: () => void;
+    onError?: (err: Event) => void;
+    onBeforeReconnect?: (count: number) => void;
+    onReconnect?: () => void;
+}
+
+interface ConnectionOptions {
+    onSuccess?: () => void;
+    onFail?: () => void;
+}
+
+export default class {
+    private socket: WebSocket | null = null; // WebSocket 对象
+    private connectionId = 0;
+    private url;
+    private protocols?: string | string[];
+    private isReconnecting = false; // 是否正在重连
+    private messageTimer = 0; // 消息超时定时器
+    private messageTimeout = 1000 * 15; // 消息超时时间
+    private heartbeatTimer = 0; // 心跳定时器
+    private heartbeatInterval = 1000 * 30; // 心跳间隔时间
+    private reconnectTimer = 0; // 重连定时器
+    private reconnectCount = 0; // 本次已重连次数
+    private reconnectLimit = 10; // 限制重连次数,0 = 无限制
+
+    private heartbeatMessage?: () => string | ArrayBufferLike | Blob | ArrayBufferView; // 心跳消息
+    private onOpen?: () => void; // 连接成功的事件
+    private onMessage: (data: MessageEvent['data']) => void; // 消息事件
+    private onClose?: () => void; // 连接断开的事件
+    private onError?: (err: Event) => void; // 连接发生错误的事件
+    private onBeforeReconnect?: (count: number) => void; // 重连之前的事件
+    private onReconnect?: () => void; // 重连成功之后的事件
+
+    constructor(options: WebSocketOptions) {
+        this.url = options.url
+        this.protocols = options.protocols
+        this.heartbeatMessage = options.heartbeatMessage
+        this.onOpen = options.onOpen
+        this.onMessage = options.onMessage
+        this.onClose = options.onClose
+        this.onError = options.onError
+        this.onBeforeReconnect = options.onBeforeReconnect
+        this.onReconnect = options.onReconnect
+    }
+
+    private isConnecting = false
+    private connectionCallbacks = new Map<symbol, ConnectionOptions>()
+
+    // 查询连接状态
+    isConnected() {
+        return !this.isReconnecting && this.socket?.readyState === WebSocket.OPEN
+    }
+
+    // 创建 WebSocket 连接
+    connection(options?: ConnectionOptions) {
+        const key = Symbol()
+
+        if (this.isConnected()) {
+            options?.onSuccess?.()
+        } else {
+            if (options) {
+                this.connectionCallbacks.set(key, options)
+            }
+
+            if (!this.isConnecting) {
+                clearTimeout(this.reconnectTimer)
+                this.stopHeartbeat()
+                this.socket?.close()
+
+                const ws = this.protocols ? new WebSocket(this.url, this.protocols) : new WebSocket(this.url)
+                const currentConnectionId = this.connectionId + 1
+
+                this.connectionId = currentConnectionId
+                this.isConnecting = true
+                console.log(this.url, '正在连接')
+
+                // 连接成功
+                ws.onopen = () => {
+                    if (this.connectionId === currentConnectionId) {
+                        if (this.reconnectCount) {
+                            console.log(this.url, '重连成功')
+                            this.onReconnect?.()
+                        } else {
+                            console.log(this.url, '连接成功')
+                            this.onOpen?.()
+                        }
+
+                        this.reconnectCount = 0
+                        this.isConnecting = false
+                        this.startHeartbeat()
+
+                        this.connectionCallbacks.forEach(({ onSuccess }) => onSuccess?.())
+                        this.connectionCallbacks.clear()
+                    } else {
+                        console.log(this.url, '重复连接')
+                        ws.close()
+                    }
+                }
+
+                // 接收消息
+                ws.onmessage = (event) => {
+                    if (this.connectionId === currentConnectionId) {
+                        this.stopHeartbeat()
+                        this.startHeartbeat()
+                        this.onMessage?.(event.data)
+                    }
+                }
+
+                // 连接断开
+                ws.onclose = () => {
+                    // 判断是否当前实例,重连时不处理旧 WebSocket 事件
+                    if (this.connectionId === currentConnectionId) {
+                        this.stopHeartbeat()
+                        this.socket = null
+                        this.isReconnecting = false
+                        this.isConnecting = false
+                        this.reconnect() // 进行重连尝试,直到成功为止
+
+                        // 重连失败返回结果
+                        if (this.reconnectCount === 0) {
+                            console.error(this.url, '连接已断开')
+                            this.connectionCallbacks.forEach(({ onFail }) => onFail?.())
+                            this.connectionCallbacks.clear()
+                        }
+                    }
+                }
+
+                // 连接发生错误
+                ws.onerror = (error) => {
+                    if (this.connectionId === currentConnectionId) {
+                        console.error(this.url, '连接发生错误')
+                        this.onError?.(error)
+                    }
+                }
+
+                this.socket = ws
+            }
+        }
+
+        return () => this.connectionCallbacks.delete(key)
+    }
+
+    // 主动断开连接,断开后不会自动重连
+    disconnect(forced = false) {
+        return new Promise<void>((resolve) => {
+            clearTimeout(this.reconnectTimer)
+            this.stopHeartbeat()
+            this.connectionCallbacks.clear()
+            this.connectionId++
+            this.reconnectCount = 0
+            this.isReconnecting = false
+            this.isConnecting = false
+
+            if (!forced && this.socket) {
+                const listener = () => {
+                    console.warn(this.url, '主动断开')
+                    this.socket?.removeEventListener('close', listener)
+                    this.socket = null
+                    this.onClose?.()
+                    resolve()
+                }
+                this.socket.addEventListener('close', listener)
+                this.socket.close()
+            } else {
+                console.warn(this.url, '主动断开')
+                this.socket?.close()
+                this.socket = null
+                this.onClose?.()
+                resolve()
+            }
+        })
+    }
+
+    // 发送消息
+    send(data: string | ArrayBufferLike | Blob | ArrayBufferView) {
+        if (this.socket) {
+            this.socket.send(data)
+            return true
+        }
+        return false
+    }
+
+    // 断开重连
+    private reconnect() {
+        if (!this.isReconnecting) {
+            if (this.reconnectCount) {
+                console.warn(this.url, `第${this.reconnectCount}次重连失败`)
+            } else {
+                console.warn(this.url, '连接中断')
+            }
+
+            // 重连次数小于限制次数则继续重连
+            if (this.reconnectCount < this.reconnectLimit) {
+                this.isReconnecting = true
+                this.reconnectCount++
+                this.onBeforeReconnect?.(this.reconnectCount)
+
+                // 自动计算每次重试的延时,重试次数越多,延时越大
+                const delay = this.reconnectCount * 5000
+                console.log(this.url, `${delay / 1000}秒后将进行第${this.reconnectCount}次重连`)
+
+                this.reconnectTimer = setTimeout(() => {
+                    this.connection()
+                }, delay)
+            } else {
+                this.reconnectCount = 0
+            }
+        }
+    }
+
+    // 发送心跳检测
+    // 待优化:心跳超时后,再发送一次心跳消息,如果仍然超时,再进行重连
+    private startHeartbeat() {
+        const message = this.heartbeatMessage?.()
+        if (message) {
+            this.heartbeatTimer = setTimeout(() => {
+                this.send(message)
+
+                // 如果已经超过或心跳超时时长没有收到心跳回复,则认为网络已经异常,进行断网重连
+                this.messageTimer = setTimeout(() => {
+                    console.warn(this.url, '心跳超时')
+                    this.reconnect()
+                }, this.messageTimeout)
+            }, this.heartbeatInterval)
+        }
+    }
+
+    // 停止心跳检测
+    private stopHeartbeat() {
+        clearTimeout(this.messageTimer)
+        clearTimeout(this.heartbeatTimer)
+    }
+}