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://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; }
|
2026-01-26 01:21:46 +08:00
|
|
|
#app { padding: 20px; max-width: 1400px; margin: 0 auto; }
|
2025-11-21 16:08:03 +08:00
|
|
|
.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; }
|
2026-01-26 01:21:46 +08:00
|
|
|
|
|
|
|
|
/* 白名单状态标签 */
|
|
|
|
|
.whitelist-tag { cursor: pointer; }
|
|
|
|
|
.whitelist-tag:hover { opacity: 0.8; }
|
|
|
|
|
|
|
|
|
|
/* 统计卡片 */
|
|
|
|
|
.stats-row { display: flex; gap: 15px; margin-bottom: 20px; flex-wrap: wrap; }
|
|
|
|
|
.stat-card { flex: 1; min-width: 150px; background: #fff; padding: 15px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
|
|
|
|
|
.stat-card h4 { margin: 0 0 8px 0; font-size: 13px; color: #666; font-weight: normal; }
|
|
|
|
|
.stat-card .value { font-size: 28px; font-weight: bold; color: #333; }
|
|
|
|
|
.stat-card.running .value { color: #27ae60; }
|
|
|
|
|
.stat-card.stopped .value { color: #e74c3c; }
|
|
|
|
|
.stat-card.whitelist .value { color: #9b59b6; }
|
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>
|
2026-01-26 01:21:46 +08:00
|
|
|
const { createApp, ref, onMounted, onUnmounted, watch, computed } = Vue;
|
2025-11-21 16:08:03 +08:00
|
|
|
const naive = window.naive;
|
|
|
|
|
|
|
|
|
|
// --- 主组件逻辑 ---
|
|
|
|
|
const MainLayout = {
|
|
|
|
|
template: `
|
2026-01-26 01:21:46 +08:00
|
|
|
<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
|
|
|
|
2026-01-26 01:21:46 +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>
|
2026-01-26 01:21:46 +08:00
|
|
|
|
|
|
|
|
<!-- 统计卡片 -->
|
|
|
|
|
<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 style="margin-bottom: 20px;">
|
|
|
|
|
<n-space wrap>
|
|
|
|
|
<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>
|
|
|
|
|
<n-divider vertical />
|
|
|
|
|
<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>
|
|
|
|
|
<n-divider vertical />
|
|
|
|
|
<n-button type="warning" size="small" @click="triggerAutoStart">
|
|
|
|
|
🚀 手动触发自动启动
|
|
|
|
|
</n-button>
|
|
|
|
|
<n-tag type="info" size="small">
|
|
|
|
|
今日已自动启动: {{ whitelistAutoStarted ? '是' : '否' }}
|
|
|
|
|
</n-tag>
|
2025-11-21 16:08:03 +08:00
|
|
|
</n-space>
|
2026-01-26 01:21:46 +08:00
|
|
|
</n-card>
|
|
|
|
|
|
|
|
|
|
<!-- 策略列表 -->
|
|
|
|
|
<n-card title="📋 策略列表" hoverable>
|
|
|
|
|
<template #header-extra>
|
|
|
|
|
<n-space>
|
|
|
|
|
<n-button text @click="selectAll" size="small">全选</n-button>
|
|
|
|
|
<n-button text @click="clearSelection" size="small">清空</n-button>
|
|
|
|
|
</n-space>
|
|
|
|
|
</template>
|
|
|
|
|
<n-table :single-line="false" striped>
|
|
|
|
|
<thead>
|
|
|
|
|
<tr>
|
|
|
|
|
<th style="width: 40px;">
|
|
|
|
|
<n-checkbox :checked="allSelected" :indeterminate="partialSelected" @update:checked="toggleSelectAll" />
|
|
|
|
|
</th>
|
|
|
|
|
<th>策略标识</th>
|
|
|
|
|
<th>策略名称</th>
|
|
|
|
|
<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" :class="{ 'n-data-table-tr--selected': selectedKeys.includes(key) }">
|
|
|
|
|
<td>
|
|
|
|
|
<n-checkbox :checked="selectedKeys.includes(key)" @update:checked="toggleSelect(key)" />
|
|
|
|
|
</td>
|
|
|
|
|
<td><strong>{{ key }}</strong></td>
|
|
|
|
|
<td>{{ info.config.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>
|
|
|
|
|
<n-tag :type="info.in_whitelist ? 'success' : 'default'" size="small" class="whitelist-tag"
|
|
|
|
|
@click="toggleWhitelist(key)">
|
|
|
|
|
{{ info.in_whitelist ? '✓ 在白名单' : '✗ 不在' }}
|
|
|
|
|
</n-tag>
|
|
|
|
|
</td>
|
|
|
|
|
<td>
|
|
|
|
|
<n-tag v-if="info.in_whitelist" :type="info.whitelist_enabled ? 'success' : 'warning'" size="small">
|
|
|
|
|
{{ info.whitelist_enabled ? '启用' : '禁用' }}
|
|
|
|
|
</n-tag>
|
|
|
|
|
<span v-else style="color: #999;">-</span>
|
|
|
|
|
</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="9" style="text-align: center; padding: 30px; color: #999;">暂无策略</td>
|
|
|
|
|
</tr>
|
|
|
|
|
</tbody>
|
|
|
|
|
</n-table>
|
|
|
|
|
</n-card>
|
|
|
|
|
|
|
|
|
|
<!-- 日志弹窗 -->
|
|
|
|
|
<n-modal v-model:show="showLogModal" style="width: 900px;" 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" :class="getLogClass(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>
|
|
|
|
|
</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
|
|
|
|
2026-01-26 01:21:46 +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;
|
|
|
|
|
|
2026-01-26 01:21:46 +08:00
|
|
|
// 计算属性
|
|
|
|
|
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 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';
|
2026-01-26 01:21:46 +08:00
|
|
|
whitelistAutoStarted.value = !data.whitelist_auto_start_today;
|
2025-11-21 16:08:03 +08:00
|
|
|
lastUpdated.value = new Date().toLocaleTimeString();
|
2026-01-26 01:21:46 +08:00
|
|
|
|
|
|
|
|
// 清理已删除策略的选中状态
|
|
|
|
|
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("已切换为手动刷新模式");
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-26 01:21:46 +08:00
|
|
|
// 选择操作
|
|
|
|
|
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 = [];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 策略操作
|
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("请求错误"); }
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-26 01:21:46 +08:00
|
|
|
// 批量操作
|
|
|
|
|
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 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);
|
|
|
|
|
|
2026-01-26 01:21:46 +08:00
|
|
|
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();
|
2026-01-26 01:21:46 +08:00
|
|
|
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; }
|
2026-01-26 01:21:46 +08:00
|
|
|
};
|
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 {
|
2026-01-26 01:21:46 +08:00
|
|
|
strategies, loading, lastUpdated, gitInfo,
|
2025-11-21 16:08:03 +08:00
|
|
|
refreshInterval, intervalOptions,
|
|
|
|
|
showLogModal, currentLogKey, logLines, logLoading,
|
2026-01-26 01:21:46 +08:00
|
|
|
fetchStatus, handleAction, viewLogs, fetchLogs, getLogClass,
|
|
|
|
|
|
|
|
|
|
// 白名单
|
|
|
|
|
whitelistAutoStarted,
|
|
|
|
|
selectedKeys,
|
|
|
|
|
runningCount, stoppedCount, whitelistCount,
|
|
|
|
|
allSelected, partialSelected,
|
|
|
|
|
toggleSelect, toggleSelectAll, selectAll, clearSelection,
|
|
|
|
|
batchStart, batchStop, batchRestart,
|
|
|
|
|
batchAddToWhitelist, batchRemoveFromWhitelist,
|
|
|
|
|
batchEnableInWhitelist, batchDisableInWhitelist,
|
|
|
|
|
toggleWhitelist, triggerAutoStart
|
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>
|
2026-01-26 01:21:46 +08:00
|
|
|
</html>
|