持仓订单接MQTT
This commit is contained in:
parent
639aa79892
commit
40621376e5
|
|
@ -8,4 +8,7 @@ VITE_API_BASE_URL = http://18.167.120.9:8085/api
|
||||||
VITE_REQUEST_TIMEOUT = 10000
|
VITE_REQUEST_TIMEOUT = 10000
|
||||||
|
|
||||||
# 可选:开发环境其他配置(如是否开启调试)
|
# 可选:开发环境其他配置(如是否开启调试)
|
||||||
VITE_DEBUG = true
|
VITE_DEBUG = true
|
||||||
|
|
||||||
|
# 交易mqtt
|
||||||
|
VITE_MQTT_TRANSACTION_HOST=ws://43.198.76.61:8083/mqtt
|
||||||
|
|
@ -8,4 +8,7 @@ VITE_API_BASE_URL = http://18.167.120.9:8085/api
|
||||||
VITE_REQUEST_TIMEOUT = 10000
|
VITE_REQUEST_TIMEOUT = 10000
|
||||||
|
|
||||||
# 可选:生产环境其他配置(如关闭调试)
|
# 可选:生产环境其他配置(如关闭调试)
|
||||||
VITE_DEBUG = false
|
VITE_DEBUG = false
|
||||||
|
|
||||||
|
# 交易mqtt
|
||||||
|
VITE_MQTT_TRANSACTION_HOST=ws://43.198.76.61:8083/mqtt
|
||||||
|
|
@ -28,6 +28,7 @@
|
||||||
"dayjs": "^1.11.20",
|
"dayjs": "^1.11.20",
|
||||||
"element-plus": "^2.13.2",
|
"element-plus": "^2.13.2",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
|
"mqtt": "^5.15.1",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"vue": "^3.5.29",
|
"vue": "^3.5.29",
|
||||||
|
|
|
||||||
|
|
@ -86,3 +86,10 @@ export async function editUserApi(params: {
|
||||||
}) {
|
}) {
|
||||||
return request.post<boolean>('/user/editUser', params);
|
return request.post<boolean>('/user/editUser', params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查账号是否为子账号的交易账户
|
||||||
|
*/
|
||||||
|
export async function checkAccountIdIsSonForUser(params: { account_id: string }) {
|
||||||
|
return request.get<boolean>('/user/checkAccountIdIsSonForUser', params)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ListFrameThrottler } from '@/utils/ListFrameThrottler.ts'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { formatDate } from '@/utils/timeUtils.ts'
|
||||||
|
|
||||||
|
interface PositionObj {
|
||||||
|
id?: string
|
||||||
|
account?: string
|
||||||
|
typeCode?: string
|
||||||
|
orderNo?: string
|
||||||
|
direction?: string
|
||||||
|
lots?: string
|
||||||
|
takeProfit?: string
|
||||||
|
stopLoss?: string
|
||||||
|
entryPrice?: string
|
||||||
|
currentPrice?: string
|
||||||
|
floatingPL?: string
|
||||||
|
margin?: string
|
||||||
|
commission?: string
|
||||||
|
overnightFee?: string
|
||||||
|
timestamp?: string
|
||||||
|
singleId?: string | number
|
||||||
|
sort?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useOrderPositionStore = defineStore('orderPosition', () => {
|
||||||
|
const positionOrderList = ref<PositionObj[]>([])
|
||||||
|
|
||||||
|
const handleInfoListData = (positionObj: PositionObj) => {
|
||||||
|
if (positionObj?.orderNo) {
|
||||||
|
const existingIndex = positionOrderList.value.findIndex(
|
||||||
|
(p: PositionObj) => p.orderNo === positionObj.orderNo,
|
||||||
|
)
|
||||||
|
if (existingIndex !== -1) {
|
||||||
|
positionOrderList.value[existingIndex] = positionObj
|
||||||
|
} else {
|
||||||
|
positionOrderList.value.unshift(positionObj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
positionOrderList.value.sort((a: PositionObj, b: PositionObj) => b.sort - a.sort)
|
||||||
|
}
|
||||||
|
|
||||||
|
const listFrameThrottler = new ListFrameThrottler(handleInfoListData)
|
||||||
|
|
||||||
|
const handlePositionOrderList = async (item:string) => {
|
||||||
|
const fields = item.split(':')
|
||||||
|
const positionObj: PositionObj = {
|
||||||
|
id: fields[2], // 订单号
|
||||||
|
account: fields[0], // 账户id
|
||||||
|
typeCode: fields[1], // 品种代码
|
||||||
|
orderNo: fields[2], // 订单号
|
||||||
|
direction: fields[3], // 1 做多 2 做空
|
||||||
|
lots: fields[4], // 手数
|
||||||
|
takeProfit: fields[5], // 止盈
|
||||||
|
stopLoss: fields[6], // 止损
|
||||||
|
entryPrice: fields[7], // 买入价格
|
||||||
|
currentPrice: fields[8], // 当前价格
|
||||||
|
floatingPL: (fields[9] * 1).toFixed(2), // 浮动盈亏
|
||||||
|
margin: (fields[10] * 1).toFixed(2), // 保证金
|
||||||
|
commission: (fields[11] * 1).toFixed(2), // 手续费
|
||||||
|
overnightFee: (fields[12] * 1).toFixed(2), // 隔夜费
|
||||||
|
timestamp:
|
||||||
|
fields[13] === -1 || fields[13] === '' || fields[13] === '-1'
|
||||||
|
? ''
|
||||||
|
: formatDate(fields[13] * 1000, 'YYYY-MM-DD HH:mm:ss'), // 时间戳
|
||||||
|
singleId: fields[14] * 1 === 0 ? '--' : fields[14], // 带单人id
|
||||||
|
sort: fields[13] * 1,
|
||||||
|
}
|
||||||
|
listFrameThrottler.push(positionObj)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
positionOrderList,
|
||||||
|
handlePositionOrderList,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,131 @@
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { MQTTClient } from '@/utils/mqttClient'
|
||||||
|
import { useUserStore } from './user'
|
||||||
|
import { useOrderPositionStore } from '@/stores/modules/orderPosition.ts'
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 类型声明
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/** 持仓单项数据结构(根据实际业务接口调整字段) */
|
||||||
|
export interface PositionItem {
|
||||||
|
id: string
|
||||||
|
code: string
|
||||||
|
name?: string
|
||||||
|
volume?: number
|
||||||
|
direction?: number // 1-做多, 2-做空
|
||||||
|
open_price?: number
|
||||||
|
current_price?: number
|
||||||
|
profit?: number
|
||||||
|
take_profit?: number
|
||||||
|
stop_loss?: number
|
||||||
|
open_time?: string
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 用户信息最小化类型(用于类型断言) */
|
||||||
|
interface UserInfo {
|
||||||
|
redis_key: string
|
||||||
|
token: string
|
||||||
|
account_id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Vite 环境变量扩展(如项目已有 env.d.ts 可忽略) */
|
||||||
|
declare const __VITE_MQTT_TRANSACTION_HOST__: string
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Store 定义
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
export const useTransactionStore = defineStore('transaction', () => {
|
||||||
|
// ------------------ 依赖注入 ------------------
|
||||||
|
const mqttClient = new MQTTClient()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const orderStore = useOrderPositionStore()
|
||||||
|
|
||||||
|
// ------------------ 状态 ------------------
|
||||||
|
/** 持仓列表 */
|
||||||
|
const positionList = ref<PositionItem[]>([])
|
||||||
|
|
||||||
|
// ------------------ 初始化 ------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化行情 MQTT 连接
|
||||||
|
* 建立连接 → 订阅主题 → 注册消息回调
|
||||||
|
*/
|
||||||
|
const initMarketConditionsMQTT = async (): Promise<void> => {
|
||||||
|
const { redis_key, token } = userStore.userInfo as UserInfo
|
||||||
|
|
||||||
|
await mqttClient.connect(import.meta.env.VITE_MQTT_TRANSACTION_HOST as string, {
|
||||||
|
username: `${redis_key}`,
|
||||||
|
password: `${token}`,
|
||||||
|
})
|
||||||
|
|
||||||
|
await subscribeTheme()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅主题并注册消息处理器
|
||||||
|
* 仅保留持仓列表推送(POSITION_FLOATING_BACK)
|
||||||
|
*/
|
||||||
|
const subscribeTheme = async (): Promise<void> => {
|
||||||
|
const { id } = userStore.userInfo as UserInfo
|
||||||
|
|
||||||
|
if (mqttClient.isConnected) {
|
||||||
|
const topic = `push/${id}`
|
||||||
|
await mqttClient.subscribe([topic])
|
||||||
|
|
||||||
|
mqttClient.onTopicMessages({
|
||||||
|
[topic]: (_topic: string, msg: string): void => {
|
||||||
|
console.log(_topic, msg)
|
||||||
|
|
||||||
|
const [type, data] = msg.split('|')
|
||||||
|
switch (type) {
|
||||||
|
case 'POSITION_FLOATING_BACK': {
|
||||||
|
// 持仓列表回调:交由 orderStore 处理
|
||||||
|
orderStore.handlePositionOrderList(data)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setTimeout(subscribeTheme, 30)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------ 持仓列表 ------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向服务端请求持仓列表
|
||||||
|
* 服务端通过 POSITION_FLOATING_BACK 推送结果
|
||||||
|
*/
|
||||||
|
const getPositionList = (account_id: string): void => {
|
||||||
|
|
||||||
|
if (mqttClient.isConnected) {
|
||||||
|
console.log('持仓 POSITION 持仓列表数据', `POSITION::${account_id}`)
|
||||||
|
mqttClient.publish('server/trade', `POSITION::${account_id}`)
|
||||||
|
} else {
|
||||||
|
setTimeout(getPositionList, 30)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭订阅,清空数组
|
||||||
|
* */
|
||||||
|
const disconnect = () => {
|
||||||
|
mqttClient.disconnect()
|
||||||
|
positionList.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------ 暴露 ------------------
|
||||||
|
return {
|
||||||
|
mqttClient,
|
||||||
|
positionList,
|
||||||
|
initMarketConditionsMQTT,
|
||||||
|
getPositionList,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -2,24 +2,25 @@ import type { ApiResponse } from './index';
|
||||||
|
|
||||||
// 用户信息子类型
|
// 用户信息子类型
|
||||||
export interface UserInfo {
|
export interface UserInfo {
|
||||||
id: number;
|
id: number
|
||||||
username: string;
|
username: string
|
||||||
email: string;
|
email: string
|
||||||
mobile: string;
|
mobile: string
|
||||||
role_id: number;
|
role_id: number
|
||||||
pass_admin: string;
|
pass_admin: string
|
||||||
salt_code_admin: string;
|
salt_code_admin: string
|
||||||
real_check: number;
|
real_check: number
|
||||||
par_uid: number;
|
par_uid: number
|
||||||
path: string;
|
path: string
|
||||||
lang_type: number;
|
lang_type: number
|
||||||
reg_type: number;
|
reg_type: number
|
||||||
status: number;
|
status: number
|
||||||
invite_code: string;
|
invite_code: string
|
||||||
created_at: string;
|
created_at: string
|
||||||
updated_at: string;
|
updated_at: string
|
||||||
deleted_at: string | null;
|
deleted_at: string | null
|
||||||
token: string;
|
token: string
|
||||||
|
redis_key: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// 菜单子项类型
|
// 菜单子项类型
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
/**
|
||||||
|
* 帧级节流器数据项接口
|
||||||
|
*/
|
||||||
|
interface ThrottlerDataItem {
|
||||||
|
id: string | number;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* listFrameThrottler - 帧级节流器 -- 只用于持仓订单
|
||||||
|
* 收集同一帧内的所有数据,帧结束时只推送最后一个值
|
||||||
|
*/
|
||||||
|
export class ListFrameThrottler<T extends ThrottlerDataItem> {
|
||||||
|
private callback: (data: T) => void;
|
||||||
|
private pendingData: Record<string | number, T>;
|
||||||
|
private rafId: number | null;
|
||||||
|
|
||||||
|
constructor(callback: (data: T) => void) {
|
||||||
|
this.callback = callback;
|
||||||
|
this.pendingData = {};
|
||||||
|
this.rafId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 推送数据(同一帧内多次调用只保留最后一个)
|
||||||
|
* @param data - 要推送的数据
|
||||||
|
*/
|
||||||
|
push(data: T): void {
|
||||||
|
// 将数据放入待处理数组
|
||||||
|
this.pendingData[data.id] = data;
|
||||||
|
|
||||||
|
// 如果还没有安排帧回调,则注册一个
|
||||||
|
if (!this.rafId) {
|
||||||
|
this.rafId = requestAnimationFrame(() => this._flush());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 帧结束时的回调:只推送最后一个数据
|
||||||
|
*/
|
||||||
|
private _flush(): void {
|
||||||
|
this.rafId = null;
|
||||||
|
const list = Object.values(this.pendingData);
|
||||||
|
if (list.length === 0) return;
|
||||||
|
|
||||||
|
this.pendingData = {}; // 清空缓存
|
||||||
|
for (let i = 0; i < list.length; i++) {
|
||||||
|
this.callback(list[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 立即刷新(手动触发)
|
||||||
|
*/
|
||||||
|
flush(): void {
|
||||||
|
if (this.rafId) {
|
||||||
|
cancelAnimationFrame(this.rafId);
|
||||||
|
this.rafId = null;
|
||||||
|
}
|
||||||
|
this._flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 销毁实例
|
||||||
|
*/
|
||||||
|
destroy(): void {
|
||||||
|
if (this.rafId) {
|
||||||
|
cancelAnimationFrame(this.rafId);
|
||||||
|
this.rafId = null;
|
||||||
|
}
|
||||||
|
this.pendingData = {};
|
||||||
|
this.callback = () => {}; // 避免 null 后可能的调用问题,或保持 null 但需调整类型
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,314 @@
|
||||||
|
import mqtt from 'mqtt'
|
||||||
|
import type { MqttClient, IClientOptions, IClientPublishOptions } from 'mqtt'
|
||||||
|
import type { QoS } from 'mqtt-packet'
|
||||||
|
/**
|
||||||
|
* 消息处理器类型定义
|
||||||
|
* 接收主题名称和消息内容两个参数
|
||||||
|
*/
|
||||||
|
type MessageHandler = (topic: string, message: string) => void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主题-回调映射对象类型
|
||||||
|
*/
|
||||||
|
type TopicCallbackMap = Record<string, MessageHandler>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MQTT 客户端封装类 - 支持多主题订阅管理
|
||||||
|
*/
|
||||||
|
class MQTTClient {
|
||||||
|
/** MQTT 连接实例 */
|
||||||
|
private client: MqttClient | null = null
|
||||||
|
|
||||||
|
/** 连接状态标志 */
|
||||||
|
private isConnected: boolean = false
|
||||||
|
|
||||||
|
/** 使用 Set 存储已订阅的主题,自动去重,便于重连时恢复订阅 */
|
||||||
|
private subscribedTopics: Set<string> = new Set()
|
||||||
|
|
||||||
|
/** 使用 Map 存储主题与回调函数的映射关系,实现精准的消息分发 */
|
||||||
|
private messageHandlers: Map<string, MessageHandler> = new Map()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数 - 初始化客户端状态
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
// 状态已在属性声明时初始化
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 建立 MQTT 连接
|
||||||
|
* @param url - 服务器地址,如 'wss://broker.emqx.io:8084/mqtt'
|
||||||
|
* @param options - 连接配置选项
|
||||||
|
* @returns 客户端实例,便于链式调用或外部访问
|
||||||
|
*/
|
||||||
|
connect(url: string, options: Partial<IClientOptions> = {}): MqttClient {
|
||||||
|
// 合并默认配置与用户传入的配置
|
||||||
|
const defaultOptions: IClientOptions = {
|
||||||
|
clean: true, // true: 清除会话,false: 保留会话(离线消息)
|
||||||
|
connectTimeout: 4000, // 连接超时时间(毫秒)
|
||||||
|
reconnectPeriod: 5000, // 自动重连间隔(毫秒)
|
||||||
|
// 生成唯一客户端ID,格式:vue3-client-8位随机十六进制
|
||||||
|
clientId: 'vue3-client-' + Math.random().toString(16).substr(2, 8),
|
||||||
|
...options, // 展开用户自定义选项,覆盖默认值
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 MQTT 连接实例
|
||||||
|
this.client = mqtt.connect(url, defaultOptions)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 连接成功事件
|
||||||
|
* 触发时机:首次连接成功或断线重连成功
|
||||||
|
*/
|
||||||
|
this.client.on('connect', () => {
|
||||||
|
console.log('✅ MQTT 连接成功')
|
||||||
|
this.isConnected = true
|
||||||
|
|
||||||
|
// 重连场景:自动恢复之前的所有订阅
|
||||||
|
// 避免网络波动导致订阅丢失
|
||||||
|
this.resubscribeAll()
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息到达事件
|
||||||
|
* 触发时机:收到已订阅主题的任何消息
|
||||||
|
*/
|
||||||
|
this.client.on('message', (topic: string, message: Uint8Array) => {
|
||||||
|
// 从映射表中查找该主题对应的处理函数
|
||||||
|
const handler = this.messageHandlers.get(topic)
|
||||||
|
|
||||||
|
if (handler) {
|
||||||
|
// 将 Buffer 转换为字符串后传递给回调函数
|
||||||
|
handler(topic, message.toString())
|
||||||
|
} else {
|
||||||
|
// 未找到处理函数时的降级处理
|
||||||
|
console.warn(`收到未处理的主题消息: ${topic}`, message.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 错误事件
|
||||||
|
* 触发时机:连接过程中发生错误(认证失败、网络异常等)
|
||||||
|
*/
|
||||||
|
this.client.on('error', (error: Error) => {
|
||||||
|
console.error('❌ MQTT 连接错误:', error)
|
||||||
|
this.isConnected = false
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重连事件
|
||||||
|
* 触发时机:连接断开,客户端尝试自动重连时
|
||||||
|
*/
|
||||||
|
this.client.on('reconnect', () => {
|
||||||
|
console.log('🔄 MQTT 正在重连...')
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 连接关闭事件
|
||||||
|
* 触发时机:连接被关闭(网络断开、手动断开、服务器踢出)
|
||||||
|
*/
|
||||||
|
this.client.on('close', () => {
|
||||||
|
console.log('🔌 MQTT 连接关闭')
|
||||||
|
this.isConnected = false
|
||||||
|
})
|
||||||
|
|
||||||
|
// 返回客户端实例,便于链式调用或外部访问
|
||||||
|
return this.client
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅一个或多个主题
|
||||||
|
* 支持单主题字符串或主题数组,实现批量订阅
|
||||||
|
*
|
||||||
|
* @param topics - 单个主题或主题数组
|
||||||
|
* @param qos - 服务质量等级 (0:最多一次, 1:至少一次, 2:恰好一次)
|
||||||
|
* @returns 返回 Promise,resolve 时传入成功订阅的主题列表
|
||||||
|
*
|
||||||
|
* 使用示例:
|
||||||
|
* subscribe('sensor/temp') // 订阅单个主题
|
||||||
|
* subscribe(['sensor/temp', 'sensor/hum']) // 批量订阅两个主题
|
||||||
|
*/
|
||||||
|
subscribe(topics: string | string[], qos: QoS = 0): Promise<string[]> {
|
||||||
|
// 检查连接状态,未连接时拒绝订阅请求
|
||||||
|
if (!this.isConnected || !this.client) {
|
||||||
|
console.warn('MQTT 未连接,无法订阅')
|
||||||
|
return Promise.reject('MQTT 未连接')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一转换为数组处理:如果是字符串则包装成数组,数组则直接使用
|
||||||
|
const topicArray: string[] = Array.isArray(topics) ? topics : [topics]
|
||||||
|
|
||||||
|
// 返回 Promise 便于使用 async/await 等待订阅完成
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
/**
|
||||||
|
* 调用 mqtt.js 的 subscribe 方法
|
||||||
|
* 第一个参数:主题或主题数组
|
||||||
|
* 第二个参数:选项对象,这里设置 QoS 等级
|
||||||
|
* 第三个参数:回调函数,err 为 null 表示成功
|
||||||
|
*/
|
||||||
|
this.client!.subscribe(topicArray, { qos }, (err?: Error | null) => {
|
||||||
|
if (err) {
|
||||||
|
// 订阅失败(权限不足、主题格式错误等)
|
||||||
|
console.error('订阅失败:', err)
|
||||||
|
reject(err)
|
||||||
|
} else {
|
||||||
|
// 订阅成功,将每个主题加入追踪集合
|
||||||
|
topicArray.forEach((topic: string) => {
|
||||||
|
this.subscribedTopics.add(topic)
|
||||||
|
console.log(`✅ 已订阅主题: ${topic} (QoS: ${qos})`)
|
||||||
|
})
|
||||||
|
// resolve 返回成功订阅的主题数组
|
||||||
|
resolve(topicArray)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为单个主题注册消息处理函数
|
||||||
|
* 实现主题与回调的一对一绑定
|
||||||
|
*
|
||||||
|
* @param topic - 主题名称
|
||||||
|
* @param callback - 回调函数,接收 (topic, message) 两个参数
|
||||||
|
*
|
||||||
|
* 使用示例:
|
||||||
|
* onTopicMessage('sensor/temp', (topic, msg) => {
|
||||||
|
* console.log('温度:', JSON.parse(msg).value)
|
||||||
|
* })
|
||||||
|
*/
|
||||||
|
onTopicMessage(topic: string, callback: MessageHandler): void {
|
||||||
|
// 将主题与回调函数存入 Map
|
||||||
|
this.messageHandlers.set(topic, callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量注册主题处理函数
|
||||||
|
* 通过对象映射一次性配置多个主题的处理逻辑
|
||||||
|
*
|
||||||
|
* @param topicCallbackMap - 主题-回调映射对象
|
||||||
|
* key: 主题名称
|
||||||
|
* value: 处理该主题的回调函数
|
||||||
|
*
|
||||||
|
* 使用示例:
|
||||||
|
* onTopicMessages({
|
||||||
|
* 'sensor/temp': (t, m) => handleTemp(m),
|
||||||
|
* 'sensor/hum': (t, m) => handleHum(m),
|
||||||
|
* 'alert/urgent': (t, m) => showAlert(m)
|
||||||
|
* })
|
||||||
|
*/
|
||||||
|
onTopicMessages(topicCallbackMap: TopicCallbackMap): void {
|
||||||
|
// 遍历对象的所有键值对
|
||||||
|
Object.entries(topicCallbackMap).forEach(([topic, callback]) => {
|
||||||
|
// 逐个存入 messageHandlers Map
|
||||||
|
this.messageHandlers.set(topic, callback)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重连时恢复所有订阅
|
||||||
|
* 私有方法,在 connect 事件触发时自动调用
|
||||||
|
* 确保网络波动断线重连后,订阅关系自动恢复
|
||||||
|
*/
|
||||||
|
private resubscribeAll(): void {
|
||||||
|
// 检查是否有历史订阅记录
|
||||||
|
if (this.subscribedTopics.size > 0) {
|
||||||
|
// 将 Set 转换为数组
|
||||||
|
const topics: string[] = Array.from(this.subscribedTopics)
|
||||||
|
console.log('🔄 重连后恢复订阅:', topics)
|
||||||
|
|
||||||
|
// 重新订阅所有主题
|
||||||
|
this.subscribe(topics)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消订阅一个或多个主题
|
||||||
|
* 清理订阅关系,释放资源
|
||||||
|
*
|
||||||
|
* @param topics - 要取消订阅的主题
|
||||||
|
* @returns 操作完成的 Promise
|
||||||
|
*/
|
||||||
|
unsubscribe(topics: string | string[]): Promise<void> {
|
||||||
|
// 统一处理为数组
|
||||||
|
const topicArray: string[] = Array.isArray(topics) ? topics : [topics]
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!this.client) {
|
||||||
|
reject(new Error('MQTT 客户端未初始化'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.client.unsubscribe(topicArray, (err?: Error) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err)
|
||||||
|
} else {
|
||||||
|
// 清理内部状态:从追踪集合和处理器映射中移除
|
||||||
|
topicArray.forEach((topic: string) => {
|
||||||
|
this.subscribedTopics.delete(topic)
|
||||||
|
this.messageHandlers.delete(topic)
|
||||||
|
console.log(`❌ 已取消订阅: ${topic}`)
|
||||||
|
})
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向指定主题发布消息
|
||||||
|
*
|
||||||
|
* @param topic - 目标主题
|
||||||
|
* @param message - 消息内容,对象会自动序列化为 JSON
|
||||||
|
* @param qos - 服务质量等级
|
||||||
|
* @returns 是否发送成功
|
||||||
|
*/
|
||||||
|
publish(topic: string, message: string | object, qos: number = 0): boolean {
|
||||||
|
if (!this.isConnected || !this.client) {
|
||||||
|
console.warn('MQTT 未连接,无法发送消息')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动序列化对象类型消息
|
||||||
|
const payload: string = typeof message === 'string' ? message : JSON.stringify(message)
|
||||||
|
|
||||||
|
// 调用 mqtt.js 的 publish 方法
|
||||||
|
this.client.publish(topic, payload, { qos } as IClientPublishOptions)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 断开 MQTT 连接并清理资源
|
||||||
|
* 应用退出或组件销毁时调用,防止内存泄漏
|
||||||
|
*/
|
||||||
|
disconnect(): void {
|
||||||
|
// 清空所有内部状态
|
||||||
|
this.subscribedTopics.clear()
|
||||||
|
this.messageHandlers.clear()
|
||||||
|
|
||||||
|
// 关闭连接
|
||||||
|
this.client?.end()
|
||||||
|
|
||||||
|
// 重置状态标志
|
||||||
|
this.isConnected = false
|
||||||
|
console.log('🔌 MQTT 已断开连接')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前连接状态
|
||||||
|
* @returns 是否已连接
|
||||||
|
*/
|
||||||
|
getConnected(): boolean {
|
||||||
|
return this.isConnected
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取已订阅的主题列表
|
||||||
|
* @returns 已订阅主题数组
|
||||||
|
*/
|
||||||
|
getSubscribedTopics(): string[] {
|
||||||
|
return Array.from(this.subscribedTopics)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出实例
|
||||||
|
export { MQTTClient }
|
||||||
|
export type { MessageHandler, TopicCallbackMap }
|
||||||
|
|
@ -0,0 +1,300 @@
|
||||||
|
/**
|
||||||
|
* 可接受的日期输入类型
|
||||||
|
*/
|
||||||
|
type DateInput = Date | string | number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 时间单位类型
|
||||||
|
*/
|
||||||
|
type TimeUnit = 'year' | 'month' | 'day' | 'hour' | 'minute' | 'second';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 时间段类型
|
||||||
|
*/
|
||||||
|
type RangeType = 'today' | 'week' | 'month' | 'year';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 时间范围对象
|
||||||
|
*/
|
||||||
|
export interface TimeRange {
|
||||||
|
start: Date;
|
||||||
|
end: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化日期后的完整对象
|
||||||
|
*/
|
||||||
|
interface FormattedDate {
|
||||||
|
date: string;
|
||||||
|
datetime: string;
|
||||||
|
iso: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 近七天时间范围返回结构
|
||||||
|
*/
|
||||||
|
export interface Last7DaysRange {
|
||||||
|
startTime: Date;
|
||||||
|
endTime: Date;
|
||||||
|
startTimeStr: FormattedDate;
|
||||||
|
endTimeStr: FormattedDate;
|
||||||
|
startTimestamp: number;
|
||||||
|
endTimestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化日期为指定格式
|
||||||
|
* @param date - 日期对象、字符串或时间戳
|
||||||
|
* @param format - 格式模板,如 'YYYY-MM-DD HH:mm:ss'
|
||||||
|
* @param format - 格式模板,如 'YYYY-MM-DD HH:mm:ss'
|
||||||
|
* @returns 格式化后的日期字符串
|
||||||
|
*/
|
||||||
|
export function formatDate(date: DateInput, format = 'YYYY-MM-DD HH:mm:ss'): string {
|
||||||
|
const d = new Date(date);
|
||||||
|
|
||||||
|
const map: Record<string, string | number> = {
|
||||||
|
YYYY: d.getFullYear(),
|
||||||
|
MM: String(d.getMonth() + 1).padStart(2, '0'),
|
||||||
|
DD: String(d.getDate()).padStart(2, '0'),
|
||||||
|
HH: String(d.getHours()).padStart(2, '0'),
|
||||||
|
mm: String(d.getMinutes()).padStart(2, '0'),
|
||||||
|
ss: String(d.getSeconds()).padStart(2, '0'),
|
||||||
|
};
|
||||||
|
|
||||||
|
return format.replace(/YYYY|MM|DD|HH|mm|ss/g, (match) => String(map[match]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用示例
|
||||||
|
// formatDate(new Date(), 'YYYY-MM-DD'); // "2026-03-16"
|
||||||
|
// formatDate(Date.now(), 'YYYY年MM月DD日'); // "2026年03月16日"
|
||||||
|
// formatDate('2026-03-16', 'MM/DD/YYYY HH:mm'); // "03/16/2026 08:00"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算相对于当前时间的友好提示
|
||||||
|
* @param date - 目标日期
|
||||||
|
* @returns 友好时间描述
|
||||||
|
*/
|
||||||
|
function timeAgo(date: DateInput): string {
|
||||||
|
const now = new Date();
|
||||||
|
const past = new Date(date);
|
||||||
|
const diff = now.getTime() - past.getTime();
|
||||||
|
|
||||||
|
const seconds = Math.floor(diff / 1000);
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
const days = Math.floor(hours / 24);
|
||||||
|
const months = Math.floor(days / 30);
|
||||||
|
const years = Math.floor(days / 365);
|
||||||
|
|
||||||
|
if (seconds < 60) return '刚刚';
|
||||||
|
if (minutes < 60) return `${minutes}分钟前`;
|
||||||
|
if (hours < 24) return `${hours}小时前`;
|
||||||
|
if (days < 30) return `${days}天前`;
|
||||||
|
if (months < 12) return `${months}个月前`;
|
||||||
|
return `${years}年前`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用示例
|
||||||
|
// timeAgo(Date.now() - 1000 * 60 * 5); // "5分钟前"
|
||||||
|
// timeAgo('2026-03-14'); // "2天前"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对日期进行加减运算
|
||||||
|
* @param date - 基准日期
|
||||||
|
* @param amount - 数量(可为负数)
|
||||||
|
* @param unit - 单位:year|month|day|hour|minute|second
|
||||||
|
* @returns 运算后的新 Date 对象
|
||||||
|
*/
|
||||||
|
function addTime(date: DateInput, amount: number, unit: TimeUnit): Date {
|
||||||
|
const d = new Date(date);
|
||||||
|
|
||||||
|
const map: Record<TimeUnit, string> = {
|
||||||
|
year: 'FullYear',
|
||||||
|
month: 'Month',
|
||||||
|
day: 'Date',
|
||||||
|
hour: 'Hours',
|
||||||
|
minute: 'Minutes',
|
||||||
|
second: 'Seconds',
|
||||||
|
};
|
||||||
|
|
||||||
|
const method = `set${map[unit]}` as keyof Date;
|
||||||
|
const getMethod = `get${map[unit]}` as keyof Date;
|
||||||
|
|
||||||
|
(d as any)[method]((d as any)[getMethod]() + amount);
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用示例
|
||||||
|
// addTime(new Date(), 3, 'day'); // 3天后
|
||||||
|
// addTime(new Date(), -1, 'month'); // 1个月前
|
||||||
|
// addTime('2026-03-16', 2, 'year'); // 2028-03-16
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定时间段的开始和结束时间
|
||||||
|
* @param type - 时间段类型
|
||||||
|
* @param date - 基准日期,默认为今天
|
||||||
|
* @returns 包含 start 和 end 的对象
|
||||||
|
*/
|
||||||
|
function getTimeRange(type: RangeType, date: Date = new Date()): TimeRange {
|
||||||
|
const d = new Date(date);
|
||||||
|
let start: Date;
|
||||||
|
let end: Date;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'today':
|
||||||
|
start = new Date(d.setHours(0, 0, 0, 0));
|
||||||
|
end = new Date(d.setHours(23, 59, 59, 999));
|
||||||
|
break;
|
||||||
|
case 'week': {
|
||||||
|
const day = d.getDay() || 7; // 周日设为7
|
||||||
|
start = new Date(d.setDate(d.getDate() - day + 1));
|
||||||
|
start.setHours(0, 0, 0, 0);
|
||||||
|
end = new Date(d.setDate(start.getDate() + 6));
|
||||||
|
end.setHours(23, 59, 59, 999);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'month':
|
||||||
|
start = new Date(d.getFullYear(), d.getMonth(), 1);
|
||||||
|
end = new Date(d.getFullYear(), d.getMonth() + 1, 0, 23, 59, 59, 999);
|
||||||
|
break;
|
||||||
|
case 'year':
|
||||||
|
start = new Date(d.getFullYear(), 0, 1);
|
||||||
|
end = new Date(d.getFullYear(), 11, 31, 23, 59, 59, 999);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// 兜底处理,理论上不会进入
|
||||||
|
start = new Date(d.setHours(0, 0, 0, 0));
|
||||||
|
end = new Date(d.setHours(23, 59, 59, 999));
|
||||||
|
}
|
||||||
|
|
||||||
|
return { start, end };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用示例
|
||||||
|
// getTimeRange('today'); // 今天 00:00:00 ~ 23:59:59
|
||||||
|
// getTimeRange('week'); // 本周一 00:00:00 ~ 本周日 23:59:59
|
||||||
|
// getTimeRange('month'); // 本月1号 00:00:00 ~ 本月最后一天 23:59:59
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将秒数转换为可读的持续时间
|
||||||
|
* @param seconds - 秒数
|
||||||
|
* @param showSeconds - 是否显示秒
|
||||||
|
* @returns 格式化后的时长字符串
|
||||||
|
*/
|
||||||
|
function formatDuration(seconds: number, showSeconds = true): string {
|
||||||
|
const hours = Math.floor(seconds / 3600);
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60);
|
||||||
|
const secs = Math.floor(seconds % 60);
|
||||||
|
|
||||||
|
const parts: string[] = [];
|
||||||
|
if (hours > 0) parts.push(`${hours}小时`);
|
||||||
|
if (minutes > 0 || hours > 0) parts.push(`${minutes}分钟`);
|
||||||
|
if (showSeconds && (secs > 0 || parts.length === 0)) {
|
||||||
|
parts.push(`${secs}秒`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join('') || '0秒';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用示例
|
||||||
|
// formatDuration(3665); // "1小时1分钟5秒"
|
||||||
|
// formatDuration(125, false); // "2分钟"
|
||||||
|
// formatDuration(45); // "45秒"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否为闰年
|
||||||
|
* @param year - 年份
|
||||||
|
*/
|
||||||
|
function isLeapYear(year: number): boolean {
|
||||||
|
return (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取某月的天数
|
||||||
|
* @param year - 年份
|
||||||
|
* @param month - 月份(1-12)
|
||||||
|
*/
|
||||||
|
function getDaysInMonth(year: number, month: number): number {
|
||||||
|
return new Date(year, month, 0).getDate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否为工作日(周一至周五)
|
||||||
|
* @param date - 日期
|
||||||
|
*/
|
||||||
|
function isWeekday(date: DateInput): boolean {
|
||||||
|
const day = new Date(date).getDay();
|
||||||
|
return day !== 0 && day !== 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用示例
|
||||||
|
// isLeapYear(2024); // true
|
||||||
|
// getDaysInMonth(2026, 2); // 28
|
||||||
|
// isWeekday('2026-03-16'); // true (周一)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 ISO 字符串转为时间戳
|
||||||
|
* @param isoString - ISO 8601 格式字符串
|
||||||
|
* @returns 毫秒时间戳
|
||||||
|
*/
|
||||||
|
export function toTimestamp(isoString: string): number {
|
||||||
|
return new Date(isoString).getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取近七天的开始和结束时间
|
||||||
|
* @returns 包含开始时间、结束时间及其各种格式的对象
|
||||||
|
*/
|
||||||
|
export function getLast7DaysRange(): Last7DaysRange {
|
||||||
|
const endDate = new Date();
|
||||||
|
const startDate = new Date();
|
||||||
|
|
||||||
|
// 结束时间:今天 23:59:59.999
|
||||||
|
endDate.setHours(23, 59, 59, 999);
|
||||||
|
|
||||||
|
// 开始时间:7天前 00:00:00.000(今天也算一天,所以减6)
|
||||||
|
startDate.setDate(startDate.getDate() - 6);
|
||||||
|
startDate.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
startTime: startDate,
|
||||||
|
endTime: endDate,
|
||||||
|
startTimeStr: formatDates(startDate),
|
||||||
|
endTimeStr: formatDates(endDate),
|
||||||
|
startTimestamp: startDate.getTime(),
|
||||||
|
endTimestamp: endDate.getTime(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化日期为常用字符串格式
|
||||||
|
* @param date - 日期
|
||||||
|
* @returns 包含 date、datetime、iso 的对象
|
||||||
|
*/
|
||||||
|
function formatDates(date: DateInput): FormattedDate {
|
||||||
|
const d = new Date(date);
|
||||||
|
const year = d.getFullYear();
|
||||||
|
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(d.getDate()).padStart(2, '0');
|
||||||
|
const hours = String(d.getHours()).padStart(2, '0');
|
||||||
|
const minutes = String(d.getMinutes()).padStart(2, '0');
|
||||||
|
const seconds = String(d.getSeconds()).padStart(2, '0');
|
||||||
|
|
||||||
|
return {
|
||||||
|
date: `${year}-${month}-${day}`,
|
||||||
|
datetime: `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`,
|
||||||
|
iso: d.toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 使用示例 ============
|
||||||
|
|
||||||
|
// const range = getLast7DaysRange();
|
||||||
|
|
||||||
|
// console.log('近七天时间范围:');
|
||||||
|
// console.log('开始时间对象:', range.startTime);
|
||||||
|
// console.log('结束时间对象:', range.endTime);
|
||||||
|
// console.log('开始日期:', range.startTimeStr.date); // 2026-03-24
|
||||||
|
// console.log('结束日期:', range.endTimeStr.date); // 2026-03-30
|
||||||
|
// console.log('开始时间戳:', range.startTimestamp);
|
||||||
|
// console.log('结束时间戳:', range.endTimestamp);
|
||||||
|
|
@ -1,92 +1,80 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="position-stat">
|
<div class="position-stat">
|
||||||
<!-- 搜索 -->
|
<!-- 搜索 -->
|
||||||
<PositionStatSearch :search-form="searchForm" :search-options="searchOptions" @search="handleSearch"
|
<PositionStatSearch
|
||||||
@reset="handleReset">
|
:search-form="searchForm"
|
||||||
<template #button="{ form }">
|
:search-options="searchOptions"
|
||||||
<el-button type="primary" @click="handleExport()">
|
@search="handleSearch"
|
||||||
导出
|
@reset="handleReset"
|
||||||
</el-button>
|
>
|
||||||
</template>
|
<!-- <template #button="{ form }">-->
|
||||||
</PositionStatSearch>
|
<!-- <el-button type="primary" @click="handleExport()">-->
|
||||||
|
<!-- 导出-->
|
||||||
|
<!-- </el-button>-->
|
||||||
|
<!-- </template>-->
|
||||||
|
</PositionStatSearch>
|
||||||
<!-- 表格 -->
|
<!-- 表格 -->
|
||||||
<PositionStatTable :table-data="tableTree" :columns="tableColumnsOptions" row-key="id" />
|
<PositionStatTable
|
||||||
|
:table-data="orderPositionStore.positionOrderList"
|
||||||
<!-- 分页 -->
|
:columns="tableColumnsOptions"
|
||||||
<PositionStatPagination v-model="currentPage" v-model:pageSizeValue="pageSize" :total="total" @pagination-change="handlePaginationChange" />
|
row-key="id"
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'Position'
|
name: 'Position',
|
||||||
})
|
})
|
||||||
// 组件
|
// 组件
|
||||||
import PositionStatSearch from '@/components/common/Search.vue'
|
import PositionStatSearch from '@/components/common/Search.vue'
|
||||||
import PositionStatTable from '@/components/common/Table.vue'
|
import PositionStatTable from '@/components/common/Table.vue'
|
||||||
import PositionStatPagination from '@/components/common/Pagination.vue'
|
import { checkAccountIdIsSonForUser } from '@/api/modules/user.ts'
|
||||||
// 配置以及表单验证
|
// 配置以及表单验证
|
||||||
import { search, tableColumns } from './options'
|
import { search, tableColumns } from './options'
|
||||||
|
|
||||||
// 接口
|
import { useTransactionStore } from '@/stores/modules/transaction.ts'
|
||||||
|
import { useOrderPositionStore } from '@/stores/modules/orderPosition.ts'
|
||||||
|
|
||||||
// 导出表格
|
// 导出表格
|
||||||
import {exportToExcel} from '@/utils/exportToExcel'
|
import { exportToExcel } from '@/utils/exportToExcel'
|
||||||
|
|
||||||
|
const transactionStore = useTransactionStore()
|
||||||
|
const orderPositionStore = useOrderPositionStore()
|
||||||
/**
|
/**
|
||||||
* @description: 定义搜索数据
|
* @description: 定义搜索数据
|
||||||
*/
|
*/
|
||||||
const searchOptions = ref(search)
|
const searchOptions = ref(search)
|
||||||
const searchForm = ref({
|
const searchForm = ref({
|
||||||
orderNo: '',
|
account_id: '',
|
||||||
tradeType: '',
|
|
||||||
account: '',
|
|
||||||
startTime: '',
|
|
||||||
endTime: '',
|
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
* @description: 定义表格数据
|
* @description: 定义表格数据
|
||||||
*/
|
*/
|
||||||
const tableTree = ref([])
|
const tableTree = ref([])
|
||||||
const tableColumnsOptions = ref(tableColumns)
|
const tableColumnsOptions = ref(tableColumns)
|
||||||
/**
|
|
||||||
* @description: 定义分页数据
|
|
||||||
*/
|
|
||||||
const currentPage = ref(1)
|
|
||||||
const pageSize = ref(10)
|
|
||||||
const total = ref(0)
|
|
||||||
/**
|
|
||||||
* @description: 定义公共数据处理方法
|
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* @description: 定义公共请求数据方法
|
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* @description: 页面加载完成需要请求的数据
|
|
||||||
*/
|
|
||||||
/**
|
/**
|
||||||
* @description: 定义搜索方法
|
* @description: 定义搜索方法
|
||||||
*/
|
*/
|
||||||
const handleSearch = async () => {
|
const handleSearch = async () => {
|
||||||
console.log("搜索");
|
const data = await checkAccountIdIsSonForUser({ account_id: searchForm.value.account_id })
|
||||||
|
if (!data.flag) {
|
||||||
|
return ElMessage.warning('请输入下级ID')
|
||||||
|
}
|
||||||
|
transactionStore.getPositionList(searchForm.value.account_id)
|
||||||
}
|
}
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
console.log('重置搜索')
|
console.log('重置搜索')
|
||||||
}
|
}
|
||||||
const handleExport = () => {
|
const handleExport = () => {
|
||||||
exportToExcel({
|
exportToExcel({
|
||||||
tableData: tableTree.value,
|
tableData: tableTree.value,
|
||||||
columns: tableColumnsOptions.value,
|
columns: tableColumnsOptions.value,
|
||||||
title: '导出资金明细',
|
title: '导出资金明细',
|
||||||
defaultFileNamePrefix: '资金明细',
|
defaultFileNamePrefix: '资金明细',
|
||||||
})
|
})
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @description: 定义分页方法
|
|
||||||
*/
|
|
||||||
const handlePaginationChange = (page: {currentPage: number, size: number}) => {
|
|
||||||
currentPage.value = page.currentPage
|
|
||||||
}
|
}
|
||||||
|
onMounted(() => {
|
||||||
|
transactionStore.initMarketConditionsMQTT()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,11 @@
|
||||||
export const search = [
|
export const search = [
|
||||||
// 订单号
|
{
|
||||||
{ prop: 'orderNo', label: '订单号', type: 'input' as const, placeholder: '请输入订单号',width: '200px' },
|
prop: 'account_id',
|
||||||
// 交易类型
|
label: '账户ID',
|
||||||
{ prop: 'tradeType', label: '交易类型', type: 'select' as const, placeholder: '请选择交易类型',width: '150px', options: [
|
type: 'input' as const,
|
||||||
{ label: '买入', value: '1' },
|
placeholder: '请输入账户ID',
|
||||||
{ label: '卖出', value: '2' },
|
width: '200px',
|
||||||
] },
|
},
|
||||||
//账号
|
|
||||||
{ prop: 'account', label: '账号', type: 'select' as const, placeholder: '请选择账号',width: '150px' },
|
|
||||||
//开始时间
|
|
||||||
// {
|
|
||||||
// prop: 'startTime', label: '开始时间', type: 'date' as const, dateType: 'date' as const, // 时间范围选择器
|
|
||||||
// valueFormat: 'YYYY-MM-DD', placeholder: '请选择开始时间', width: '150px'
|
|
||||||
// },
|
|
||||||
// //结束时间
|
|
||||||
// {
|
|
||||||
// prop: 'endTime', label: '结束时间', type: 'date' as const, dateType: 'date' as const, // 时间范围选择器
|
|
||||||
// valueFormat: 'YYYY-MM-DD', placeholder: '请选择结束时间', width: '150px'
|
|
||||||
// },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
export const tableColumns = [
|
export const tableColumns = [
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue