新增实盘策略:FisherTrendStrategy(FG)
This commit is contained in:
646
strategy_manager/frontend/dist/index.html
vendored
646
strategy_manager/frontend/dist/index.html
vendored
@@ -8,29 +8,251 @@
|
||||
<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>
|
||||
<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.4.21/vue.global.prod.min.js"></script>
|
||||
<!-- 2. 引入 Naive UI -->
|
||||
<script src="https://unpkg.com/naive-ui/dist/index.js"></script>
|
||||
<script src="https://cdn.bootcdn.net/ajax/libs/naive-ui/2.38.1/index.js"></script>
|
||||
|
||||
<style>
|
||||
body { font-family: 'Inter', sans-serif; background-color: #f5f7fa; margin: 0; }
|
||||
#app { padding: 20px; max-width: 1400px; 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; }
|
||||
: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;
|
||||
}
|
||||
|
||||
/* 白名单状态标签 */
|
||||
.whitelist-tag { cursor: pointer; }
|
||||
.whitelist-tag:hover { opacity: 0.8; }
|
||||
/* 头部布局优化 */
|
||||
.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: 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; }
|
||||
/* 统计卡片优化 */
|
||||
.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);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -99,117 +321,204 @@
|
||||
</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>
|
||||
</n-space>
|
||||
<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>
|
||||
|
||||
<!-- 策略列表 -->
|
||||
<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>
|
||||
<!-- 策略列表 - 分为两个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: 900px;" preset="card" :title="'📜 实时日志: ' + currentLogKey">
|
||||
<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:20px;"><n-spin size="medium" /></div>
|
||||
<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>
|
||||
<n-button size="small" @click="fetchLogs(currentLogKey)">刷新</n-button>
|
||||
<n-button size="small" @click="showLogModal = false">关闭</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
</div>
|
||||
@@ -241,6 +550,45 @@
|
||||
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);
|
||||
|
||||
@@ -308,6 +656,28 @@
|
||||
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));
|
||||
}
|
||||
};
|
||||
|
||||
// 策略操作
|
||||
const handleAction = (name, action) => {
|
||||
const map = { start: '启动', stop: '停止', restart: '重启' };
|
||||
@@ -461,6 +831,36 @@
|
||||
}
|
||||
};
|
||||
|
||||
// 单个策略加入白名单(无弹窗确认)
|
||||
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' });
|
||||
@@ -525,16 +925,26 @@
|
||||
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
|
||||
toggleWhitelist, triggerAutoStart,
|
||||
addToWhitelist, removeFromWhitelist
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user