li.shaoyi преди 2 години
родител
ревизия
b81b0e70a3

+ 1 - 1
public/config/appconfig.json

@@ -1,5 +1,5 @@
 {
   "version": "1.0.0",
   "versionCode": "100000",
-  "apiUrl": "http://192.168.31.201:8080/cfg?key=test_201"
+  "apiUrl": "http://192.168.31.204:8080/cfg?key=test_204"
 }

+ 1 - 1
src/business/bank/index.ts

@@ -157,7 +157,7 @@ export function useDoBankSign() {
         AgentCertType: 0,
         BankCardType: 0,
         BankAccountType: 1,
-        Extend_Info: JSON.stringify({ "sex": 1 }),
+        extend_info: JSON.stringify({ "sex": 1 }),
         AccountCode: accountStore.accountId.toString(),
         CertID: decryptAES(userInfo?.cardnum ?? ''),
         CertType: userInfo?.cardtypeid.toString(),

+ 2 - 2
src/business/login/index.ts

@@ -7,7 +7,7 @@ import tradeSocket from '@/services/websocket/trade'
 import quoteSocket from '@/services/websocket/quote'
 import eventBus from '@/services/bus'
 import { checkToken } from '@/business/common'
-import { encrypt50 } from '@/services/websocket/package/crypto'
+import { encryptBody } from '@/services/websocket/package/crypto'
 
 /**
  * 登录业务模块
@@ -62,7 +62,7 @@ export function useLogin(persist = false) {
                     }
                 } else {
                     const params = { ...formData }
-                    params.password = encrypt50(formData.password)
+                    params.password = encryptBody(formData.password)
                     resolve(params)
                 }
             })

+ 1 - 1
src/packages/mobile/components/modules/contact/index.vue

@@ -7,7 +7,7 @@
 <script lang="ts" setup>
 import { computed } from 'vue'
 import AppModal from '@/components/base/modal/index.vue'
-import AppAddress from '@mobile/views/mine/address/index.vue'
+import AppAddress from '@mobile/views/mine/address/Index.vue'
 
 const props = defineProps({
     show: {

+ 1 - 1
src/packages/mobile/components/modules/receipt/index.vue

@@ -7,7 +7,7 @@
 <script lang="ts" setup>
 import { computed } from 'vue'
 import AppModal from '@/components/base/modal/index.vue'
-import AppInvoice from '@mobile/views/mine/invoice/index.vue'
+import AppInvoice from '@mobile/views/mine/invoice/Index.vue'
 
 const props = defineProps({
     show: {

+ 22 - 6
src/packages/mobile/router/index.ts

@@ -166,6 +166,22 @@ const routes: Array<RouteRecordRaw> = [
     ],
   },
   {
+    path: '/actuals',
+    component: Page,
+    children: [
+      {
+        path: 'list',
+        name: 'actuals-list',
+        component: () => import('../views/actuals/list/Index.vue'),
+      },
+      {
+        path: 'detail',
+        name: 'actuals-detail',
+        component: () => import('../views/actuals/detail/Index.vue'),
+      },
+    ],
+  },
+  {
     path: '/bank',
     component: Page,
     children: [
@@ -222,14 +238,14 @@ const routes: Array<RouteRecordRaw> = [
     component: Page,
     children: [
       {
-        path: "address",
-        name: "mine-address",
-        component: () => import("../views/mine/address/Index.vue"),
+        path: 'address',
+        name: 'mine-address',
+        component: () => import('../views/mine/address/Index.vue'),
       },
       {
-        path: "invoice",
-        name: "mine-invoice",
-        component: () => import("../views/mine/invoice/Index.vue"),
+        path: 'invoice',
+        name: 'mine-invoice',
+        component: () => import('../views/mine/invoice/Index.vue'),
       },
       {
         path: 'profile',

+ 8 - 0
src/packages/mobile/views/actuals/detail/Index.vue

@@ -0,0 +1,8 @@
+<template>
+    <app-view>
+
+    </app-view>
+</template>
+
+<script lang="ts" setup>
+</script>

+ 40 - 0
src/packages/mobile/views/actuals/list/Index.vue

@@ -0,0 +1,40 @@
+<template>
+    <app-view>
+        <template #header>
+            <app-navbar title="现货挂牌" />
+        </template>
+        <app-pull-refresh ref="pullRefreshRef" class="purchase__container" v-model:loading="loading" v-model:error="error"
+            v-model:pageIndex="pageIndex" :page-count="pageCount" @refresh="run">
+            {{ dataList }}
+        </app-pull-refresh>
+    </app-view>
+</template>
+
+<script lang="ts" setup>
+import { shallowRef } from 'vue'
+import { useRequest } from '@/hooks/request'
+import { queryOrderQuote } from '@/services/api/goods'
+import AppPullRefresh from '@mobile/components/base/pull-refresh/index.vue'
+
+const error = shallowRef(false)
+const pullRefreshRef = shallowRef()
+const dataList = shallowRef<Model.THJWrstandardRsp[]>([])
+
+const { loading, pageIndex, pageCount, run } = useRequest(queryOrderQuote, {
+    manual: true,
+    params: {
+        pagesize: 20,
+        marketid: 17201,
+        wrpricetype: 1,
+    },
+    onSuccess: (res) => {
+        if (pageIndex.value === 1) {
+            dataList.value = []
+        }
+        dataList.value.push(...res.data)
+    },
+    onError: () => {
+        error.value = true
+    }
+})
+</script>

+ 11 - 4
src/packages/mobile/views/bank/sign/Index.vue

@@ -14,29 +14,36 @@
         </div>
         <div class="bank-sign__empty" v-else>
             <Empty description="您还未添加签约账户" />
-            <Button type="primary" @click="$router.push({ name: 'add-banksign' })" round>添加签约账户</Button>
+            <Button type="primary" @click="openComponent('edit')" round>添加签约账户</Button>
         </div>
         <template #footer>
             <div class="g-form__footer" v-if="bankInfo">
                 <Button type="warning" round block @click="formSubmit"
                     v-if="bankInfo.signstatus === SignStatus.Signed">解约</Button>
-                <Button type="primary" round block @click="routerTo('add-banksign')"
+                <Button type="primary" round block @click="openComponent('edit')"
                     v-if="[SignStatus.Unsigned, SignStatus.Refuse, SignStatus.Signed].includes(bankInfo.signstatus)">修改</Button>
             </div>
         </template>
+        <component ref="componentRef" :is="componentMap.get(componentId)" @closed="closeComponent" v-if="componentId" />
     </app-view>
 </template>
 
 <script lang="ts" setup>
-import { onActivated } from 'vue'
+import { onActivated, defineAsyncComponent } from 'vue'
 import { CellGroup, Cell, Button, Empty, showFailToast } from 'vant'
 import { fullloading, dialog } from '@/utils/vant'
+import { useComponent } from '@/hooks/component'
 import { getSignStatusName, SignStatus } from '@/constants/bank'
 import { useDoCancelBankSign } from '@/business/bank'
 import { useNavigation } from '@/hooks/navigation'
 
+const componentMap = new Map<string, unknown>([
+    ['edit', defineAsyncComponent(() => import('./components/edit/Index.vue'))],
+])
+
+const { componentRef, componentId, openComponent, closeComponent } = useComponent(() => formRefresh())
 const { cancelSubmit, formRefresh, bankInfo } = useDoCancelBankSign()
-const { router, routerTo } = useNavigation()
+const { router } = useNavigation()
 
 const formSubmit = () => {
     dialog({

+ 42 - 29
src/packages/mobile/views/bank/sign/components/edit/index.vue

@@ -1,30 +1,32 @@
 <template>
-    <app-view class="g-form">
-        <template #header>
-            <app-navbar>{{ bankInfo ? '修改签约账户' : '添加签约账户' }}</app-navbar>
-        </template>
-        <Form ref="formRef" class="g-form__container" @submit="formSubmit">
-            <CellGroup inset>
-                <Field name="OpenBankAccId" label="开户银行" :rules="formRules.OpenBankAccId" is-link>
-                    <template #input>
-                        <app-select v-model="formData.OpenBankAccId" placeholder="请选择开户银行" :options="banklist"
-                            :optionProps="{ label: 'bankname', value: 'bankid' }" />
-                    </template>
-                </Field>
-                <Field name="BankNo" label="银行卡号" v-model="formData.BankAccountNo" placeholder="请输入银行卡账号"
-                    :rules="formRules.BankAccountNo" />
-                <Field name="AccountName" label="姓名" readonly v-model="formData.BankAccountName" placeholder="请输入银行卡账户名"
-                    :rules="formRules.BankAccountName" />
-                <Field name="BranchBankName" label="支行名称" v-model="formData.OpenBankName" placeholder="请输入银行卡支行名称"
-                    :rules="formRules.OpenBankName" />
-            </CellGroup>
-        </Form>
-        <template #footer>
-            <div class="g-form__footer">
-                <Button type="primary" round block @click="formRef?.submit()">{{ bankInfo ? '修改' : '提交' }}</Button>
-            </div>
-        </template>
-    </app-view>
+    <app-modal direction="right" height="100%" v-model:show="showModal" :refresh="refresh">
+        <app-view class="g-form">
+            <template #header>
+                <app-navbar :title="bankInfo ? '修改签约账户' : '添加签约账户'" @back="closed" />
+            </template>
+            <Form ref="formRef" class="g-form__container" @submit="formSubmit">
+                <CellGroup inset>
+                    <Field name="OpenBankAccId" label="开户银行" :rules="formRules.OpenBankAccId" is-link>
+                        <template #input>
+                            <app-select v-model="formData.OpenBankAccId" placeholder="请选择开户银行" :options="banklist"
+                                :optionProps="{ label: 'bankname', value: 'bankid' }" />
+                        </template>
+                    </Field>
+                    <Field name="BankNo" label="银行卡号" v-model="formData.BankAccountNo" placeholder="请输入银行卡账号"
+                        :rules="formRules.BankAccountNo" />
+                    <Field name="AccountName" label="姓名" readonly v-model="formData.BankAccountName" placeholder="请输入银行卡账户名"
+                        :rules="formRules.BankAccountName" />
+                    <Field name="BranchBankName" label="支行名称" v-model="formData.OpenBankName" placeholder="请输入银行卡支行名称"
+                        :rules="formRules.OpenBankName" />
+                </CellGroup>
+            </Form>
+            <template #footer>
+                <div class="g-form__footer">
+                    <Button type="primary" round block @click="formRef?.submit()">{{ bankInfo ? '修改' : '提交' }}</Button>
+                </div>
+            </template>
+        </app-view>
+    </app-modal>
 </template>
 
 <script lang="ts" setup>
@@ -33,12 +35,13 @@ import { shallowRef } from 'vue'
 import { CellGroup, Button, Field, Form, FormInstance, FieldRule, showFailToast } from 'vant'
 import { fullloading, dialog } from '@/utils/vant'
 import { useDoBankSign } from '@/business/bank'
-import { useNavigation } from '@/hooks/navigation'
 import AppSelect from '@mobile/components/base/select/index.vue'
 import { validateRules } from '@/constants/regex'
+import AppModal from '@/components/base/modal/index.vue'
 
-const { router } = useNavigation()
 const { formData, onSubmit, banklist, bankInfo } = useDoBankSign()
+const showModal = shallowRef(true)
+const refresh = shallowRef(false) // 是否刷新父组件数据
 const formRef = shallowRef<FormInstance>()
 
 // 表单验证规则
@@ -74,7 +77,7 @@ const formSubmit = () => {
         onSubmit().then(() => {
             hideLoading()
             dialog(bankInfo ? '签约信息修改成功' : '签约提交成功,请耐心等待审核。').then(() => {
-                router.back()
+                closed(true)
             })
         }).catch((err) => {
             showFailToast(err)
@@ -82,4 +85,14 @@ const formSubmit = () => {
     })
 }
 
+// 关闭弹窗
+const closed = (isRefresh = false) => {
+    refresh.value = isRefresh
+    showModal.value = false
+}
+
+// 暴露组件属性给父组件调用
+defineExpose({
+    closed,
+})
 </script>

+ 77 - 3
src/packages/mobile/views/bank/statement/Index.vue

@@ -1,8 +1,82 @@
 <template>
-    <app-view>
-
+    <app-view class="bank-statement">
+        <template #header>
+            <app-navbar title="资金流水">
+                <template #right>
+                    <div class="button-more" @click="routerTo('bank-statement-history')">
+                        <span>更多</span>
+                    </div>
+                </template>
+            </app-navbar>
+        </template>
+        <app-pull-refresh v-model:loading="loading" v-model:error="error" v-model:pageIndex="pageIndex"
+            :page-count="pageCount" @refresh="run">
+            <app-list class="bank-statement__table" :columns="columns" :data-list="dataList">
+                <template #createtime="{ value }">
+                    <span>{{ formatDate(value, 'YYYY-MM-DD') }}</span>
+                    <span>{{ formatDate(value, 'HH:mm:ss') }}</span>
+                </template>
+                <template #businesscode="{ value }">
+                    {{ getAccountBusinessCodeName(value) }}
+                </template>
+            </app-list>
+        </app-pull-refresh>
     </app-view>
 </template>
 
 <script lang="ts" setup>
-</script>
+import { shallowRef } from 'vue'
+import { formatDate } from '@/filters'
+import { useRequest } from '@/hooks/request'
+import { useNavigation } from '@/hooks/navigation'
+import { getAccountBusinessCodeName } from '@/constants/bank'
+import { queryAmountLog } from '@/services/api/bank'
+import AppPullRefresh from '@mobile/components/base/pull-refresh/index.vue'
+import AppList from '@mobile/components/base/list/index.vue'
+
+const { routerTo } = useNavigation()
+const dataList = shallowRef<Model.AmountLogRsp[]>([])
+const error = shallowRef(false)
+
+const columns: Model.TableColumn[] = [
+    { prop: 'createtime', label: '时间' },
+    { prop: 'businesscode', label: '操作类型' },
+    { prop: 'amount', label: '金额' },
+]
+
+const { loading, pageIndex, pageCount, run } = useRequest(queryAmountLog, {
+    manual: true,
+    params: {
+        pagesize: 20,
+        pageflag: 1,
+    },
+    onSuccess: (res) => {
+        if (pageIndex.value === 1) {
+            dataList.value = []
+        }
+        dataList.value.push(...res.data)
+    },
+    onError: () => {
+        error.value = true
+    }
+})
+</script>
+
+<style lang="less">
+.bank-statement {
+    &__table {
+        td.app-list__column {
+            &:first-child {
+                span:last-child {
+                    color: #999;
+                    font-size: .24rem;
+                }
+            }
+
+            &:not(:first-child) {
+                font-size: .32rem;
+            }
+        }
+    }
+}
+</style>

+ 69 - 3
src/packages/mobile/views/bank/statement/history/Index.vue

@@ -1,8 +1,74 @@
 <template>
-    <app-view>
-
+    <app-view class="bank-hisstatement">
+        <template #header>
+            <app-navbar title="历史资金流水" />
+        </template>
+        <app-pull-refresh v-model:loading="loading" v-model:error="error" v-model:pageIndex="pageIndex"
+            :page-count="pageCount" @refresh="run">
+            <app-list class="bank-hisstatement__table" :columns="columns" :data-list="dataList">
+                <template #createtime="{ value }">
+                    <span>{{ formatDate(value, 'YYYY-MM-DD') }}</span>
+                    <span>{{ formatDate(value, 'HH:mm:ss') }}</span>
+                </template>
+                <template #businesscode="{ value }">
+                    {{ getAccountBusinessCodeName(value) }}
+                </template>
+            </app-list>
+        </app-pull-refresh>
     </app-view>
 </template>
 
 <script lang="ts" setup>
-</script>
+import { shallowRef } from 'vue'
+import { formatDate } from '@/filters'
+import { useRequest } from '@/hooks/request'
+import { getAccountBusinessCodeName } from '@/constants/bank'
+import { queryHisAmountLog } from '@/services/api/bank'
+import AppPullRefresh from '@mobile/components/base/pull-refresh/index.vue'
+import AppList from '@mobile/components/base/list/index.vue'
+
+const dataList = shallowRef<Model.HisAmountLogRsp[]>([])
+const error = shallowRef(false)
+
+const columns: Model.TableColumn[] = [
+    { prop: 'createtime', label: '时间' },
+    { prop: 'businesscode', label: '操作类型' },
+    { prop: 'amount', label: '金额' },
+]
+
+const { loading, pageIndex, pageCount, run } = useRequest(queryHisAmountLog, {
+    manual: true,
+    params: {
+        pagesize: 20,
+        pageflag: 1,
+    },
+    onSuccess: (res) => {
+        if (pageIndex.value === 1) {
+            dataList.value = []
+        }
+        dataList.value.push(...res.data)
+    },
+    onError: () => {
+        error.value = true
+    }
+})
+</script>
+
+<style lang="less">
+.bank-hisstatement {
+    &__table {
+        td.app-list__column {
+            &:first-child {
+                span:last-child {
+                    color: #999;
+                    font-size: .24rem;
+                }
+            }
+
+            &:not(:first-child) {
+                font-size: .32rem;
+            }
+        }
+    }
+}
+</style>

+ 10 - 10
src/packages/mobile/views/home/main/Index.vue

@@ -15,31 +15,31 @@
     <PullRefresh class="home-main__container" v-model="refreshing" @refresh="onRefresh">
       <app-block class="home-main__iconbar bg">
         <ul>
-          <li @click="routerTo('product')">
+          <li @click="routerTo('home-presale')">
             <img src="@mobile/assets/icons/cpjs.svg" />
-            <span>产品介绍</span>
+            <span>预售竞拍</span>
           </li>
-          <li @click="routerTo('Market')">
+          <li @click="routerTo('home-transfer')">
             <img src="@mobile/assets/icons/cpjg.svg" />
-            <span>产品价格</span>
+            <span>订单转让</span>
           </li>
-          <li @click="routerTo('NavigationPTGZ')">
+          <li @click="routerTo('home-swap')">
             <img src="@mobile/assets/icons/ptgz.svg" />
-            <span>平台规则</span>
+            <span>掉期贸易</span>
           </li>
         </ul>
         <ul>
           <li @click="routerTo('credit-signin')">
             <img src="@mobile/assets/icons/wdrw.svg" />
-            <span>我的任务</span>
+            <span>订单挂牌</span>
           </li>
-          <li @click="routerTo('contract')">
+          <li @click="routerTo('actuals-list')">
             <img src="@mobile/assets/icons/htzr.svg" />
-            <span>合同转让</span>
+            <span>现货贸易</span>
           </li>
           <li @click="routerTo('rules-ccwl')">
             <img src="@mobile/assets/icons/ccwl.svg" />
-            <span>仓储物流</span>
+            <span>参考行情</span>
           </li>
         </ul>
       </app-block>

+ 9 - 8
src/packages/mobile/views/mine/invoice/components/edit/Index.vue

@@ -6,7 +6,8 @@
             </template>
             <Form ref="formRef" class="g-form__container" @submit="formSubmit">
                 <CellGroup inset>
-                    <Field name="ReceiptType" label="发票类型" :rules="formRules.ReceiptType" :is-link="!selectedRow.autoid">
+                    <Field name="ReceiptType" label="发票类型" :rules="formRules.ReceiptType"
+                        :is-link="!selectedRow.autoid">
                         <template #input>
                             <app-select v-model="formData.ReceiptType" :options="getReceiptTypeList()"
                                 :readonly="!!selectedRow.autoid" />
@@ -17,14 +18,14 @@
                     <template v-if="formData.ReceiptType === ReceiptType.Company">
                         <Field v-model="formData.TaxpayerID" :rules="formRules.TaxpayerID" name="TaxpayerID" label="税号"
                             placeholder="必填" />
-                        <Field v-model="formData.ReceiptBank" :rules="formRules.ReceiptBank" name="ReceiptBank" label="开户银行"
-                            placeholder="选填" />
+                        <Field v-model="formData.ReceiptBank" :rules="formRules.ReceiptBank" name="ReceiptBank"
+                            label="开户银行" placeholder="选填" />
                         <Field v-model="formData.ReceiptAccount" :rules="formRules.ReceiptAccount" name="ReceiptAccount"
                             label="银行账号" placeholder="选填" />
                         <Field v-model="formData.Address" :rules="formRules.Address" name="Address" label="企业地址"
                             placeholder="选填" />
-                        <Field v-model="formData.ContactInfo" :rules="formRules.ContactInfo" name="ContactInfo" label="企业电话"
-                            placeholder="选填" />
+                        <Field v-model="formData.ContactInfo" :rules="formRules.ContactInfo" name="ContactInfo"
+                            label="企业电话" placeholder="选填" />
                     </template>
                     <Field v-model="formData.Email" :rules="formRules.Email" name="Email" label="邮箱" placeholder="选填" />
                 </CellGroup>
@@ -41,7 +42,7 @@
 
 <script lang="ts" setup>
 import { shallowRef, PropType } from 'vue'
-import { CellGroup, Button, Field, Form, FormInstance, FieldRule } from 'vant'
+import { CellGroup, Button, Field, Form, FormInstance, showFailToast, FieldRule } from 'vant'
 import { fullloading, dialog } from '@/utils/vant'
 import { ReceiptType, getReceiptTypeList } from '@/constants/receipt'
 import { validateRules } from '@/constants/regex'
@@ -88,7 +89,7 @@ const formSubmit = () => {
             hideLoading()
             closed(true)
         }).catch((err) => {
-            hideLoading(err, 'fail')
+            showFailToast(err)
         })
     })
 }
@@ -104,7 +105,7 @@ const formDelete = () => {
                 hideLoading()
                 closed(true)
             }).catch((err) => {
-                hideLoading(err, 'fail')
+                showFailToast(err)
             })
         })
     })

+ 20 - 2
src/packages/mobile/views/mine/setting/Index.vue

@@ -1,8 +1,26 @@
 <template>
-    <app-view>
-
+    <app-view class="navmenu-setting" :loading="false">
+        <template #header>
+            <app-navbar title="设置" />
+        </template>
+        <div class="g-navmenu" style="margin-top:.2rem">
+            <CellGroup>
+                <Cell is-link :to="{ name: 'user-password' }">
+                    <template #title>
+                        <app-iconfont icon="icon-yonghuzhucexieyi">修改密码</app-iconfont>
+                    </template>
+                </Cell>
+                <Cell is-link :to="{ name: 'user-cancel' }">
+                    <template #title>
+                        <app-iconfont icon="icon-zhuxiaofuwu">注销服务</app-iconfont>
+                    </template>
+                </Cell>
+            </CellGroup>
+        </div>
     </app-view>
 </template>
 
 <script lang="ts" setup>
+import { Cell, CellGroup } from 'vant'
+import AppIconfont from '@mobile/components/base/iconfont/index.vue'
 </script>

+ 7 - 0
src/packages/mobile/views/transfer/list/Index.vue

@@ -7,4 +7,11 @@
 </template>
 
 <script lang="ts" setup>
+import { onActivated, onDeactivated } from 'vue'
+import quoteSocket from '@/services/websocket/quote'
+
+const subscribe = quoteSocket.addSubscribe(['XAUUSD'])
+
+onActivated(() => subscribe.start())
+onDeactivated(() => subscribe.stop())
 </script>

+ 11 - 0
src/services/api/quote/index.ts

@@ -2,6 +2,17 @@ import http from '@/services/http'
 import { RequestConfig } from '@/services/http/types'
 
 /**
+ * 实时行情
+ */
+export function quoteSubscribe(config: RequestConfig<Model.QuoteSubscribeReq> = {}) {
+    return http.goRequest({
+        method: 'post',
+        url: '/Quote/QuoteSubscribe',
+        data: config.data,
+    })
+}
+
+/**
  * 查询行情历史数据
  */
 export function queryHistoryDatas(config: RequestConfig<{ goodscode: string }> = {}) {

+ 3 - 3
src/services/http/index.ts

@@ -3,7 +3,7 @@ import { v4 } from 'uuid'
 //import qs from 'qs'
 import cryptojs from 'crypto-js'
 //import { addPending, removePending } from './pending'
-import { encrypt50, decrypt50 } from '@/services/websocket/package/crypto'
+import { encryptBody, decryptBody } from '@/services/websocket/package/crypto'
 import { IMessageHead } from '@/types/proto/proto'
 import { useLoginStore, useAccountStore, useErrorInfoStore } from '@/stores'
 import { CommonResult, ResultCode, SendMsgToMQ } from './types'
@@ -185,7 +185,7 @@ export default new (class {
             method: 'post',
             url: '/MQ/SendMsgToMQ',
             data: {
-                data: encrypt50(JSON.stringify(data ?? '{}')),
+                data: encryptBody(JSON.stringify(data ?? '{}')),
                 funCodeReq: requestId,
                 funCodeRsp: responseId,
                 isEncrypted: true,
@@ -207,7 +207,7 @@ export default new (class {
      * @returns 
      */
     private package50Parse<T>(responseCode: keyof typeof FunCode, data: string) {
-        const decryptedData = decrypt50(data)
+        const decryptedData = decryptBody(data)
         const res = JSON.parse(decryptedData)
         console.log(responseCode, res)
 

+ 1 - 1
src/services/websocket/build/index.ts

@@ -64,7 +64,7 @@ export default class {
     /**
      * 连接服务器
      */
-    private connect() {
+    connect() {
         if (!this.wsReady || this.readyState === ReadyState.Closed) {
             clearTimeout(this.reconnectTimer)
             this.stopHeartbeat()

+ 40 - 15
src/services/websocket/package/crypto.ts

@@ -118,9 +118,10 @@ function hexStringToUint8Array(str: string) {
  * 5.0报文数据加密方法
  * @param plainText 明文
  */
-export const encrypt50 = (data: string) => {
-    const text = new TextEncoder()
-    const plainText = text.encode(data)
+export const encrypt50 = (plainText: Uint8Array) => {
+    if (plainText === null) {
+        return null;
+    }
 
     const a = CryptoJS.AES.encrypt(uint8ArrayToWordArray(plainText), aeskey, aesOption);
     const a1 = CryptoJS.enc.Base64.parse(a.toString());
@@ -139,29 +140,53 @@ export const encrypt50 = (data: string) => {
     mDataTemp.set(new Uint8Array(4), 4);
     const macData = dataMacAnsiX919(mDataTemp);
     if (macData === null) {
-        return '';
+        return null;
     }
     data1.set(macData, 4 + a2.length);
 
-    return CryptoJS.enc.Base64.stringify(uint8ArrayToWordArray(data1));
-}
+    return data1;
+};
 
 /**
  * 5.0报文数据解密方法
- * @param encryptData 密文
+ * @param encryptData 
+ * @returns 
  */
-export const decrypt50 = (base64: string) => {
-    const words = CryptoJS.enc.Base64.parse(base64); // 解析base64
-    const uint8Array = wordArrayToUint8Array(words);
-    const encryptData = uint8Array.subarray(4, uint8Array.length - 8);
-
+export const decrypt50 = (encryptData: Uint8Array) => {
+    encryptData = encryptData.subarray(4, encryptData.length - 8);
     const cipherParams = CryptoJS.lib.CipherParams.create({
         ciphertext: uint8ArrayToWordArray(encryptData),
     });
-
     const decrytped = CryptoJS.AES.decrypt(cipherParams, aeskey, aesOption);
-    const h = wordArrayToUint8Array(decrytped);
-    return new TextDecoder().decode(h);
+    return wordArrayToUint8Array(decrytped);
+};
+
+/**
+ * 5.0报文数据加密方法
+ * @param data 
+ * @returns 
+ */
+export const encryptBody = (data: string) => {
+    const text = new TextEncoder()
+    const content = text.encode(data)
+    const encryptData = encrypt50(content)
+    if (encryptData) {
+        const words = uint8ArrayToWordArray(encryptData)
+        return CryptoJS.enc.Base64.stringify(words)
+    }
+    return ''
+}
+
+/**
+ * 5.0报文数据解密方法
+ * @param base64 
+ * @returns 
+ */
+export const decryptBody = (base64: string) => {
+    const words = CryptoJS.enc.Base64.parse(base64) // 解析base64
+    const content = wordArrayToUint8Array(words)
+    const decryptData = decrypt50(content)
+    return new TextDecoder().decode(decryptData)
 }
 
 /**

+ 505 - 0
src/services/websocket/package/index.ts

@@ -0,0 +1,505 @@
+import { encrypt50, decrypt50 } from './crypto';
+
+/**
+ *  4.0 行情报文协议(Len >= 15 byte,大端字节序)
+ *  Header          byte    1   包头  0xFF
+ *  Length          UInt32  4   长度
+ *  mainClassNumber byte    1   大类号
+ *  subClassNumber  UInt16  2   小类号
+ *  SerialNumber    UInt32  4   流水号
+ *  Mode            byte    1   内容类型(0:ProtoBuff; 1:Json; 2:Zip+ProtoBuff)
+ *  Version         byte    1   版本号
+ *  Content         byte[]  n   数据体 心跳数据体长度为0,目前不对4.0(行情)报文进行加解密
+ *  End             byte    1   包尾  0x0
+ */
+export class Package40 {
+    /** 包头 */
+    header = 0xff;
+    /** 包长度 */
+    packageLength = 0;
+    /** 大类(服务端会自动变成回复的大类号) */
+    mainClassNumber = 0;
+    /** 小类 */
+    subClassNumber = 0;
+    /** 流水号 */
+    serialNumber = 0;
+    /** 内容类型 */
+    mode = 0;
+    /** 版本号 */
+    version = 0;
+    /** 数据体 */
+    content?: Uint8Array;
+    /** 包尾 */
+    end = 0x0;
+
+    /**
+     * 构造函数,把报文字节流装箱成报文对象
+     */
+    constructor(contentOrNumber: number | Uint8Array, content?: Uint8Array) {
+        if (contentOrNumber instanceof Uint8Array) {
+            // 接收信息装箱
+            if (contentOrNumber.length < 15) return;
+
+            this.header = contentOrNumber[0];
+            this.packageLength = new DataView(new Uint8Array(contentOrNumber.subarray(1, 5)).buffer).getUint32(0, false);
+            this.mainClassNumber = contentOrNumber[5];
+            this.subClassNumber = new DataView(new Uint8Array(contentOrNumber.subarray(6, 8)).buffer).getUint16(0, false);
+            this.serialNumber = new DataView(new Uint8Array(contentOrNumber.subarray(8, 12)).buffer).getUint32(0, false);
+            this.mode = contentOrNumber[12];
+            this.version = contentOrNumber[13];
+
+            if (contentOrNumber.length > 15) {
+                this.content = contentOrNumber.subarray(14, contentOrNumber.length - 1);
+            }
+            this.end = contentOrNumber[contentOrNumber.length - 1];
+        } else {
+            // 发送信息装箱
+            this.mainClassNumber = contentOrNumber;
+            this.content = content;
+        }
+    }
+
+    /**
+     * 获取可发送的包数据流
+     */
+    data(): Uint8Array {
+        if (this.content) {
+            this.packageLength = this.content.length + 15;
+        } else {
+            this.packageLength = 15;
+        }
+
+        const rst = new Uint8Array(this.packageLength);
+        let pos = 0;
+        // 包头
+        rst[pos] = this.header;
+        pos++;
+        // 包长度
+        let dataView = new DataView(new ArrayBuffer(4));
+        dataView.setUint32(0, this.packageLength, false);
+        rst.set(new Uint8Array(dataView.buffer), pos);
+        pos += 4;
+        // 大类号
+        rst[pos] = this.mainClassNumber;
+        pos++;
+        // 小类号
+        dataView = new DataView(new ArrayBuffer(2));
+        dataView.setUint16(0, this.subClassNumber, false);
+        rst.set(new Uint8Array(dataView.buffer), pos);
+        pos += 2;
+        // 流水号
+        dataView = new DataView(new ArrayBuffer(4));
+        dataView.setUint32(0, this.serialNumber, false);
+        rst.set(new Uint8Array(dataView.buffer), pos);
+        pos += 4;
+        // 内容类型
+        rst[pos] = this.mode;
+        pos++;
+        // 版本号
+        rst[pos] = this.version;
+        pos++;
+        // 数据体
+        if (this.content) rst.set(this.content, pos);
+        // 包尾
+        rst[this.packageLength - 1] = this.end;
+
+        return rst;
+    }
+}
+
+/**
+ *  5.0 交易报文协议(Len >= 24 byte,小端字节序)
+ * Header          byte    1   包头      0xFF
+ * Length          UInt32  4   长度
+ * FunCode         UInt32  4   功能码(0代表心跳)
+ * SessionID       UInt32  4   会话ID
+ * Mode            byte    1   内容类型(0:ProtoBuff;1:Json;2:Zip+ProtoBuff)
+ * Version         byte    1   版本号
+ * SerialNumber    UInt32  4   流水号
+ * Content         byte[]  n   数据内容(心跳数据体长度为0)
+ * CRC             UInt32  4   检验和    头19个字节检验和
+ * End             byte    1   包尾      0x0
+ */
+export class Package50 {
+    /** 包头 */
+    header = 0xff;
+    /** 长度 */
+    packageLength = 0;
+    /** 功能码(0代表心跳) */
+    funCode = 0;
+    /** 会话ID */
+    sessionID = 0;
+    /** 内容类型(0:ProtoBuff;1:Json;2:Zip+ProtoBuff) */
+    mode = 0;
+    /** 版本号 */
+    version = 0;
+    /** 流水号 */
+    serialNumber = 0;
+    /** 数据内容(心跳数据体长度为0) */
+    content?: Uint8Array;
+    /** 检验和 */
+    crc = 0;
+    /** 包尾 */
+    end = 0;
+
+    constructor(funCodeOrNumber: number | Uint8Array, content?: Uint8Array) {
+        if (funCodeOrNumber instanceof Uint8Array) {
+            // 接收信息装箱
+            if (funCodeOrNumber.length < 24) return;
+
+            // 先尝试对数据体进行解密
+            let decryptData: Uint8Array | null = null;
+            if (funCodeOrNumber.length > 24) {
+                const content = funCodeOrNumber.subarray(19, funCodeOrNumber.length - 4 - 1);
+                decryptData = decrypt50(content);
+            }
+
+            // 长度
+            this.packageLength = new DataView(new Uint8Array(funCodeOrNumber.subarray(1, 5)).buffer).getUint32(0, true);
+            // 功能码
+            this.funCode = new DataView(new Uint8Array(funCodeOrNumber.subarray(5, 9)).buffer).getUint32(0, true);
+            // 流水号
+            this.serialNumber = new DataView(new Uint8Array(funCodeOrNumber.subarray(15, 19)).buffer).getUint32(0, true);
+            // 内容
+            if (decryptData) {
+                this.content = decryptData;
+            }
+        } else {
+            // 发送信息装箱
+            this.funCode = funCodeOrNumber;
+            this.content = content;
+        }
+    }
+
+    data(): Uint8Array {
+        // 先尝试加密内容
+        let encryptData: Uint8Array | null = null;
+        if (this.content) {
+            encryptData = encrypt50(this.content);
+        }
+
+        // 重新计算整包长度
+        this.packageLength = 24;
+        if (encryptData) {
+            this.packageLength = 19 + encryptData.length + 5;
+        }
+
+        const rst = new Uint8Array(this.packageLength);
+        let pos = 0;
+
+        // 报文头
+        rst[pos] = this.header;
+        pos++;
+        // 长度
+        let dataView = new DataView(new ArrayBuffer(4));
+        dataView.setUint32(0, this.packageLength, true);
+        rst.set(new Uint8Array(dataView.buffer), pos);
+        pos += 4;
+        // 功能码
+        dataView = new DataView(new ArrayBuffer(4));
+        dataView.setUint32(0, this.funCode, true);
+        rst.set(new Uint8Array(dataView.buffer), pos);
+        pos += 4;
+        // 会话ID
+        dataView = new DataView(new ArrayBuffer(4));
+        dataView.setUint32(0, this.sessionID, true);
+        rst.set(new Uint8Array(dataView.buffer), pos);
+        pos += 4;
+        // 内容类型
+        rst[pos] = 0x0;
+        pos++;
+        // 版本号
+        rst[pos] = 0x0;
+        pos++;
+        // 流水号
+        dataView = new DataView(new ArrayBuffer(4));
+        dataView.setUint32(0, this.serialNumber, true);
+        rst.set(new Uint8Array(dataView.buffer), pos);
+        pos += 4;
+        // 报文内容
+        if (encryptData) {
+            rst.set(encryptData, pos);
+            pos += encryptData.length;
+        }
+        // 校验和
+        const crcByte = rst.subarray(0, 19); // 复制报文头,以便于校验和计算
+        dataView = new DataView(new ArrayBuffer(4));
+        dataView.setUint32(0, this.getCRC32(crcByte), true);
+        rst.set(new Uint8Array(dataView.buffer), pos);
+        // 包尾
+        rst[this.packageLength - 1] = this.end;
+
+        return rst;
+    }
+
+    /**
+     * 计算检验和
+     * @param crcByte 目标数据
+     */
+    getCRC32(crcByte: Uint8Array) {
+        const table = [
+            0x00000000,
+            0x77073096,
+            0xee0e612c,
+            0x990951ba,
+            0x076dc419,
+            0x706af48f,
+            0xe963a535,
+            0x9e6495a3,
+            0x0edb8832,
+            0x79dcb8a4,
+            0xe0d5e91e,
+            0x97d2d988,
+            0x09b64c2b,
+            0x7eb17cbd,
+            0xe7b82d07,
+            0x90bf1d91,
+            0x1db71064,
+            0x6ab020f2,
+            0xf3b97148,
+            0x84be41de,
+            0x1adad47d,
+            0x6ddde4eb,
+            0xf4d4b551,
+            0x83d385c7,
+            0x136c9856,
+            0x646ba8c0,
+            0xfd62f97a,
+            0x8a65c9ec,
+            0x14015c4f,
+            0x63066cd9,
+            0xfa0f3d63,
+            0x8d080df5,
+            0x3b6e20c8,
+            0x4c69105e,
+            0xd56041e4,
+            0xa2677172,
+            0x3c03e4d1,
+            0x4b04d447,
+            0xd20d85fd,
+            0xa50ab56b,
+            0x35b5a8fa,
+            0x42b2986c,
+            0xdbbbc9d6,
+            0xacbcf940,
+            0x32d86ce3,
+            0x45df5c75,
+            0xdcd60dcf,
+            0xabd13d59,
+            0x26d930ac,
+            0x51de003a,
+            0xc8d75180,
+            0xbfd06116,
+            0x21b4f4b5,
+            0x56b3c423,
+            0xcfba9599,
+            0xb8bda50f,
+            0x2802b89e,
+            0x5f058808,
+            0xc60cd9b2,
+            0xb10be924,
+            0x2f6f7c87,
+            0x58684c11,
+            0xc1611dab,
+            0xb6662d3d,
+            0x76dc4190,
+            0x01db7106,
+            0x98d220bc,
+            0xefd5102a,
+            0x71b18589,
+            0x06b6b51f,
+            0x9fbfe4a5,
+            0xe8b8d433,
+            0x7807c9a2,
+            0x0f00f934,
+            0x9609a88e,
+            0xe10e9818,
+            0x7f6a0dbb,
+            0x086d3d2d,
+            0x91646c97,
+            0xe6635c01,
+            0x6b6b51f4,
+            0x1c6c6162,
+            0x856530d8,
+            0xf262004e,
+            0x6c0695ed,
+            0x1b01a57b,
+            0x8208f4c1,
+            0xf50fc457,
+            0x65b0d9c6,
+            0x12b7e950,
+            0x8bbeb8ea,
+            0xfcb9887c,
+            0x62dd1ddf,
+            0x15da2d49,
+            0x8cd37cf3,
+            0xfbd44c65,
+            0x4db26158,
+            0x3ab551ce,
+            0xa3bc0074,
+            0xd4bb30e2,
+            0x4adfa541,
+            0x3dd895d7,
+            0xa4d1c46d,
+            0xd3d6f4fb,
+            0x4369e96a,
+            0x346ed9fc,
+            0xad678846,
+            0xda60b8d0,
+            0x44042d73,
+            0x33031de5,
+            0xaa0a4c5f,
+            0xdd0d7cc9,
+            0x5005713c,
+            0x270241aa,
+            0xbe0b1010,
+            0xc90c2086,
+            0x5768b525,
+            0x206f85b3,
+            0xb966d409,
+            0xce61e49f,
+            0x5edef90e,
+            0x29d9c998,
+            0xb0d09822,
+            0xc7d7a8b4,
+            0x59b33d17,
+            0x2eb40d81,
+            0xb7bd5c3b,
+            0xc0ba6cad,
+            0xedb88320,
+            0x9abfb3b6,
+            0x03b6e20c,
+            0x74b1d29a,
+            0xead54739,
+            0x9dd277af,
+            0x04db2615,
+            0x73dc1683,
+            0xe3630b12,
+            0x94643b84,
+            0x0d6d6a3e,
+            0x7a6a5aa8,
+            0xe40ecf0b,
+            0x9309ff9d,
+            0x0a00ae27,
+            0x7d079eb1,
+            0xf00f9344,
+            0x8708a3d2,
+            0x1e01f268,
+            0x6906c2fe,
+            0xf762575d,
+            0x806567cb,
+            0x196c3671,
+            0x6e6b06e7,
+            0xfed41b76,
+            0x89d32be0,
+            0x10da7a5a,
+            0x67dd4acc,
+            0xf9b9df6f,
+            0x8ebeeff9,
+            0x17b7be43,
+            0x60b08ed5,
+            0xd6d6a3e8,
+            0xa1d1937e,
+            0x38d8c2c4,
+            0x4fdff252,
+            0xd1bb67f1,
+            0xa6bc5767,
+            0x3fb506dd,
+            0x48b2364b,
+            0xd80d2bda,
+            0xaf0a1b4c,
+            0x36034af6,
+            0x41047a60,
+            0xdf60efc3,
+            0xa867df55,
+            0x316e8eef,
+            0x4669be79,
+            0xcb61b38c,
+            0xbc66831a,
+            0x256fd2a0,
+            0x5268e236,
+            0xcc0c7795,
+            0xbb0b4703,
+            0x220216b9,
+            0x5505262f,
+            0xc5ba3bbe,
+            0xb2bd0b28,
+            0x2bb45a92,
+            0x5cb36a04,
+            0xc2d7ffa7,
+            0xb5d0cf31,
+            0x2cd99e8b,
+            0x5bdeae1d,
+            0x9b64c2b0,
+            0xec63f226,
+            0x756aa39c,
+            0x026d930a,
+            0x9c0906a9,
+            0xeb0e363f,
+            0x72076785,
+            0x05005713,
+            0x95bf4a82,
+            0xe2b87a14,
+            0x7bb12bae,
+            0x0cb61b38,
+            0x92d28e9b,
+            0xe5d5be0d,
+            0x7cdcefb7,
+            0x0bdbdf21,
+            0x86d3d2d4,
+            0xf1d4e242,
+            0x68ddb3f8,
+            0x1fda836e,
+            0x81be16cd,
+            0xf6b9265b,
+            0x6fb077e1,
+            0x18b74777,
+            0x88085ae6,
+            0xff0f6a70,
+            0x66063bca,
+            0x11010b5c,
+            0x8f659eff,
+            0xf862ae69,
+            0x616bffd3,
+            0x166ccf45,
+            0xa00ae278,
+            0xd70dd2ee,
+            0x4e048354,
+            0x3903b3c2,
+            0xa7672661,
+            0xd06016f7,
+            0x4969474d,
+            0x3e6e77db,
+            0xaed16a4a,
+            0xd9d65adc,
+            0x40df0b66,
+            0x37d83bf0,
+            0xa9bcae53,
+            0xdebb9ec5,
+            0x47b2cf7f,
+            0x30b5ffe9,
+            0xbdbdf21c,
+            0xcabac28a,
+            0x53b39330,
+            0x24b4a3a6,
+            0xbad03605,
+            0xcdd70693,
+            0x54de5729,
+            0x23d967bf,
+            0xb3667a2e,
+            0xc4614ab8,
+            0x5d681b02,
+            0x2a6f2b94,
+            0xb40bbe37,
+            0xc30c8ea1,
+            0x5a05df1b,
+            0x2d02ef8d,
+        ];
+        let crc = 0xffffffff;
+        for (let i = 0; i < crcByte.length; i++) {
+            crc = (crc >>> 8) ^ table[(crc ^ crcByte[i]) & 0xff];
+        }
+        crc = crc ^ 0xffffffff;
+        return crc;
+    }
+}

+ 24 - 16
src/services/websocket/quote.ts

@@ -2,8 +2,8 @@ import { v4 } from 'uuid'
 import { useLoginStore } from '@/stores'
 import service from '@/services'
 import eventBus from '@/services/bus'
+import { quoteSubscribe } from '@/services/api/quote'
 import Socket from './build'
-import { RequestCode, MessageType } from './build/types'
 
 export default new (class {
     /** 行情链路 */
@@ -14,9 +14,12 @@ export default new (class {
 
     private async connect() {
         if (!this.socket) {
+            const loginStore = useLoginStore()
             const config = await service.onReady()
+
             this.socket = new Socket(config.quoteUrl, {
                 heartbeat: true,
+                protocols: loginStore.token,
             })
 
             this.socket.onReconnect = () => {
@@ -36,25 +39,29 @@ export default new (class {
      * 开始行情订阅
      */
     private subscribe = () => {
-        const subscribeData: string[] = []
+        const quoteGoodses: Model.QuoteSubscribeReq['quoteGoodses'] = []
+
         this.subscribeMap.forEach((list) => {
-            subscribeData.push(...list)
+            const data = list.map((code) => ({
+                exchangeId: 250,
+                goodsCode: code,
+            }))
+            quoteGoodses.push(...data)
         })
 
-        if (subscribeData.length) {
-            console.log('开始行情订阅', subscribeData)
+        if (quoteGoodses.length) {
+            console.log('开始行情订阅', quoteGoodses)
             this.connect().then((ws) => {
-                ws.send<Proto.QuoteReq[]>({
-                    headers: {
-                        requestId: v4(),
-                        requestCode: RequestCode.QuoteSubscribe,
-                        messageType: MessageType.Push,
-                    },
-                    data: subscribeData
-                }).then((res) => {
-                    console.log('行情订阅成功', res)
-                }).catch((err) => {
-                    console.error('行情订阅失败', err)
+                ws.connect().then(() => {
+                    quoteSubscribe({
+                        data: {
+                            quoteGoodses
+                        }
+                    }).then((res) => {
+                        console.log('行情订阅成功', res)
+                    }).catch((err) => {
+                        console.error('行情订阅失败', err)
+                    })
                 })
             })
         } else {
@@ -121,5 +128,6 @@ export default new (class {
      */
     close() {
         this.socket?.close()
+        this.socket = undefined
     }
 })

+ 8 - 0
src/types/model/quote.d.ts

@@ -1,4 +1,12 @@
 declare namespace Model {
+    /** 实时行情 请求 */
+    interface QuoteSubscribeReq {
+        quoteGoodses: {
+            exchangeId: number; // 交易所代码,一般写死250
+            goodsCode: string; // 商品代码,全大写
+        }[];
+    }
+
     /** 获取商品盘面信息 请求 */
     interface QuoteDayReq {
         goodsCodes: string; // 此参数不填则查所有;商品代码列表,格式:CU2102,CU2103,AL2107

+ 1 - 1
src/types/proto/bank.d.ts

@@ -38,7 +38,7 @@ declare global {
             MobilePhone: string; // 移动电话
             IdentifyCode: string; // 验证码
             email: string; // 电子邮箱
-            Extend_Info: string; // 扩展信息(JSON串,参考配置要求进行填充)
+            extend_info: string; // 扩展信息(JSON串,参考配置要求进行填充)
         }
 
         /** 签约应答 */