Files
NewStock/qmt/dashboard.html

237 lines
12 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>
<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;
}
.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; }
.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>
<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>
</div>
<div>
<el-tag :type="status.running ? 'success' : 'info'" effect="dark" style="margin-right: 10px;">
系统: {{ status.running ? '运行中' : '离线' }}
</el-tag>
<el-button type="primary" :icon="Refresh" @click="manualRefresh" :loading="loading">手动刷新</el-button>
</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>
</div>
</el-col>
</el-row>
</el-card>
</el-header>
<el-main style="padding: 0;">
<el-row :gutter="20">
<!-- 左侧:多账号实盘持仓 -->
<el-col :span="13">
<el-card class="box-card" shadow="hover">
<template #header>
<div class="card-header">
<span><el-icon><Suitcase /></el-icon> 实盘真实持仓 (按终端分组)</span>
</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>
</el-card>
</el-col>
<!-- 右侧Redis 虚拟账本 -->
<el-col :span="11">
<el-card class="box-card" shadow="hover">
<template #header>
<div class="card-header">
<span><el-icon><Memo /></el-icon> Redis 虚拟账本 (策略隔离)</span>
</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>
</el-table>
</div>
</el-card>
</el-col>
</el-row>
<!-- 底部:日志 -->
<el-row>
<el-col :span="24">
<el-card class="box-card">
<template #header>
<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>
</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;
const app = createApp({
setup() {
const status = ref({ running: false, terminals: [], start_time: "" });
const positions = ref({ real_positions: {}, virtual_positions: {} });
const logs = ref([]);
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();
if (day === 0 || day === 6) return false;
const val = now.getHours() * 100 + now.getMinutes();
return (val >= 900 && val <= 1515);
};
const tradingStatusText = computed(() => {
return isTradingTime() ? "市场开放中" : "非交易时段";
});
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);
}
} catch (e) {
console.error("Fetch Error:", e);
} 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';
};
const formatVirtual = (obj) => {
return Object.keys(obj).map(k => ({ code: k, vol: obj[k] }));
};
const manualRefresh = () => fetchData();
onMounted(() => {
fetchData();
timer = setInterval(() => {
if (autoRefresh.value && isTradingTime()) fetchData();
}, 60000);
});
onUnmounted(() => { if (timer) clearInterval(timer); });
return {
status, positions, logs, autoRefresh, loading, logBox,
manualRefresh, formatVirtual, tradingStatusText,
getTerminalAlias, getTerminalStatusClass,
Monitor, Refresh, Suitcase, Memo
};
}
});
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component);
}
app.use(ElementPlus);
app.mount('#app');
</script>
</body>
</html>