Files
NewQuant/strategy_manager/frontend/dist/index.html

253 lines
10 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>策略控制台</title>
<!-- 字体 -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<!-- 1. 引入 Vue 3 -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<!-- 2. 引入 Naive UI -->
<script src="https://unpkg.com/naive-ui/dist/index.js"></script>
<style>
body { font-family: 'Inter', sans-serif; background-color: #f5f7fa; margin: 0; }
#app { padding: 20px; max-width: 1200px; margin: 0 auto; }
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.log-container { background: #1e1e1e; padding: 15px; border-radius: 4px; height: 400px; overflow: auto; font-family: monospace; font-size: 12px; color: #ddd; }
.log-line { margin: 2px 0; border-bottom: 1px solid #333; padding-bottom: 2px; }
</style>
</head>
<body>
<div id="app">
<n-config-provider :locale="zhCN" :date-locale="dateZhCN">
<n-message-provider>
<n-dialog-provider>
<main-layout></main-layout>
</n-dialog-provider>
</n-message-provider>
</n-config-provider>
</div>
<script>
const { createApp, ref, onMounted, onUnmounted, watch } = Vue;
const naive = window.naive;
// --- 主组件逻辑 ---
const MainLayout = {
template: `
<div class="header">
<h2 style="margin:0; color: #333;">📈 量化策略控制台</h2>
<n-space align="center">
<!-- 1. 刷新频率选择器 -->
<n-select
v-model:value="refreshInterval"
:options="intervalOptions"
size="small"
style="width: 130px"
></n-select>
<!-- 2. 手动刷新按钮 -->
<n-button type="primary" size="small" @click="fetchStatus" :loading="loading">
刷新状态
</n-button>
<n-tag type="info" size="small">更新于: {{ lastUpdated }}</n-tag>
</n-space>
</div>
<n-card title="策略列表" hoverable>
<n-table :single-line="false" striped>
<thead>
<tr>
<th>策略标识</th>
<th>策略名称</th>
<th>运行状态</th>
<th>PID</th>
<th>运行时长</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(info, key) in strategies" :key="key">
<td><strong>{{ key }}</strong></td>
<td>{{ info.config.strategy_name }} <br><small style="color:#999">{{ info.symbol }}</small></td>
<td>
<n-tag :type="info.status === 'running' ? 'success' : 'error'" size="small">
{{ info.status === 'running' ? '运行中' : '已停止' }}
</n-tag>
</td>
<td>{{ info.pid || '-' }}</td>
<td>{{ info.uptime || '-' }}</td>
<td>
<n-space>
<n-button v-if="info.status === 'stopped'" type="success" size="small" ghost @click="handleAction(key, 'start')">启动</n-button>
<n-button v-if="info.status === 'running'" type="error" size="small" ghost @click="handleAction(key, 'stop')">停止</n-button>
<n-button v-if="info.status === 'running'" type="warning" size="small" ghost @click="handleAction(key, 'restart')">重启</n-button>
<n-button size="small" @click="viewLogs(key)">日志</n-button>
</n-space>
</td>
</tr>
<tr v-if="Object.keys(strategies).length === 0">
<td colspan="6" style="text-align: center; padding: 30px; color: #999;">暂无策略</td>
</tr>
</tbody>
</n-table>
</n-card>
<!-- 日志弹窗 -->
<n-modal v-model:show="showLogModal" style="width: 800px;" preset="card" :title="'📜 实时日志: ' + currentLogKey">
<div class="log-container" id="logBox">
<div v-if="logLoading" style="text-align:center; padding:20px;"><n-spin size="medium" /></div>
<div v-else v-for="(line, index) in logLines" :key="index" class="log-line">{{ line }}</div>
</div>
<template #footer>
<n-space justify="end">
<n-button size="small" @click="fetchLogs(currentLogKey)">刷新</n-button>
<n-button size="small" @click="showLogModal = false">关闭</n-button>
</n-space>
</template>
</n-modal>
`,
setup() {
const message = naive.useMessage();
const dialog = naive.useDialog();
const strategies = ref({});
const loading = ref(false);
const lastUpdated = ref('-');
// --- 修改点:默认值为 0 (仅手动) ---
const refreshInterval = ref(0);
const intervalOptions = [
{ label: '✋ 仅手动', value: 0 }, // 建议将手动选项放在第一个
{ label: '⚡ 3秒自动', value: 3000 },
{ label: '⏱ 5秒自动', value: 5000 },
{ label: '🐢 10秒自动', value: 10000 }
];
let timer = null;
// --- 核心数据获取 ---
const fetchStatus = async () => {
if (loading.value) return;
loading.value = true;
try {
const res = await fetch('/api/status');
if (!res.ok) throw new Error("Error");
const data = await res.json();
strategies.value = data.strategies;
lastUpdated.value = new Date().toLocaleTimeString();
} catch (e) {
message.error("连接服务器失败");
} finally {
loading.value = false;
}
};
// --- 定时器管理 ---
const resetTimer = () => {
if (timer) clearInterval(timer);
// 只有大于 0 才启动定时器
if (refreshInterval.value > 0) {
timer = setInterval(fetchStatus, refreshInterval.value);
}
};
// 监听下拉框变化
watch(refreshInterval, () => {
resetTimer();
if (refreshInterval.value > 0) {
message.info(`已切换为 ${refreshInterval.value/1000} 秒自动刷新`);
fetchStatus();
} else {
message.info("已切换为手动刷新模式");
}
});
// --- 其他逻辑 ---
const handleAction = (name, action) => {
const map = { start: '启动', stop: '停止', restart: '重启' };
dialog.warning({
title: '确认操作',
content: `确定要 ${map[action]} ${name} 吗?`,
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
try {
const res = await fetch(`/api/strategy/${name}/${action}`, { method: 'POST' });
if (res.ok) {
message.success("操作成功");
fetchStatus();
} else {
message.error("操作失败");
}
} catch (e) { message.error("请求错误"); }
}
});
};
const showLogModal = ref(false);
const currentLogKey = ref('');
const logLines = ref([]);
const logLoading = ref(false);
const fetchLogs = async (name) => {
logLoading.value = true;
try {
const res = await fetch(`/api/logs/${name}?lines=100`);
const data = await res.json();
logLines.value = data.lines;
setTimeout(() => {
const el = document.getElementById('logBox');
if(el) el.scrollTop = el.scrollHeight;
}, 100);
} catch(e) { message.error("日志获取失败"); }
finally { logLoading.value = false; }
}
const viewLogs = (name) => {
currentLogKey.value = name;
showLogModal.value = true;
fetchLogs(name);
};
onMounted(() => {
fetchStatus(); // 页面加载时请求一次
resetTimer(); // 初始化定时器当前为0所以不启动
});
onUnmounted(() => {
if (timer) clearInterval(timer);
});
return {
strategies, loading, lastUpdated,
refreshInterval, intervalOptions,
showLogModal, currentLogKey, logLines, logLoading,
fetchStatus, handleAction, viewLogs, fetchLogs
};
}
};
const app = createApp({
components: { MainLayout },
setup() {
return { zhCN: naive.zhCN, dateZhCN: naive.dateZhCN }
}
});
Object.keys(naive).forEach(key => {
if (key.startsWith('N') && !key.startsWith('NTh') && !key.startsWith('use')) {
app.component(key, naive[key]);
}
});
app.mount('#app');
</script>
</body>
</html>