持仓订单接MQTT

This commit is contained in:
Songsl 2026-05-13 09:16:10 +08:00
parent 639aa79892
commit 40621376e5
12 changed files with 982 additions and 95 deletions

View File

@ -9,3 +9,6 @@ VITE_REQUEST_TIMEOUT = 10000
# 可选:开发环境其他配置(如是否开启调试)
VITE_DEBUG = true
# 交易mqtt
VITE_MQTT_TRANSACTION_HOST=ws://43.198.76.61:8083/mqtt

View File

@ -9,3 +9,6 @@ VITE_REQUEST_TIMEOUT = 10000
# 可选:生产环境其他配置(如关闭调试)
VITE_DEBUG = false
# 交易mqtt
VITE_MQTT_TRANSACTION_HOST=ws://43.198.76.61:8083/mqtt

View File

@ -28,6 +28,7 @@
"dayjs": "^1.11.20",
"element-plus": "^2.13.2",
"file-saver": "^2.0.5",
"mqtt": "^5.15.1",
"pinia": "^3.0.4",
"qrcode": "^1.5.4",
"vue": "^3.5.29",

View File

@ -86,3 +86,10 @@ export async function editUserApi(params: {
}) {
return request.post<boolean>('/user/editUser', params);
}
/**
*
*/
export async function checkAccountIdIsSonForUser(params: { account_id: string }) {
return request.get<boolean>('/user/checkAccountIdIsSonForUser', params)
}

View File

@ -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,
}
})

View File

@ -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,
}
})

View File

@ -2,24 +2,25 @@ import type { ApiResponse } from './index';
// 用户信息子类型
export interface UserInfo {
id: number;
username: string;
email: string;
mobile: string;
role_id: number;
pass_admin: string;
salt_code_admin: string;
real_check: number;
par_uid: number;
path: string;
lang_type: number;
reg_type: number;
status: number;
invite_code: string;
created_at: string;
updated_at: string;
deleted_at: string | null;
token: string;
id: number
username: string
email: string
mobile: string
role_id: number
pass_admin: string
salt_code_admin: string
real_check: number
par_uid: number
path: string
lang_type: number
reg_type: number
status: number
invite_code: string
created_at: string
updated_at: string
deleted_at: string | null
token: string
redis_key: string
}
// 菜单子项类型

View File

@ -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 但需调整类型
}
}

314
src/utils/mqttClient.ts Normal file
View File

@ -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 Promiseresolve
*
* 使
* 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 }

300
src/utils/timeUtils.ts Normal file
View File

@ -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 datedatetimeiso
*/
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);

View File

@ -1,76 +1,67 @@
<template>
<div class="position-stat">
<!-- 搜索 -->
<PositionStatSearch :search-form="searchForm" :search-options="searchOptions" @search="handleSearch"
@reset="handleReset">
<template #button="{ form }">
<el-button type="primary" @click="handleExport()">
导出
</el-button>
</template>
<PositionStatSearch
:search-form="searchForm"
:search-options="searchOptions"
@search="handleSearch"
@reset="handleReset"
>
<!-- <template #button="{ form }">-->
<!-- <el-button type="primary" @click="handleExport()">-->
<!-- 导出-->
<!-- </el-button>-->
<!-- </template>-->
</PositionStatSearch>
<!-- 表格 -->
<PositionStatTable :table-data="tableTree" :columns="tableColumnsOptions" row-key="id" />
<!-- 分页 -->
<PositionStatPagination v-model="currentPage" v-model:pageSizeValue="pageSize" :total="total" @pagination-change="handlePaginationChange" />
<PositionStatTable
:table-data="orderPositionStore.positionOrderList"
:columns="tableColumnsOptions"
row-key="id"
/>
</div>
</template>
<script setup lang="ts">
defineOptions({
name: 'Position'
name: 'Position',
})
//
import PositionStatSearch from '@/components/common/Search.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 { useTransactionStore } from '@/stores/modules/transaction.ts'
import { useOrderPositionStore } from '@/stores/modules/orderPosition.ts'
//
import { exportToExcel } from '@/utils/exportToExcel'
const transactionStore = useTransactionStore()
const orderPositionStore = useOrderPositionStore()
/**
* @description: 定义搜索数据
*/
const searchOptions = ref(search)
const searchForm = ref({
orderNo: '',
tradeType: '',
account: '',
startTime: '',
endTime: '',
account_id: '',
})
/**
* @description: 定义表格数据
*/
const tableTree = ref([])
const tableColumnsOptions = ref(tableColumns)
/**
* @description: 定义分页数据
*/
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(0)
/**
* @description: 定义公共数据处理方法
*/
/**
* @description: 定义公共请求数据方法
*/
/**
* @description: 页面加载完成需要请求的数据
*/
/**
* @description: 定义搜索方法
*/
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 = () => {
console.log('重置搜索')
@ -83,10 +74,7 @@ const handleExport = () => {
defaultFileNamePrefix: '资金明细',
})
}
/**
* @description: 定义分页方法
*/
const handlePaginationChange = (page: {currentPage: number, size: number}) => {
currentPage.value = page.currentPage
}
onMounted(() => {
transactionStore.initMarketConditionsMQTT()
})
</script>

View File

@ -1,23 +1,11 @@
export const search = [
// 订单号
{ prop: 'orderNo', label: '订单号', type: 'input' as const, placeholder: '请输入订单号',width: '200px' },
// 交易类型
{ prop: 'tradeType', label: '交易类型', type: 'select' as const, placeholder: '请选择交易类型',width: '150px', options: [
{ label: '买入', value: '1' },
{ label: '卖出', value: '2' },
] },
//账号
{ 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'
// },
{
prop: 'account_id',
label: '账户ID',
type: 'input' as const,
placeholder: '请输入账户ID',
width: '200px',
},
]
export const tableColumns = [