feat: persist browser sessions and update admin workflows
This commit is contained in:
@@ -88,6 +88,7 @@ export interface UpstreamData {
|
||||
balance_endpoint: string
|
||||
balance_response_path: string
|
||||
balance_divisor: number
|
||||
balance_alert_threshold: number | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
@@ -106,6 +107,7 @@ export interface UpstreamForm {
|
||||
balance_endpoint: string
|
||||
balance_response_path: string
|
||||
balance_divisor: number
|
||||
balance_alert_threshold: number | null
|
||||
}
|
||||
|
||||
export interface GeneratedUpstreamKey {
|
||||
@@ -284,6 +286,7 @@ export const websitesApi = {
|
||||
platform_mode?: string
|
||||
concurrency?: number
|
||||
priority?: number
|
||||
auto_priority_by_rate?: boolean
|
||||
}) => api.post<{ success: boolean; message: string; items: ImportAccountItem[] }>(`/api/websites/${id}/accounts/import-upstream-keys`, data),
|
||||
listBindings: () => api.get<GroupBindingData[]>('/api/group-bindings'),
|
||||
createBinding: (data: GroupBindingForm) => api.post<GroupBindingData>('/api/group-bindings', data),
|
||||
|
||||
@@ -19,13 +19,6 @@
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-title">监控中枢</div>
|
||||
<nav class="sidebar-nav">
|
||||
<router-link to="/websites" class="nav-item" active-class="active" @click="closeMobileNav">
|
||||
<span class="nav-icon"><el-icon><OfficeBuilding /></el-icon></span>
|
||||
<span class="nav-copy">
|
||||
<strong>网站管理</strong>
|
||||
<small>目标站点、分组映射、自动同步</small>
|
||||
</span>
|
||||
</router-link>
|
||||
<router-link to="/upstreams" class="nav-item" active-class="active" @click="closeMobileNav">
|
||||
<span class="nav-icon"><el-icon><Connection /></el-icon></span>
|
||||
<span class="nav-copy">
|
||||
@@ -33,6 +26,13 @@
|
||||
<small>轮询、健康度、倍率快照</small>
|
||||
</span>
|
||||
</router-link>
|
||||
<router-link to="/websites" class="nav-item" active-class="active" @click="closeMobileNav">
|
||||
<span class="nav-icon"><el-icon><OfficeBuilding /></el-icon></span>
|
||||
<span class="nav-copy">
|
||||
<strong>网站管理</strong>
|
||||
<small>目标站点、分组映射、自动同步</small>
|
||||
</span>
|
||||
</router-link>
|
||||
<router-link to="/webhooks" class="nav-item" active-class="active" @click="closeMobileNav">
|
||||
<span class="nav-icon"><el-icon><Bell /></el-icon></span>
|
||||
<span class="nav-copy">
|
||||
@@ -317,6 +317,23 @@ watch([() => route.path, customPages], () => {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* 优化侧边栏滚动条,使其不超出圆角范围 */
|
||||
.sidebar::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
.sidebar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.sidebar::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 244, 232, 0.12);
|
||||
border-radius: 99px;
|
||||
}
|
||||
.sidebar::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 244, 232, 0.25);
|
||||
}
|
||||
|
||||
.sidebar.open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ const router = createRouter({
|
||||
path: '/',
|
||||
component: () => import('@/components/AppLayout.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
redirect: '/websites',
|
||||
redirect: '/upstreams',
|
||||
children: [
|
||||
{ path: 'upstreams', component: () => import('@/views/Upstreams.vue') },
|
||||
{ path: 'websites', component: () => import('@/views/Websites.vue') },
|
||||
@@ -32,7 +32,7 @@ router.beforeEach((to, _from, next) => {
|
||||
if (to.meta.requiresAuth && !auth.token) {
|
||||
next('/login')
|
||||
} else if (to.path === '/login' && auth.token) {
|
||||
next('/websites')
|
||||
next('/upstreams')
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
|
||||
@@ -1,30 +1,25 @@
|
||||
<template>
|
||||
<div class="shell-page shell-page-fluid page-section">
|
||||
<div class="page-header surface-card page-block">
|
||||
<div class="page-heading">
|
||||
<p class="page-kicker">Delivery Trace</p>
|
||||
<h2 class="page-title">通知日志</h2>
|
||||
<p class="page-desc">查看所有 Webhook 通知的发送记录</p>
|
||||
<div class="panel">
|
||||
<div class="panel-head">
|
||||
<div class="panel-title">通知日志</div>
|
||||
<div class="panel-actions filters">
|
||||
<el-select v-model="filterStatus" placeholder="状态" clearable style="width:110px" @change="handleFilterChange">
|
||||
<el-option label="成功" value="success" />
|
||||
<el-option label="失败" value="failed" />
|
||||
</el-select>
|
||||
<el-select v-model="filterEvent" placeholder="事件类型" clearable style="width:150px" @change="handleFilterChange">
|
||||
<el-option label="上游倍率变更" value="upstream_rate_changed" />
|
||||
<el-option label="网站倍率变更" value="website_rate_changed" />
|
||||
<el-option label="服务异常" value="upstream_unhealthy" />
|
||||
<el-option label="服务恢复" value="upstream_recovered" />
|
||||
<el-option label="余额不足" value="upstream_balance_low" />
|
||||
<el-option label="测试" value="test" />
|
||||
</el-select>
|
||||
<el-button size="small" text @click="loadList" :loading="tableLoading">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filters">
|
||||
<el-select v-model="filterStatus" placeholder="状态" clearable style="width:110px" @change="handleFilterChange">
|
||||
<el-option label="成功" value="success" />
|
||||
<el-option label="失败" value="failed" />
|
||||
</el-select>
|
||||
<el-select v-model="filterEvent" placeholder="事件类型" clearable style="width:150px" @change="handleFilterChange">
|
||||
<el-option label="上游倍率变更" value="upstream_rate_changed" />
|
||||
<el-option label="网站倍率变更" value="website_rate_changed" />
|
||||
<el-option label="服务异常" value="upstream_unhealthy" />
|
||||
<el-option label="服务恢复" value="upstream_recovered" />
|
||||
<el-option label="测试" value="test" />
|
||||
</el-select>
|
||||
<el-button @click="loadList" :loading="tableLoading">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card page-block">
|
||||
<el-table :data="list" v-loading="tableLoading" style="width:100%">
|
||||
<el-table-column label="时间" width="150">
|
||||
<template #default="{ row }">
|
||||
@@ -51,15 +46,23 @@
|
||||
<span class="muted small">{{ row.response_text?.substring(0, 80) || '—' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="详情" width="80" fixed="right">
|
||||
<el-table-column label="操作" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" text @click="viewDetail(row)"><el-icon><View /></el-icon></el-button>
|
||||
<el-button size="small" text @click="viewDetail(row)" title="查看通知发送的Payload以及接口返回的响应详情">详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination">
|
||||
<div class="page-info">
|
||||
第 {{ currentPage }} 页 · 每页 {{ pageSize }} 条
|
||||
第 {{ currentPage }} 页
|
||||
<span style="margin: 0 8px;">·</span>
|
||||
每页
|
||||
<el-select v-model="pageSize" style="width: 76px; margin: 0 4px;" size="small" @change="handlePageSizeChange">
|
||||
<el-option :value="20" label="20" />
|
||||
<el-option :value="50" label="50" />
|
||||
<el-option :value="100" label="100" />
|
||||
</el-select>
|
||||
条
|
||||
</div>
|
||||
<div class="page-actions">
|
||||
<el-button :disabled="offset === 0 || tableLoading" @click="prevPage" size="small">上一页</el-button>
|
||||
@@ -106,20 +109,21 @@ const detailRow = ref<LogData | null>(null)
|
||||
const filterStatus = ref('')
|
||||
const filterEvent = ref('')
|
||||
const offset = ref(0)
|
||||
const pageSize = 50
|
||||
const pageSize = ref(20)
|
||||
const hasNextPage = ref(false)
|
||||
const currentPage = computed(() => Math.floor(offset.value / pageSize) + 1)
|
||||
const currentPage = computed(() => Math.floor(offset.value / pageSize.value) + 1)
|
||||
|
||||
const EVENT_LABELS: Record<string, string> = {
|
||||
upstream_rate_changed: '上游倍率变更',
|
||||
website_rate_changed: '网站倍率变更',
|
||||
upstream_unhealthy: '服务异常',
|
||||
upstream_recovered: '服务恢复',
|
||||
upstream_balance_low: '余额不足',
|
||||
test: '测试通知',
|
||||
}
|
||||
const eventLabel = (e: string) => EVENT_LABELS[e] || e
|
||||
const eventTagType = (e: string) =>
|
||||
({ upstream_rate_changed: 'primary', website_rate_changed: 'warning', upstream_unhealthy: 'danger', upstream_recovered: 'success', test: 'info' }[e] || '')
|
||||
({ upstream_rate_changed: 'primary', website_rate_changed: 'warning', upstream_unhealthy: 'danger', upstream_recovered: 'success', upstream_balance_low: 'warning', test: 'info' }[e] || '')
|
||||
const toUTC = (t: string) => /[Z+\-]\d*$/.test(t.trim()) ? t : t + 'Z'
|
||||
const fmtTime = (t: string) => dayjs(toUTC(t)).format('MM-DD HH:mm:ss')
|
||||
|
||||
@@ -132,11 +136,11 @@ async function loadList() {
|
||||
const res = await logsApi.list({
|
||||
status: filterStatus.value || undefined,
|
||||
event_type: filterEvent.value || undefined,
|
||||
limit: pageSize + 1,
|
||||
limit: pageSize.value + 1,
|
||||
offset: offset.value,
|
||||
})
|
||||
hasNextPage.value = res.data.length > pageSize
|
||||
list.value = res.data.slice(0, pageSize)
|
||||
hasNextPage.value = res.data.length > pageSize.value
|
||||
list.value = res.data.slice(0, pageSize.value)
|
||||
} finally {
|
||||
tableLoading.value = false
|
||||
}
|
||||
@@ -152,12 +156,17 @@ function handleFilterChange() {
|
||||
loadList()
|
||||
}
|
||||
|
||||
function handlePageSizeChange() {
|
||||
offset.value = 0
|
||||
loadList()
|
||||
}
|
||||
|
||||
function prevPage() {
|
||||
offset.value = Math.max(0, offset.value - pageSize)
|
||||
offset.value = Math.max(0, offset.value - pageSize.value)
|
||||
loadList()
|
||||
}
|
||||
function nextPage() {
|
||||
offset.value += pageSize
|
||||
offset.value += pageSize.value
|
||||
loadList()
|
||||
}
|
||||
|
||||
@@ -169,8 +178,32 @@ onMounted(loadList)
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
border-radius: var(--radius-shell);
|
||||
.panel-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
padding: 0 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.panel-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
<template>
|
||||
<div class="shell-page shell-page-fluid page-section">
|
||||
<div class="page-header surface-card page-block">
|
||||
<div class="page-heading">
|
||||
<p class="page-kicker">Delivery Mesh</p>
|
||||
<h2 class="page-title">Webhook 通知</h2>
|
||||
<p class="page-desc">配置 Webhook 接收器,支持通用 JSON 和钉钉机器人</p>
|
||||
<div class="panel">
|
||||
<div class="panel-head">
|
||||
<div class="panel-title">Webhook 接收器</div>
|
||||
<div class="panel-actions">
|
||||
<el-button size="small" text @click="loadList" :loading="tableLoading">刷新</el-button>
|
||||
<el-button size="small" type="primary" @click="openCreate">新增 Webhook</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-button type="primary" @click="openCreate">
|
||||
<el-icon><Plus /></el-icon> 新增 Webhook
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="card page-block">
|
||||
<el-table :data="list" v-loading="tableLoading" style="width:100%">
|
||||
<el-table-column label="名称" min-width="140">
|
||||
<template #default="{ row }">
|
||||
@@ -40,13 +36,13 @@
|
||||
<span class="time-text">{{ fmtTime(row.created_at) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<el-table-column label="操作" width="240">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" text @click="openEdit(row)"><el-icon><Edit /></el-icon></el-button>
|
||||
<el-button size="small" text type="success" @click="testWebhook(row)" :loading="row._testing">
|
||||
测试
|
||||
</el-button>
|
||||
<el-button size="small" text type="danger" @click="confirmDelete(row)"><el-icon><Delete /></el-icon></el-button>
|
||||
<div class="action-row">
|
||||
<el-button size="small" text @click="openEdit(row)" title="修改 Webhook 名称、地址或绑定的订阅事件">编辑</el-button>
|
||||
<el-button size="small" text type="success" @click="testWebhook(row)" :loading="row._testing" title="发送一条测试 Payload 到该 Webhook URL 以验证可达性">测试</el-button>
|
||||
<el-button size="small" text type="danger" @click="confirmDelete(row)" title="删除此 Webhook 接收器配置">删除</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -76,6 +72,7 @@
|
||||
<el-checkbox label="website_rate_changed">网站倍率变更</el-checkbox>
|
||||
<el-checkbox label="upstream_unhealthy">服务异常</el-checkbox>
|
||||
<el-checkbox label="upstream_recovered">服务恢复</el-checkbox>
|
||||
<el-checkbox label="upstream_balance_low">余额不足</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="启用">
|
||||
@@ -123,10 +120,11 @@ const EVENT_LABELS: Record<string, string> = {
|
||||
website_rate_changed: '网站倍率变更',
|
||||
upstream_unhealthy: '服务异常',
|
||||
upstream_recovered: '服务恢复',
|
||||
upstream_balance_low: '余额不足',
|
||||
}
|
||||
const eventLabel = (e: string) => EVENT_LABELS[e] || e
|
||||
const eventTagType = (e: string) =>
|
||||
({ upstream_rate_changed: 'primary', website_rate_changed: 'warning', upstream_unhealthy: 'danger', upstream_recovered: 'success' }[e] || '')
|
||||
({ upstream_rate_changed: 'primary', website_rate_changed: 'warning', upstream_unhealthy: 'danger', upstream_recovered: 'success', upstream_balance_low: 'warning' }[e] || '')
|
||||
|
||||
const toUTC = (t: string) => /[Z+\-]\d*$/.test(t.trim()) ? t : t + 'Z'
|
||||
const fmtTime = (t: string) => dayjs(toUTC(t)).format('MM-DD HH:mm')
|
||||
@@ -221,7 +219,31 @@ onMounted(loadList)
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
border-radius: var(--radius-shell);
|
||||
.panel-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.panel-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.action-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0;
|
||||
}
|
||||
.action-row .el-button {
|
||||
justify-self: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,23 +1,11 @@
|
||||
<template>
|
||||
<div class="shell-page shell-page-fluid page-section websites-page">
|
||||
<div class="page-header surface-card page-block">
|
||||
<div class="page-heading">
|
||||
<p class="page-kicker">Sync Orchestration</p>
|
||||
<h2 class="page-title">网站管理</h2>
|
||||
<p class="page-desc">管理自己的 sub2api 网站,并把网站分组倍率同步到上游监听结果</p>
|
||||
</div>
|
||||
<el-button type="primary" @click="openWebsiteCreate">
|
||||
<el-icon><Plus /></el-icon> 新增网站
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<div class="panel-head">
|
||||
<div class="panel-title">网站</div>
|
||||
<div class="panel-title">检测与变更控制台</div>
|
||||
<div class="panel-actions">
|
||||
<el-button size="small" text :disabled="websites.length === 0" @click="openImportGroups(selectedWebsite || websites[0])">导入上游分组</el-button>
|
||||
<el-button size="small" text :disabled="websites.length === 0" @click="openImportAccounts(selectedWebsite || websites[0])">导入为账号管理账号</el-button>
|
||||
<el-button size="small" text @click="loadAll">刷新</el-button>
|
||||
<el-button size="small" type="primary" @click="openWebsiteCreate">新增网站</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-table :data="websites" v-loading="websiteLoading" row-key="id" style="width:100%">
|
||||
@@ -48,43 +36,16 @@
|
||||
<span v-else class="muted">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="240" align="right">
|
||||
<el-table-column label="操作" width="340">
|
||||
<template #default="{ row }">
|
||||
<div class="action-row">
|
||||
<el-tooltip content="编辑网站配置" placement="top" :show-after="300">
|
||||
<el-button size="small" text class="btn-edit" @click="openWebsiteEdit(row)">
|
||||
<el-icon class="btn-edit-icon"><Edit /></el-icon><span>编辑</span>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="查看分组" placement="top" :show-after="300">
|
||||
<el-button size="small" circle text @click="selectWebsite(row)">
|
||||
<el-icon><Grid /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-dropdown trigger="click" @command="(cmd: string) => handleMoreAction(cmd, row)">
|
||||
<el-button size="small" text class="btn-more" :loading="row._testing">
|
||||
更多<el-icon v-if="!row._testing" class="el-icon--right"><ArrowDown /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="test" :disabled="row._testing">
|
||||
<el-icon><Connection /></el-icon>连接测试
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="binding">
|
||||
<el-icon><Link /></el-icon>新增绑定
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="importGroups">
|
||||
<el-icon><Upload /></el-icon>导入上游分组
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="importAccounts">
|
||||
<el-icon><Key /></el-icon>导入为账号管理账号
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item divided command="delete" class="btn-more-delete">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-button size="small" text @click="openWebsiteEdit(row)" title="编辑网站配置(API凭证、参数等)">编辑</el-button>
|
||||
<el-button size="small" text @click="selectWebsite(row)" title="查看当前网站的分组和绑定配置">查看</el-button>
|
||||
<el-button size="small" text @click="testWebsite(row)" :loading="row._testing" title="仅验证连通性,测试API是否正常响应">测试连接</el-button>
|
||||
<el-button size="small" text @click="openBindingCreate(row)" title="为此网站创建一个新的上游倍率绑定规则">新增绑定</el-button>
|
||||
<el-button size="small" text @click="openImportGroups(row)" title="一键从上游导入可用分组到此网站">导入分组</el-button>
|
||||
<el-button size="small" text @click="openImportAccounts(row)" title="将此网站下的所有账户批量导入账号管理">导入账号</el-button>
|
||||
<el-button size="small" text type="danger" @click="deleteWebsite(row)" title="删除此网站及其所有的绑定配置">删除</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -380,7 +341,25 @@
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="优先级">
|
||||
<el-input-number v-model="importAccountsForm.priority" :min="0" style="width:100%" />
|
||||
<template #label>
|
||||
<span>优先级</span>
|
||||
<el-tooltip content="按倍率自动分配优先级:倍率最低的上游分组优先级最高(priority=1),依次递增" placement="top" :show-after="300">
|
||||
<el-icon style="margin-left:4px;vertical-align:middle;color:var(--text-muted)"><WarningFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<div style="display:flex;align-items:center;gap:8px">
|
||||
<el-switch
|
||||
v-model="importAccountsForm.auto_priority_by_rate"
|
||||
active-text="按倍率自动分配"
|
||||
inactive-text="手动设置"
|
||||
/>
|
||||
<el-input-number
|
||||
v-if="!importAccountsForm.auto_priority_by_rate"
|
||||
v-model="importAccountsForm.priority"
|
||||
:min="0"
|
||||
style="width:160px"
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@@ -458,7 +437,7 @@ import { computed, onMounted, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import dayjs from 'dayjs'
|
||||
import { ArrowDown, Delete, Edit, Plus, Grid, Connection, Link, Upload, Key, Refresh } from '@element-plus/icons-vue'
|
||||
import { ArrowDown, Delete, Edit, Plus, Grid, Connection, Link, Upload, Key, Refresh, WarningFilled } from '@element-plus/icons-vue'
|
||||
import {
|
||||
upstreamsApi,
|
||||
websitesApi,
|
||||
@@ -571,6 +550,7 @@ const importAccountsForm = ref({
|
||||
platform_mode: 'auto',
|
||||
concurrency: 10,
|
||||
priority: 1,
|
||||
auto_priority_by_rate: true,
|
||||
})
|
||||
const importAccountResults = ref<ImportAccountItem[]>([])
|
||||
|
||||
@@ -1027,6 +1007,7 @@ async function openImportAccounts(site?: WebsiteData | null) {
|
||||
platform_mode: 'auto',
|
||||
concurrency: 10,
|
||||
priority: 1,
|
||||
auto_priority_by_rate: true,
|
||||
}
|
||||
importAccountResults.value = []
|
||||
importSyncStatus.value = null
|
||||
@@ -1151,6 +1132,7 @@ async function submitImportAccounts() {
|
||||
platform_mode: importAccountsForm.value.platform_mode,
|
||||
concurrency: importAccountsForm.value.concurrency,
|
||||
priority: importAccountsForm.value.priority,
|
||||
auto_priority_by_rate: importAccountsForm.value.auto_priority_by_rate,
|
||||
})
|
||||
importAccountResults.value = res.data.items
|
||||
ElMessage[res.data.success ? 'success' : 'warning'](res.data.message)
|
||||
@@ -1205,47 +1187,12 @@ onMounted(loadAll)
|
||||
.error-text { font-size: 12px; color: var(--color-danger); cursor: pointer; }
|
||||
.rate-value { font-weight: 600; color: var(--color-primary-strong); font-family: monospace; }
|
||||
.action-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: nowrap;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 0;
|
||||
}
|
||||
.action-row .el-button.is-circle {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
margin-left: 0;
|
||||
}
|
||||
.action-row .btn-edit {
|
||||
color: var(--text-primary);
|
||||
font-weight: 500;
|
||||
gap: 3px;
|
||||
padding: 0 6px;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.action-row .btn-edit-icon {
|
||||
font-size: 13px;
|
||||
}
|
||||
.action-row .btn-edit:hover {
|
||||
color: var(--el-color-primary);
|
||||
background: var(--el-color-primary-light-9);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.action-row .btn-more {
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
.action-row .btn-more:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
.action-row .btn-more .el-icon--right {
|
||||
margin-left: 1px;
|
||||
}
|
||||
.btn-more-delete {
|
||||
color: var(--el-color-danger);
|
||||
.action-row .el-button {
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
.site-switcher {
|
||||
|
||||
Reference in New Issue
Block a user