/** * Sanguo VeighNa Web 应用 * Vue.js 3 单页应用 */ const { createApp, ref, computed, onMounted, onUnmounted, watch, nextTick } = Vue; createApp({ setup() { // ============================================ // 状态定义 // ============================================ // 认证状态 const isLoggedIn = ref(false); const currentUser = ref({ username: '' }); const loginForm = ref({ username: '', password: '' }); const loginError = ref(''); const isLoading = ref(false); // 当前页面 const currentPage = ref('dashboard'); // 导航菜单 const navItems = [ { id: 'dashboard', label: '总览', icon: '' }, { id: 'market', label: '行情', icon: '' }, { id: 'trading', label: '交易', icon: '' }, { id: 'active_orders', label: '活动委托', icon: '' }, { id: 'trades', label: '成交', icon: '' }, { id: 'position', label: '持仓', icon: '' }, { id: 'account', label: '资金', icon: '' }, { id: 'contracts', label: '合约', icon: '' }, { id: 'gateway', label: '网关', icon: '' }, { id: 'strategy', label: '策略', icon: '' }, { id: 'global_settings', label: '全局配置', icon: '' }, { id: 'wechat', label: '微信通知', icon: '' }, { id: 'log', label: '日志', icon: '' } ]; // WebSocket 连接状态 const wsConnected = ref(false); // 系统状态 const systemStatus = ref({ vn_ready: false, main_engine: false, trading_engine: false }); // 网关状态 const gatewayStatus = ref({ online: false }); // 行情数据 const marketSearch = ref(''); const contracts = ref([]); const ticks = ref([]); const subscribedSymbols = ref(new Set()); // 账户数据 const accounts = ref([]); // 持仓数据 const positions = ref([]); // 订单数据 const allOrders = ref([]); const activeOrders = ref([]); // 策略数据 const strategies = ref([]); // 成交数据 const trades = ref([]); // 所有账户数据 const allAccounts = ref([]); // 网关数据 const gateways = ref([]); const selectedGateway = ref(null); const gatewayForm = ref({}); const showConnectDialog = ref(false); const isConnecting = ref(false); // 日志数据 const logs = ref([]); const autoScroll = ref(true); const logLevelFilter = ref(''); const logsContainer = ref(null); // 合约数据 const allContracts = ref([]); const contractSearch = ref(''); const contractFilter = ref({ field: '', value: '' }); // 选中的合约(用于市场深度盘口) const selectedTick = ref(null); // 全局配置 const globalSettings = ref({}); const globalSettingsLoading = ref(false); const globalSettingsError = ref(''); const isSavingSettings = ref(false); // 下单表单 const orderForm = ref({ symbol: '', direction: 'buy', order_type: 'limit', price: 0, volume: 1 }); const orderError = ref(''); const isOrdering = ref(false); // ============================================ // 计算属性 // ============================================ const currentPageTitle = computed(() => { const item = navItems.find(i => i.id === currentPage.value); return item ? item.label : 'Sanguo VeighNa'; }); const filteredTicks = computed(() => { if (!marketSearch.value) { return ticks.value; } const search = marketSearch.value.toLowerCase(); return ticks.value.filter(t => t.vt_symbol?.toLowerCase().includes(search) ); }); const recentLogs = computed(() => { return logs.value.slice(-5); }); const filteredLogs = computed(() => { if (!logLevelFilter.value) { return logs.value; } return logs.value.filter(l => l.level === logLevelFilter.value); }); // 表格排序状态 const tableSort = ref({ field: '', order: 'asc' // 'asc' or 'desc' }); // 合约过滤 const filteredContracts = computed(() => { let result = allContracts.value; // 搜索过滤 if (contractSearch.value) { const search = contractSearch.value.toLowerCase(); result = result.filter(c => c.vt_symbol?.toLowerCase().includes(search) || c.symbol?.toLowerCase().includes(search) || c.name?.toLowerCase().includes(search) || c.exchange?.toLowerCase().includes(search) ); } // 字段过滤 if (contractFilter.value.field && contractFilter.value.value) { result = result.filter(c => String(c[contractFilter.value.field])?.toLowerCase().includes(contractFilter.value.value.toLowerCase()) ); } return result; }); const totalPnL = computed(() => { return positions.value.reduce((sum, p) => sum + (p.pnl || 0), 0); }); // ============================================ // 格式化函数 // ============================================ const formatNumber = (num) => { if (num === null || num === undefined) return '-'; return Number(num).toFixed(2); }; const formatPercent = (num) => { if (num === null || num === undefined) return '-'; return (Number(num) * 100).toFixed(2) + '%'; }; const formatTime = (timestamp) => { if (!timestamp) return '-'; const date = new Date(timestamp); return date.toLocaleString('zh-CN'); }; const getPriceClass = (current, previous) => { if (current === null || current === undefined) return ''; if (previous === null || previous === undefined) return ''; if (current > previous) return 'price-up'; if (current < previous) return 'price-down'; return 'price-flat'; }; const getPnLClass = (pnl) => { if (pnl === null || pnl === undefined) return ''; if (pnl > 0) return 'pnl-positive'; if (pnl < 0) return 'pnl-negative'; return ''; }; const getDirectionClass = (direction) => { if (!direction) return ''; const dir = direction.toLowerCase(); if (dir === 'long' || dir === 'buy') return 'buy'; if (dir === 'short' || dir === 'sell') return 'sell'; return ''; }; // ============================================ // 成交相关 // ============================================ const refreshTrades = async () => { try { trades.value = await api.getLatestTrades(50); } catch (e) { console.error('Failed to refresh trades:', e); } }; // ============================================ // 资金相关 // ============================================ const refreshAllAccounts = async () => { try { allAccounts.value = await api.getAllAccounts(); } catch (e) { console.error('Failed to refresh accounts:', e); } }; // ============================================ // 网关相关 // ============================================ const refreshGateways = async () => { try { gateways.value = await api.getAllGateways(); } catch (e) { console.error('Failed to refresh gateways:', e); } }; const openConnectDialog = async (gatewayName) => { try { const setting = await api.getGatewaySetting(gatewayName); gatewayForm.value = { ...setting }; selectedGateway.value = gatewayName; showConnectDialog.value = true; } catch (e) { console.error('Failed to get gateway setting:', e); } }; const closeConnectDialog = () => { showConnectDialog.value = false; selectedGateway.value = null; gatewayForm.value = {}; }; const connectGateway = async () => { isConnecting.value = true; try { await api.connectGateway(selectedGateway.value, gatewayForm.value); closeConnectDialog(); await refreshGateways(); addLog({ level: 'INFO', message: `网关 ${selectedGateway.value} 连接请求已发送` }); } catch (e) { addLog({ level: 'ERROR', message: `连接网关失败: ${e.message || '未知错误'}` }); } finally { isConnecting.value = false; } }; const disconnectGateway = async (gatewayName) => { try { await api.disconnectGateway(gatewayName); await refreshGateways(); addLog({ level: 'INFO', message: `网关 ${gatewayName} 已断开连接` }); } catch (e) { addLog({ level: 'ERROR', message: `断开网关失败: ${e.message || '未知错误'}` }); } }; const getGatewayStatusClass = (gateway) => { if (gateway.connected) return 'status-online'; return 'status-offline'; }; const getGatewayStatusText = (gateway) => { if (gateway.connected) return '已连接'; return '未连接'; }; const getFormFieldClass = (fieldName) => { // 如果字段名包含"密码",返回密码类型 if (fieldName.includes('密码') || fieldName.toLowerCase().includes('password')) { return 'password'; } return 'text'; }; const isFormFieldPassword = (fieldName) => { return fieldName.includes('密码') || fieldName.toLowerCase().includes('password'); }; // ============================================ // 行情相关 // ============================================ const isSubscribed = (symbol) => { return subscribedSymbols.value.has(symbol); }; const subscribeSymbol = (symbol) => { if (subscribedSymbols.value.has(symbol)) { // 取消订阅 subscribedSymbols.value.delete(symbol); wsClient.unsubscribeSymbol([symbol]); } else { // 订阅 subscribedSymbols.value.add(symbol); wsClient.subscribeSymbol([symbol]); } }; const refreshMarket = async () => { try { ticks.value = await api.getTicks(); } catch (e) { console.error('Failed to refresh market data:', e); } }; // ============================================ // 双击交互处理 // ============================================ const handleTickDoubleClick = (tick) => { // 双击行情记录 → 更新交易表单 orderForm.value.symbol = tick.vt_symbol || tick.symbol; // 如果有最新价,更新价格 if (tick.last_price) { orderForm.value.price = tick.last_price; } // 切换到交易页面 currentPage.value = 'trading'; addLog({ level: 'INFO', message: `已从行情选择合约: ${tick.vt_symbol}` }); }; const handleOrderDoubleClick = (order) => { // 双击委托/活动委托 → 撤单 if (canCancel(order.status)) { cancelOrder(order.order_id); } else { addLog({ level: 'WARNING', message: `订单 ${order.order_id} 状态为 ${order.status},无法撤销` }); } }; const handlePositionDoubleClick = (position) => { // 双击持仓记录 → 更新交易表单(反向平仓) orderForm.value.symbol = position.vt_symbol || `${position.symbol}.${position.exchange}`; // 反向方向 if (position.direction?.toLowerCase() === 'long' || position.direction?.toLowerCase() === 'buy') { orderForm.value.direction = 'sell'; } else { orderForm.value.direction = 'buy'; } // 设置平仓数量 orderForm.value.volume = position.volume - (position.frozen || 0); // 切换到交易页面 currentPage.value = 'trading'; addLog({ level: 'INFO', message: `已从持仓选择合约: ${position.symbol},准备平仓` }); }; // ============================================ // CSV 导出功能 // ============================================ const exportTable = (tableType) => { let data = []; let filename = ''; let headers = []; switch (tableType) { case 'market': data = filteredTicks.value; filename = 'market_data'; headers = ['合约', '最新价', '买价', '卖价', '成交量']; break; case 'orders': data = allOrders.value; filename = 'orders'; headers = ['订单ID', '合约', '方向', '价格', '数量', '已成交', '状态']; break; case 'active_orders': data = activeOrders.value; filename = 'active_orders'; headers = ['订单ID', '合约', '方向', '价格', '数量', '已成交', '状态', '时间']; break; case 'positions': data = positions.value; filename = 'positions'; headers = ['合约', '交易所', '方向', '数量', '可用', '均价', '盈亏', '盈亏率']; break; case 'trades': data = trades.value; filename = 'trades'; headers = ['成交号', '委托号', '合约', '方向', '开平', '价格', '数量', '时间', '接口']; break; case 'accounts': data = allAccounts.value; filename = 'accounts'; headers = ['账号', '余额', '冻结', '可用', '接口']; break; case 'contracts': data = filteredContracts.value; filename = 'contracts'; headers = ['本地代码', '代码', '交易所', '名称', '分类', '乘数', '跳动点', '最小量']; break; default: return; } // 生成 CSV 内容 let csv = headers.join(',') + '\n'; data.forEach(row => { const values = []; switch (tableType) { case 'market': values.push(row.vt_symbol, row.last_price, row.bid_price_1, row.ask_price_1, row.volume); break; case 'orders': values.push(row.order_id, row.symbol, row.direction, row.price, row.volume, row.traded, row.status); break; case 'active_orders': values.push(row.order_id, row.symbol, row.direction, row.price, row.volume, row.traded, row.status, formatTime(row.time)); break; case 'positions': values.push(row.symbol, row.exchange, row.direction, row.volume, row.volume - (row.frozen || 0), row.price, row.pnl, formatPercent(row.pnl_ratio)); break; case 'trades': values.push(row.tradeid, row.orderid, row.symbol, row.direction, row.offset, row.price, row.volume, row.datetime, row.gateway_name); break; case 'accounts': values.push(row.accountid, row.balance, row.frozen, row.available, row.gateway_name); break; case 'contracts': values.push(row.vt_symbol, row.symbol, row.exchange, row.name, row.product, row.size, row.pricetick, row.min_volume); break; } csv += values.map(v => `"${v}"`).join(',') + '\n'; }); // 创建 Blob 并触发下载 const blob = new Blob(['' + csv], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); const url = URL.createObjectURL(blob); const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); link.setAttribute('href', url); link.setAttribute('download', `${filename}_${timestamp}.csv`); link.style.visibility = 'hidden'; document.body.appendChild(link); link.click(); document.body.removeChild(link); addLog({ level: 'INFO', message: `已导出 ${filename} 数据,共 ${data.length} 条记录` }); }; // ============================================ // 交易相关 // ============================================ const canCancel = (status) => { return ['submitted', 'pending', 'partial_filled'].includes( status?.toLowerCase() ); }; const sendOrder = async () => { orderError.value = ''; isOrdering.value = true; try { const result = await api.sendOrder({ symbol: orderForm.value.symbol.split('.')[0], exchange: orderForm.value.symbol.split('.')[1] || 'SIM', direction: orderForm.value.direction, order_type: orderForm.value.order_type, volume: orderForm.value.volume, price: orderForm.value.order_type === 'limit' ? orderForm.value.price : null }); // 刷新订单列表 await refreshOrders(); // 重置表单 orderForm.value = { symbol: '', direction: 'buy', order_type: 'limit', price: 0, volume: 1 }; } catch (e) { orderError.value = e.message || '下单失败'; } finally { isOrdering.value = false; } }; const cancelOrder = async (orderId) => { try { await api.cancelOrder(orderId); await refreshOrders(); } catch (e) { console.error('Failed to cancel order:', e); } }; const refreshOrders = async () => { try { const [all, active] = await Promise.all([ api.getOrders(), api.getActiveOrders() ]); allOrders.value = all; activeOrders.value = active; } catch (e) { console.error('Failed to refresh orders:', e); } }; // ============================================ // 持仓相关 // ============================================ const refreshPositions = async () => { try { positions.value = await api.getPositions(); } catch (e) { console.error('Failed to refresh positions:', e); } }; // ============================================ // 策略相关 // ============================================ const refreshStrategies = async () => { try { strategies.value = await api.getStrategies(); } catch (e) { console.error('Failed to refresh strategies:', e); } }; const startStrategy = async (name) => { try { await api.startStrategy(name); await refreshStrategies(); } catch (e) { console.error('Failed to start strategy:', e); } }; const stopStrategy = async (name) => { try { await api.stopStrategy(name); await refreshStrategies(); } catch (e) { console.error('Failed to stop strategy:', e); } }; // ============================================ // 全局配置相关 // ============================================ const refreshGlobalSettings = async () => { globalSettingsLoading.value = true; globalSettingsError.value = ''; try { const settings = await api.getGlobalSettings(); globalSettings.value = settings; } catch (e) { globalSettingsError.value = e.message || '加载全局配置失败'; console.error('Failed to load global settings:', e); } finally { globalSettingsLoading.value = false; } }; const saveGlobalSettings = async () => { isSavingSettings.value = true; try { await api.updateGlobalSettings(globalSettings.value); addLog({ level: 'INFO', message: '全局配置已保存,重启后生效' }); } catch (e) { addLog({ level: 'ERROR', message: `保存全局配置失败: ${e.message || '未知错误'}` }); } finally { isSavingSettings.value = false; } }; const getSettingType = (value) => { if (value === null || value === undefined) return 'null'; const type = typeof value; if (type === 'object') return Array.isArray(value) ? 'array' : 'object'; return type; }; // ============================================ // 日志相关 // ============================================ const addLog = (logData) => { logs.value.push({ time: logData.time || new Date().toISOString(), level: logData.level || 'INFO', message: logData.message || '' }); // 限制日志数量 if (logs.value.length > 1000) { logs.value = logs.value.slice(-1000); } // 自动滚动 if (autoScroll.value) { nextTick(() => { if (logsContainer.value) { logsContainer.value.scrollTop = logsContainer.value.scrollHeight; } }); } }; const clearLogs = () => { logs.value = []; }; // ============================================ // 排序功能 // ============================================ const sortTable = (field) => { if (tableSort.value.field === field) { // 切换排序方向 tableSort.value.order = tableSort.value.order === 'asc' ? 'desc' : 'asc'; } else { // 新字段,默认升序 tableSort.value.field = field; tableSort.value.order = 'asc'; } }; const getSortIcon = (field) => { if (tableSort.value.field !== field) { return '⇅'; } return tableSort.value.order === 'asc' ? '↑' : '↓'; }; // 排序数据 const sortData = (data, field, order) => { if (!field) return data; const sorted = [...data].sort((a, b) => { const aVal = a[field]; const bVal = b[field]; if (aVal === null || aVal === undefined) return 1; if (bVal === null || bVal === undefined) return -1; if (aVal === bVal) return 0; // 数字比较 if (typeof aVal === 'number' && typeof bVal === 'number') { return order === 'asc' ? aVal - bVal : bVal - aVal; } // 字符串比较 const aStr = String(aVal).toLowerCase(); const bStr = String(bVal).toLowerCase(); if (order === 'asc') { return aStr < bStr ? -1 : 1; } else { return aStr > bStr ? -1 : 1; } }); return sorted; }; // ============================================ // 合约相关 // ============================================ const refreshContracts = async () => { try { const data = await api.getContracts(); allContracts.value = data; } catch (e) { console.error('Failed to refresh contracts:', e); } }; const selectTick = (tick) => { selectedTick.value = tick; }; // ============================================ // 认证相关 // ============================================ const login = async () => { loginError.value = ''; isLoading.value = true; try { await api.login(loginForm.value.username, loginForm.value.password); isLoggedIn.value = true; currentUser.value = { username: loginForm.value.username }; // 连接 WebSocket if (api.token) { wsClient.connect(api.token); } // 加载初始数据(不阻塞登录成功) loadInitialData().catch(err => { console.warn('部分数据加载失败:', err); addLog({ level: 'WARNING', message: '部分功能数据加载失败,请检查服务连接' }); }); } catch (e) { loginError.value = e.message || '登录失败'; throw e; } finally { isLoading.value = false; } }; const logout = async () => { try { await api.logout(); } catch (e) { console.error('Logout error:', e); } finally { isLoggedIn.value = false; currentUser.value = { username: '' }; wsClient.disconnect(); } }; // ============================================ // 初始数据加载 // ============================================ const loadInitialData = async () => { try { // 并行加载所有数据 const [ sysInfo, contractsData, accountsData, positionsData, ordersData, strategiesData, tradesData, allAccountsData, gatewaysData ] = await Promise.all([ api.getSystemInfo().catch(() => null), api.getContracts().catch(() => []), api.getAccounts().catch(() => []), api.getPositions().catch(() => []), api.getOrders().catch(() => []), api.getStrategies().catch(() => []), api.getLatestTrades(50).catch(() => []), api.getAllAccounts().catch(() => []), api.getAllGateways().catch(() => []) ]); // 加载全局配置(不阻塞其他数据) refreshGlobalSettings().catch(() => { console.warn('Failed to load global settings'); }); // 更新系统状态 if (sysInfo) { systemStatus.value = { vn_ready: sysInfo.vn_ready || false, main_engine: sysInfo.main_engine || false, trading_engine: sysInfo.trading_engine || false }; } contracts.value = contractsData; allContracts.value = contractsData; // 更新系统状态 if (sysInfo) { systemStatus.value = { vn_ready: sysInfo.vn_ready || false, main_engine: sysInfo.main_engine || false, trading_engine: sysInfo.trading_engine || false }; } contracts.value = contractsData; accounts.value = accountsData; positions.value = positionsData; allOrders.value = ordersData; activeOrders.value = ordersData.filter(o => ['submitted', 'pending', 'partial_filled'].includes(o.status?.toLowerCase()) ); strategies.value = strategiesData; trades.value = tradesData; allAccounts.value = allAccountsData; gateways.value = gatewaysData; // 获取行情数据 const ticksData = await api.getTicks().catch(() => []); ticks.value = ticksData; // 订阅 WebSocket 消息 wsClient.subscribe(['tick', 'order', 'trade', 'position', 'account', 'log', 'gateway']); } catch (e) { console.error('Failed to load initial data:', e); } }; // ============================================ // WebSocket 消息处理 // ============================================ const setupWebSocketHandlers = () => { // 连接状态 wsClient.on('connected', () => { wsConnected.value = true; }); wsClient.on('disconnected', () => { wsConnected.value = false; }); // 行情推送 wsClient.on('tick', (data) => { // 更新或添加行情数据 const index = ticks.value.findIndex(t => t.vt_symbol === data.vt_symbol); if (index >= 0) { ticks.value[index] = data; } else { ticks.value.push(data); } // 如果是选中的合约,更新选中数据 if (selectedTick.value && selectedTick.value.vt_symbol === data.vt_symbol) { selectedTick.value = data; } }); // 订单推送 wsClient.on('order', (data) => { // 更新订单列表 const index = allOrders.value.findIndex(o => o.order_id === data.order_id); if (index >= 0) { allOrders.value[index] = data; } else { allOrders.value.push(data); } // 更新活动订单 if (['submitted', 'pending', 'partial_filled'].includes(data.status?.toLowerCase())) { const activeIndex = activeOrders.value.findIndex(o => o.order_id === data.order_id); if (activeIndex >= 0) { activeOrders.value[activeIndex] = data; } else { activeOrders.value.push(data); } } else { activeOrders.value = activeOrders.value.filter(o => o.order_id !== data.order_id); } }); // 成交推送 wsClient.on('trade', (data) => { // 成交记录插入到数组头部 trades.value.unshift(data); // 限制数量 if (trades.value.length > 100) { trades.value = trades.value.slice(0, 100); } }); // 持仓推送 wsClient.on('position', (data) => { const index = positions.value.findIndex( p => p.symbol === data.symbol && p.exchange === data.exchange ); if (index >= 0) { positions.value[index] = data; } else { positions.value.push(data); } }); // 账户推送 wsClient.on('account', (data) => { // 更新 dashboard 中的账户 if (accounts.value.length > 0) { accounts.value[0] = data; } else { accounts.value.push(data); } // 更新资金监控页面中的账户 const index = allAccounts.value.findIndex(a => a.accountid === data.accountid); if (index >= 0) { allAccounts.value[index] = data; } else { allAccounts.value.push(data); } }); // 网关状态推送 wsClient.on('gateway', (data) => { const index = gateways.value.findIndex(g => g.name === data.name); if (index >= 0) { gateways.value[index] = data; } else { gateways.value.push(data); } }); // 日志推送 wsClient.on('log', (data) => { addLog(data); }); // 合约推送 wsClient.on('contract', (data) => { // 更新合约列表 const index = allContracts.value.findIndex(c => c.vt_symbol === data.vt_symbol); if (index >= 0) { allContracts.value[index] = data; } else { allContracts.value.push(data); } }); }; // ============================================ // 生命周期 // ============================================ onMounted(() => { // 检查是否已登录 if (api.isTokenValid()) { isLoggedIn.value = true; const tokenPayload = JSON.parse(atob(api.token.split('.')[1])); currentUser.value = { username: tokenPayload.sub }; // 连接 WebSocket wsClient.connect(api.token); // 加载初始数据 loadInitialData(); } // 设置 WebSocket 处理器 setupWebSocketHandlers(); }); onUnmounted(() => { wsClient.disconnect(); }); // ============================================ // 返回 // ============================================ return { // 状态 isLoggedIn, currentUser, loginForm, loginError, isLoading, currentPage, navItems, wsConnected, systemStatus, gatewayStatus, marketSearch, contracts, ticks, accounts, positions, allOrders, activeOrders, strategies, trades, allAccounts, gateways, selectedGateway, gatewayForm, showConnectDialog, isConnecting, logs, autoScroll, logLevelFilter, logsContainer, orderForm, orderError, isOrdering, allContracts, contractSearch, contractFilter, selectedTick, tableSort, globalSettings, globalSettingsLoading, globalSettingsError, isSavingSettings, // 计算属性 currentPageTitle, filteredTicks, recentLogs, filteredLogs, totalPnL, filteredContracts, // 方法 login, logout, formatNumber, formatPercent, formatTime, getPriceClass, getPnLClass, getDirectionClass, isSubscribed, subscribeSymbol, refreshMarket, canCancel, sendOrder, cancelOrder, refreshOrders, refreshPositions, refreshStrategies, startStrategy, stopStrategy, refreshTrades, refreshAllAccounts, refreshGateways, openConnectDialog, closeConnectDialog, connectGateway, disconnectGateway, getGatewayStatusClass, getGatewayStatusText, getFormFieldClass, isFormFieldPassword, clearLogs, sortTable, getSortIcon, sortData, refreshContracts, selectTick, handleTickDoubleClick, handleOrderDoubleClick, handlePositionDoubleClick, exportTable, refreshGlobalSettings, saveGlobalSettings, getSettingType }; } }).mount('#app');