更新qmt代码,支持多端qmt登录
This commit is contained in:
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user