li.shaoyi 2 năm trước cách đây
mục cha
commit
0a2b5338c5

+ 113 - 0
package-lock.json

@@ -40,6 +40,7 @@
         "@vue/cli-plugin-typescript": "~5.0.0",
         "@vue/cli-service": "~5.0.0",
         "@vue/eslint-config-typescript": "^9.1.0",
+        "compression-webpack-plugin": "^10.0.0",
         "eslint": "^7.32.0",
         "eslint-plugin-vue": "^8.0.3",
         "less": "^4.0.0",
@@ -4702,6 +4703,67 @@
         "node": ">= 0.8.0"
       }
     },
+    "node_modules/compression-webpack-plugin": {
+      "version": "10.0.0",
+      "resolved": "https://registry.npmmirror.com/compression-webpack-plugin/-/compression-webpack-plugin-10.0.0.tgz",
+      "integrity": "sha512-wLXLIBwpul/ALcm7Aj+69X0pYT3BYt6DdPn3qrgBIh9YejV9Bju9ShhlAsjujLyWMo6SAweFIWaUoFmXZNuNrg==",
+      "dev": true,
+      "dependencies": {
+        "schema-utils": "^4.0.0",
+        "serialize-javascript": "^6.0.0"
+      },
+      "engines": {
+        "node": ">= 14.15.0"
+      },
+      "peerDependencies": {
+        "webpack": "^5.1.0"
+      }
+    },
+    "node_modules/compression-webpack-plugin/node_modules/ajv": {
+      "version": "8.12.0",
+      "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.12.0.tgz",
+      "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
+      "dev": true,
+      "dependencies": {
+        "fast-deep-equal": "^3.1.1",
+        "json-schema-traverse": "^1.0.0",
+        "require-from-string": "^2.0.2",
+        "uri-js": "^4.2.2"
+      }
+    },
+    "node_modules/compression-webpack-plugin/node_modules/ajv-keywords": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmmirror.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
+      "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
+      "dev": true,
+      "dependencies": {
+        "fast-deep-equal": "^3.1.3"
+      },
+      "peerDependencies": {
+        "ajv": "^8.8.2"
+      }
+    },
+    "node_modules/compression-webpack-plugin/node_modules/json-schema-traverse": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+      "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+      "dev": true
+    },
+    "node_modules/compression-webpack-plugin/node_modules/schema-utils": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-4.0.0.tgz",
+      "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==",
+      "dev": true,
+      "dependencies": {
+        "@types/json-schema": "^7.0.9",
+        "ajv": "^8.8.0",
+        "ajv-formats": "^2.1.1",
+        "ajv-keywords": "^5.0.0"
+      },
+      "engines": {
+        "node": ">= 12.13.0"
+      }
+    },
     "node_modules/compression/node_modules/debug": {
       "version": "2.6.9",
       "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -16282,6 +16344,57 @@
         }
       }
     },
+    "compression-webpack-plugin": {
+      "version": "10.0.0",
+      "resolved": "https://registry.npmmirror.com/compression-webpack-plugin/-/compression-webpack-plugin-10.0.0.tgz",
+      "integrity": "sha512-wLXLIBwpul/ALcm7Aj+69X0pYT3BYt6DdPn3qrgBIh9YejV9Bju9ShhlAsjujLyWMo6SAweFIWaUoFmXZNuNrg==",
+      "dev": true,
+      "requires": {
+        "schema-utils": "^4.0.0",
+        "serialize-javascript": "^6.0.0"
+      },
+      "dependencies": {
+        "ajv": {
+          "version": "8.12.0",
+          "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.12.0.tgz",
+          "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
+          "dev": true,
+          "requires": {
+            "fast-deep-equal": "^3.1.1",
+            "json-schema-traverse": "^1.0.0",
+            "require-from-string": "^2.0.2",
+            "uri-js": "^4.2.2"
+          }
+        },
+        "ajv-keywords": {
+          "version": "5.1.0",
+          "resolved": "https://registry.npmmirror.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
+          "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
+          "dev": true,
+          "requires": {
+            "fast-deep-equal": "^3.1.3"
+          }
+        },
+        "json-schema-traverse": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+          "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+          "dev": true
+        },
+        "schema-utils": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-4.0.0.tgz",
+          "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==",
+          "dev": true,
+          "requires": {
+            "@types/json-schema": "^7.0.9",
+            "ajv": "^8.8.0",
+            "ajv-formats": "^2.1.1",
+            "ajv-keywords": "^5.0.0"
+          }
+        }
+      }
+    },
     "concat-map": {
       "version": "0.0.1",
       "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",

+ 1 - 0
package.json

@@ -42,6 +42,7 @@
     "@vue/cli-plugin-typescript": "~5.0.0",
     "@vue/cli-service": "~5.0.0",
     "@vue/eslint-config-typescript": "^9.1.0",
+    "compression-webpack-plugin": "^10.0.0",
     "eslint": "^7.32.0",
     "eslint-plugin-vue": "^8.0.3",
     "less": "^4.0.0",

+ 0 - 49
src/business/auth/index.ts

@@ -1,49 +0,0 @@
-import { ref, reactive } from 'vue'
-import { v4 } from 'uuid'
-import { ClientType } from '@/constants/client'
-import { loginStore, enumStore, errorInfoStore } from '@/stores'
-import { initBaseData, checkTokenLoop } from '@/business/common'
-import eventBus from '@/services/bus'
-
-export function useAuth() {
-    const loading = ref(false);
-    const user = reactive<Proto.LoginReq>({
-        LoginID: '110000000001',
-        LoginPWD: '123456',
-        GUID: v4(),
-        LoginType: 0,
-        ClientType: ClientType.Web,
-        Version: '2.0.0.0',
-        DeviceID: ''
-    })
-
-    // 用户登录
-    const login = async () => {
-        try {
-            loading.value = true
-            // 等待请求枚举
-            await enumStore.actions.getAllEnumList()
-            // 等待请求系统错误信息
-            await errorInfoStore.actions.getErrorInfoList()
-            // 等待登录
-            await loginStore.actions.userLogin(user)
-            // 等待业务数据初始化
-            await initBaseData(() => checkTokenLoop())
-        } catch (err) {
-            loading.value = false
-            return Promise.reject(err)
-        }
-    }
-
-    // 用户登出
-    const logout = () => {
-        eventBus.$emit('LogoutNotify')
-    }
-
-    return {
-        loading,
-        user,
-        login,
-        logout,
-    }
-}

+ 7 - 45
src/business/common/index.ts

@@ -1,46 +1,10 @@
 import { timerTask } from '@/utils/timer'
-import { loginStore, accountStore, userStore, futuresStore, menuStore, performanceStore, favoriteStore, exrateStore } from '@/stores'
+import { loginStore } from '@/stores'
 import { tokenCheck } from '@/services/api/account'
 import eventBus from '@/services/bus'
-import socket from '@/services/socket'
 
 /**
- * 退出登录
- */
-export function logout(callback?: () => void) {
-    socket.closeAll()
-    timerTask.clearAll()
-    loginStore.actions.reset()
-    accountStore.actions.reset()
-    callback && callback()
-}
-
-/**
- * 初始化业务数据
- * @param callback 
- */
-export async function initBaseData(callback?: () => void) {
-    const asyncTask = [
-        userStore.actions.getUserData(),
-        menuStore.actions.getUserMenuList(),
-        futuresStore.actions.getGoodsList(),
-        performanceStore.actions.getPerformanceTemplateList(),
-        performanceStore.actions.getPerformanceStepTypeList(),
-        exrateStore.actions.getExrateList(),
-        //tableColumnStore.actions.getTableColumnList(),
-    ]
-
-    await Promise.all(asyncTask).then(() => {
-        accountStore.actions.getAccountList()
-        favoriteStore.actions.getFavoriteList()
-        callback && callback()
-    }).catch(() => {
-        return Promise.reject('初始化失败')
-    })
-}
-
-/**
- * 令牌效验
+ * 令牌校验
  */
 export function checkToken() {
     const { loginId, token } = loginStore.$mapGetters()
@@ -49,24 +13,22 @@ export function checkToken() {
             LoginID: loginId.value,
             Token: token.value,
         },
-        fail: () => {
-            loginStore.actions.reset()
-        }
+        fail: () => eventBus.$emit('LogoutNotify')
     })
 }
 
 /**
- * 轮询验令牌
+ * 轮询验令牌
  */
 export function checkTokenLoop() {
-    const delay = 1 * 60 * 1000 // 每1分钟验一次令牌
+    const delay = 1 * 60 * 1000 // 每1分钟验一次令牌
     timerTask.setTimeout(() => {
-        checkToken().then(() => checkTokenLoop()).catch(() => eventBus.$emit('LogoutNotify'))
+        checkToken().then(() => checkTokenLoop())
     }, delay, 'checkToken')
 }
 
 /**
- * 停止令牌
+ * 停止令牌
  */
 export function stopCheckToken() {
     timerTask.clearTimeout('checkToken')

+ 125 - 0
src/business/login/index.ts

@@ -0,0 +1,125 @@
+import { reactive } from 'vue'
+import { v4 } from 'uuid'
+import { encryptAES, decryptAES } from '@/utils/crypto'
+import { timerTask } from '@/utils/timer'
+import { queryLoginId, login } from '@/services/api/account'
+import { sessionData, localData } from '@/stores/storage'
+import { loginStore, enumStore, errorInfoStore, userStore, futuresStore, accountStore } from '@/stores'
+import { checkToken, checkTokenLoop } from '@/business/common'
+import service from '@/services'
+import socket from '@/services/socket'
+import eventBus from '@/services/bus'
+import cryptojs from 'crypto-js'
+
+export function useLogin() {
+    const { logining } = loginStore.$mapState()
+    const { token } = loginStore.$mapGetters()
+
+    const formData = reactive<Proto.LoginReq>({
+        LoginID: localStorage.getItem('gz_loginId') ?? '',
+        LoginPWD: '',
+        GUID: v4(),
+        LoginType: 0,
+        ClientType: 4,
+        Version: '2.0.0.0',
+        DeviceID: ''
+    })
+
+    const loadBaseData = async () => {
+        await service.onReady() // 等待服务初始化
+        await Promise.all([
+            errorInfoStore.actions.getErrorInfoList(),
+            enumStore.actions.getAllEnumList(),
+        ])
+    }
+
+    const loadUserData = async () => {
+        await checkToken() // 令牌校验
+        await userStore.actions.getUserData()
+        futuresStore.actions.getGoodsList()
+        accountStore.actions.getAccountList()
+        checkTokenLoop()
+    }
+
+    const loginAction = async (params: Proto.LoginReq) => {
+        params.GUID = v4()
+        await login({
+            data: params,
+            success: async (res) => {
+                const encryptedData = encryptAES(JSON.stringify(params)) // 数据加密
+                localData.setValue('autoLoginEncryptedData', encryptedData)
+                loginStore.$setState((state) => {
+                    state.loginInfo = res
+                })
+                localStorage.setItem('gz_loginId', formData.LoginID) // 记住登录ID
+            }
+        })
+        await loadUserData()
+        eventBus.$emit('LoginNotify') // 登录成功通知
+    }
+
+    // 初始化业务数据
+    const initBaseData = async (autoLogin = false) => {
+        logining.value = true
+        try {
+            // 等待加载业务数据
+            await loadBaseData()
+            // 自动登录
+            if (autoLogin) {
+                const encryptedData = localData.getValue('autoLoginEncryptedData')
+                if (encryptedData) {
+                    try {
+                        const decryptedString = decryptAES(encryptedData)
+                        return await loginAction(JSON.parse(decryptedString))
+                    } catch (err) {
+                        console.error(err)
+                        eventBus.$emit('LogoutNotify')
+                    }
+                }
+            }
+            if (token.value) {
+                await loadUserData()
+            }
+        } finally {
+            logining.value = false
+        }
+    }
+
+    // 用户登录
+    const userLogin = async () => {
+        logining.value = true
+        try {
+            const params = { ...formData }
+            await loadBaseData()
+            await queryLoginId({
+                data: {
+                    username: formData.LoginID
+                },
+                success: (res) => {
+                    params.LoginID = res.data
+                    params.LoginPWD = cryptojs.SHA256(res.data + formData.LoginPWD).toString()
+                }
+            })
+            return await loginAction(params)
+        } finally {
+            logining.value = false
+        }
+    }
+
+    // 用户登出
+    const userLogout = (callback?: () => void) => {
+        socket.closeAll()
+        timerTask.clearAll()
+        sessionData.reset()
+        localData.reset('autoLoginEncryptedData')
+        callback && callback()
+    }
+
+    return {
+        logining,
+        formData,
+        initBaseData,
+        userLogin,
+        userLogout,
+    }
+}

+ 1 - 1
src/business/table/columns.ts

@@ -122,7 +122,7 @@ export const pcTableColumnMap = new Map<TableColumnKey, Ermcp.TableColumn[]>([
         { prop: 'qty', label: '重量' },
         { prop: 'price', label: '价格' },
         { prop: 'exchangerate', label: '汇率' },
-        { prop: 'tradeprice', label: '成交价格' },
+        { prop: 'tradeamount', label: '成交价格' },
         { prop: 'tradetime', label: '成交时间', width: 200 },
         { prop: 'operate', label: '操作' }
     ]],

+ 6 - 6
src/packages/mobile/views/auth/login/index.vue

@@ -6,13 +6,13 @@
     <div class="login-logo">用户登录</div>
     <Form @submit="formSubmit">
       <CellGroup inset>
-        <Field v-model="user.LoginID" name="account" label="用户名" placeholder="请输入用户名"
+        <Field v-model="formData.LoginID" name="account" label="用户名" placeholder="请输入用户名"
           :rules="[{ required: true, message: '随便输入' }]" />
-        <Field v-model="user.LoginPWD" name="password" type="password" label="密码" placeholder="请输入密码"
+        <Field v-model="formData.LoginPWD" name="password" type="password" label="密码" placeholder="请输入密码"
           :rules="[{ required: true, message: '随便输入' }]" autocomplete="on" />
       </CellGroup>
       <div class="login-button">
-        <Button type="primary" :loading="loading" loading-text="正在登录..." native-type="submit" round block>登录</Button>
+        <Button type="primary" :loading="logining" loading-text="正在登录..." native-type="submit" round block>登录</Button>
       </div>
     </Form>
   </app-view>
@@ -22,17 +22,17 @@
 import { computed } from 'vue'
 import { useRoute, useRouter } from 'vue-router'
 import { Button, Field, CellGroup, Form, Dialog } from 'vant'
-import { useAuth } from '@/business/auth'
+import { useLogin } from '@/business/login'
 import animateRouter from '@mobile/router/animateRouter'
 
-const { loading, user, login } = useAuth();
+const { logining, formData, userLogin } = useLogin();
 const { state } = animateRouter;
 const route = useRoute()
 const router = useRouter()
 const showBackButton = computed(() => state.history.length > 1);
 
 const formSubmit = () => {
-  login().then(() => {
+  userLogin().then(() => {
     const redirect = route.query.redirect;
     if (redirect) {
       router.replace(redirect.toString());

+ 3 - 2
src/packages/pc/App.vue

@@ -15,19 +15,20 @@ export default {
 import { ref, watch } from 'vue'
 import { useRouter, useRoute } from 'vue-router'
 import { ElMessageBox } from 'element-plus'
-import { logout } from '@/business/common'
+import { useLogin } from '@/business/login'
 import { loginStore } from '@/stores'
 import zhCn from 'element-plus/lib/locale/lang/zh-cn'
 import eventBus from '@/services/bus'
 
 const { token } = loginStore.$mapGetters()
+const { userLogout } = useLogin()
 const route = useRoute()
 const router = useRouter()
 const hasLogin = ref(false)
 
 // 接收用户登出通知
 eventBus.$on('LogoutNotify', (msg) => {
-  logout(() => {
+  userLogout(() => {
     if (msg) {
       ElMessageBox.alert(msg as string)
     }

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

@@ -33,7 +33,8 @@
                 <template #dropdown>
                     <el-dropdown-menu>
                         <!-- <el-dropdown-item icon="Key">修改密码</el-dropdown-item> -->
-                        <el-dropdown-item icon="SwitchButton" @click="logout()">退出登录</el-dropdown-item>
+                        <el-dropdown-item icon="SwitchButton"
+                            @click="eventBus.$emit('LogoutNotify')">退出登录</el-dropdown-item>
                     </el-dropdown-menu>
                 </template>
             </el-dropdown>
@@ -47,9 +48,9 @@
 import { ref, onMounted, defineAsyncComponent } from 'vue'
 import { useRouter } from 'vue-router'
 import { userStore, themeStore } from '@/stores'
-import { useAuth } from '@/business/auth'
 import { useComponent } from '@/hooks/component'
 import { useNotice } from '@/business/notice'
+import eventBus from '@/services/bus'
 import AppIcon from '@pc/components/base/icon/index.vue'
 
 const componentMap = new Map<string, unknown>([
@@ -62,7 +63,6 @@ const { componentId, openComponent, closeComponent } = useComponent()
 const { dataList, unreadList, getNoticeList } = useNotice()
 const { isMobile } = themeStore.$mapState()
 const { accountName } = userStore.$mapGetters()
-const { logout } = useAuth()
 const router = useRouter()
 const fullScreen = ref(false)
 

+ 21 - 22
src/packages/pc/index.html

@@ -1,25 +1,24 @@
+<!DOCTYPE html>
+<html lang="zh-cn">
 
-  <!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">
+  <link rel="icon" href="<%= BASE_URL %>favicon.ico">
+  <title>
+    <%= htmlWebpackPlugin.options.title %>
+  </title>
+  <!-- <script type="text/javascript" src="./tinymce/tinymce.min.js"></script> -->
+</head>
 
-  <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">
-    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
-    <title>
-      <%= htmlWebpackPlugin.options.title %>
-    </title>
-    <!-- <script type="text/javascript" src="./tinymce/tinymce.min.js"></script> -->
-  </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>
+  <!-- built files will be auto injected -->
+</body>
 
-  <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>
-    <!-- built files will be auto injected -->
-  </body>
-
-  </html>
+</html>

+ 20 - 12
src/packages/pc/views/auth/login/index.vue

@@ -1,11 +1,11 @@
 <template>
   <sign-layout class="user-login" title="登录">
-    <el-form ref="formRef" :model="user" :rules="formRules">
+    <el-form ref="formRef" :model="formData" :rules="formRules">
       <el-form-item prop="LoginID">
-        <el-input placeholder="用户名/账号/手机号" v-model="user.LoginID"></el-input>
+        <el-input placeholder="用户名/账号/手机号" v-model="formData.LoginID"></el-input>
       </el-form-item>
       <el-form-item prop="LoginPWD">
-        <el-input type="password" placeholder="请输入您的登录密码" v-model="user.LoginPWD">
+        <el-input type="password" placeholder="请输入您的登录密码" v-model="formData.LoginPWD">
         </el-input>
       </el-form-item>
       <el-form-item>
@@ -27,14 +27,16 @@ import { shallowRef } from 'vue'
 import { useRoute, useRouter } from 'vue-router'
 import { ElMessage } from 'element-plus'
 import type { FormInstance, FormRules } from 'element-plus'
-import { useAuth } from '@/business/auth'
+import { useLogin } from '@/business/login'
+import { menuStore } from '@/stores'
 import SignLayout from '../components/layout/index.vue'
 
-const { loading, user, login } = useAuth()
+const { formData, userLogin } = useLogin()
 const route = useRoute()
 const router = useRouter()
 const formRef = shallowRef<FormInstance>()
-const remember = shallowRef(false) // 记住账号
+const loading = shallowRef(false)
+const remember = shallowRef(true) // 记住账号
 
 const formRules: FormRules = {
   LoginID: [
@@ -46,16 +48,22 @@ const formRules: FormRules = {
 }
 
 const formSubmit = () => {
-  formRef.value?.validate((valid) => {
+  formRef.value?.validate(async (valid) => {
     if (valid) {
-      login().then(() => {
-        const redirect = route.query.redirect;
+      try {
+        loading.value = true
+        await userLogin()
+        await menuStore.actions.getUserMenuList()
+        const redirect = route.query.redirect
         if (redirect) {
-          router.replace(redirect.toString());
+          router.replace(redirect.toString())
         } else {
-          router.replace('/');
+          router.replace('/')
         }
-      }).catch((err) => ElMessage.error(err))
+      } catch (err) {
+        loading.value = false
+        ElMessage.error(err as string)
+      }
     }
   })
 }

+ 0 - 1
src/packages/pc/views/bonded/expense/components/details/index.vue

@@ -39,7 +39,6 @@
 
 <script lang="ts" setup>
 import { shallowRef, PropType } from 'vue'
-import { handleNoneValue } from '@/filters'
 import { getGZBSCPayStatusName, getGZBSCPayModeName } from '@/constants/bonded'
 import { useGzbscusermonthpayDetails } from '@/business/bonded'
 import AppDrawer from '@pc/components/base/drawer/index.vue'

+ 4 - 25
src/packages/pc/views/boot/index.vue

@@ -1,37 +1,16 @@
 <template>
-  <div class="boot" v-loading="loading"></div>
+  <div class="boot" v-loading="true"></div>
 </template>
 
 <script lang="ts" setup>
-import { ref } from 'vue'
 import { useRoute, useRouter } from 'vue-router'
-import { initBaseData, checkToken, checkTokenLoop } from '@/business/common'
-import { enumStore, errorInfoStore } from '@/stores'
-import service from '@/services'
-import socket from '@/services/socket'
+import { useLogin } from '@/business/login'
 
+const { initBaseData } = useLogin()
 const route = useRoute()
 const router = useRouter()
-const loading = ref(true)
 
-// 初始化数据
-const onLoad = (async () => {
-  // 等待服务初始化
-  await service.onReady()
-  // 等待请求枚举
-  await enumStore.actions.getAllEnumList()
-  // 等待请求系统错误信息
-  await errorInfoStore.actions.getErrorInfoList()
-  // 等待连接交易服务
-  await socket.connectTrade()
-  // 等待令牌效验
-  await checkToken()
-  // 等待业务数据初始化
-  await initBaseData()
-})()
-
-onLoad.then(() => {
-  checkTokenLoop()
+initBaseData().then(() => {
   const redirect = route.query.redirect
   if (redirect) {
     router.replace(redirect.toString())

+ 1 - 0
src/services/bus/interface.ts

@@ -4,6 +4,7 @@
 export enum EventKey {
     QuotePushNotify, // 行情推送通知
     QuoteServerReconnectNotify, // 行情服务重连成功通知
+    LoginNotify, // 用户登入通知
     LogoutNotify, // 用户登出通知
     MoneyChangedNotify, // 资金变动通知
 }

+ 69 - 74
src/services/http/index.ts

@@ -1,4 +1,4 @@
-import axios, { AxiosRequestConfig, Method, AxiosInstance } from 'axios'
+import axios, { AxiosRequestConfig, Method } from 'axios'
 //import qs from 'qs'
 //import cryptojs from 'crypto-js'
 //import { addPending, removePending } from './pending'
@@ -7,118 +7,113 @@ import { HttpParams, CommonParams, HttpResponse, Payload, ResultCode } from './i
 import service from '@/services'
 
 const httpService = new (class {
-    private axiosInstance?: AxiosInstance
+    private axiosInstance = axios.create({
+        timeout: 30000,
+    })
 
     constructor() {
-        this.init()
-    }
-
-    private init = async () => {
-        if (!this.axiosInstance) {
-            const config = await service.onReady()
-            this.axiosInstance = axios.create({
-                timeout: 30000,
-                baseURL: config.goCommonSearchUrl,
-            })
-            // 请求拦截器
-            this.axiosInstance.interceptors.request.use(
-                (config) => {
-                    //removePending(config) //在请求开始前,对之前的请求做检查取消操作
-                    //addPending(config) //将当前请求添加到列表中
-
-                    //请求头签名
-                    const sign = {
-                        token: loginStore.getters.token,
-                        signsecret: 'qz7qWOMXKTMT5JlDs5w4NTPwWeR3xhF1v6wqbZ9cExmP6cc3spvNAp1wJJ1SqRI5',
-                        timestamp: new Date().getTime(),
-                    }
-                    //设置请求头
-                    config.headers = {
-                        Authorization: sign.token,
-                        //Signid: 'eecd3f37625f4501b88e9f0fa14b4b51',
-                        //Sign: cryptojs.SHA256(qs.stringify(sign)).toString(),
-                        //Timestamp: sign.timestamp.toString(),
-                    }
-
-                    return config
-                },
-                (err) => {
-                    //此处异常触发条件不明
-                    console.error(err)
-                    return Promise.reject('网络异常,请稍后再试')
+        // 请求拦截器
+        this.axiosInstance.interceptors.request.use(
+            (config) => {
+                //addPending(config) //将当前请求添加到列表中
+                //请求头签名
+                const sign = {
+                    token: loginStore.getters.token,
+                    signsecret: 'qz7qWOMXKTMT5JlDs5w4NTPwWeR3xhF1v6wqbZ9cExmP6cc3spvNAp1wJJ1SqRI5',
+                    timestamp: new Date().getTime(),
                 }
-            )
-            // 响应拦截器
-            this.axiosInstance.interceptors.response.use(
-                (res) => {
-                    //removePending(res) //在请求结束后,移除本次请求
-                    return res
-                },
-                (err) => {
-                    const { msg, message } = err.response.data
-                    if (!axios.isCancel(err)) {
-                        console.error(err)
+                //设置请求头
+                config.headers = {
+                    Authorization: sign.token,
+                    //Signid: 'eecd3f37625f4501b88e9f0fa14b4b51',
+                    //Sign: cryptojs.SHA256(qs.stringify(sign)).toString(),
+                    //Timestamp: sign.timestamp.toString(),
+                }
+                return config
+            },
+            (err) => {
+                console.error(err)
+                return Promise.reject('出现错误,请稍后再试')
+            }
+        )
+        // 响应拦截器
+        this.axiosInstance.interceptors.response.use(
+            (res) => {
+                //removePending(res) //在请求结束后,移除本次请求
+                return res
+            },
+            (err) => {
+                if (err.message === 'Network Error') {
+                    return Promise.reject('无网络连接,请检查网络')
+                }
+                if (err.response) {
+                    const { msg, message } = err.response.data ?? {}
+                    switch (err.response.status) {
+                        case 408: {
+                            return Promise.reject('请求超时,请稍后再试')
+                        }
+                        default: {
+                            return Promise.reject(msg || message)
+                        }
                     }
-                    // 异常提示待优化
-                    return Promise.reject(msg || message || '服务器异常,请稍后再试')
                 }
-            )
-        }
-        return Promise.resolve(this.axiosInstance)
+                return Promise.reject('出现错误,请稍后再试')
+            }
+        )
     }
 
-    private request = async (url: string, method: Method, params?: unknown) => {
+    private request = async (url: string, method: Method, payload?: unknown) => {
+        const config = await service.onReady()
+        this.axiosInstance.defaults.baseURL = config.goCommonSearchUrl
         const requestConfig: AxiosRequestConfig = {
             url,
             method,
         }
-        if (params instanceof Object) {
+        if (payload instanceof Object) {
             if (['post', 'POST', 'put', 'PUT', 'patch', 'PATCH'].includes(method)) {
-                requestConfig.data = params
+                requestConfig.data = payload
             } else {
-                requestConfig.params = params
+                requestConfig.params = payload
             }
         } else {
-            requestConfig.url = url + (params ?? '')
+            requestConfig.url = url + (payload ?? '')
         }
-        const instance = await this.init()
-        return instance(requestConfig)
+        return this.axiosInstance(requestConfig)
     }
 
-    commonRequest = async <T extends Payload>(url: string, method: Method, params: CommonParams<T>) => {
+    commonRequest = async <T extends Payload>(url: string, method: Method, params: CommonParams<T>, errMsg?: string) => {
         const { data, success, fail, complete } = params
-        await this.request(url, method, data).then((res) => {
+        return await this.request(url, method, data).then((res) => {
             const data = res.data as T['rsp']
             success && success(data)
+            return Promise.resolve(data)
         }).catch((err) => {
-            fail && fail(err)
-            return Promise.reject(err)
+            const msg = err ?? (errMsg ? '请求失败: ' + errMsg : '请求失败,请稍后重试')
+            fail && fail(msg)
+            return Promise.reject(msg)
         }).finally(() => {
             complete && complete()
         })
     }
 
-    httpRequest = async <T extends Payload>(url: string, method: Method, params: HttpParams<T>) => {
+    httpRequest = async <T extends Payload>(url: string, method: Method, params: HttpParams<T>, errMsg?: string) => {
         const { data, success, fail, complete } = params
-        await this.request(url, method, data).then((res) => {
+        return await this.request(url, method, data).then((res) => {
             const data = res.data as HttpResponse<T['rsp']>
             switch (data.code) {
                 case ResultCode.InvalidToken:
-                    //退出登录
-                    //store.dispatch("user/logout", () => {
-                    //    window.location.reload()
-                    //})
                     return Promise.reject('令牌无效')
                 case ResultCode.Success:
                     success && success(data)
-                    return Promise.resolve()
+                    return Promise.resolve(data)
                 default:
                     fail && fail(data.msg)
                     return Promise.reject(data.msg)
             }
         }).catch((err) => {
-            fail && fail(err)
-            return Promise.reject(err)
+            const msg = err ?? (errMsg ? '请求失败: ' + errMsg : '请求失败,请稍后重试')
+            fail && fail(msg)
+            return Promise.reject(msg)
         }).finally(() => {
             complete && complete()
         })

+ 25 - 25
src/services/http/pending/index.ts

@@ -1,46 +1,46 @@
-import axios, { AxiosRequestConfig } from 'axios';
-import qs from 'qs';
+import axios, { AxiosRequestConfig } from 'axios'
+import qs from 'qs'
 
 /**
  * 缓存请求列表,防止重复请求
  */
-const pending = new Map();
+const pending = new Map()
 
-/**
- * 添加请求
- */
-export function addPending(config: AxiosRequestConfig) {
-    const url = [
+const getRequestKey = (config: AxiosRequestConfig) => {
+    return [
         config.method,
         config.url,
         qs.stringify(config.params),
         qs.stringify(config.data),
-    ].join('&');
+    ].join('&')
+}
+
+/**
+ * 添加请求
+ */
+export function addPending(config: AxiosRequestConfig) {
+    removePending(config) //在请求开始前,对之前的请求做检查取消操作
+    const key = getRequestKey(config)
     config.cancelToken =
         config.cancelToken ||
         new axios.CancelToken((cancel) => {
-            if (!pending.has(url)) {
+            if (!pending.has(key)) {
                 //如果列表中不存在当前请求,则添加进去
-                pending.set(url, cancel);
+                pending.set(key, cancel)
             }
-        });
+        })
 }
 
 /**
  * 移除请求
  */
 export function removePending(config: AxiosRequestConfig) {
-    const url = [
-        config.method,
-        config.url,
-        qs.stringify(config.params),
-        qs.stringify(config.data),
-    ].join('&');
-    if (pending.has(url)) {
+    const key = getRequestKey(config)
+    if (pending.has(key)) {
         //如果在列表中存在当前请求,需要取消当前请求,并且移除
-        const cancel = pending.get(url);
-        cancel(url);
-        pending.delete(url);
+        const cancel = pending.get(key)
+        cancel(key)
+        pending.delete(key)
     }
 }
 
@@ -48,8 +48,8 @@ export function removePending(config: AxiosRequestConfig) {
  * 清空等待中的请求(在路由跳转时调用)
  */
 export function clearPending() {
-    for (const [url, cancel] of pending) {
-        cancel(url);
+    for (const [key, cancel] of pending) {
+        cancel(key)
     }
-    pending.clear();
+    pending.clear()
 }

+ 50 - 17
src/services/index.ts

@@ -1,9 +1,7 @@
 import axios from 'axios'
+import plus from '@/utils/h5plus'
 
 export default new (class {
-    /** 应用配置信息 */
-    private appConfig = axios('./config/appconfig.json')
-
     /** 服务配置信息 */
     config = {
         commSearchUrl: '',
@@ -26,6 +24,7 @@ export default new (class {
         oem: '',
         iOS: '',
         android: '',
+        androidUpdateUrl: '',
         pcNewsUrl: '',
         pcMangerUrl: '',
     }
@@ -37,31 +36,66 @@ export default new (class {
     private isPending = true
 
     /**
-     * 尝试自动初始化,若断网或其它原因导致初始化失败,需手动初始化
+     * 自动初始化,若断网或其它原因导致初始化失败,需手动初始化
+     */
+    private onload = this.init()
+
+    /**
+     * 重试次数
      */
-    private tryInit = this.init()
+    private retryCount = 0
+
+    /**
+     * 失败时重新尝试初始化,直到成功为止
+     * @param msg 
+     */
+    private tryinit = (msg: string) => {
+        return new Promise<typeof this.config>((resolve, reject) => {
+            if (this.retryCount >= 5) {
+                this.retryCount = 0
+                this.isPending = false
+                reject(msg)
+            } else {
+                this.retryCount++
+                // 自动计算每次重试的延时,重试次数越多,延时越大
+                const delay = this.retryCount * 5000
+                setTimeout(() => {
+                    resolve(this.init())
+                }, delay)
+            }
+        })
+    }
 
     /**
      * 初始化服务配置
+     * https://uniapp.dcloud.net.cn/tutorial/app-ios-uiwebview.html
      */
     private init(): Promise<typeof this.config> {
         this.isPending = true
-        return new Promise((resolve, reject) => {
-            this.appConfig.then((res) => {
-                const { apiUrl } = res.data
+        return new Promise((resolve) => {
+            const filePath = './config/appconfig.json'
+            const getAppConfig = async () => {
+                if (plus.hasPlus()) {
+                    const res = await plus.getLocalFileContent(filePath)
+                    return JSON.parse(res)
+                } else {
+                    const res = await axios(filePath)
+                    return res.data
+                }
+            }
+            getAppConfig().then((res) => {
                 // 获取服务接口地址
-                axios(apiUrl).then((res) => {
+                axios(res.apiUrl).then((res) => {
                     this.config = res.data.data
                     this.isReady = true
                     resolve(this.config)
                 }).catch(() => {
-                    reject('获取服务配置地址失败')
-                }).finally(() => {
-                    this.isPending = false
+                    const result = this.tryinit('服务地址加载失败')
+                    resolve(result)
                 })
             }).catch(() => {
-                this.isPending = false
-                reject('获取配置信息失败')
+                const result = this.tryinit('配置文件加载失败')
+                resolve(result)
             })
         })
     }
@@ -70,11 +104,10 @@ export default new (class {
      * 服务初始化完成时触发
      */
     onReady() {
-        // 初始化失败时重新初始化
         if (!this.isReady && !this.isPending) {
-            this.tryInit = this.init()
+            this.onload = this.init()
         }
         // 确保当前只有一个初始化实例
-        return this.tryInit
+        return this.onload
     }
 })

+ 2 - 2
src/services/socket/index.ts

@@ -64,12 +64,12 @@ export default new (class {
         }
 
         this.tradeServer.onBeforeReconnect = () => {
-            // 停止令牌
+            // 停止令牌
             stopCheckToken();
         }
 
         this.tradeServer.onReconnect = () => {
-            // 重新进行令牌
+            // 重新进行令牌
             checkToken().then(() => checkTokenLoop()).catch(() => eventBus.$emit('LogoutNotify'));
         }
     }

+ 3 - 3
src/services/socket/trade/index.ts

@@ -76,12 +76,12 @@ function tradeServerMiddleware<Req, Rsp>(reqKey: keyof typeof FunCode, rspKey: k
 export async function tradeServerRequest<Req, Rsp>(reqKey: keyof typeof FunCode, rspKey: keyof typeof FunCode, params: TradeParams<Req, Rsp & TradeResponse>, marketId?: number) {
     const { success, fail, complete } = params;
 
-    await tradeServerMiddleware(reqKey, rspKey, params, marketId).then((res) => {
+    return await tradeServerMiddleware(reqKey, rspKey, params, marketId).then((res) => {
         const { RetCode, Status, RetDesc } = { ...res };
         switch (RetCode) {
             case 0: {
                 success && success(res);
-                return Promise.resolve();
+                return Promise.resolve(res);
             }
             case -1: {
                 // 管理端错误消息
@@ -98,7 +98,7 @@ export async function tradeServerRequest<Req, Rsp>(reqKey: keyof typeof FunCode,
                 // 银行 业务 以 Status 作为判断依据
                 if (Status === 0) {
                     success && success(res);
-                    return Promise.resolve();
+                    return Promise.resolve(res);
                 }
                 const msg = errorInfoStore.actions.getErrorInfoByCode(RetCode || Status);
                 const error = String(RetDesc || RetCode);

+ 5 - 3
src/services/subscribe/index.ts

@@ -66,9 +66,11 @@ export default new (class {
         const value = this.quoteSubscribeMap.get(uuid) ?? []
 
         const start = () => {
-            // 对相同 key 订阅的商品进行合并处理
-            this.quoteSubscribeMap.set(uuid, [...value, ...goodsCodes])
-            this.quoteSubscribe()
+            if (token.value) {
+                // 对相同 key 订阅的商品进行合并处理
+                this.quoteSubscribeMap.set(uuid, [...value, ...goodsCodes])
+                this.quoteSubscribe()
+            }
         }
 
         return {

+ 1 - 43
src/stores/modules/login.ts

@@ -1,8 +1,5 @@
-import { v4 } from 'uuid'
-import { login, queryLoginId } from '@/services/api/account' // 引入可能会引起 Cannot access 'loginStore' before initialization
 import { createStore } from '../base'
 import { sessionData } from '../storage'
-import cryptojs from 'crypto-js'
 
 /**
  * 登录存储对象
@@ -10,7 +7,7 @@ import cryptojs from 'crypto-js'
 export const loginStore = createStore({
     state() {
         return {
-            loading: false,
+            logining: false,
             loginInfo: sessionData.getRef('loginInfo'),
         }
     },
@@ -34,48 +31,9 @@ export const loginStore = createStore({
         },
     },
     actions: {
-        // 用户登录
-        userLogin(param: Proto.LoginReq) {
-            this.state.loading = true
-            return new Promise<Proto.LoginRsp>((resolve, reject) => {
-                queryLoginId({
-                    data: {
-                        username: param.LoginID
-                    },
-                    success: (res) => {
-                        login({
-                            data: {
-                                ...param,
-                                GUID: v4(),
-                                LoginID: res.data,
-                                LoginPWD: cryptojs.SHA256(res.data + param.LoginPWD).toString(),
-                            },
-                            success: (res) => {
-                                this.state.loginInfo = res
-                                resolve(res)
-                            },
-                            fail: (err) => {
-                                reject(err)
-                            },
-                            complete: () => {
-                                this.state.loading = false
-                            }
-                        })
-                    },
-                    fail: (err) => {
-                        this.state.loading = false
-                        reject(err)
-                    }
-                })
-            })
-        },
         // 获取用户登录信息
         getLoginInfo<K extends keyof Proto.LoginRsp>(key: K) {
             return this.state.loginInfo[key]
         },
-        // 重置数据
-        reset() {
-            sessionData.reset('loginInfo')
-        }
     }
 })

+ 1 - 0
src/stores/storage.ts

@@ -6,6 +6,7 @@ function createLocalData() {
     return {
         appLanguage: Language.ZhCN,
         appTheme: AppTheme.Default,
+        autoLoginEncryptedData: '', // 自动登录加密数据
     }
 }
 

+ 58 - 0
src/utils/crypto/index.ts

@@ -0,0 +1,58 @@
+import cryptojs from 'crypto-js'
+
+/**
+ * 利用 canvas 创建浏览器唯一标识
+ * 由于不同客户端绘制 canvas 时渲染参数、抗锯齿等算法不同,因此绘制成图片数据的 CRC 校验也不一样
+ * @param text 
+ * @returns 
+ */
+export function getClientUUID(text = 'canvas') {
+    const canvas = document.createElement('canvas')
+    canvas.width = 100
+    canvas.height = 100
+
+    const ctx = canvas.getContext('2d')
+    if (ctx) {
+        ctx.font = '14px Arial'
+        ctx.fillStyle = 'black'
+        ctx.textAlign = 'center'
+        ctx.textBaseline = 'middle'
+        ctx.fillText(text, canvas.width / 2, canvas.height / 2) // 文本水平垂直居中
+    }
+
+    const base64Url = canvas.toDataURL()
+    return cryptojs.MD5(base64Url).toString()
+}
+
+/**
+ * AES加密
+ * @param data 
+ * @param key 
+ * @returns 
+ */
+export function encryptAES(data: string, key = getClientUUID(), iv?: string) {
+    const { enc: { Utf8 }, AES } = cryptojs
+    // 统一将传入的字符串转成UTF8编码
+    const _data = Utf8.parse(data) // 需要加密的数据
+    const _key = Utf8.parse(key) // 秘钥
+    const _iv = Utf8.parse(iv ?? key) // 偏移量
+    const encrypted = AES.encrypt(_data, _key, { iv: _iv, })
+    return encrypted.ciphertext.toString() //  返回加密后的值
+}
+
+/**
+ * AES解密
+ * @param encryptedData 
+ * @param key 
+ * @returns 
+ */
+export function decryptAES(encryptedData: string, key = getClientUUID(), iv?: string) {
+    const { enc: { Utf8, Hex, Base64 }, AES } = cryptojs
+    // 统一将传入的字符串转成UTF8编码
+    const wordarray = Hex.parse(encryptedData)
+    const _data = Base64.stringify(wordarray)
+    const _key = Utf8.parse(key) // 秘钥
+    const _iv = Utf8.parse(iv ?? key) // 偏移量
+    const decrypted = AES.decrypt(_data, _key, { iv: _iv })
+    return decrypted.toString(Utf8)
+}

+ 202 - 21
src/utils/h5plus/index.ts

@@ -1,36 +1,37 @@
+/* eslint-disable */
+
 interface HTML5 extends Window {
-    plus?: any; // eslint-disable-line
+    plus?: any;
 }
 
 export default new (class {
     private h5 = new Promise<HTML5>((resolve) => {
         document.addEventListener('plusready', () => {
-            resolve(window);
+            resolve(window)
         })
     })
 
     constructor() {
         // 监听返回按钮事件
         this.onPlusReady((plus) => {
-            let firstBack = true;
-            const webview = plus.webview.currentWebview();
+            let firstBack = true
+            const webview = plus.webview.currentWebview()
 
             plus.key.addEventListener('backbutton', () => {
-                // eslint-disable-next-line
                 webview.canBack((e: any) => {
                     // 判断能否继续返回
                     if (e.canBack) {
-                        webview.back();
+                        webview.back()
                     } else {
                         // 1秒内连续两次按返回键退出应用
                         if (firstBack) {
-                            firstBack = false;
-                            plus.nativeUI.toast('再按一次退出应用');
+                            firstBack = false
+                            plus.nativeUI.toast('再按一次退出应用')
                             setTimeout(() => {
-                                firstBack = true;
-                            }, 1000);
+                                firstBack = true
+                            }, 1000)
                         } else {
-                            plus.runtime.quit();
+                            plus.runtime.quit()
                         }
                     }
                 })
@@ -38,21 +39,57 @@ export default new (class {
         })
     }
 
-    // eslint-disable-next-line
     onPlusReady(callback: (plus: any) => void) {
         this.h5.then((res) => {
             callback(res.plus)
         })
     }
 
+    hasPlus() {
+        return Object.prototype.hasOwnProperty.call(window, 'plus')
+    }
+
+    /**
+     * 退出应用程序
+     */
+    quit() {
+        this.onPlusReady((plus) => {
+            plus.runtime.quit()
+        })
+    }
+
+    /**
+     * 客户端的版本名称
+     * @returns 
+     */
+    getVersion(callback: (version: string) => void) {
+        this.onPlusReady((plus) => {
+            plus.runtime.getProperty(plus.runtime.appid, (info: { version: string }) => {
+                callback(info.version)
+            })
+        })
+    }
+
+    /**
+     * 获取客户端的版本号
+     * @returns 
+     */
+    getVersionCode(callback: (versionCode: string) => void) {
+        this.onPlusReady((plus) => {
+            plus.runtime.getProperty(plus.runtime.appid, (info: { versionCode: string }) => {
+                callback(info.versionCode)
+            })
+        })
+    }
+
     /**
      * 获取状态栏高度
      * @param callback 
      */
     getStatusBarHeight(callback: (statusbarHeight: number) => void) {
         this.onPlusReady((plus) => {
-            const height = plus.navigator.getStatusbarHeight();
-            callback(height);
+            const height = plus.navigator.getStatusbarHeight()
+            callback(height)
         })
     }
 
@@ -62,7 +99,7 @@ export default new (class {
      */
     setStatusBarStyle(color: 'dark' | 'light') {
         this.onPlusReady((plus) => {
-            plus.navigator.setStatusBarStyle(color);
+            plus.navigator.setStatusBarStyle(color)
         })
     }
 
@@ -71,7 +108,7 @@ export default new (class {
      */
     hideStatusBar() {
         this.onPlusReady((plus) => {
-            plus.navigator.setFullscreen(true);
+            plus.navigator.setFullscreen(true)
         })
     }
 
@@ -80,7 +117,7 @@ export default new (class {
      */
     showStatusBar() {
         this.onPlusReady((plus) => {
-            plus.navigator.setFullscreen(false);
+            plus.navigator.setFullscreen(false)
         })
     }
 
@@ -89,8 +126,8 @@ export default new (class {
      */
     setFullSreen() {
         this.onPlusReady((plus) => {
-            this.hideStatusBar();
-            plus.navigator.hideSystemNavigation();
+            this.hideStatusBar()
+            plus.navigator.hideSystemNavigation()
         })
     }
 
@@ -99,8 +136,152 @@ export default new (class {
      */
     exitFullSreen() {
         this.onPlusReady((plus) => {
-            this.showStatusBar();
-            plus.navigator.showSystemNavigation();
+            this.showStatusBar()
+            plus.navigator.showSystemNavigation()
+        })
+    }
+
+    /**
+     * 更新应用
+     * @param url 
+     */
+    updateApp(url: string) {
+        this.onPlusReady((plus) => {
+            const dtask = plus.downloader.createDownload(
+                url,
+                {
+                    filename: ''
+                },
+                function (d: { filename: string }, status: number) {
+                    if (status == 200) {
+                        // 当前下载的状态
+                        installApp(d.filename) // 调用安装的方法
+                    } else {
+                        //plus.nativeUI.alert('下载失败')
+                    }
+                }
+            )
+
+            dtask.start() // 开启下载的任务
+            // app自动更新进度
+            dtask.addEventListener('statechanged', function (task: { state: number }) {
+                // 给下载任务设置一个监听 并根据状态  做操作
+                switch (task.state) {
+                    case 1:
+                        console.log('正在下载')
+                        break
+                    case 2:
+                        console.log('已连接到服务器')
+                        break
+                    case 3:
+                    // console.log(task)
+                    // console.log(task.downloadedSize)//当前的大
+                    // console.log(task.totalSize)//安装包的大小
+                }
+            })
+
+            // 自动更新
+            function installApp(path: string) {
+                plus.nativeUI.showWaiting('正在更新...')
+                plus.runtime.install(
+                    path,
+                    {
+                        // true表示强制安装,不进行版本号的校验;false则需要版本号校验,如果将要安装应用的版本号不高于现有应用的版本号则终止安装,并返回安装失败。 仅安装wgt和wgtu时生效,默认值 false
+                        force: false
+                    },
+                    function () {
+                        plus.nativeUI.closeWaiting()
+                        console.log('更新成功!')
+                        plus.runtime.restart()
+                    },
+                    function (e: { message: string }) {
+                        plus.nativeUI.closeWaiting()
+                        plus.nativeUI.alert('更新失败:' + e.message)
+                    }
+                )
+            }
+        })
+    }
+
+    /**
+     * 保存图片到相册
+     * @param base64Data 
+     */
+    saveImage(base64Data: string, fileName?: string) {
+        this.onPlusReady((plus) => {
+            const bitmap = new plus.nativeObj.Bitmap()
+            const filename = fileName ?? new Date().getTime()
+            bitmap.loadBase64Data(base64Data)
+            bitmap.save(`_doc/${filename}.jpg`, { overwrite: true, quality: 100, }, (e: Event) => {
+                //保存到系统相册
+                plus.gallery.save(
+                    e.target,
+                    () => {
+                        //销毁Bitmap图片
+                        bitmap.clear()
+                        plus.nativeUI.toast('已保存到相册中')
+                    },
+                    () => {
+                        //销毁Bitmap图片
+                        bitmap.clear()
+                        plus.nativeUI.toast('保存失败')
+                    }
+                )
+            }, (err: { message: string }) => {
+                plus.nativeUI.toast(err.message)
+            })
+        })
+    }
+
+    /**
+     * 打开本地文件(安卓生产包可能无效)
+     * https://www.html5plus.org/doc/zh_cn/runtime.html#plus.runtime.openFile
+     * @param filePath 
+     */
+    openFile(filePath: string) {
+        if (this.hasPlus()) {
+            this.onPlusReady((plus) => {
+                plus.runtime.openFile('_www/' + filePath)
+            })
+        } else {
+            window.open(filePath)
+        }
+    }
+
+    /**
+     * https://www.html5plus.org/doc/zh_cn/runtime.html#plus.runtime.openURL
+     * @param url 
+     */
+    openURL(url: string) {
+        if (this.hasPlus()) {
+            this.onPlusReady((plus) => {
+                plus.runtime.openURL(url)
+            })
+        } else {
+            window.open(url)
+        }
+    }
+
+    /**
+     * 读取本地文件内容
+     * @param filePath 
+     * @returns 
+     */
+    getLocalFileContent(filePath: string) {
+        return new Promise<any>((resolve, reject) => {
+            this.onPlusReady((plus) => {
+                plus.io.resolveLocalFileSystemURL('_www/' + filePath, (entry: any) => {
+                    entry.file((file: any) => {
+                        const fileReader = new plus.io.FileReader()
+                        fileReader.readAsText(file, 'utf-8')
+                        fileReader.onloadend = (evt: any) => {
+                            resolve(evt.target.result)
+                        }
+                    })
+                }, (e: any) => {
+                    reject(e.message)
+                })
+            })
         })
     }
 })

+ 5 - 9
src/utils/websocket/index.ts

@@ -27,8 +27,6 @@ export class MTP2WebSocket<T extends Package40 | Package50>{
     private currentSerial = 1;
     /** 信息发送异步建值对 */
     private asyncTaskMap = new Map<number, AsyncTask<T>>();
-    /** 初始默认重连间隔时长 */
-    private defaultReconnectInterval = 5 * 1000;
     /** 连接准备完成 */
     private onReady?: Promise<MTP2WebSocket<T>>;
 
@@ -51,8 +49,6 @@ export class MTP2WebSocket<T extends Package40 | Package50>{
     reconnectCount = 0;
     /** 心跳间隔时长,默认为30秒 */
     beatInterval = 30 * 1000;
-    /** 重连间隔时长,默认为5秒 */
-    reconnectInterval = this.defaultReconnectInterval;
     /** 消息超时时长,默认为15秒 */
     timeoutInterval = 15 * 1000;
 
@@ -125,7 +121,6 @@ export class MTP2WebSocket<T extends Package40 | Package50>{
         this.stopHeartBeat();
         this.uuid = v4(); // 改变实例标识,取消重连状态
         this.reconnectCount = 0;
-        this.reconnectInterval = this.defaultReconnectInterval;
 
         if (this.ws) {
             this.ws.close();
@@ -194,23 +189,24 @@ export class MTP2WebSocket<T extends Package40 | Package50>{
         if (this.connState !== 'Connecting') {
             this.reconnectCount++;
             this.onBeforeReconnect && this.onBeforeReconnect(this.reconnectCount);
-            console.log(this.Package.name, this.host, `${this.reconnectInterval / 1000}秒后将进行第${this.reconnectCount}次重连`);
+
+            // 自动计算每次重试的延时,重试次数越多,延时越大
+            const delay = this.reconnectCount * 5000
+            console.log(this.Package.name, this.host, `${delay / 1000}秒后将进行第${this.reconnectCount}次重连`);
 
             this.reconnectTimer = window.setTimeout(() => {
                 this.onReady = undefined;
                 this.connect().then(() => {
                     console.log(this.Package.name, this.host, '重连成功,可开始进行业务操作');
                     this.reconnectCount = 0;
-                    this.reconnectInterval = this.defaultReconnectInterval;
                     this.onReconnect && this.onReconnect();
                 }).catch(() => {
                     // 重连失败会进入 ws.onclose 再次发起重连
                     if (this.reconnectCount) {
-                        this.reconnectInterval += this.defaultReconnectInterval; // 重连间隔时间每次递增5秒
                         console.warn(this.Package.name, this.host, `第${this.reconnectCount}次重连失败`);
                     }
                 })
-            }, this.reconnectInterval);
+            }, delay);
         }
     }
 

+ 19 - 0
vue.config.js

@@ -2,7 +2,9 @@ const { merge } = require('webpack-merge')
 const { resolve } = require('path')
 const moment = require('moment')
 const { defineConfig } = require('@vue/cli-service')
+const CompressionPlugin = require('compression-webpack-plugin')
 const tsImportPluginFactory = require('ts-import-plugin')
+const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')
 
 module.exports = defineConfig({
   transpileDependencies: true,
@@ -19,6 +21,10 @@ module.exports = defineConfig({
       }
     }
   },
+  css: {
+    extract: true, // 是否使用css分离插件 ExtractTextPlugin
+    sourceMap: false,
+  },
   chainWebpack: (config) => {
     config.resolve.alias
       .set('@' + process.env.VUE_APP_ENV, resolve(__dirname, process.env.VUE_APP_ROOT))
@@ -45,5 +51,18 @@ module.exports = defineConfig({
         })
         return options;
       })
+
+    config.plugin('compressionPlugin')
+      .use(new CompressionPlugin({
+        test: /\.js$|\.html$|\.css/, // 匹配文件名
+        threshold: 10240, // 对超过10k的数据压缩
+      }))
+
+    config.plugin('fork-ts-checker')
+      .use(new ForkTsCheckerWebpackPlugin({
+        typescript: {
+          memoryLimit: 4096,
+        },
+      }))
   }
 })