Files
NewStock/qmt/dashboard.html

237 lines
12 KiB
HTML
Raw Normal View History

2025-12-19 14:11:32 +08:00
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>QMT 多终端监控看板</title>
2025-12-19 14:11:32 +08:00
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css" />
<script src="https://unpkg.com/element-plus"></script>
<script src="https://unpkg.com/@element-plus/icons-vue"></script>
<style>
body { background-color: #f0f2f5; margin: 0; padding: 20px; font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', sans-serif; }
.card-header { display: flex; justify-content: space-between; align-items: center; }
.box-card { margin-bottom: 20px; }
.log-box {
background: #1e1e1e; color: #d4d4d4; padding: 10px; border-radius: 4px;
height: 350px; overflow-y: scroll; font-family: 'Consolas', monospace;
font-size: 12px; line-height: 1.5;
2025-12-19 14:11:32 +08:00
}
.log-line { margin: 0; border-bottom: 1px solid #333; white-space: pre-wrap; word-break: break-all; }
.terminal-group { margin-bottom: 15px; border: 1px solid #ebeef5; border-radius: 8px; padding: 10px; background: #fff; }
.terminal-title { font-weight: bold; font-size: 14px; margin-bottom: 8px; color: #409EFF; display: flex; align-items: center; }
2025-12-19 14:11:32 +08:00
.status-badge { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 5px; }
.bg-green { background-color: #67C23A; }
.bg-red { background-color: #F56C6C; }
2025-12-19 14:11:32 +08:00
.bg-gray { background-color: #909399; }
.virtual-item { margin-bottom: 15px; border-left: 4px solid #E6A23C; padding-left: 10px; }
2025-12-19 14:11:32 +08:00
</style>
</head>
<body>
<div id="app">
<el-container>
<el-header height="auto" style="padding: 0;">
<el-card class="box-card">
<template #header>
<div class="card-header">
<div style="display:flex; align-items:center;">
<el-icon size="24" style="margin-right: 10px;"><Monitor /></el-icon>
<span style="font-weight: bold; font-size: 20px;">QMT 多账号实盘守护系统</span>
2025-12-19 14:11:32 +08:00
</div>
<div>
<el-tag :type="status.running ? 'success' : 'info'" effect="dark" style="margin-right: 10px;">
系统: {{ status.running ? '运行中' : '离线' }}
2025-12-19 14:11:32 +08:00
</el-tag>
<el-button type="primary" :icon="Refresh" @click="manualRefresh" :loading="loading">手动刷新</el-button>
2025-12-19 14:11:32 +08:00
</div>
</div>
</template>
<!-- 终端状态概览 -->
<el-row :gutter="20">
<el-col :span="6" v-for="t in status.terminals" :key="t.qmt_id">
<el-card shadow="never" style="background: #fcfcfc;">
<div style="font-size: 14px; font-weight: bold; margin-bottom: 5px;">{{ t.alias }}</div>
<div style="font-size: 12px; color: #909399;">ID: {{ t.account_id }}</div>
<div style="margin-top: 8px; display: flex; align-items: center; justify-content: space-between;">
<el-tag :type="t.is_connected ? 'success' : 'danger'" size="small">
{{ t.is_connected ? '已连接' : '已断开' }}
</el-tag>
<span style="font-size: 11px; color: #c0c4cc;">{{ t.last_heartbeat }}</span>
</div>
</el-card>
</el-col>
<el-col :span="6">
<div style="padding: 10px;">
<div style="font-size: 12px; color: #909399;">启动时间: {{ status.start_time }}</div>
<div style="margin-top: 10px;">
<el-checkbox v-model="autoRefresh" label="自动刷新 (1min)" border size="small"></el-checkbox>
<span style="font-size: 12px; margin-left: 8px; color: #E6A23C;">{{ tradingStatusText }}</span>
</div>
2025-12-19 14:11:32 +08:00
</div>
</el-col>
</el-row>
2025-12-19 14:11:32 +08:00
</el-card>
</el-header>
<el-main style="padding: 0;">
<el-row :gutter="20">
<!-- 左侧:多账号实盘持仓 -->
<el-col :span="13">
2025-12-19 14:11:32 +08:00
<el-card class="box-card" shadow="hover">
<template #header>
<div class="card-header">
<span><el-icon><Suitcase /></el-icon> 实盘真实持仓 (按终端分组)</span>
2025-12-19 14:11:32 +08:00
</div>
</template>
<div v-if="Object.keys(positions.real_positions).length === 0" style="text-align:center; padding:20px; color:#909399;">暂无持仓数据</div>
<div v-for="(posList, qmtId) in positions.real_positions" :key="qmtId" class="terminal-group">
<div class="terminal-title">
<span class="status-badge" :class="getTerminalStatusClass(qmtId)"></span>
{{ getTerminalAlias(qmtId) }}
</div>
<el-table :data="posList" style="width: 100%" border size="small" empty-text="当前空仓">
<el-table-column prop="code" label="代码" width="100"></el-table-column>
<el-table-column prop="volume" label="持仓" width="90"></el-table-column>
<el-table-column prop="can_use" label="可用" width="90"></el-table-column>
<el-table-column prop="market_value" label="市值"></el-table-column>
</el-table>
</div>
2025-12-19 14:11:32 +08:00
</el-card>
</el-col>
<!-- 右侧Redis 虚拟账本 -->
<el-col :span="11">
2025-12-19 14:11:32 +08:00
<el-card class="box-card" shadow="hover">
<template #header>
<div class="card-header">
<span><el-icon><Memo /></el-icon> Redis 虚拟账本 (策略隔离)</span>
2025-12-19 14:11:32 +08:00
</div>
</template>
<div v-for="(posObj, strategyName) in positions.virtual_positions" :key="strategyName" class="virtual-item">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:5px;">
<span style="font-weight:bold; font-size:13px; color:#606266;">{{ strategyName }}</span>
<el-tag size="small" type="warning">虚拟占位: {{ Object.keys(posObj).length }}</el-tag>
</div>
<el-table :data="formatVirtual(posObj)" style="width: 100%;" border size="small">
<el-table-column prop="code" label="股票代码"></el-table-column>
<el-table-column prop="vol" label="记账数量"></el-table-column>
2025-12-19 14:11:32 +08:00
</el-table>
</div>
</el-card>
</el-col>
</el-row>
<!-- 底部:日志 -->
2025-12-19 14:11:32 +08:00
<el-row>
<el-col :span="24">
<el-card class="box-card">
2025-12-19 14:11:32 +08:00
<template #header>
<div class="card-header"><span>系统实时日志 (最新 50 条)</span></div>
2025-12-19 14:11:32 +08:00
</template>
<div class="log-box" ref="logBox">
<div v-for="(line, index) in logs" :key="index" class="log-line">{{ line }}</div>
</div>
</el-card>
</el-col>
</el-row>
</el-main>
</el-container>
</div>
<script>
const { createApp, ref, onMounted, onUnmounted, computed } = Vue;
const { Monitor, Refresh, Suitcase, Memo } = ElementPlusIconsVue;
2025-12-19 14:11:32 +08:00
const app = createApp({
setup() {
const status = ref({ running: false, terminals: [], start_time: "" });
const positions = ref({ real_positions: {}, virtual_positions: {} });
2025-12-19 14:11:32 +08:00
const logs = ref([]);
const autoRefresh = ref(true);
2025-12-19 14:11:32 +08:00
const loading = ref(false);
const logBox = ref(null);
let timer = null;
const API_BASE = "";
const isTradingTime = () => {
const now = new Date();
const day = now.getDay();
if (day === 0 || day === 6) return false;
const val = now.getHours() * 100 + now.getMinutes();
return (val >= 900 && val <= 1515);
2025-12-19 14:11:32 +08:00
};
const tradingStatusText = computed(() => {
return isTradingTime() ? "市场开放中" : "非交易时段";
2025-12-19 14:11:32 +08:00
});
const fetchData = async () => {
loading.value = true;
try {
const sRes = await fetch(`${API_BASE}/api/status`);
if(sRes.ok) status.value = await sRes.json();
const pRes = await fetch(`${API_BASE}/api/positions`);
if(pRes.ok) positions.value = await pRes.json();
const lRes = await fetch(`${API_BASE}/api/logs`);
if(lRes.ok) {
const data = await lRes.json();
logs.value = data.logs;
setTimeout(() => {
if(logBox.value) logBox.value.scrollTop = logBox.value.scrollHeight;
}, 100);
2025-12-19 14:11:32 +08:00
}
} catch (e) {
console.error("Fetch Error:", e);
2025-12-19 14:11:32 +08:00
} finally {
loading.value = false;
}
};
const getTerminalAlias = (qmtId) => {
const t = status.value.terminals.find(x => x.qmt_id === qmtId);
return t ? t.alias : qmtId;
};
const getTerminalStatusClass = (qmtId) => {
const t = status.value.terminals.find(x => x.qmt_id === qmtId);
return t && t.is_connected ? 'bg-green' : 'bg-red';
2025-12-19 14:11:32 +08:00
};
const formatVirtual = (obj) => {
return Object.keys(obj).map(k => ({ code: k, vol: obj[k] }));
2025-12-19 14:11:32 +08:00
};
const manualRefresh = () => fetchData();
2025-12-19 14:11:32 +08:00
onMounted(() => {
fetchData();
timer = setInterval(() => {
if (autoRefresh.value && isTradingTime()) fetchData();
}, 60000);
2025-12-19 14:11:32 +08:00
});
onUnmounted(() => { if (timer) clearInterval(timer); });
2025-12-19 14:11:32 +08:00
return {
status, positions, logs, autoRefresh, loading, logBox,
manualRefresh, formatVirtual, tradingStatusText,
getTerminalAlias, getTerminalStatusClass,
Monitor, Refresh, Suitcase, Memo
2025-12-19 14:11:32 +08:00
};
}
});
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component);
2025-12-19 14:11:32 +08:00
}
app.use(ElementPlus);
app.mount('#app');
</script>
</body>
</html>