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">
|
2026-01-10 04:06:35 +08:00
|
|
|
|
<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 {
|
2026-01-10 04:06:35 +08:00
|
|
|
|
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; }
|
2026-01-10 04:06:35 +08:00
|
|
|
|
.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; }
|
2026-01-10 04:06:35 +08:00
|
|
|
|
.bg-red { background-color: #F56C6C; }
|
2025-12-19 14:11:32 +08:00
|
|
|
|
.bg-gray { background-color: #909399; }
|
2026-01-10 04:06:35 +08:00
|
|
|
|
.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>
|
2026-01-10 04:06:35 +08:00
|
|
|
|
<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;">
|
2026-01-10 04:06:35 +08:00
|
|
|
|
系统: {{ status.running ? '运行中' : '离线' }}
|
2025-12-19 14:11:32 +08:00
|
|
|
|
</el-tag>
|
2026-01-10 04:06:35 +08:00
|
|
|
|
<el-button type="primary" :icon="Refresh" @click="manualRefresh" :loading="loading">手动刷新</el-button>
|
2025-12-19 14:11:32 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
2026-01-10 04:06:35 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 终端状态概览 -->
|
|
|
|
|
|
<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>
|
2026-01-10 04:06:35 +08:00
|
|
|
|
</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">
|
2026-01-10 04:06:35 +08:00
|
|
|
|
<!-- 左侧:多账号实盘持仓 -->
|
|
|
|
|
|
<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">
|
2026-01-10 04:06:35 +08:00
|
|
|
|
<span><el-icon><Suitcase /></el-icon> 实盘真实持仓 (按终端分组)</span>
|
2025-12-19 14:11:32 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
2026-01-10 04:06:35 +08:00
|
|
|
|
|
|
|
|
|
|
<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>
|
|
|
|
|
|
|
2026-01-10 04:06:35 +08:00
|
|
|
|
<!-- 右侧: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">
|
2026-01-10 04:06:35 +08:00
|
|
|
|
<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">
|
2026-01-10 04:06:35 +08:00
|
|
|
|
<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>
|
|
|
|
|
|
|
2026-01-10 04:06:35 +08:00
|
|
|
|
<!-- 底部:日志 -->
|
2025-12-19 14:11:32 +08:00
|
|
|
|
<el-row>
|
|
|
|
|
|
<el-col :span="24">
|
2026-01-10 04:06:35 +08:00
|
|
|
|
<el-card class="box-card">
|
2025-12-19 14:11:32 +08:00
|
|
|
|
<template #header>
|
2026-01-10 04:06:35 +08:00
|
|
|
|
<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;
|
2026-01-10 04:06:35 +08:00
|
|
|
|
const { Monitor, Refresh, Suitcase, Memo } = ElementPlusIconsVue;
|
2025-12-19 14:11:32 +08:00
|
|
|
|
|
|
|
|
|
|
const app = createApp({
|
|
|
|
|
|
setup() {
|
2026-01-10 04:06:35 +08:00
|
|
|
|
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([]);
|
2026-01-10 04:06:35 +08:00
|
|
|
|
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;
|
2026-01-10 04:06:35 +08:00
|
|
|
|
const val = now.getHours() * 100 + now.getMinutes();
|
|
|
|
|
|
return (val >= 900 && val <= 1515);
|
2025-12-19 14:11:32 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const tradingStatusText = computed(() => {
|
2026-01-10 04:06:35 +08:00
|
|
|
|
return isTradingTime() ? "市场开放中" : "非交易时段";
|
2025-12-19 14:11:32 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const fetchData = async () => {
|
|
|
|
|
|
loading.value = true;
|
|
|
|
|
|
try {
|
2026-01-10 04:06:35 +08:00
|
|
|
|
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) {
|
2026-01-10 04:06:35 +08:00
|
|
|
|
console.error("Fetch Error:", e);
|
2025-12-19 14:11:32 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-10 04:06:35 +08:00
|
|
|
|
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) => {
|
2026-01-10 04:06:35 +08:00
|
|
|
|
return Object.keys(obj).map(k => ({ code: k, vol: obj[k] }));
|
2025-12-19 14:11:32 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-10 04:06:35 +08:00
|
|
|
|
const manualRefresh = () => fetchData();
|
|
|
|
|
|
|
2025-12-19 14:11:32 +08:00
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
fetchData();
|
|
|
|
|
|
timer = setInterval(() => {
|
2026-01-10 04:06:35 +08:00
|
|
|
|
if (autoRefresh.value && isTradingTime()) fetchData();
|
|
|
|
|
|
}, 60000);
|
2025-12-19 14:11:32 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-10 04:06:35 +08:00
|
|
|
|
onUnmounted(() => { if (timer) clearInterval(timer); });
|
2025-12-19 14:11:32 +08:00
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
status, positions, logs, autoRefresh, loading, logBox,
|
2026-01-10 04:06:35 +08:00
|
|
|
|
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)) {
|
2026-01-10 04:06:35 +08:00
|
|
|
|
app.component(key, component);
|
2025-12-19 14:11:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
app.use(ElementPlus);
|
|
|
|
|
|
app.mount('#app');
|
|
|
|
|
|
</script>
|
|
|
|
|
|
</body>
|
|
|
|
|
|
</html>
|