feat: 添加 Redis 消息展示功能到监控面板

- 新增 /api/messages API 接口,支持从 Redis Stream 读取消息
- 支持按策略筛选消息和分页展示
- 前端新增消息列表卡片,展示时间、策略、股票代码、动作、价格和状态
- 自动判断消息处理状态(已处理/待处理)
- 消息列表每30秒自动刷新,支持手动刷新
This commit is contained in:
2026-03-01 22:06:42 +08:00
parent e88ba5bcf9
commit 7bb0a0537b
14 changed files with 1814 additions and 806 deletions

View File

@@ -131,6 +131,28 @@
</n-gi>
</n-grid>
<!-- 消息列表 -->
<n-card title="消息列表" style="margin-top: 20px; border-radius: 8px; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);">
<template #header-extra>
<div style="display: flex; align-items: center; gap: 12px;">
<n-select v-model:value="messageFilter.strategy" :options="strategyOptions" size="small" style="width: 120px;" @update:value="fetchMessages" />
<n-tag :type="messageStats.unprocessed > 0 ? 'error' : 'success'">
{{ messageStats.unprocessed }} 未处理 / {{ messages.length }} 总计
</n-tag>
<n-button type="primary" size="small" @click="fetchMessages" :loading="messageLoading">刷新</n-button>
</div>
</template>
<n-data-table
:columns="messageColumns"
:data="messages"
size="small"
:bordered="false"
:single-line="false"
:pagination="messagePagination"
:loading="messageLoading"
/>
</n-card>
<!-- 日志 -->
<n-card title="系统日志" style="margin-top: 20px; border-radius: 8px; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);">
<template #header-extra>
@@ -212,8 +234,8 @@
</div>
<script>
const { createApp, ref, onMounted, onUnmounted, computed, watch } = Vue;
const { NCard, NButton, NTag, NGrid, NGi, NDescriptions, NDescriptionsItem, NDataTable, NStatistic, NConfigProvider, NMessageProvider, NNotificationProvider, NDialogProvider, NModal, NTabs, NTabPane, NEmpty } = naive;
const { createApp, ref, onMounted, onUnmounted, computed, watch, h } = Vue;
const { NCard, NButton, NTag, NGrid, NGi, NDescriptions, NDescriptionsItem, NDataTable, NStatistic, NConfigProvider, NMessageProvider, NNotificationProvider, NDialogProvider, NModal, NTabs, NTabPane, NEmpty, NSelect } = naive;
const app = createApp({
setup() {
@@ -230,6 +252,74 @@
const showConfigModal = ref(false);
const fileConfig = ref({ redis: {}, qmt_terminals: [], strategies: {}, raw_config: "", config_path: "" });
const configLoading = ref(false);
// 消息相关
const messages = ref([]);
const messageLoading = ref(false);
const messageFilter = ref({ strategy: 'all' });
const messagePagination = ref({ page: 1, pageSize: 10, showSizePicker: true, pageSizes: [10, 20, 50] });
// 消息表格列
const messageColumns = [
{ title: '时间', key: 'timestamp', width: 150, fixed: 'left' },
{
title: '策略',
key: 'strategy',
width: 120,
render: (row) => row.data.strategy_name || '-'
},
{
title: '股票代码',
key: 'stock_code',
width: 100,
render: (row) => row.data.stock_code || '-'
},
{
title: '动作',
key: 'action',
width: 80,
render: (row) => {
const action = row.data.action;
if (action === 'BUY') return h(NTag, { type: 'error', size: 'small' }, { default: () => '买入' });
if (action === 'SELL') return h(NTag, { type: 'success', size: 'small' }, { default: () => '卖出' });
return action || '-';
}
},
{
title: '价格',
key: 'price',
width: 100,
render: (row) => row.data.price ? '¥' + row.data.price.toFixed(2) : '-'
},
{ title: '状态', key: 'is_processed', width: 100, render: (row) => {
return row.is_processed
? h(NTag, { type: 'success', size: 'small' }, { default: () => '已处理' })
: h(NTag, { type: 'warning', size: 'small' }, { default: () => '待处理' });
}},
{
title: '消息ID',
key: 'message_id',
width: 180,
ellipsis: { tooltip: true }
},
];
// 策略选项
const strategyOptions = computed(() => {
const options = [{ label: '全部策略', value: 'all' }];
if (fileConfig.value.strategies) {
Object.keys(fileConfig.value.strategies).forEach(name => {
options.push({ label: name, value: name });
});
}
return options;
});
// 消息统计
const messageStats = computed(() => {
const unprocessed = messages.value.filter(m => !m.is_processed).length;
return { unprocessed, total: messages.value.length };
});
// 表格列(使用简单 render不依赖 h 函数)
const realPosColumns = [
@@ -326,6 +416,23 @@
}
};
// 获取消息列表
const fetchMessages = async () => {
messageLoading.value = true;
try {
const strategy = messageFilter.value.strategy;
const url = `${API_BASE}/api/messages?strategy=${strategy}&count=50`;
const res = await fetch(url).then(r => r.ok ? r.json() : { messages: [] });
messages.value = res.messages || [];
console.log('[DEBUG] 消息已更新', messages.value.length);
} catch (e) {
console.error('[MESSAGES] 获取消息失败:', e);
messages.value = [];
} finally {
messageLoading.value = false;
}
};
// 复制配置到剪贴板
const copyConfig = () => {
if (fileConfig.value.raw_config) {
@@ -353,7 +460,7 @@
}
});
onMounted(() => { fetchData(); timer = setInterval(fetchData, 30000); });
onMounted(() => { fetchData(); fetchMessages(); timer = setInterval(() => { fetchData(); fetchMessages(); }, 30000); });
onUnmounted(() => { if (timer) clearInterval(timer); unwatch(); });
return {
@@ -366,7 +473,10 @@
realPosColumns, virtualPosColumns,
// 配置相关
showConfigModal, fileConfig, configLoading,
fetchConfig, copyConfig
fetchConfig, copyConfig,
// 消息相关
messages, messageLoading, messageFilter, messagePagination,
messageColumns, strategyOptions, messageStats, fetchMessages
};
}
});
@@ -375,4 +485,4 @@
app.mount('#app');
</script>
</body>
</html>
</html>