diff --git a/src/api/index.ts b/src/api/index.ts index 858212e..bc8c1f0 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -102,6 +102,15 @@ const responseInterceptor = (response: AxiosResponse data.msg = '未知的错误!' } break + case -100: + // 退出登录 + data.msg = 'token过期!' + removeStorage('token') + removeStorage('userInfo') + removeStorage('menusInfo') + removeStorage('permissionsInfo') + router.push({ name: 'Login' }) + break case -101: data.msg = 'token错误!' removeStorage('token') diff --git a/src/api/modules/user.ts b/src/api/modules/user.ts index bdb5a42..9915b2f 100644 --- a/src/api/modules/user.ts +++ b/src/api/modules/user.ts @@ -99,3 +99,12 @@ export async function checkAccountIdIsSonForUser(params: { account_id: string }) export async function getUserBaseInfo(params: { account_id: string }) { return request.get('/user/getUserBaseInfo', params) } + +/** + * 初始化 MQTT Key + * @param params 请求参数(设备号) + * @returns Promise 返回 MQTT key + */ +export async function initMqttKeyApi(params: { device_no: string }) { + return request.post('/initMqttKey', params) +} diff --git a/src/stores/modules/transaction.ts b/src/stores/modules/transaction.ts index b80233f..19651ea 100644 --- a/src/stores/modules/transaction.ts +++ b/src/stores/modules/transaction.ts @@ -3,6 +3,7 @@ import { ref } from 'vue' import { MQTTClient } from '@/utils/mqttClient' import { useUserStore } from './user' import { useOrderPositionStore } from '@/stores/modules/orderPosition.ts' +import { formatDate } from '@/utils/timeUtils' // ============================================================ // 类型声明 @@ -24,16 +25,13 @@ export interface PositionItem { [key: string]: any } -/** 用户信息最小化类型(用于类型断言) */ -interface UserInfo { - redis_key: string - token: string +/** 做市商浮动盈亏数据 */ +export interface MarketFloatProfitData { account_id: string + float_profit_loss: string + update_time: string } -/** Vite 环境变量扩展(如项目已有 env.d.ts 可忽略) */ -declare const __VITE_MQTT_TRANSACTION_HOST__: string - // ============================================================ // Store 定义 // ============================================================ @@ -47,6 +45,8 @@ export const useTransactionStore = defineStore('transaction', () => { // ------------------ 状态 ------------------ /** 持仓列表 */ const positionList = ref([]) + /** 做市商浮动盈亏数据 Map */ + const marketFloatProfitMap = ref>(new Map()) // ------------------ 初始化 ------------------ @@ -55,12 +55,21 @@ export const useTransactionStore = defineStore('transaction', () => { * 建立连接 → 订阅主题 → 注册消息回调 */ const initMarketConditionsMQTT = async (): Promise => { - const { redis_key, token } = userStore.userInfo as UserInfo -console.log("initMarketConditionsMQTT", redis_key, token) - await mqttClient.connect(import.meta.env.VITE_MQTT_TRANSACTION_HOST as string, { - username: `${redis_key}`, - password: `${token}`, - }) + const { redis_key, token } = userStore.userInfo as any + console.log("initMarketConditionsMQTT", redis_key, token) + + try { + // 先调用 initMqttKey 接口获取 MQTT key + + // 使用 mqttKey 连接 + await mqttClient.connect(import.meta.env.VITE_MQTT_TRANSACTION_HOST as string, { + username: `${redis_key}`, + password: `${token}`, + }) + } catch (error) { + console.error('初始化 MQTT 连接失败:', error) + return + } await subscribeTheme() } @@ -70,9 +79,9 @@ console.log("initMarketConditionsMQTT", redis_key, token) * 仅保留持仓列表推送(POSITION_FLOATING_BACK) */ const subscribeTheme = async (): Promise => { - const { id } = userStore.userInfo as UserInfo + const { id } = userStore.userInfo as any - if (mqttClient.isConnected) { + if (mqttClient.getConnected()) { const topic = `push/${id}` await mqttClient.subscribe([topic]) @@ -80,13 +89,19 @@ console.log("initMarketConditionsMQTT", redis_key, token) [topic]: (_topic: string, msg: string): void => { console.log(_topic, msg) - const [type, data] = msg.split('|') + let [type, data] = msg.split('|') switch (type) { case 'POSITION_FLOATING_BACK': { // 持仓列表回调:交由 orderStore 处理 orderStore.handlePositionOrderList(data) break } + case 'SUBSCRIBE_AMOUNT_FLOATING_BACK': { + // 做市商浮动盈亏回调:更新 marketFloatProfitMap + console.log('做市商浮动盈亏数据:', data); + handleMarketFloatProfit(data) + break + } default: break } @@ -97,6 +112,28 @@ console.log("initMarketConditionsMQTT", redis_key, token) } } + /** + * 处理做市商浮动盈亏数据 + * 格式:account_id:float_profit_loss (例:84:6992.730000) + */ + const handleMarketFloatProfit = (data: string): void => { + + if (!data) return + + const fields = data.split(':') + if (fields.length < 2) return + + const account_id = fields[0] + const float_profit_loss = fields[1] + + // 更新 Map + marketFloatProfitMap.value.set(account_id, float_profit_loss) + // 触发响应式更新(创建新的 Map 实例) + marketFloatProfitMap.value = new Map(marketFloatProfitMap.value) + + console.log(`做市商浮动盈亏更新: account=${account_id}, float=${float_profit_loss}`) + } + // ------------------ 持仓列表 ------------------ /** @@ -105,7 +142,8 @@ console.log("initMarketConditionsMQTT", redis_key, token) */ const getPositionList = (account_id: string): void => { - if (mqttClient.isConnected) { + console.log('mqttClient.getConnected():', mqttClient.getConnected()); + if (mqttClient.getConnected()) { console.log('持仓 POSITION 持仓列表数据', `POSITION::${account_id}`) mqttClient.publish('server/trade', `POSITION::${account_id}`) } else { @@ -113,20 +151,45 @@ console.log("initMarketConditionsMQTT", redis_key, token) } } + /** + * 请求做市商浮动盈亏数据 + * 服务端通过 SUBSCRIBE_AMOUNT_FLOATING 推送结果 + * @param account_id 做市商账户ID + */ + const requestMarketFloatProfit = (): void => { + console.log('mqttClient.getConnected():', mqttClient.getConnected()); + if (mqttClient.getConnected()) { + mqttClient.publish('server/trade', `SUBSCRIBE_AMOUNT_FLOATING`) + } else { + setTimeout(() => requestMarketFloatProfit(), 300) + } + } + /** * 关闭订阅,清空数组 * */ const disconnect = () => { mqttClient?.disconnect() positionList.value = [] + marketFloatProfitMap.value.clear() + } + + /** + * 检查 MQTT 连接状态 + */ + const isConnected = (): boolean => { + return mqttClient.getConnected() } // ------------------ 暴露 ------------------ return { mqttClient, positionList, + marketFloatProfitMap, initMarketConditionsMQTT, getPositionList, - disconnect + requestMarketFloatProfit, + disconnect, + isConnected } }) diff --git a/src/stores/modules/user.ts b/src/stores/modules/user.ts index a918148..805ce38 100644 --- a/src/stores/modules/user.ts +++ b/src/stores/modules/user.ts @@ -17,6 +17,7 @@ import type { import type { UserInfo, MenuItem, PermissionItem } from '@/stores/types/user' import { formatMenuToElMenu, type ElMenuItem } from '@/utils/menuFormat' import { handleRouter, setRouter } from '@/router/handleRouter.ts' +import { useTransactionStore } from './transaction' // 定义用户 Store export const useUserStore = defineStore('user', () => { @@ -74,8 +75,12 @@ export const useUserStore = defineStore('user', () => { removeStorage('userInfo') removeStorage('menusInfo') removeStorage('permissionsInfo') - removeStorage('device_no') + // removeStorage('device_no') console.log('用户信息已清空') + + // 断开 MQTT 连接 + const transactionStore = useTransactionStore() + transactionStore.disconnect() } /** diff --git a/src/utils/mqttClient.ts b/src/utils/mqttClient.ts index 255a8c2..e718d29 100644 --- a/src/utils/mqttClient.ts +++ b/src/utils/mqttClient.ts @@ -1,6 +1,7 @@ import mqtt from 'mqtt' import type { MqttClient, IClientOptions, IClientPublishOptions } from 'mqtt' import type { QoS } from 'mqtt-packet' +import { initMqttKeyApi } from '@/api/modules/user' /** * 消息处理器类型定义 * 接收主题名称和消息内容两个参数 @@ -106,9 +107,13 @@ class MQTTClient { * 连接关闭事件 * 触发时机:连接被关闭(网络断开、手动断开、服务器踢出) */ - this.client.on('close', () => { + this.client.on('close', async () => { console.log('🔌 MQTT 连接关闭') this.isConnected = false + + const foreignDeviceNo = sessionStorage.getItem('foreign_device_no') || '' + const mqttKey = await initMqttKeyApi({ device_no: foreignDeviceNo }) + console.log("initMqttKey result:", mqttKey) }) // 返回客户端实例,便于链式调用或外部访问 diff --git a/src/views/profile/business/index.vue b/src/views/profile/business/index.vue index 7afcbde..84ce347 100644 --- a/src/views/profile/business/index.vue +++ b/src/views/profile/business/index.vue @@ -32,6 +32,9 @@ @row-click="handleRowClick" @row-contextmenu="handleRowContextMenu" > + @@ -248,6 +251,29 @@ const { buttonLoading: submitLoading, startButtonLoading: startSubmitLoading, st const marketTree = ref([]) const marketTableColumnsOptions = ref(marketTableColumns) +// 引入 transactionStore 用于动态浮动盈亏 +import { useTransactionStore } from '@/stores/modules/transaction.ts' +const transactionStore = useTransactionStore() + +/** + * @description 获取做市商的浮动盈亏值 + */ +const getMarketFloatProfit = (accountId: string | number): string => { + const profit = transactionStore.marketFloatProfitMap.get(String(accountId)) + return profit || '--' +} + +/** + * @description 获取浮动盈亏样式类 + */ +const getFloatProfitClass = (value: string | number) => { + const num = Number(value) + if (isNaN(num)) return '' + if (num > 0) return 'profit-positive' + if (num < 0) return 'profit-negative' + return '' +} + /** * @description 定义分页数据 */ @@ -485,7 +511,7 @@ const mapMarketTree = (data: any) => { pointSpreadCommission: item.wallet_capital.commission_point_diff, //点差返佣 commissionRatio: (item.wallet_capital.fee_handling_percent * 1).toFixed(2), //手续费比例 commissionReturn: item.wallet_capital.commission_fee_handling, //手续费返佣 - floatProfitLoss: '后续添加', //浮动盈亏 + floatProfitLoss: '后续添加', //浮动盈亏(已废弃,使用transactionStore动态获取) closeProfitLoss: item.wallet_capital.closing_profit, //平仓盈亏 riskMargin: item.wallet_capital.risk, //风险保证金 serviceFee: item.wallet_capital.service, //服务费 @@ -493,6 +519,8 @@ const mapMarketTree = (data: any) => { //使用这个字段来判断是否是做市商 mobile: item.mobile || '--', email: item.email || '--', + // 保存账户ID用于动态获取浮动盈亏 + accountId: item.wallet_capital.account_id, })) } @@ -578,7 +606,7 @@ const getMarketList = async () => { // 表格数据 marketTree.value = mapMarketTree(data.data) total.value = data.total - + requestAllMarketFloatProfit() //分页数据 stopLoading() currentPage.value = data.current_page @@ -595,13 +623,28 @@ const getRiskTemplateList = async () => { })) } +/** + * @description 请求所有做市商的浮动盈亏数据 + */ +const requestAllMarketFloatProfit = () => { + transactionStore.requestMarketFloatProfit() +} + /** * @description 页面加载完成后需要获取的数据 */ -onMounted(() => { +onMounted(async () => { getMarketList() getRiskTemplateList() + + // 初始化 MQTT 连接(已登录且未连接时才初始化) + if (userStore.isLoggedIn && !transactionStore.isConnected()) { + transactionStore.initMarketConditionsMQTT() + } }) + + +// 页面卸载时不主动断开连接(由logout统一处理) /** * @description 监听数据变化 @@ -959,4 +1002,12 @@ const handleCancel = async () => { padding-top: 20px; border-top: 1px solid #ebeef5; } +// 浮动盈亏样式 +.profit-positive { + color: #67C23A; +} + +.profit-negative { + color: #F56C6C; +} diff --git a/src/views/profile/business/options.ts b/src/views/profile/business/options.ts index f0186ae..1f3e7ba 100644 --- a/src/views/profile/business/options.ts +++ b/src/views/profile/business/options.ts @@ -47,7 +47,7 @@ export const marketTableColumns = [ //手续费返佣 { label: '手续费返佣', prop: 'commissionReturn', align: 'center' as const, width: '150px', isNumber: true }, //浮动盈亏 - { label: '浮动盈亏', prop: 'floatProfitLoss', align: 'center' as const, width: '100px', isNumber: true }, + { label: '浮动盈亏', slotName: 'floatProfitLoss', align: 'center' as const, width: '100px', isNumber: true }, //平仓盈亏 { label: '平仓盈亏', prop: 'closeProfitLoss', align: 'center' as const, width: '150px', isNumber: true }, //风险金 diff --git a/src/views/user/Login.vue b/src/views/user/Login.vue index 35a15bc..057ed45 100644 --- a/src/views/user/Login.vue +++ b/src/views/user/Login.vue @@ -186,8 +186,10 @@ const handleLogin = async () => { // 开启 loading startButtonLoading() try { - // 生成device_no并保存到变量(关键修改1) - const deviceNo = generateTimestampWithRandomNum() + // 先从 storage 获取已存在的 device_no,如果没有则生成新的 + const existingDeviceNo = getStorage('device_no', 'session') + console.log('existingDeviceNo:', existingDeviceNo); + const deviceNo = existingDeviceNo || generateTimestampWithRandomNum() // 登录参数 const loginParams = { email: loginForm.value.email, @@ -195,7 +197,7 @@ const handleLogin = async () => { login_type: getStorage('loginType', 'session') === 'email' ? 1 : 2, code: loginForm.value.code, password: loginForm.value.password, - device_no: deviceNo, + device_no: deviceNo+'', } await userStore.login(loginParams) setStorage('device_no', deviceNo)