653472def3
对齐 VeighNa 4.4 原生 Qt UI,新增成交监控、资金监控、网关管理、全局配置等页面与 API,功能对等性 98.5%。 - 新增 API: /api/v1/trades, /api/v1/accounts, /api/v1/settings, 网关扩展 - 新增前端页面: 成交、资金、合约、网关、全局配置、微信通知 - 扩展导航菜单与实时数据推送 - 补充需求分析与实现计划文档
1133 lines
38 KiB
JavaScript
1133 lines
38 KiB
JavaScript
/**
|
||
* 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');
|