li.shaoyi 3 سال پیش
والد
کامیت
488d00a9ee

+ 44 - 42
src/business/common/index.ts

@@ -2,60 +2,47 @@ import { shallowRef } from 'vue'
 import { timerTask } from '@/utils/timer'
 import { queryTableDefine } from '@/services/api/common'
 import { tokenCheck } from '@/services/api/account'
-import { sessionData, commonStore, futuresStore, resetStore, accountStore } from '@/stores'
-import service from '@/services'
+import { commonStore, accountStore, futuresStore, sessionData } from '@/stores'
 import eventBus from '@/services/bus'
 import socket from '@/services/socket'
 
-export const business = new (class {
-    logoutNotify
-    moneyChangedNotify
-
-    constructor() {
-        // 接收用户登出通知
-        this.logoutNotify = eventBus.$on('LogoutNotify', () => {
-            socket.closeQuoteServer()
-            socket.closeTradeServer()
-            timerTask.clearAll()
-            resetStore()
-        })
-        // 接收资金变动通知
-        this.moneyChangedNotify = eventBus.$on('MoneyChangedNotify', () => {
-            accountStore.getAccountList()
-        })
-    }
-})
+/**
+ * 退出登录
+ */
+export function logout(callback?: () => void) {
+    socket.closeAll()
+    timerTask.clearAll()
+    sessionData.reset()
+    accountStore.reset()
+    callback && callback()
+}
 
 /**
  * 初始化业务数据
  * @param callback 
  */
 export async function initBaseData(callback?: () => void) {
-    await service.onReady(async () => {
-        if (sessionData.getLoginInfo('Token')) {
-            // 连接交易服务
-            socket.connectTrade()
-            await checkToken()
+    if (sessionData.getLoginInfo('Token')) {
+        checkTokenLoop()
 
-            const asyncTask = [
-                commonStore.getLoginData(),
-                futuresStore.getGoodsList(),
-            ]
+        const asyncTask = [
+            commonStore.getLoginData(),
+            futuresStore.getGoodsList(),
+        ]
 
-            await Promise.all(asyncTask).then(() => {
-                accountStore.getAccountList()
-                callback && callback()
-            }).catch(() => {
-                return Promise.reject('初始化失败')
-            })
-        } else {
+        await Promise.all(asyncTask).then(() => {
+            accountStore.getAccountList()
             callback && callback()
-        }
-    })
+        }).catch(() => {
+            return Promise.reject('初始化失败')
+        })
+    } else {
+        callback && callback()
+    }
 }
 
 /**
- * Token 效验
+ * 令牌效验
  */
 export function checkToken() {
     const { LoginID, Token } = sessionData.getValue('loginInfo')
@@ -64,13 +51,28 @@ export function checkToken() {
             LoginID,
             Token,
         },
-        fail: () => {
-            // 失败应该退到登录页面
-        }
+        fail: () => eventBus.$emit('LogoutNotify')
     })
 }
 
 /**
+ * 轮询效验令牌
+ */
+export function checkTokenLoop() {
+    const delay = 1 * 60 * 1000 // 每1分钟效验一次令牌
+    timerTask.setTimeout(() => {
+        checkToken().then(() => checkTokenLoop())
+    }, delay, 'checkToken')
+}
+
+/**
+ * 停止令牌效验
+ */
+export function stopCheckToken() {
+    timerTask.clearTimeout('checkToken')
+}
+
+/**
  * 获取表头列数据
  * @param tableName 
  */

+ 0 - 1
src/business/sign/index.ts

@@ -41,7 +41,6 @@ export function useSign() {
                         success: (res) => {
                             sessionData.setValue('loginInfo', res);
                             initBaseData(() => {
-                                loading.value = false;
                                 resolve(res);
                             }).catch((err) => {
                                 loading.value = false;

+ 1 - 1
src/packages/mobile/views/boot/index.vue

@@ -45,7 +45,7 @@ const initService = () => {
   state.isError = false;
   // 模拟等待初始化
   window.setTimeout(() => {
-    service.onReady(() => state.loading = false).catch(() => state.isError = true)
+    service.onReady().then(() => state.loading = false).catch(() => state.isError = true)
   }, 1000);
 }
 

+ 16 - 0
src/packages/pc/App.vue

@@ -12,5 +12,21 @@ export default {
 </script>-->
 
 <script lang="ts" setup>
+import { useRouter } from 'vue-router'
+import { ElMessageBox } from 'element-plus'
+import { logout } from '@/business/common'
 import zhCn from 'element-plus/lib/locale/lang/zh-cn'
+import eventBus from '@/services/bus'
+
+const router = useRouter()
+
+// 接收用户登出通知
+eventBus.$on('LogoutNotify', (msg) => {
+  logout(() => {
+    if (msg) {
+      ElMessageBox.alert(msg as string)
+    }
+    router.replace({ name: 'login' })
+  })
+})
 </script>

+ 2 - 5
src/packages/pc/components/layouts/sidebar/index.less

@@ -57,6 +57,8 @@
     }
 
     &__menu {
+        --el-menu-sub-item-height: 32px;
+
         .el-menu {
             border          : 0;
             background-color: transparent;
@@ -113,11 +115,6 @@
                 line-height: 48px;
             }
 
-            .el-menu-item {
-                height     : 32px;
-                line-height: 32px;
-            }
-
             .el-sub-menu {
                 .menu-icon {
                     font-size: inherit;

+ 3 - 3
src/packages/pc/components/layouts/sidebar/index.vue

@@ -32,16 +32,16 @@ const hideSidebar = () => {
 }
 
 // 菜单跳转
-const routerTo = (routeName: string) => {
+const routerTo = (active: string) => {
   if (state.isMobile) {
     hideSidebar()
   }
-  const submenus = getChildrenMenus(routeName)
+  const submenus = getChildrenMenus(active)
   // 判断是否存在子菜单
   if (submenus.length) {
     router.push({ name: submenus[0].name })
   } else {
-    router.push({ name: routeName })
+    router.push({ name: active })
   }
 }
 

+ 1 - 5
src/packages/pc/components/layouts/sidemenu/index.vue

@@ -15,10 +15,6 @@ const menus = getMenus(level) // 如果是无限级菜单,activeMenu 应该直
 
 // 高亮菜单
 const activeMenu = computed(() => {
-    const flag = menus.some((e) => route.matched.some((m) => m.name === e.name))
-    if (flag) {
-        return route.matched[level - 1].name
-    }
-    return ''
+    return route.matched[level - 1]?.name ?? route.name
 })
 </script>

+ 46 - 20
src/packages/pc/views/boot/index.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="boot">
-    <el-button :loading="loading" v-if="loading">正在烧烤...</el-button>
-    <el-button @click="initService" v-else>重新烧烤</el-button>
+    <el-button :loading="loading" v-if="loading">正在初始化...</el-button>
+    <el-button @click="initService" v-else>重</el-button>
   </div>
 </template>
 
@@ -9,25 +9,51 @@
 import { ref } from 'vue'
 import { ElMessage } from 'element-plus'
 import { useRoute, useRouter } from 'vue-router'
-import { initBaseData } from '@/business/common'
-
-const route = useRoute(),
-  router = useRouter(),
-  loading = ref(false);
-
-const initService = () => {
-  loading.value = true;
-  initBaseData(() => {
-    const redirect = route.query.redirect;
-    if (redirect) {
-      router.replace(redirect.toString());
-    } else {
-      router.replace('/');
-    }
-  }).catch((err) => {
-    ElMessage.error(err);
-    loading.value = false;
+import { initBaseData, checkToken } from '@/business/common'
+import { sessionData } from '@/stores'
+import service from '@/services'
+import eventBus from '@/services/bus'
+import socket from '@/services/socket'
+
+const route = useRoute()
+const router = useRouter()
+const loading = ref(false)
+
+const initService = async () => {
+  loading.value = true
+
+  // 等待服务初始化
+  await service.onReady().catch((err) => {
+    ElMessage.error(err)
+    loading.value = false
   })
+
+  if (sessionData.getLoginInfo('Token')) {
+    // 等待连接交易服务
+    await socket.connectTrade().catch((err) => {
+      ElMessage.error(err)
+      loading.value = false
+    })
+
+    // 等待令牌效验
+    await checkToken().catch(() => {
+      eventBus.$emit('LogoutNotify')
+    })
+
+    // 等待业务数据初始化
+    await initBaseData().catch((err) => {
+      ElMessage.error(err)
+      loading.value = false
+    })
+  }
+
+  // 路由跳转
+  const redirect = route.query.redirect
+  if (redirect) {
+    router.replace(redirect.toString())
+  } else {
+    router.replace('/')
+  }
 }
 
 initService()

+ 3 - 8
src/packages/pc/views/sign/login/index.vue

@@ -12,14 +12,9 @@
         <el-checkbox label="记住账号"></el-checkbox>
       </el-form-item>
       <el-form-item>
-        <el-button class="submit" type="primary" :disabled="loading" @click="userlogin">
-          <template v-if="loading">
-            <i class="el-icon-loading"></i>
-            <span>正在登录</span>
-          </template>
-          <template v-else>
-            <span>登录</span>
-          </template>
+        <el-button class="submit" type="primary" :loading="loading" @click="userlogin">
+          <span v-if="loading">正在登录</span>
+          <span v-else>登录</span>
         </el-button>
         <!-- <el-button>注册</el-button> -->
       </el-form-item>

+ 4 - 5
src/services/index.ts

@@ -45,7 +45,7 @@ export default new (class {
     /**
      * 初始化服务配置
      */
-    private init(): Promise<void> {
+    private init(): Promise<typeof this.config> {
         this.isPending = true;
         return new Promise((resolve, reject) => {
             this.appConfig.then((res) => {
@@ -56,7 +56,7 @@ export default new (class {
                         console.log('服务配置信息', res.data);
                         this.config = res.data as typeof this.config;
                         this.isReady = true;
-                        resolve();
+                        resolve(this.config);
                     },
                     fail: () => {
                         reject('获取服务配置地址失败');
@@ -74,14 +74,13 @@ export default new (class {
 
     /**
      * 服务初始化完成时触发
-     * @param callback 
      */
-    async onReady(callback?: () => void) {
+    onReady() {
         // 初始化失败时重新初始化
         if (!this.isReady && !this.isPending) {
             this.tryInit = this.init();
         }
         // 确保当前只有一个初始化实例
-        await this.tryInit.then(() => callback && callback());
+        return this.tryInit;
     }
 })

+ 40 - 21
src/services/socket/index.ts

@@ -4,6 +4,8 @@ import { Package40, Package50 } from '@/utils/websocket/package'
 import { timerInterceptor } from '@/utils/timer'
 import { FunCode } from '@/constants/enum/funcode'
 import { parseReceivePush } from './quote/build/decode'
+import protobuf from './trade/protobuf'
+import { checkToken, stopCheckToken, checkTokenLoop } from '@/business/common'
 import service from '@/services'
 import eventBus from '@/services/bus'
 
@@ -31,7 +33,8 @@ export default new (class {
             eventBus.$emit('QuoteServerReconnectNotify');
         }
 
-        this.tradeServer.onPush = ({ funCode }) => {
+        this.tradeServer.onPush = (p) => {
+            const { funCode, content } = p;
             const delay = 1000; // 延迟推送消息,防止短时间内重复请求
 
             switch (funCode) {
@@ -43,8 +46,10 @@ export default new (class {
                     break;
                 }
                 case FunCode.LogoutRsp: {
-                    // 通知上层 账号登出
-                    eventBus.$emit('LogoutNotify');
+                    // 通知上层 用户登出
+                    protobuf.responseDecode<Proto.LogoutRsp>(FunCode[funCode], content).then(({ RetCode, RetDesc }) => {
+                        eventBus.$emit('LogoutNotify', (RetDesc || RetCode)?.toString());
+                    })
                     break;
                 }
                 default: {
@@ -54,36 +59,44 @@ export default new (class {
                 }
             }
         }
+
+        this.tradeServer.onBeforeReconnect = () => {
+            // 停止令牌效验
+            stopCheckToken();
+        }
+
+        this.tradeServer.onReconnect = () => {
+            // 重新进行令牌效验
+            checkToken().then(() => checkTokenLoop());
+        }
     }
 
     /** 主动连接行情服务 */
-    connectQuote(callback?: () => void) {
-        service.onReady(() => {
-            const { quoteUrl } = service.config;
-            this.quoteServer.connect(quoteUrl).then(() => {
-                callback && callback();
-            })
-        })
+    async connectQuote() {
+        const res = await service.onReady();
+        await this.quoteServer.connect(res.quoteUrl);
     }
 
     /** 主动连接交易服务 */
-    connectTrade(callback?: () => void) {
-        service.onReady(() => {
-            const { tradeUrl } = service.config;
-            this.tradeServer.connect(tradeUrl).then(() => {
-                callback && callback();
-            })
-        })
+    async connectTrade() {
+        const res = await service.onReady();
+        await this.tradeServer.connect(res.tradeUrl);
     }
 
     /** 向行情服务器发送请求 */
-    sendQuoteServer(msg: SendMessage<Package40>) {
-        this.connectQuote(() => this.quoteServer.send(msg));
+    async sendQuoteServer(msg: SendMessage<Package40>) {
+        await this.connectQuote().catch((err) => {
+            msg.fail && msg.fail(err);
+        })
+        return this.quoteServer.send(msg);
     }
 
     /** 向交易服务器发送请求 */
-    sendTradeServer(msg: SendMessage<Package50>) {
-        this.connectTrade(() => this.tradeServer.send(msg));
+    async sendTradeServer(msg: SendMessage<Package50>) {
+        await this.connectTrade().catch((err) => {
+            msg.fail && msg.fail(err);
+        })
+        return this.tradeServer.send(msg);
     }
 
     /** 主动关闭行情服务 */
@@ -95,4 +108,10 @@ export default new (class {
     closeTradeServer() {
         this.tradeServer.close();
     }
+
+    /** 关闭所有服务 */
+    closeAll() {
+        this.closeQuoteServer();
+        this.closeTradeServer();
+    }
 })

+ 8 - 188
src/services/subscribe/index.ts

@@ -1,206 +1,24 @@
 import { v4 } from 'uuid'
 import { quoteServerRequest } from '@/services/socket/quote'
-import { futuresStore } from '@/stores'
-import { timerInterceptor } from '@/utils/timer'
+import { sessionData } from '@/stores'
 import eventBus from '@/services/bus'
 import socket from '@/services/socket'
-import moment from 'moment'
 
 /**
- * 订阅通知
+ * 订阅行情
  */
 export default new (class {
     /** 行情订阅列表 */
     private quoteSubscribeMap = new Map<string, string[]>()
-    private quotes: Proto.Quote[] = []
-
-    quotePushNotify
-    quoteServerReconnectNotify
 
     constructor() {
-        // 接收行情推送通知
-        this.quotePushNotify = eventBus.$on('QuotePushNotify', (res) => {
-            const data = res as Proto.Quote[]
-            data.forEach((item) => {
-                const index = this.quotes.findIndex((e) => e.goodscode === item.goodscode)
-                if (index > -1) {
-                    this.quotes[index] = item
-                } else {
-                    this.quotes.push(item)
-                }
-            })
-            this.handleQuote()
-        })
-
         // 接收行情服务断线重连成功通知
-        this.quoteServerReconnectNotify = eventBus.$on('QuoteServerReconnectNotify', () => {
-            this.quoteSubscribe()
+        eventBus.$on('QuoteServerReconnectNotify', () => {
+            this.quoteSubscribe();
         })
     }
 
     /**
-     * 处理行情数据
-     */
-    private handleQuote = timerInterceptor.setThrottle(() => {
-        const quoteList = futuresStore.quoteDayList.value
-        this.quotes.forEach((item) => {
-            const quote = quoteList.find((e) => e.goodscode.toUpperCase() === item.goodscode?.toUpperCase())
-            const last = item.last ?? 0
-            const lasttime = (item.date && item.time) ? moment(item.date + item.time, 'YYYYMMDDHHmmss').format('YYYY-MM-DD HH:mm:ss') : ''
-
-            if (quote) {
-                Object.entries(item).forEach(([key, value]) => {
-                    // 只更新存在的属性
-                    if (Reflect.has(quote, key)) {
-                        const prop = key as keyof Ermcp.QuoteDay;
-                        (<K extends typeof prop>(key: K) => quote[key] = value)(prop);
-                    }
-                })
-                // 处理最高最低价
-                if (last) {
-                    if (last > quote.highest) {
-                        quote.highest = last;
-                    }
-                    if (last < quote.lowest) {
-                        quote.lowest = last;
-                    }
-                }
-                // 处理最新时间
-                if (lasttime) {
-                    quote.lasttime = lasttime;
-                }
-            } else {
-                console.warn('行情推送的商品 ' + item.goodscode + ' 缺少盘面信息')
-                quoteList.push({
-                    Lastturnover: 0,
-                    ask: item.ask ?? 0,
-                    ask2: item.ask2 ?? 0,
-                    ask3: item.ask3 ?? 0,
-                    ask4: item.ask4 ?? 0,
-                    ask5: item.ask5 ?? 0,
-                    ask6: 0,
-                    ask7: 0,
-                    ask8: 0,
-                    ask9: 0,
-                    ask10: 0,
-                    askorderid: 0,
-                    askorderid2: 0,
-                    askorderid3: 0,
-                    askorderid4: 0,
-                    askorderid5: 0,
-                    askordervolume: 0,
-                    askordervolume2: 0,
-                    askordervolume3: 0,
-                    askordervolume4: 0,
-                    askordervolume5: 0,
-                    askordervolume6: 0,
-                    askordervolume7: 0,
-                    askordervolume8: 0,
-                    askordervolume9: 0,
-                    askordervolume10: 0,
-                    askqueueinfo: "",
-                    askvolume: item.askvolume ?? 0,
-                    askvolume2: item.askvolume2 ?? 0,
-                    askvolume3: item.askvolume3 ?? 0,
-                    askvolume4: item.askvolume4 ?? 0,
-                    askvolume5: item.askvolume5 ?? 0,
-                    askvolume6: 0,
-                    askvolume7: 0,
-                    askvolume8: 0,
-                    askvolume9: 0,
-                    askvolume10: 0,
-                    averageprice: 0,
-                    bid: item.bid ?? 0,
-                    bid2: item.bid2 ?? 0,
-                    bid3: item.bid3 ?? 0,
-                    bid4: item.bid4 ?? 0,
-                    bid5: item.bid5 ?? 0,
-                    bid6: 0,
-                    bid7: 0,
-                    bid8: 0,
-                    bid9: 0,
-                    bid10: 0,
-                    bidorderid: 0,
-                    bidorderid2: 0,
-                    bidorderid3: 0,
-                    bidorderid4: 0,
-                    bidorderid5: 0,
-                    bidordervolume: 0,
-                    bidordervolume2: 0,
-                    bidordervolume3: 0,
-                    bidordervolume4: 0,
-                    bidordervolume5: 0,
-                    bidordervolume6: 0,
-                    bidordervolume7: 0,
-                    bidordervolume8: 0,
-                    bidordervolume9: 0,
-                    bidordervolume10: 0,
-                    bidqueueinfo: "",
-                    bidvolume: item.bidvolume ?? 0,
-                    bidvolume2: item.bidvolume2 ?? 0,
-                    bidvolume3: item.bidvolume3 ?? 0,
-                    bidvolume4: item.bidvolume4 ?? 0,
-                    bidvolume5: item.bidvolume5 ?? 0,
-                    bidvolume6: 0,
-                    bidvolume7: 0,
-                    bidvolume8: 0,
-                    bidvolume9: 0,
-                    bidvolume10: 0,
-                    calloptionpremiums: item.calloptionpremiums ?? 0,
-                    calloptionpremiums2: item.calloptionpremiums2 ?? 0,
-                    calloptionpremiums3: item.calloptionpremiums3 ?? 0,
-                    calloptionpremiums4: item.calloptionpremiums4 ?? 0,
-                    calloptionpremiums5: item.calloptionpremiums5 ?? 0,
-                    cleartime: 0,
-                    exchangecode: item.exchangecode ?? 0,
-                    exchangedate: item.exchangedate ?? 0,
-                    goodscode: item.goodscode ?? '',
-                    grepmarketprice: 0,
-                    highest: item.highest ?? 0,
-                    holdincrement: 0,
-                    holdvolume: item.holdvolume ?? 0,
-                    iep: 0,
-                    iev: 0,
-                    inventory: item.inventory ?? 0,
-                    iscleared: 0,
-                    issettled: 0,
-                    last,
-                    lastlot: 0,
-                    lasttime,
-                    lastvolume: item.lastvolume ?? 0,
-                    limitdown: item.limitlow ?? 0,
-                    limitup: item.limithigh ?? 0,
-                    lowest: item.lowest ?? 0,
-                    nontotalholdervolume: 0,
-                    nontotallot: 0,
-                    nontotalturnover: 0,
-                    nontotalvolume: 0,
-                    opened: item.opened ?? 0,
-                    opentime: '',
-                    orderid: 0,
-                    preclose: item.preclose ?? 0,
-                    preholdvolume: item.preholdvolume ?? 0,
-                    presettle: item.presettle ?? 0,
-                    publictradetype: '',
-                    putoptionpremiums: item.putoptionpremiums ?? 0,
-                    putoptionpremiums2: item.putoptionpremiums2 ?? 0,
-                    putoptionpremiums3: item.putoptionpremiums3 ?? 0,
-                    putoptionpremiums4: item.putoptionpremiums4 ?? 0,
-                    putoptionpremiums5: item.putoptionpremiums5 ?? 0,
-                    settle: item.settle ?? 0,
-                    strikeprice: 0,
-                    totalaskvolume: 0,
-                    totalbidvolume: 0,
-                    totallot: 0,
-                    totalturnover: item.totalturnover ?? 0,
-                    totalvolume: item.totalvolume ?? 0,
-                    utclasttime: ''
-                })
-            }
-        })
-    }, 200)
-
-    /**
      * 开始行情订阅
      */
     private quoteSubscribe = () => {
@@ -260,7 +78,9 @@ export default new (class {
                 if (flag) {
                     console.log('删除订阅', uuid)
                 }
-                this.quoteSubscribe()
+                if (sessionData.getLoginInfo('Token')) {
+                    this.quoteSubscribe()
+                }
                 return flag
             },
         }
@@ -278,7 +98,7 @@ export default new (class {
                 }
             })
         } else {
-            console.log('清空订阅', keys)
+            console.log('取消订阅')
             this.quoteSubscribeMap.clear()
         }
         this.quoteSubscribe()

+ 183 - 0
src/stores/modules/futures.ts

@@ -1,11 +1,16 @@
 import { ref, computed } from 'vue'
+import { timerInterceptor } from '@/utils/timer'
 import { queryGoodsList } from '@/services/api/goods'
 import { sessionData } from './storage'
+import moment from 'moment'
+import eventBus from '@/services/bus'
 
 /**
  * 商品存储类
  */
 export default new (class {
+    private quotes: Proto.Quote[] = [] // 行情数据
+
     loading = ref(false)
     goodsList = ref<Ermcp.GoodsRsp[]>([]) // 商品列表
     quoteDayList = ref<Ermcp.QuoteDay[]>([]) // 盘面列表
@@ -42,6 +47,184 @@ export default new (class {
         }, [] as Store.Quotation[])
     })
 
+    constructor() {
+        // 接收行情推送通知
+        eventBus.$on('QuotePushNotify', (res) => {
+            const data = res as Proto.Quote[]
+            data.forEach((item) => {
+                const index = this.quotes.findIndex((e) => e.goodscode === item.goodscode)
+                if (index > -1) {
+                    this.quotes[index] = item
+                } else {
+                    this.quotes.push(item)
+                }
+            })
+            this.handleQuote()
+        })
+    }
+
+    /**
+     * 处理行情数据
+     */
+    private handleQuote = timerInterceptor.setThrottle(() => {
+        const quoteList = this.quoteDayList.value
+        this.quotes.forEach((item) => {
+            const quote = quoteList.find((e) => e.goodscode.toUpperCase() === item.goodscode?.toUpperCase())
+            const last = item.last ?? 0
+            const lasttime = (item.date && item.time) ? moment(item.date + item.time, 'YYYYMMDDHHmmss').format('YYYY-MM-DD HH:mm:ss') : ''
+
+            if (quote) {
+                Object.entries(item).forEach(([key, value]) => {
+                    // 只更新存在的属性
+                    if (Reflect.has(quote, key)) {
+                        const prop = key as keyof Ermcp.QuoteDay;
+                        (<K extends typeof prop>(key: K) => quote[key] = value)(prop);
+                    }
+                })
+                // 处理最高最低价
+                if (last) {
+                    if (last > quote.highest) {
+                        quote.highest = last;
+                    }
+                    if (last < quote.lowest) {
+                        quote.lowest = last;
+                    }
+                }
+                // 处理最新时间
+                if (lasttime) {
+                    quote.lasttime = lasttime;
+                }
+            } else {
+                console.warn('行情推送的商品 ' + item.goodscode + ' 缺少盘面信息')
+                quoteList.push({
+                    Lastturnover: 0,
+                    ask: item.ask ?? 0,
+                    ask2: item.ask2 ?? 0,
+                    ask3: item.ask3 ?? 0,
+                    ask4: item.ask4 ?? 0,
+                    ask5: item.ask5 ?? 0,
+                    ask6: 0,
+                    ask7: 0,
+                    ask8: 0,
+                    ask9: 0,
+                    ask10: 0,
+                    askorderid: 0,
+                    askorderid2: 0,
+                    askorderid3: 0,
+                    askorderid4: 0,
+                    askorderid5: 0,
+                    askordervolume: 0,
+                    askordervolume2: 0,
+                    askordervolume3: 0,
+                    askordervolume4: 0,
+                    askordervolume5: 0,
+                    askordervolume6: 0,
+                    askordervolume7: 0,
+                    askordervolume8: 0,
+                    askordervolume9: 0,
+                    askordervolume10: 0,
+                    askqueueinfo: "",
+                    askvolume: item.askvolume ?? 0,
+                    askvolume2: item.askvolume2 ?? 0,
+                    askvolume3: item.askvolume3 ?? 0,
+                    askvolume4: item.askvolume4 ?? 0,
+                    askvolume5: item.askvolume5 ?? 0,
+                    askvolume6: 0,
+                    askvolume7: 0,
+                    askvolume8: 0,
+                    askvolume9: 0,
+                    askvolume10: 0,
+                    averageprice: 0,
+                    bid: item.bid ?? 0,
+                    bid2: item.bid2 ?? 0,
+                    bid3: item.bid3 ?? 0,
+                    bid4: item.bid4 ?? 0,
+                    bid5: item.bid5 ?? 0,
+                    bid6: 0,
+                    bid7: 0,
+                    bid8: 0,
+                    bid9: 0,
+                    bid10: 0,
+                    bidorderid: 0,
+                    bidorderid2: 0,
+                    bidorderid3: 0,
+                    bidorderid4: 0,
+                    bidorderid5: 0,
+                    bidordervolume: 0,
+                    bidordervolume2: 0,
+                    bidordervolume3: 0,
+                    bidordervolume4: 0,
+                    bidordervolume5: 0,
+                    bidordervolume6: 0,
+                    bidordervolume7: 0,
+                    bidordervolume8: 0,
+                    bidordervolume9: 0,
+                    bidordervolume10: 0,
+                    bidqueueinfo: "",
+                    bidvolume: item.bidvolume ?? 0,
+                    bidvolume2: item.bidvolume2 ?? 0,
+                    bidvolume3: item.bidvolume3 ?? 0,
+                    bidvolume4: item.bidvolume4 ?? 0,
+                    bidvolume5: item.bidvolume5 ?? 0,
+                    bidvolume6: 0,
+                    bidvolume7: 0,
+                    bidvolume8: 0,
+                    bidvolume9: 0,
+                    bidvolume10: 0,
+                    calloptionpremiums: item.calloptionpremiums ?? 0,
+                    calloptionpremiums2: item.calloptionpremiums2 ?? 0,
+                    calloptionpremiums3: item.calloptionpremiums3 ?? 0,
+                    calloptionpremiums4: item.calloptionpremiums4 ?? 0,
+                    calloptionpremiums5: item.calloptionpremiums5 ?? 0,
+                    cleartime: 0,
+                    exchangecode: item.exchangecode ?? 0,
+                    exchangedate: item.exchangedate ?? 0,
+                    goodscode: item.goodscode ?? '',
+                    grepmarketprice: 0,
+                    highest: item.highest ?? 0,
+                    holdincrement: 0,
+                    holdvolume: item.holdvolume ?? 0,
+                    iep: 0,
+                    iev: 0,
+                    inventory: item.inventory ?? 0,
+                    iscleared: 0,
+                    issettled: 0,
+                    last,
+                    lastlot: 0,
+                    lasttime,
+                    lastvolume: item.lastvolume ?? 0,
+                    limitdown: item.limitlow ?? 0,
+                    limitup: item.limithigh ?? 0,
+                    lowest: item.lowest ?? 0,
+                    nontotalholdervolume: 0,
+                    nontotallot: 0,
+                    nontotalturnover: 0,
+                    nontotalvolume: 0,
+                    opened: item.opened ?? 0,
+                    opentime: '',
+                    orderid: 0,
+                    preclose: item.preclose ?? 0,
+                    preholdvolume: item.preholdvolume ?? 0,
+                    presettle: item.presettle ?? 0,
+                    publictradetype: '',
+                    putoptionpremiums: item.putoptionpremiums ?? 0,
+                    putoptionpremiums2: item.putoptionpremiums2 ?? 0,
+                    putoptionpremiums3: item.putoptionpremiums3 ?? 0,
+                    putoptionpremiums4: item.putoptionpremiums4 ?? 0,
+                    putoptionpremiums5: item.putoptionpremiums5 ?? 0,
+                    settle: item.settle ?? 0,
+                    strikeprice: 0,
+                    totalaskvolume: 0,
+                    totalbidvolume: 0,
+                    totallot: 0,
+                    totalturnover: item.totalturnover ?? 0,
+                    totalvolume: item.totalvolume ?? 0,
+                    utclasttime: ''
+                })
+            }
+        })
+    }, 200)
+
     /**
      * 获取商品列表
      */

+ 14 - 0
src/stores/modules/storage.ts

@@ -72,6 +72,13 @@ export const localData = new (class extends WebStorage<Store.GlobalStorage>{
         document.documentElement.setAttribute('theme', theme)
         this.setValue('appTheme', theme)
     }
+
+    /**
+     * 重置数据
+     */
+    reset = () => {
+        this.clear('loginInfo', 'userMenus')
+    }
 })
 
 /**
@@ -123,4 +130,11 @@ export const sessionData = new (class extends WebStorage<Store.GlobalStorage>{
             }
         })
     }
+
+    /**
+     * 重置数据
+     */
+    reset = () => {
+        this.clear('loginInfo', 'userMenus')
+    }
 })

+ 7 - 0
src/types/proto/account.d.ts

@@ -66,5 +66,12 @@ declare global {
             AccountIDs: number; // 账户ID列表(有权限的)
             SystemTime: number; // 返回服务器最新时间
         }
+
+        /** 用户登出应答 */
+        interface LogoutRsp {
+            Header?: IMessageHead; // 消息头
+            RetCode: number; // 返回码
+            RetDesc?: string; // 描述信息
+        }
     }
 }

+ 70 - 34
src/utils/storage/index.ts

@@ -1,65 +1,101 @@
-import { readonly, toRef, ref, Ref } from 'vue'
-import { merge } from '../object'
+import { reactive, toRef, readonly, computed, WritableComputedRef, UnwrapNestedRefs } from 'vue'
 
 /**
- * 存储类
+ * 本地存储类
  */
 export default class <T extends object> {
     private readonly storage: Storage
-    private readonly source // 始数据
+    private readonly source // 始数据
     private state
 
     constructor(storage: Storage, source: T) {
         this.storage = storage
         this.source = source
-        this.state = ref(source)
+        this.state = reactive({ ...source })
+
+        // 读取本地数据
+        for (const key in this.state) {
+            const strValue = this.storage.getItem(key)
+            if (strValue !== 'undefined' && strValue !== null) {
+                this.state[key] = JSON.parse(strValue)
+            }
+        }
     }
 
-    /** 获取存储数据 */
-    private getStorage<K extends keyof T>(key: K) {
-        const str = this.storage.getItem(key.toString())
-        if (str === 'undefined' || str === null) {
-            this.storage.removeItem(key.toString())
-            this.state.value[key] = this.source[key]
-        } else {
-            const value = JSON.parse(str)
-            this.state.value[key] = value
+    /**
+     * 获取状态数据 (不建议使用,可能存在对象内部值变更无法同步 Storage 的问题)
+     * @returns 
+     */
+    getState = () => {
+        const state = {} as { [key in keyof UnwrapNestedRefs<T>]: WritableComputedRef<UnwrapNestedRefs<T>[key]> }
+        for (const key in this.state) {
+            state[key] = computed({
+                get: () => this.getValue(key),
+                set: (value) => this.setValue(key, value)
+            })
         }
+        return state
+    }
+
+    /**
+     * 获取属性响应值 (不建议使用,可能存在对象内部值变更无法同步 Storage 的问题)
+     * @param key 
+     * @returns 
+     */
+    getComputedRef<K extends keyof UnwrapNestedRefs<T>>(key: K) {
+        const state = this.getState()
+        return state[key]
     }
 
-    /** 获取属性响应值 */
-    getRef<K extends keyof T>(key: K): Readonly<Ref<T[K]>> {
-        this.getStorage(key);
-        return readonly(toRef(this.state.value, key));
+    /**
+     * 获取属性响应值 (只读)
+     * @param key 
+     * @returns 
+     */
+    getRef<K extends keyof UnwrapNestedRefs<T>>(key: K) {
+        return readonly(toRef(this.state, key))
     }
 
-    /** 获取属性值 */
-    getValue<K extends keyof T>(key: K): T[K] {
-        this.getStorage(key);
-        return this.state.value[key];
+    /**
+     * 获取属性值
+     * @param key 
+     * @returns 
+     */
+    getValue<K extends keyof UnwrapNestedRefs<T>>(key: K) {
+        return this.state[key]
     }
 
-    /** 设置属性值 */
-    setValue<K extends keyof T>(key: K, value: T[K]) {
+    /**
+     * 设置属性值
+     * @param key 
+     * @param value 
+     */
+    setValue<K extends keyof UnwrapNestedRefs<T>>(key: K, value: UnwrapNestedRefs<T>[K]) {
         if (value === undefined || value === null) {
-            this.clear(key);
+            this.clear(key)
         } else {
-            const str = JSON.stringify(value);
-            this.storage.setItem(key.toString(), str);
-            this.state.value[key] = value;
+            const strValue = JSON.stringify(value)
+            this.storage.setItem(key.toString(), strValue)
+            this.state[key] = value
         }
     }
 
-    /** 清除属性值 */
-    clear<K extends keyof T>(...keys: K[]) {
+    /**
+     * 清除属性值
+     * @param keys 
+     */
+    clear<K extends keyof UnwrapNestedRefs<T>>(...keys: K[]) {
+        const source = reactive({ ...this.source })
         if (keys.length) {
             keys.forEach((key) => {
-                this.storage.removeItem(key.toString());
-                this.state.value[key] = this.source[key];
+                this.storage.removeItem(key.toString())
+                this.state[key] = source[key]
             })
         } else {
-            this.storage.clear();
-            merge(this.state.value, this.source);
+            for (const key in this.state) {
+                this.storage.removeItem(key)
+                this.state[key] = source[key]
+            }
         }
     }
 }

+ 44 - 36
src/utils/websocket/index.ts

@@ -40,8 +40,10 @@ export class MTP2WebSocket<T extends Package40 | Package50>{
     onError?: (e: Event) => void;
     /** 接收到推送类报文的回调 */
     onPush?: (data: T) => void;
-    /** 重连状态发生改变时的回调 */
-    onReconnect?: (socket: MTP2WebSocket<T>) => void;
+    /** 在重连之前回调 */
+    onBeforeReconnect?: (count: number) => void;
+    /** 在重连成功之后回调 */
+    onReconnect?: () => void;
 
     /** 当前连接状态 */
     connState: keyof typeof ConnectionState = 'Unconnected';
@@ -62,7 +64,7 @@ export class MTP2WebSocket<T extends Package40 | Package50>{
     /**
      * 连接服务器
      */
-    connect(host?: string): Promise<MTP2WebSocket<T>> {
+    async connect(host?: string): Promise<MTP2WebSocket<T>> {
         if (!this.onReady) {
             clearTimeout(this.reconnectTimer);
             this.stopHeartBeat();
@@ -71,44 +73,48 @@ export class MTP2WebSocket<T extends Package40 | Package50>{
             console.log(this.Package.name, this.host, '正在连接');
 
             this.onReady = new Promise((resolve, reject) => {
+                const errMsg = this.host + ' 连接发生错误';
                 const uuid = v4();
                 this.uuid = uuid;
-                this.ws = new WebSocket(this.host);
 
-                // 连接成功
-                this.ws.onopen = () => {
-                    console.log(this.Package.name, this.host, '连接成功');
-                    this.connState = 'Connected';
-                    this.startHeartBeat();
-                    this.onOpen && this.onOpen(this);
-                    resolve(this);
-                }
-                // 连接断开(CLOSING有可能延迟)
-                this.ws.onclose = () => {
-                    // 判断是否当前实例
-                    // 如果连接断开后不等待 onclose 响应,由于 onclose 延迟的原因可能会在创建新的 ws 实例后触发,导致刚创建的实例被断开进入重连机制
-                    if (this.uuid === uuid) {
-                        console.warn(this.Package.name, this.host, '连接已断开');
-                        this.reset();
-                        this.reconnect();
+                try {
+                    this.ws = new WebSocket(this.host);
+                    // 连接成功
+                    this.ws.onopen = () => {
+                        console.log(this.Package.name, this.host, '连接成功');
+                        this.connState = 'Connected';
+                        this.startHeartBeat();
+                        this.onOpen && this.onOpen(this);
+                        resolve(this);
+                    }
+                    // 连接断开(CLOSING有可能延迟)
+                    this.ws.onclose = () => {
+                        // 判断是否当前实例
+                        // 如果连接断开后不等待 onclose 响应,由于 onclose 延迟的原因可能会在创建新的 ws 实例后触发,导致刚创建的实例被断开进入重连机制
+                        if (this.uuid === uuid) {
+                            console.warn(this.Package.name, this.host, '连接已断开');
+                            this.reset();
+                            this.reconnect(); // 重连失败会不断尝试,直到成功为止
+                        }
+                    }
+                    // 连接发生错误
+                    this.ws.onerror = (e) => {
+                        this.onError && this.onError(e);
+                        reject(errMsg);
                     }
-                }
-                // 连接发生错误
-                this.ws.onerror = (e) => {
-                    const message = this.host + '连接发生错误';
-                    this.onError && this.onError(e);
-                    reject(message);
-                }
-                // 接收数据
-                this.ws.onmessage = (e) => {
                     // 接收数据
-                    new Response(e.data).arrayBuffer().then((res) => {
-                        this.disposeReceiveDatas(new Uint8Array(res));
-                    })
+                    this.ws.onmessage = (e) => {
+                        // 接收数据
+                        new Response(e.data).arrayBuffer().then((res) => {
+                            this.disposeReceiveDatas(new Uint8Array(res));
+                        })
+                    }
+                } catch {
+                    reject(errMsg);
                 }
             })
         }
-        return this.onReady;
+        return this.onReady
     }
 
     /**
@@ -117,7 +123,7 @@ export class MTP2WebSocket<T extends Package40 | Package50>{
     close() {
         clearTimeout(this.reconnectTimer);
         this.stopHeartBeat();
-        this.uuid = v4();
+        this.uuid = v4(); // 改变实例标识,取消重连状态
         this.reconnectCount = 0;
         this.reconnectInterval = this.defaultReconnectInterval;
 
@@ -181,12 +187,13 @@ export class MTP2WebSocket<T extends Package40 | Package50>{
     }
 
     /**
-     * 断网重连方法,在重连尝试失败时会递归调用自己
+     * 断网重连方法,在重连尝试失败时会再次重试
      */
     private reconnect() {
         this.stopHeartBeat();
         if (this.connState !== 'Connecting') {
             this.reconnectCount++;
+            this.onBeforeReconnect && this.onBeforeReconnect(this.reconnectCount);
             console.log(this.Package.name, this.host, `${this.reconnectInterval / 1000}秒后将进行第${this.reconnectCount}次重连`);
 
             this.reconnectTimer = window.setTimeout(() => {
@@ -195,8 +202,9 @@ export class MTP2WebSocket<T extends Package40 | Package50>{
                     console.log(this.Package.name, this.host, '重连成功,可开始进行业务操作');
                     this.reconnectCount = 0;
                     this.reconnectInterval = this.defaultReconnectInterval;
-                    this.onReconnect && this.onReconnect(this);
+                    this.onReconnect && this.onReconnect();
                 }).catch(() => {
+                    // 重连失败会进入 ws.onclose 再次发起重连
                     if (this.reconnectCount) {
                         this.reconnectInterval += this.defaultReconnectInterval; // 重连间隔时间每次递增5秒
                         console.warn(this.Package.name, this.host, `第${this.reconnectCount}次重连失败`);