feat: persist browser sessions and update admin workflows
This commit is contained in:
@@ -1,228 +1,93 @@
|
||||
<template>
|
||||
<div class="shell-page shell-page-fluid page-section upstreams-page">
|
||||
<section class="page-header upstreams-hero surface-card">
|
||||
<div class="page-heading">
|
||||
<p class="page-kicker">Monitoring Matrix</p>
|
||||
<h2 class="page-title">上游管理</h2>
|
||||
<p class="page-desc">
|
||||
管理 API 上游服务、认证方式与轮询策略。这里优先展示健康度、检测节奏和错误信号,减少你在异常发生时的定位成本。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-cluster hero-actions">
|
||||
<el-button @click="loadList" :loading="tableLoading">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新列表
|
||||
</el-button>
|
||||
<el-button type="primary" @click="openCreate">
|
||||
<el-icon><Plus /></el-icon>
|
||||
新增上游
|
||||
</el-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="metric-grid">
|
||||
<article class="surface-card metric-card">
|
||||
<div class="metric-label">Total Sources</div>
|
||||
<div class="metric-value">{{ metrics.total }}</div>
|
||||
<p class="metric-note">当前纳管的上游节点总数</p>
|
||||
</article>
|
||||
<article class="surface-card metric-card">
|
||||
<div class="metric-label">Healthy</div>
|
||||
<div class="metric-value">{{ metrics.healthy }}</div>
|
||||
<p class="metric-note">最近一次检测返回健康状态</p>
|
||||
</article>
|
||||
<article class="surface-card metric-card">
|
||||
<div class="metric-label">Enabled</div>
|
||||
<div class="metric-value">{{ metrics.enabled }}</div>
|
||||
<p class="metric-note">已启用定时检测的上游节点</p>
|
||||
</article>
|
||||
<article class="surface-card metric-card">
|
||||
<div class="metric-label">Attention</div>
|
||||
<div class="metric-value">{{ metrics.unhealthy }}</div>
|
||||
<p class="metric-note">需要处理错误或网络异常的节点</p>
|
||||
</article>
|
||||
<article class="surface-card metric-card">
|
||||
<div class="metric-label">Balance</div>
|
||||
<div class="metric-value">{{ metrics.balanceCount }}</div>
|
||||
<p class="metric-note">已配置余额接口的上游节点数</p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="upstreams-content">
|
||||
<section class="surface-card data-stage">
|
||||
<div class="section-header data-stage-head">
|
||||
<div>
|
||||
<div class="section-caption">Upstream Registry</div>
|
||||
<h3 class="data-stage-title brand-type">检测与变更控制台</h3>
|
||||
</div>
|
||||
<p class="data-stage-note">点击详情可查看快照历史、分组倍率与最近错误。</p>
|
||||
<section class="surface-card data-stage">
|
||||
<div class="section-header data-stage-head">
|
||||
<div>
|
||||
<div class="section-caption">Upstream Registry</div>
|
||||
<h3 class="data-stage-title brand-type">检测与变更控制台</h3>
|
||||
</div>
|
||||
<div class="toolbar-cluster">
|
||||
<el-button size="small" text @click="loadList" :loading="tableLoading">刷新</el-button>
|
||||
<el-button size="small" type="primary" @click="openCreate">新增上游</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table :data="list" v-loading="tableLoading" row-key="id" style="width: 100%">
|
||||
<el-table-column label="来源" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<div class="cell-name">{{ row.name }}</div>
|
||||
<div class="cell-url mono">{{ row.base_url }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table :data="list" v-loading="tableLoading" row-key="id" style="width: 100%">
|
||||
<el-table-column label="来源" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<div class="cell-name">{{ row.name }}</div>
|
||||
<div class="cell-url mono">{{ row.base_url }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="状态" width="118">
|
||||
<template #default="{ row }">
|
||||
<span :class="['status-badge', row.last_status]">
|
||||
<span class="dot" />
|
||||
{{ statusLabel(row.last_status) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="118">
|
||||
<template #default="{ row }">
|
||||
<span :class="['status-badge', row.last_status]">
|
||||
<span class="dot" />
|
||||
{{ statusLabel(row.last_status) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="余额" width="140" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.balance !== null && row.balance !== undefined" class="balance-value mono">
|
||||
{{ formatBalance(row.balance) }}
|
||||
</span>
|
||||
<span v-else class="muted">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="余额" width="140" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.balance !== null && row.balance !== undefined" class="balance-value mono">
|
||||
{{ formatBalance(row.balance) }}
|
||||
</span>
|
||||
<span v-else class="muted">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="启用" width="88" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-switch v-model="row.enabled" @change="toggleEnabled(row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="启用" width="88" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-switch v-model="row.enabled" @change="toggleEnabled(row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="认证" width="132">
|
||||
<template #default="{ row }">
|
||||
<span class="status-badge auth-badge">{{ authLabel(row.auth_type) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="认证" width="132">
|
||||
<template #default="{ row }">
|
||||
<span class="status-badge auth-badge">{{ authLabel(row.auth_type) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="检测间隔" width="112">
|
||||
<template #default="{ row }">
|
||||
<span class="mono time-inline">{{ row.check_interval_seconds }}s</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="检测间隔" width="112">
|
||||
<template #default="{ row }">
|
||||
<span class="mono time-inline">{{ row.check_interval_seconds }}s</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="最近检测" min-width="168">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.last_checked_at" class="time-text mono">{{ fmtTime(row.last_checked_at) }}</span>
|
||||
<span v-else class="muted">未检测</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最近检测" min-width="168">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.last_checked_at" class="time-text mono">{{ fmtTime(row.last_checked_at) }}</span>
|
||||
<span v-else class="muted">未检测</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="最近错误" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip v-if="row.last_error" :content="row.last_error" placement="top" :show-after="300">
|
||||
<span class="error-text">{{ shrinkError(row.last_error) }}</span>
|
||||
</el-tooltip>
|
||||
<span v-else class="muted">无异常</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最近错误" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip v-if="row.last_error" :content="row.last_error" placement="top" :show-after="300">
|
||||
<span class="error-text">{{ shrinkError(row.last_error) }}</span>
|
||||
</el-tooltip>
|
||||
<span v-else class="muted">无异常</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="258" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div class="action-row">
|
||||
<el-button size="small" text @click="openEdit(row)" title="编辑">
|
||||
<el-icon><Edit /></el-icon>
|
||||
</el-button>
|
||||
<el-button size="small" text @click="testUpstream(row)" :loading="row._testing">测试</el-button>
|
||||
<el-button size="small" text @click="checkNow(row)" :loading="row._checking">检测</el-button>
|
||||
<el-button size="small" text @click="openKeyGenerate(row)" title="确保每个分组有一个 SmartUp Key">
|
||||
<el-icon><Key /></el-icon>
|
||||
</el-button>
|
||||
<el-button size="small" text @click="openDetail(row)">
|
||||
<el-icon><List /></el-icon>
|
||||
详情
|
||||
</el-button>
|
||||
<el-button size="small" text type="danger" @click="confirmDelete(row)" title="删除">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</section>
|
||||
|
||||
<aside class="upstreams-side surface-card">
|
||||
<section class="side-section overview-section">
|
||||
<div class="section-header insight-head">
|
||||
<div>
|
||||
<div class="section-caption">Runtime Snapshot</div>
|
||||
<h3 class="insight-title">运行概览</h3>
|
||||
<el-table-column label="操作" width="280">
|
||||
<template #default="{ row }">
|
||||
<div class="action-row">
|
||||
<el-button size="small" text @click="openEdit(row)" title="编辑上游配置(认证、接口、余额等)">编辑</el-button>
|
||||
<el-button size="small" text @click="testUpstream(row)" :loading="row._testing" title="仅验证连通性:登录 + 拉取分组列表,不写快照、不触发通知">测试连接</el-button>
|
||||
<el-button size="small" text @click="checkNow(row)" :loading="row._checking" title="完整同步:拉取倍率 → 生成快照 → 对比变化 → 触发 Webhook → 同步 Key">立即同步</el-button>
|
||||
<el-button size="small" text @click="openKeyGenerate(row)" title="为每个分组确保存在一个 SmartUp 托管 Key">生成Key</el-button>
|
||||
<el-button size="small" text @click="openDetail(row)" title="查看快照历史、分组倍率与已创建 Key">详情</el-button>
|
||||
<el-button size="small" text type="danger" @click="confirmDelete(row)" title="删除该上游及其所有快照和 Key 记录">删除</el-button>
|
||||
</div>
|
||||
<span class="insight-pill">健康率 {{ healthyRate }}%</span>
|
||||
</div>
|
||||
<div class="insight-grid">
|
||||
<div class="insight-metric">
|
||||
<span>异常节点</span>
|
||||
<strong>{{ metrics.unhealthy }}</strong>
|
||||
</div>
|
||||
<div class="insight-metric">
|
||||
<span>已启用</span>
|
||||
<strong>{{ metrics.enabled }}</strong>
|
||||
</div>
|
||||
<div class="insight-metric">
|
||||
<span>待检测</span>
|
||||
<strong>{{ pendingChecks }}</strong>
|
||||
</div>
|
||||
<div class="insight-metric">
|
||||
<span>最近检测</span>
|
||||
<strong>{{ latestCheckedAt ? fmtTime(latestCheckedAt) : '—' }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="side-section">
|
||||
<div class="section-header insight-head compact">
|
||||
<div>
|
||||
<div class="section-caption">Need Attention</div>
|
||||
<h3 class="insight-title">待关注节点</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="attentionList.length > 0" class="feed-list">
|
||||
<button
|
||||
v-for="row in attentionList"
|
||||
:key="row.id"
|
||||
type="button"
|
||||
class="feed-item"
|
||||
@click="openDetail(row)"
|
||||
>
|
||||
<div class="feed-main">
|
||||
<div class="feed-top">
|
||||
<span class="feed-name">{{ row.name }}</span>
|
||||
<span :class="['status-badge', row.last_status]">
|
||||
<span class="dot" />{{ statusLabel(row.last_status) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="feed-meta">{{ row.last_error ? shrinkError(row.last_error) : '最近状态异常,建议查看快照详情' }}</div>
|
||||
</div>
|
||||
<span class="feed-link">详情</span>
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="empty-hint side-empty">当前没有需要关注的异常节点</div>
|
||||
</section>
|
||||
|
||||
<section class="side-section">
|
||||
<div class="section-header insight-head compact">
|
||||
<div>
|
||||
<div class="section-caption">Recent Activity</div>
|
||||
<h3 class="insight-title">最近检测</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="recentChecks.length > 0" class="timeline-list">
|
||||
<div v-for="row in recentChecks" :key="row.id" class="timeline-item">
|
||||
<div class="timeline-main">
|
||||
<div class="timeline-name">{{ row.name }}</div>
|
||||
<div class="timeline-meta mono">{{ fmtTime(row.last_checked_at!) }}</div>
|
||||
</div>
|
||||
<el-button size="small" text @click="openDetail(row)">查看</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-hint side-empty">还没有检测记录</div>
|
||||
</section>
|
||||
</aside>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</section>
|
||||
|
||||
|
||||
<el-drawer
|
||||
v-model="drawerVisible"
|
||||
:title="editingId ? '编辑上游' : '新增上游'"
|
||||
@@ -314,6 +179,10 @@
|
||||
<el-input-number v-model="form.balance_divisor" :min="1" :max="999999999" style="width: 100%" />
|
||||
<div class="form-hint">原始值除以该数得到实际余额。New-API 填 <code>500000</code>,Sub2API 填 <code>1</code></div>
|
||||
</el-form-item>
|
||||
<el-form-item label="余额告警阈值">
|
||||
<el-input-number v-model="form.balance_alert_threshold" :min="0" :precision="2" :value-on-clear="null" style="width: 100%" />
|
||||
<div class="form-hint">余额低于此值时发送 Webhook 通知,留空/0 表示不监控</div>
|
||||
</el-form-item>
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="检测间隔(秒)">
|
||||
@@ -553,6 +422,7 @@ const defaultForm = () => ({
|
||||
balance_endpoint: '',
|
||||
balance_response_path: '',
|
||||
balance_divisor: 1.0,
|
||||
balance_alert_threshold: null as number | null,
|
||||
})
|
||||
const form = ref(defaultForm())
|
||||
const rules = {
|
||||
@@ -775,6 +645,7 @@ function openEdit(row: UpstreamData) {
|
||||
balance_endpoint: row.balance_endpoint || '',
|
||||
balance_response_path: row.balance_response_path || '',
|
||||
balance_divisor: row.balance_divisor ?? 1.0,
|
||||
balance_alert_threshold: row.balance_alert_threshold ?? null,
|
||||
}
|
||||
drawerVisible.value = true
|
||||
}
|
||||
@@ -1366,6 +1237,16 @@ onMounted(loadList)
|
||||
}
|
||||
}
|
||||
|
||||
.action-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.action-row .el-button {
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
.action-row .el-button--danger {
|
||||
--el-button-bg-color: transparent;
|
||||
--el-button-border-color: transparent;
|
||||
@@ -1374,4 +1255,12 @@ onMounted(loadList)
|
||||
--el-button-bg-color: var(--el-color-danger-light-8, #fef0f0);
|
||||
--el-button-border-color: var(--el-color-danger-light-5, #fbc4c4);
|
||||
}
|
||||
|
||||
:deep(.el-table__fixed-right) {
|
||||
background: var(--surface-bg, #1a1a1a);
|
||||
}
|
||||
|
||||
:deep(.el-table__fixed-right .el-table__cell) {
|
||||
background: var(--surface-bg, #1a1a1a) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user