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

970 lines
43 KiB
HTML
Raw Normal View History

2025-11-21 16:08:03 +08:00
<!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://cdn.bootcdn.net/ajax/libs/vue/3.4.21/vue.global.prod.min.js"></script>
2025-11-21 16:08:03 +08:00
<!-- 2. 引入 Naive UI -->
<script src="https://cdn.bootcdn.net/ajax/libs/naive-ui/2.38.1/index.js"></script>
2025-11-21 16:08:03 +08:00
<style>
:root {
--header-bg: #ffffff;
--card-bg: #ffffff;
--border-color: #e8e8e8;
--text-primary: #262626;
--text-secondary: #8c8c8c;
--shadow-sm: 0 2px 8px rgba(0,0,0,0.06);
--shadow-md: 0 4px 16px rgba(0,0,0,0.08);
--spacing-xs: 8px;
--spacing-sm: 12px;
--spacing-md: 16px;
--spacing-lg: 24px;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background-color: #f5f7fa;
margin: 0;
color: var(--text-primary);
-webkit-font-smoothing: antialiased;
}
#app {
padding: var(--spacing-lg);
max-width: 1600px;
margin: 0 auto;
}
/* 头部布局优化 */
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-lg);
padding: var(--spacing-md) var(--spacing-lg);
background: var(--header-bg);
border-radius: 12px;
box-shadow: var(--shadow-sm);
}
.header h2 {
margin: 0;
font-size: 20px;
font-weight: 600;
color: var(--text-primary);
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.header-controls {
display: flex;
align-items: center;
gap: var(--spacing-md);
}
/* 统计卡片优化 */
.stats-row {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--spacing-md);
margin-bottom: var(--spacing-lg);
}
.stat-card {
background: var(--card-bg);
padding: var(--spacing-md) var(--spacing-lg);
border-radius: 12px;
box-shadow: var(--shadow-sm);
transition: transform 0.2s ease, box-shadow 0.2s ease;
border: 1px solid var(--border-color);
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.stat-card h4 {
margin: 0 0 var(--spacing-xs) 0;
font-size: 13px;
color: var(--text-secondary);
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.stat-card .value {
font-size: 32px;
font-weight: 700;
color: var(--text-primary);
line-height: 1.2;
}
.stat-card.running .value { color: #27ae60; }
.stat-card.stopped .value { color: #e74c3c; }
.stat-card.whitelist .value { color: #9b59b6; }
/* 操作工具栏优化 */
.toolbar-card {
margin-bottom: var(--spacing-lg);
border-radius: 12px;
box-shadow: var(--shadow-sm);
}
.toolbar-card .n-card__content {
padding: var(--spacing-md) var(--spacing-lg);
}
.toolbar-section {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--spacing-sm);
}
.toolbar-group {
display: flex;
align-items: center;
gap: var(--spacing-xs);
padding-right: var(--spacing-md);
border-right: 1px solid var(--border-color);
margin-right: var(--spacing-xs);
}
.toolbar-group:last-child {
border-right: none;
margin-right: 0;
padding-right: 0;
}
.toolbar-label {
font-size: 12px;
color: var(--text-secondary);
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-right: var(--spacing-xs);
}
/* 表格优化 */
.strategy-tabs {
background: var(--card-bg);
border-radius: 12px;
box-shadow: var(--shadow-sm);
overflow: hidden;
}
.n-tabs-nav {
padding: 0 var(--spacing-lg);
background: #fafafa;
border-bottom: 1px solid var(--border-color);
}
.tab-content {
padding: var(--spacing-lg);
}
.n-table-wrapper {
border-radius: 8px;
overflow: hidden;
}
.n-table {
font-size: 13px;
}
.n-table thead .n-th {
background: #fafafa;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
font-size: 11px;
letter-spacing: 0.5px;
padding: var(--spacing-md) var(--spacing-sm);
}
.n-table tbody .n-td {
padding: var(--spacing-sm) var(--spacing-sm);
border-bottom: 1px solid #f5f5f5;
}
.n-table tbody .n-tr:hover {
background: #fafafa;
}
.strategy-name {
font-weight: 600;
color: var(--text-primary);
}
.strategy-info {
font-size: 12px;
color: var(--text-secondary);
}
/* 日志容器优化 */
.log-container {
background: #1e1e1e;
padding: var(--spacing-md);
border-radius: 8px;
height: 420px;
overflow: auto;
font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
font-size: 12px;
line-height: 1.6;
color: #ddd;
}
.log-line {
margin: 2px 0;
padding: 2px 0;
border-bottom: 1px solid #2a2a2a;
}
.log-line.error { color: #ff6b6b; }
.log-line.warning { color: #feca57; }
.log-line.info { color: #54a0ff; }
.log-line.success { color: #1dd1a1; }
/* 标签样式 */
.status-tag {
font-weight: 500;
}
.whitelist-tag {
cursor: pointer;
transition: opacity 0.2s ease;
}
.whitelist-tag:hover {
opacity: 0.8;
}
/* 空状态样式 */
.empty-state {
text-align: center;
padding: 48px 24px;
color: var(--text-secondary);
}
.empty-state-icon {
font-size: 48px;
margin-bottom: var(--spacing-md);
opacity: 0.5;
}
/* 响应式调整 */
@media (max-width: 1200px) {
.stats-row {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.header {
flex-direction: column;
gap: var(--spacing-md);
}
.stats-row {
grid-template-columns: 1fr;
}
.toolbar-group {
border-right: none;
padding-right: 0;
margin-right: 0;
margin-bottom: var(--spacing-xs);
}
}
2025-11-21 16:08:03 +08:00
</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, computed } = Vue;
2025-11-21 16:08:03 +08:00
const naive = window.naive;
// --- 主组件逻辑 ---
const MainLayout = {
template: `
<div>
<div class="header">
<h2 style="margin:0; color: #333;">📈 量化策略控制台</h2>
<n-space align="center">
<!-- Git 版本信息 -->
<n-tag :bordered="false" type="default" size="small">
📦 Version: {{ gitInfo }}
</n-tag>
2025-11-21 16:08:03 +08:00
<!-- 刷新频率选择器 -->
<n-select
v-model:value="refreshInterval"
:options="intervalOptions"
size="small"
style="width: 130px"
></n-select>
<!-- 手动刷新按钮 -->
<n-button type="primary" size="small" @click="fetchStatus" :loading="loading">
刷新状态
</n-button>
<n-tag type="info" size="small">更新于: {{ lastUpdated }}</n-tag>
</n-space>
2025-11-21 16:08:03 +08:00
</div>
<!-- 统计卡片 -->
<div class="stats-row">
<div class="stat-card">
<h4>策略总数</h4>
<div class="value">{{ Object.keys(strategies).length }}</div>
</div>
<div class="stat-card running">
<h4>运行中</h4>
<div class="value">{{ runningCount }}</div>
</div>
<div class="stat-card stopped">
<h4>已停止</h4>
<div class="value">{{ stoppedCount }}</div>
</div>
<div class="stat-card whitelist">
<h4>白名单策略</h4>
<div class="value">{{ whitelistCount }}</div>
</div>
</div>
<!-- 白名单管理工具栏 -->
<n-card title="🛠️ 批量操作" hoverable class="toolbar-card">
<div class="toolbar-section">
<div class="toolbar-group">
<span class="toolbar-label">生命周期</span>
<n-button type="success" size="small" @click="batchStart" :disabled="selectedKeys.length === 0">
启动选中
</n-button>
<n-button type="error" size="small" @click="batchStop" :disabled="selectedKeys.length === 0">
停止选中
</n-button>
<n-button type="warning" size="small" @click="batchRestart" :disabled="selectedKeys.length === 0">
重启选中
</n-button>
</div>
<div class="toolbar-group">
<span class="toolbar-label">白名单</span>
<n-button type="primary" size="small" @click="batchAddToWhitelist" :disabled="selectedKeys.length === 0">
添加到白名单
</n-button>
<n-button type="info" size="small" @click="batchRemoveFromWhitelist" :disabled="selectedKeys.length === 0">
从白名单移除
</n-button>
<n-button type="success" size="small" @click="batchEnableInWhitelist" :disabled="selectedKeys.length === 0">
启用
</n-button>
<n-button type="warning" size="small" @click="batchDisableInWhitelist" :disabled="selectedKeys.length === 0">
禁用
</n-button>
</div>
<div class="toolbar-group">
<n-button type="warning" size="small" @click="triggerAutoStart">
🚀 手动触发自动启动
</n-button>
<n-tag type="info" size="small">
今日已自动启动: {{ whitelistAutoStarted ? '是' : '否' }}
</n-tag>
</div>
</div>
</n-card>
<!-- 策略列表 - 分为两个Tab -->
<n-tabs type="line" animated class="strategy-tabs">
<!-- 白名单策略Tab -->
<n-tab-pane name="whitelist" tab="✅ 白名单策略" :tab-style="{ paddingLeft: '16px', paddingRight: '16px' }">
<n-card hoverable :bordered="false">
<template #header-extra>
<n-space align="center">
<n-button text @click="selectAllWhitelist" size="small">全选</n-button>
<n-button text @click="clearSelection" size="small">清空</n-button>
<n-tag type="success" size="small">{{ Object.keys(whitelistStrategies).length }} 个</n-tag>
</n-space>
</template>
<n-table :single-line="false" striped size="small">
<thead>
<tr>
<th style="width: 48px; text-align: center;">
<n-checkbox :checked="allWhitelistSelected" :indeterminate="partialWhitelistSelected" @update:checked="toggleSelectAllWhitelist" />
</th>
<th>策略标识</th>
<th>策略名称</th>
<th style="width: 100px;">运行状态</th>
<th style="width: 100px;">白名单状态</th>
<th style="width: 80px;">PID</th>
<th style="width: 100px;">运行时长</th>
<th style="width: 240px;">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(info, key) in whitelistStrategies" :key="key" :class="{ 'n-data-table-tr--selected': selectedKeys.includes(key) }">
<td style="text-align: center;">
<n-checkbox :checked="selectedKeys.includes(key)" @update:checked="toggleSelect(key)" />
</td>
<td><span class="strategy-name">{{ key }}</span></td>
<td>
<span class="strategy-name">{{ info.config.name }}</span>
<br>
<span class="strategy-info">{{ info.symbol }}</span>
</td>
<td>
<n-tag :type="info.status === 'running' ? 'success' : 'error'" size="small" class="status-tag">
{{ info.status === 'running' ? '运行中' : '已停止' }}
</n-tag>
</td>
<td>
<n-tag :type="info.whitelist_enabled ? 'success' : 'warning'" size="small" class="status-tag">
{{ info.whitelist_enabled ? '启用' : '禁用' }}
</n-tag>
</td>
<td>{{ info.pid || '-' }}</td>
<td>{{ info.uptime || '-' }}</td>
<td>
<n-space size="small">
<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-button size="small" @click="removeFromWhitelist(key)" type="warning" quaternary>移出</n-button>
</n-space>
</td>
</tr>
<tr v-if="Object.keys(whitelistStrategies).length === 0">
<td colspan="8">
<div class="empty-state">
<div class="empty-state-icon">📭</div>
<div>白名单中暂无策略</div>
</div>
</td>
</tr>
</tbody>
</n-table>
</n-card>
</n-tab-pane>
<!-- 非白名单策略Tab -->
<n-tab-pane name="non-whitelist" tab="❌ 非白名单策略" :tab-style="{ paddingLeft: '16px', paddingRight: '16px' }">
<n-card hoverable :bordered="false">
<template #header-extra>
<n-space align="center">
<n-button text @click="selectAllNonWhitelist" size="small">全选</n-button>
<n-button text @click="clearSelection" size="small">清空</n-button>
<n-tag type="default" size="small">{{ Object.keys(nonWhitelistStrategies).length }} 个</n-tag>
</n-space>
</template>
<n-table :single-line="false" striped size="small">
<thead>
<tr>
<th style="width: 48px; text-align: center;">
<n-checkbox :checked="allNonWhitelistSelected" :indeterminate="partialNonWhitelistSelected" @update:checked="toggleSelectAllNonWhitelist" />
</th>
<th>策略标识</th>
<th>策略名称</th>
<th style="width: 100px;">运行状态</th>
<th style="width: 100px;">白名单</th>
<th style="width: 80px;">PID</th>
<th style="width: 100px;">运行时长</th>
<th style="width: 240px;">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(info, key) in nonWhitelistStrategies" :key="key" :class="{ 'n-data-table-tr--selected': selectedKeys.includes(key) }">
<td style="text-align: center;">
<n-checkbox :checked="selectedKeys.includes(key)" @update:checked="toggleSelect(key)" />
</td>
<td><span class="strategy-name">{{ key }}</span></td>
<td>
<span class="strategy-name">{{ info.config.name }}</span>
<br>
<span class="strategy-info">{{ info.symbol }}</span>
</td>
<td>
<n-tag :type="info.status === 'running' ? 'success' : 'error'" size="small" class="status-tag">
{{ info.status === 'running' ? '运行中' : '已停止' }}
</n-tag>
</td>
<td>
<n-tag type="default" size="small" class="status-tag">✗ 不在</n-tag>
</td>
<td>{{ info.pid || '-' }}</td>
<td>{{ info.uptime || '-' }}</td>
<td>
<n-space size="small">
<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-button size="small" @click="addToWhitelist(key)" type="success" quaternary>加入</n-button>
</n-space>
</td>
</tr>
<tr v-if="Object.keys(nonWhitelistStrategies).length === 0">
<td colspan="8">
<div class="empty-state">
<div class="empty-state-icon"></div>
<div>所有策略都已在白名单中</div>
</div>
</td>
</tr>
</tbody>
</n-table>
</n-card>
</n-tab-pane>
</n-tabs>
<!-- 日志弹窗 -->
<n-modal v-model:show="showLogModal" style="width: 1000px;" preset="card" :title="'📜 实时日志: ' + currentLogKey" :bordered="false">
<div class="log-container" id="logBox">
<div v-if="logLoading" style="text-align:center; padding:40px;"><n-spin size="medium" /></div>
<div v-else v-for="(line, index) in logLines" :key="index" class="log-line" :class="getLogClass(line)">{{ line }}</div>
<div v-if="!logLoading && logLines.length === 0" class="empty-state">
<div class="empty-state-icon">📭</div>
<div>暂无日志内容</div>
</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>
</div>
2025-11-21 16:08:03 +08:00
`,
setup() {
const message = naive.useMessage();
const dialog = naive.useDialog();
const strategies = ref({});
const loading = ref(false);
const lastUpdated = ref('-');
2025-12-16 00:04:02 +08:00
const gitInfo = ref('Loading...');
2025-11-21 16:08:03 +08:00
// 白名单相关
const whitelistAutoStarted = ref(false);
const selectedKeys = ref([]);
2025-12-16 00:04:02 +08:00
const refreshInterval = ref(0);
2025-11-21 16:08:03 +08:00
const intervalOptions = [
2025-12-16 00:04:02 +08:00
{ label: '✋ 仅手动', value: 0 },
2025-11-21 16:08:03 +08:00
{ label: '⚡ 3秒自动', value: 3000 },
{ label: '⏱ 5秒自动', value: 5000 },
{ label: '🐢 10秒自动', value: 10000 }
];
let timer = null;
// 计算属性
const runningCount = computed(() => Object.values(strategies.value).filter(s => s.status === 'running').length);
const stoppedCount = computed(() => Object.values(strategies.value).filter(s => s.status === 'stopped').length);
const whitelistCount = computed(() => Object.values(strategies.value).filter(s => s.in_whitelist).length);
// 分离白名单和非白名单策略
const whitelistStrategies = computed(() => {
const result = {};
for (const [key, info] of Object.entries(strategies.value)) {
if (info.in_whitelist) result[key] = info;
}
return result;
});
const nonWhitelistStrategies = computed(() => {
const result = {};
for (const [key, info] of Object.entries(strategies.value)) {
if (!info.in_whitelist) result[key] = info;
}
return result;
});
// 白名单策略选择状态
const allWhitelistSelected = computed(() => {
const keys = Object.keys(whitelistStrategies.value);
return keys.length > 0 && keys.every(k => selectedKeys.value.includes(k));
});
const partialWhitelistSelected = computed(() => {
const keys = Object.keys(whitelistStrategies.value);
const selected = keys.filter(k => selectedKeys.value.includes(k));
return selected.length > 0 && selected.length < keys.length;
});
// 非白名单策略选择状态
const allNonWhitelistSelected = computed(() => {
const keys = Object.keys(nonWhitelistStrategies.value);
return keys.length > 0 && keys.every(k => selectedKeys.value.includes(k));
});
const partialNonWhitelistSelected = computed(() => {
const keys = Object.keys(nonWhitelistStrategies.value);
const selected = keys.filter(k => selectedKeys.value.includes(k));
return selected.length > 0 && selected.length < keys.length;
});
const allSelected = computed(() => selectedKeys.value.length > 0 && selectedKeys.value.length === Object.keys(strategies.value).length);
const partialSelected = computed(() => selectedKeys.value.length > 0 && selectedKeys.value.length < Object.keys(strategies.value).length);
2025-11-21 16:08:03 +08:00
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;
2025-12-16 00:04:02 +08:00
gitInfo.value = data.git_info || 'N/A';
whitelistAutoStarted.value = !data.whitelist_auto_start_today;
2025-11-21 16:08:03 +08:00
lastUpdated.value = new Date().toLocaleTimeString();
// 清理已删除策略的选中状态
selectedKeys.value = selectedKeys.value.filter(k => k in strategies.value);
2025-11-21 16:08:03 +08:00
} catch (e) {
message.error("连接服务器失败");
} finally {
loading.value = false;
}
};
const resetTimer = () => {
if (timer) clearInterval(timer);
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 toggleSelect = (key) => {
const idx = selectedKeys.value.indexOf(key);
if (idx >= 0) {
selectedKeys.value.splice(idx, 1);
} else {
selectedKeys.value.push(key);
}
};
const toggleSelectAll = (checked) => {
if (checked) {
selectedKeys.value = Object.keys(strategies.value);
} else {
selectedKeys.value = [];
}
};
const selectAll = () => {
selectedKeys.value = Object.keys(strategies.value);
};
const clearSelection = () => {
selectedKeys.value = [];
};
// 白名单Tab选择函数
const selectAllWhitelist = () => {
selectedKeys.value = [...new Set([...selectedKeys.value, ...Object.keys(whitelistStrategies.value)])];
};
const selectAllNonWhitelist = () => {
selectedKeys.value = [...new Set([...selectedKeys.value, ...Object.keys(nonWhitelistStrategies.value)])];
};
const toggleSelectAllWhitelist = (checked) => {
if (checked) {
selectedKeys.value = [...new Set([...selectedKeys.value, ...Object.keys(whitelistStrategies.value)])];
} else {
selectedKeys.value = selectedKeys.value.filter(k => !(k in whitelistStrategies.value));
}
};
const toggleSelectAllNonWhitelist = (checked) => {
if (checked) {
selectedKeys.value = [...new Set([...selectedKeys.value, ...Object.keys(nonWhitelistStrategies.value)])];
} else {
selectedKeys.value = selectedKeys.value.filter(k => !(k in nonWhitelistStrategies.value));
}
};
// 策略操作
2025-11-21 16:08:03 +08:00
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 batchAction = async (action, apiEndpoint) => {
const results = [];
for (const name of selectedKeys.value) {
try {
const res = await fetch(`/api/${apiEndpoint}/${name}/${action}`, { method: 'POST' });
if (res.ok) {
results.push(name);
}
} catch (e) {
console.error(`操作失败: ${name}`, e);
}
}
if (results.length > 0) {
message.success(`成功操作 ${results.length} 个策略`);
fetchStatus();
} else {
message.warning("没有成功的操作");
}
};
const batchStart = () => batchAction('start', 'api/strategy');
const batchStop = () => batchAction('stop', 'api/strategy');
const batchRestart = () => batchAction('restart', 'api/strategy');
// 白名单操作
const batchAddToWhitelist = async () => {
const results = [];
for (const name of selectedKeys.value) {
try {
const res = await fetch(`/api/whitelist/${name}/add`, { method: 'POST' });
if (res.ok) results.push(name);
} catch (e) { console.error(e); }
}
if (results.length > 0) {
message.success(`已添加到白名单: ${results.join(', ')}`);
fetchStatus();
} else {
message.warning("没有成功的操作");
}
};
const batchRemoveFromWhitelist = async () => {
const results = [];
for (const name of selectedKeys.value) {
try {
const res = await fetch(`/api/whitelist/${name}/remove`, { method: 'POST' });
if (res.ok) results.push(name);
} catch (e) { console.error(e); }
}
if (results.length > 0) {
message.success(`已从白名单移除: ${results.join(', ')}`);
fetchStatus();
} else {
message.warning("没有成功的操作");
}
};
const batchEnableInWhitelist = async () => {
const results = [];
for (const name of selectedKeys.value) {
try {
const res = await fetch(`/api/whitelist/${name}/enable`, { method: 'POST' });
if (res.ok) results.push(name);
} catch (e) { console.error(e); }
}
if (results.length > 0) {
message.success(`已启用: ${results.join(', ')}`);
fetchStatus();
} else {
message.warning("没有成功的操作");
}
};
const batchDisableInWhitelist = async () => {
const results = [];
for (const name of selectedKeys.value) {
try {
const res = await fetch(`/api/whitelist/${name}/disable`, { method: 'POST' });
if (res.ok) results.push(name);
} catch (e) { console.error(e); }
}
if (results.length > 0) {
message.success(`已禁用: ${results.join(', ')}`);
fetchStatus();
} else {
message.warning("没有成功的操作");
}
};
const toggleWhitelist = async (name) => {
const info = strategies.value[name];
if (!info) return;
try {
if (info.in_whitelist) {
// 在白名单中,询问是否移除
dialog.warning({
title: '白名单操作',
content: `${name} 已在白名单中,是否移除?`,
positiveText: '移除',
negativeText: '取消',
onPositiveClick: async () => {
const res = await fetch(`/api/whitelist/${name}/remove`, { method: 'POST' });
if (res.ok) {
message.success("已从白名单移除");
fetchStatus();
}
}
});
} else {
// 不在白名单中,询问是否添加
dialog.info({
title: '白名单操作',
content: `将 ${name} 添加到白名单?`,
positiveText: '添加',
negativeText: '取消',
onPositiveClick: async () => {
const res = await fetch(`/api/whitelist/${name}/add`, { method: 'POST' });
if (res.ok) {
message.success("已添加到白名单");
fetchStatus();
}
}
});
}
} catch (e) {
message.error("操作失败");
}
};
// 单个策略加入白名单(无弹窗确认)
const addToWhitelist = async (name) => {
try {
const res = await fetch(`/api/whitelist/${name}/add`, { method: 'POST' });
if (res.ok) {
message.success(`已添加到白名单: ${name}`);
fetchStatus();
} else {
message.error("添加失败");
}
} catch (e) {
message.error("操作失败");
}
};
// 单个策略移出白名单(无弹窗确认)
const removeFromWhitelist = async (name) => {
try {
const res = await fetch(`/api/whitelist/${name}/remove`, { method: 'POST' });
if (res.ok) {
message.success(`已从白名单移除: ${name}`);
fetchStatus();
} else {
message.error("移除失败");
}
} catch (e) {
message.error("操作失败");
}
};
const triggerAutoStart = async () => {
try {
const res = await fetch('/api/whitelist/auto-start', { method: 'POST' });
const data = await res.json();
if (data.success) {
message.success(`自动启动完成: 成功 ${data.success_count}, 失败 ${data.fail_count}`);
fetchStatus();
} else {
message.warning("今天已执行过或无需启动");
}
} catch (e) {
message.error("自动启动失败");
}
};
// 日志相关
2025-11-21 16:08:03 +08:00
const showLogModal = ref(false);
const currentLogKey = ref('');
const logLines = ref([]);
const logLoading = ref(false);
const getLogClass = (line) => {
if (line.includes('[ERROR]') || line.includes('❌')) return 'error';
if (line.includes('[WARNING]') || line.includes('⚠️')) return 'warning';
if (line.includes('[INFO]') || line.includes('✅')) return 'info';
if (line.includes('成功') || line.includes('success')) return 'success';
return '';
};
2025-11-21 16:08:03 +08:00
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 || [];
2025-11-21 16:08:03 +08:00
setTimeout(() => {
const el = document.getElementById('logBox');
if(el) el.scrollTop = el.scrollHeight;
}, 100);
} catch(e) { message.error("日志获取失败"); }
finally { logLoading.value = false; }
};
2025-11-21 16:08:03 +08:00
const viewLogs = (name) => {
currentLogKey.value = name;
showLogModal.value = true;
fetchLogs(name);
};
onMounted(() => {
2025-12-16 00:04:02 +08:00
fetchStatus();
resetTimer();
2025-11-21 16:08:03 +08:00
});
onUnmounted(() => {
if (timer) clearInterval(timer);
});
return {
strategies, loading, lastUpdated, gitInfo,
2025-11-21 16:08:03 +08:00
refreshInterval, intervalOptions,
showLogModal, currentLogKey, logLines, logLoading,
fetchStatus, handleAction, viewLogs, fetchLogs, getLogClass,
// 分离的策略列表
whitelistStrategies, nonWhitelistStrategies,
// 分离的选择状态
allWhitelistSelected, partialWhitelistSelected,
allNonWhitelistSelected, partialNonWhitelistSelected,
// 白名单
whitelistAutoStarted,
selectedKeys,
runningCount, stoppedCount, whitelistCount,
allSelected, partialSelected,
toggleSelect, toggleSelectAll, selectAll, clearSelection,
selectAllWhitelist, selectAllNonWhitelist,
toggleSelectAllWhitelist, toggleSelectAllNonWhitelist,
batchStart, batchStop, batchRestart,
batchAddToWhitelist, batchRemoveFromWhitelist,
batchEnableInWhitelist, batchDisableInWhitelist,
toggleWhitelist, triggerAutoStart,
addToWhitelist, removeFromWhitelist
2025-11-21 16:08:03 +08:00
};
}
};
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>