更新qmt代码,支持多端qmt登录

This commit is contained in:
2026-01-10 04:06:35 +08:00
parent dd60589280
commit 50ee1a5a0a
6 changed files with 487 additions and 495 deletions

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>QMT 实盘监控看板</title>
<title>QMT 多终端监控看板</title>
<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>
@@ -13,23 +13,18 @@
.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: 400px;
overflow-y: scroll;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 13px;
line-height: 1.5;
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;
}
.log-line { margin: 0; border-bottom: 1px solid #333; white-space: pre-wrap; word-break: break-all; }
.log-line:hover { background-color: #2a2a2a; }
.virtual-item { margin-bottom: 20px; border-left: 4px solid #409EFF; padding-left: 10px; }
.virtual-title { font-weight: bold; font-size: 14px; margin-bottom: 8px; color: #606266; }
.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; }
.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; }
.bg-gray { background-color: #909399; }
.virtual-item { margin-bottom: 15px; border-left: 4px solid #E6A23C; padding-left: 10px; }
</style>
</head>
<body>
@@ -41,86 +36,100 @@
<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>
<span style="font-weight: bold; font-size: 20px;">QMT 多账号实盘守护系统</span>
</div>
<div>
<el-tag :type="status.running ? 'success' : 'info'" effect="dark" style="margin-right: 10px;">
API: {{ status.running ? 'Running' : 'Offline' }}
</el-tag>
<el-tag :type="status.qmt_connected ? 'success' : 'danger'" effect="dark">
QMT: {{ status.qmt_connected ? 'Connected' : 'Disconnected' }}
系统: {{ status.running ? '运行中' : '离线' }}
</el-tag>
<el-button type="primary" :icon="Refresh" @click="manualRefresh" :loading="loading">手动刷新</el-button>
</div>
</div>
</template>
<el-descriptions border :column="4" size="large">
<el-descriptions-item label="资金账号">{{ status.account_id || '---' }}</el-descriptions-item>
<el-descriptions-item label="启动时间">{{ status.start_time || '---' }}</el-descriptions-item>
<el-descriptions-item label="心跳时间">
<span :style="{color: isHeartbeatStalled ? 'red' : 'green', fontWeight: 'bold'}">
{{ status.last_loop_update || '---' }}
</span>
</el-descriptions-item>
<el-descriptions-item label="控制">
<el-button type="primary" :icon="Refresh" @click="manualRefresh" :loading="loading">手动刷新</el-button>
<div style="margin-left: 15px; display: inline-flex; align-items: center;">
<el-checkbox v-model="autoRefresh" label="自动刷新(1min)" border></el-checkbox>
<span style="font-size: 12px; margin-left: 8px; color: #909399;">
{{ tradingStatusText }}
</span>
<!-- 终端状态概览 -->
<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>
</div>
</el-descriptions-item>
</el-descriptions>
</el-col>
</el-row>
</el-card>
</el-header>
<el-main style="padding: 0;">
<el-row :gutter="20">
<el-col :span="12">
<!-- 左侧:多账号实盘持仓 -->
<el-col :span="13">
<el-card class="box-card" shadow="hover">
<template #header>
<div class="card-header">
<span><span class="status-badge bg-green"></span>实盘真实持仓 (QMT)</span>
<span><el-icon><Suitcase /></el-icon> 实盘真实持仓 (按终端分组)</span>
</div>
</template>
<el-table :data="positions.real_positions" style="width: 100%" border stripe size="small" empty-text="当前空仓">
<el-table-column prop="code" label="代码" width="100" sortable></el-table-column>
<el-table-column prop="volume" label="总持仓" width="100"></el-table-column>
<el-table-column prop="can_use" label="可用" width="100"></el-table-column>
<el-table-column prop="market_value" label="市值"></el-table-column>
</el-table>
<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>
</el-card>
</el-col>
<el-col :span="12">
<!-- 右侧Redis 虚拟账本 -->
<el-col :span="11">
<el-card class="box-card" shadow="hover">
<template #header>
<div class="card-header">
<span><span class="status-badge bg-gray"></span>Redis 虚拟账本 (策略隔离)</span>
<span><el-icon><Memo /></el-icon> Redis 虚拟账本 (策略隔离)</span>
</div>
</template>
<div v-if="Object.keys(positions.virtual_positions).length === 0" style="color:#909399; text-align:center; padding: 20px;">
暂无策略数据 / Redis未连接
</div>
<div v-for="(posObj, strategyName) in positions.virtual_positions" :key="strategyName" class="virtual-item">
<div class="virtual-title">{{ strategyName }}</div>
<el-table :data="formatVirtual(posObj)" style="width: 100%;" border size="small" empty-text="该策略当前空仓">
<el-table-column prop="code" label="代码"></el-table-column>
<el-table-column prop="vol" label="记账数量">
<template #default="scope"><span style="font-weight: bold;">{{ scope.row.vol }}</span></template>
</el-table-column>
<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>
</el-table>
</div>
</el-card>
</el-col>
</el-row>
<!-- 底部:日志 -->
<el-row>
<el-col :span="24">
<el-card class="box-card" shadow="never">
<el-card class="box-card">
<template #header>
<div class="card-header"><span>系统实时日志 (Last 50 lines)</span></div>
<div class="card-header"><span>系统实时日志 (最新 50 条)</span></div>
</template>
<div class="log-box" ref="logBox">
<div v-for="(line, index) in logs" :key="index" class="log-line">{{ line }}</div>
@@ -134,122 +143,92 @@
<script>
const { createApp, ref, onMounted, onUnmounted, computed } = Vue;
const { Monitor, Refresh } = ElementPlusIconsVue;
const { Monitor, Refresh, Suitcase, Memo } = ElementPlusIconsVue;
const app = createApp({
setup() {
const status = ref({});
const positions = ref({ real_positions: [], virtual_positions: {} });
const status = ref({ running: false, terminals: [], start_time: "" });
const positions = ref({ real_positions: {}, virtual_positions: {} });
const logs = ref([]);
const autoRefresh = ref(true); // 默认开启自动刷新
const autoRefresh = ref(true);
const loading = ref(false);
const logBox = ref(null);
let timer = null;
const API_BASE = "";
// === 核心逻辑修改:判断是否为交易时间 ===
const isTradingTime = () => {
const now = new Date();
const day = now.getDay();
const hour = now.getHours();
const minute = now.getMinutes();
const currentTimeVal = hour * 100 + minute;
// 1. 判断是否为周末 (0是周日, 6是周六)
if (day === 0 || day === 6) return false;
// 2. 判断时间段 (09:00 - 15:10)
// 包含集合竞价和收盘清算时间
if (currentTimeVal >= 900 && currentTimeVal <= 1510) {
return true;
}
return false;
const val = now.getHours() * 100 + now.getMinutes();
return (val >= 900 && val <= 1515);
};
// 界面显示的提示文本
const tradingStatusText = computed(() => {
if (!autoRefresh.value) return "已关闭";
return isTradingTime() ? "监控中..." : "休市暂停";
});
const isHeartbeatStalled = computed(() => {
if (!status.value.last_loop_update) return true;
return false;
return isTradingTime() ? "市场开放中" : "非交易时段";
});
const fetchData = async () => {
loading.value = true;
try {
const resStatus = await fetch(`${API_BASE}/api/status`);
if(resStatus.ok) status.value = await resStatus.json();
else status.value = { running: false };
const sRes = await fetch(`${API_BASE}/api/status`);
if(sRes.ok) status.value = await sRes.json();
const resPos = await fetch(`${API_BASE}/api/positions`);
if(resPos.ok) positions.value = await resPos.json();
const pRes = await fetch(`${API_BASE}/api/positions`);
if(pRes.ok) positions.value = await pRes.json();
const resLogs = await fetch(`${API_BASE}/api/logs`);
if(resLogs.ok) {
const logData = await resLogs.json();
const needScroll = (logs.value.length !== logData.logs.length);
logs.value = logData.logs;
// 只有在自动刷新且有新日志时才自动滚动
if (needScroll && autoRefresh.value) {
setTimeout(() => {
if(logBox.value) logBox.value.scrollTop = logBox.value.scrollHeight;
}, 100);
}
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);
}
} catch (e) {
console.error("API Error:", e);
status.value.running = false;
console.error("Fetch Error:", e);
} finally {
loading.value = false;
}
};
// 手动刷新按钮:不受时间限制
const manualRefresh = () => {
fetchData();
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';
};
const formatVirtual = (obj) => {
if (!obj) return [];
return Object.keys(obj).map(key => ({ code: key, vol: obj[key] }));
return Object.keys(obj).map(k => ({ code: k, vol: obj[k] }));
};
const manualRefresh = () => fetchData();
onMounted(() => {
// 页面加载时先拉取一次
fetchData();
// === 修改定时器:每 60 秒触发一次 ===
timer = setInterval(() => {
// 只有在 "开关开启" 且 "处于交易时间" 时才请求
if (autoRefresh.value && isTradingTime()) {
fetchData();
}
}, 60000); // 60000ms = 1分钟
if (autoRefresh.value && isTradingTime()) fetchData();
}, 60000);
});
onUnmounted(() => {
if (timer) clearInterval(timer);
});
onUnmounted(() => { if (timer) clearInterval(timer); });
return {
status, positions, logs, autoRefresh, loading, logBox,
manualRefresh, // 绑定到按钮
fetchData,
formatVirtual,
isHeartbeatStalled,
tradingStatusText, // 绑定到提示文本
Monitor, Refresh
manualRefresh, formatVirtual, tradingStatusText,
getTerminalAlias, getTerminalStatusClass,
Monitor, Refresh, Suitcase, Memo
};
}
});
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
app.component(key, component);
}
app.use(ElementPlus);
app.mount('#app');