Files
sanguo_vnpy_v2/sanguo_web/static/js/app.js
T
claude_dev 918bbed0fc fix: 修复登录500错误和移除明文密码提示
- 修复 deps.py 中 get_vn_service 的引用错误 (vn_service.vn_service -> vn_service)
- 移除登录页面上的明文密码提示
- 改进前端错误处理,避免数据加载失败导致登录显示错误
2026-07-02 12:23:55 +08:00

583 lines
18 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: 'position', label: '持仓', icon: '' },
{ id: 'strategy', 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 logs = ref([]);
const autoScroll = ref(true);
const logLevelFilter = ref('');
const logsContainer = ref(null);
// 下单表单
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 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 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 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 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 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
] = await Promise.all([
api.getSystemInfo().catch(() => null),
api.getContracts().catch(() => []),
api.getAccounts().catch(() => []),
api.getPositions().catch(() => []),
api.getOrders().catch(() => []),
api.getStrategies().catch(() => [])
]);
// 更新系统状态
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;
// 获取行情数据
const ticksData = await api.getTicks().catch(() => []);
ticks.value = ticksData;
// 订阅 WebSocket 消息
wsClient.subscribe(['tick', 'order', 'trade', 'position', 'account', 'log']);
} 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);
}
});
// 订单推送
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) => {
// 可以添加成交记录
console.log('Trade update:', data);
});
// 持仓推送
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) => {
if (accounts.value.length > 0) {
accounts.value[0] = data;
} else {
accounts.value.push(data);
}
});
// 日志推送
wsClient.on('log', (data) => {
addLog(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,
logs,
autoScroll,
logLevelFilter,
logsContainer,
orderForm,
orderError,
isOrdering,
// 计算属性
currentPageTitle,
filteredTicks,
recentLogs,
filteredLogs,
totalPnL,
// 方法
login,
logout,
formatNumber,
formatPercent,
formatTime,
getPriceClass,
getPnLClass,
isSubscribed,
subscribeSymbol,
refreshMarket,
canCancel,
sendOrder,
cancelOrder,
refreshOrders,
refreshPositions,
refreshStrategies,
startStrategy,
stopStrategy,
clearLogs
};
}
}).mount('#app');