feat: remote browser login persistence + balance display + UI consistency
- Retain login state in remote browser profiles (don't delete on disconnect)
- Add GET /api/browser-sessions/{id}/clipboard for clipboard sync
- Add POST /api/browser-sessions/{id}/autofill-login for manual credential fill
- Add DELETE /api/browser-sessions/profiles/{custom_page_id} for login clear
- Add balance tracking with configurable divisor (balance_divisor)
- Health check on session reuse, idle TTL eviction, background cleanup
- Add first-frame watchdog (10s timeout) to prevent infinite loading
- Reconnect browser on active=true when session was closed
- UI: uniform text-only inline buttons (websites + upstreams pages)
- Fix page switch race with closingRemoteSessionPromise
This commit is contained in:
@@ -42,6 +42,11 @@
|
||||
<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">
|
||||
@@ -71,6 +76,15 @@
|
||||
</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)" />
|
||||
@@ -111,9 +125,9 @@
|
||||
<el-button size="small" text @click="openEdit(row)" title="编辑">
|
||||
<el-icon><Edit /></el-icon>
|
||||
</el-button>
|
||||
<el-button size="small" text type="success" @click="testUpstream(row)" :loading="row._testing">测试</el-button>
|
||||
<el-button size="small" text type="primary" @click="checkNow(row)" :loading="row._checking">检测</el-button>
|
||||
<el-button size="small" text type="info" @click="openDetail(row)">
|
||||
<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="openDetail(row)">
|
||||
<el-icon><List /></el-icon>
|
||||
详情
|
||||
</el-button>
|
||||
@@ -198,7 +212,7 @@
|
||||
<div class="timeline-name">{{ row.name }}</div>
|
||||
<div class="timeline-meta mono">{{ fmtTime(row.last_checked_at!) }}</div>
|
||||
</div>
|
||||
<el-button size="small" text type="primary" @click="openDetail(row)">查看</el-button>
|
||||
<el-button size="small" text @click="openDetail(row)">查看</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-hint side-empty">还没有检测记录</div>
|
||||
@@ -286,6 +300,17 @@
|
||||
<el-form-item label="倍率接口">
|
||||
<el-input v-model="form.rate_endpoint" placeholder="/groups/rates" />
|
||||
</el-form-item>
|
||||
<el-form-item label="余额接口">
|
||||
<el-input v-model="form.balance_endpoint" placeholder="留空则不获取余额,如 /auth/me" />
|
||||
</el-form-item>
|
||||
<el-form-item label="余额字段路径">
|
||||
<el-input v-model="form.balance_response_path" placeholder="如 balance、data.quota" />
|
||||
<div class="form-hint">JSON 点分路径,例如 <code>balance</code> 或 <code>data.quota</code></div>
|
||||
</el-form-item>
|
||||
<el-form-item label="余额除数">
|
||||
<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-row :gutter="12">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="检测间隔(秒)">
|
||||
@@ -322,6 +347,14 @@
|
||||
<span class="dot" />{{ statusLabel(detailUpstream.last_status) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="surface-card info-card" v-if="detailUpstream.balance !== null">
|
||||
<div class="info-label">余额</div>
|
||||
<div class="info-value mono">{{ formatBalance(detailUpstream.balance) }}</div>
|
||||
</div>
|
||||
<div class="surface-card info-card" v-if="detailUpstream.balance_updated_at">
|
||||
<div class="info-label">余额更新于</div>
|
||||
<div class="info-value mono">{{ fmtTimeFull(detailUpstream.balance_updated_at) }}</div>
|
||||
</div>
|
||||
<div class="surface-card info-card">
|
||||
<div class="info-label">最近检测</div>
|
||||
<div class="info-value mono">{{ detailUpstream.last_checked_at ? fmtTimeFull(detailUpstream.last_checked_at) : '未检测' }}</div>
|
||||
@@ -443,6 +476,9 @@ const defaultForm = () => ({
|
||||
enabled: true,
|
||||
check_interval_seconds: 600,
|
||||
timeout_seconds: 30,
|
||||
balance_endpoint: '',
|
||||
balance_response_path: '',
|
||||
balance_divisor: 1.0,
|
||||
})
|
||||
const form = ref(defaultForm())
|
||||
const rules = {
|
||||
@@ -513,11 +549,16 @@ function handlePlatformChange(val: string) {
|
||||
form.value.auth_type = 'login_password'
|
||||
form.value.auth_config.login_path = '/auth/login'
|
||||
form.value.auth_config.username_field = 'email'
|
||||
form.value.balance_endpoint = '/auth/me'
|
||||
form.value.balance_response_path = 'data.balance'
|
||||
} else if (val === 'new-api') {
|
||||
form.value.api_prefix = ''
|
||||
form.value.groups_endpoint = '/api/group/'
|
||||
form.value.rate_endpoint = '/api/option/?key=GroupRatio'
|
||||
form.value.auth_type = 'bearer'
|
||||
form.value.balance_endpoint = '/api/user/self'
|
||||
form.value.balance_response_path = 'data.quota'
|
||||
form.value.balance_divisor = 500000
|
||||
} else if (val === 'new-api-user') {
|
||||
form.value.api_prefix = ''
|
||||
form.value.groups_endpoint = '/api/user/self/groups'
|
||||
@@ -525,6 +566,12 @@ function handlePlatformChange(val: string) {
|
||||
form.value.auth_type = 'login_password'
|
||||
form.value.auth_config.login_path = '/api/user/login'
|
||||
form.value.auth_config.username_field = 'username'
|
||||
form.value.balance_endpoint = '/api/user/self'
|
||||
form.value.balance_response_path = 'data.quota'
|
||||
form.value.balance_divisor = 500000
|
||||
} else {
|
||||
form.value.balance_endpoint = ''
|
||||
form.value.balance_response_path = ''
|
||||
}
|
||||
}
|
||||
|
||||
@@ -541,6 +588,7 @@ const metrics = computed(() => ({
|
||||
healthy: list.value.filter((item) => item.last_status === 'healthy').length,
|
||||
enabled: list.value.filter((item) => item.enabled).length,
|
||||
unhealthy: list.value.filter((item) => item.last_status === 'unhealthy').length,
|
||||
balanceCount: list.value.filter((item) => item.balance_endpoint).length,
|
||||
}))
|
||||
|
||||
const healthyRate = computed(() => {
|
||||
@@ -573,6 +621,11 @@ const recentChecks = computed(() =>
|
||||
|
||||
const statusLabel = (s: string) => ({ healthy: '健康', unhealthy: '异常', unknown: '未知' }[s] || s)
|
||||
const authLabel = (s: string) => ({ none: '无认证', bearer: 'Bearer', cookie: 'Cookie', api_key: 'API Key', login_password: '邮箱密码' }[s] || s)
|
||||
|
||||
function formatBalance(value: number | null | undefined): string {
|
||||
if (value === null || value === undefined) return '—'
|
||||
return value.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
}
|
||||
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')
|
||||
const fmtTimeFull = (t: string) => dayjs(toUTC(t)).format('YYYY-MM-DD HH:mm:ss')
|
||||
@@ -616,6 +669,9 @@ function openEdit(row: UpstreamData) {
|
||||
enabled: row.enabled,
|
||||
check_interval_seconds: row.check_interval_seconds,
|
||||
timeout_seconds: row.timeout_seconds,
|
||||
balance_endpoint: row.balance_endpoint || '',
|
||||
balance_response_path: row.balance_response_path || '',
|
||||
balance_divisor: row.balance_divisor ?? 1.0,
|
||||
}
|
||||
drawerVisible.value = true
|
||||
}
|
||||
@@ -1076,6 +1132,25 @@ onMounted(loadList)
|
||||
color: var(--color-warning);
|
||||
}
|
||||
|
||||
.balance-value {
|
||||
color: var(--color-success);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-soft);
|
||||
margin-top: 0.25rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.form-hint code {
|
||||
background: var(--bg-soft);
|
||||
padding: 0 0.3em;
|
||||
border-radius: 3px;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.snap-pagination {
|
||||
justify-content: center;
|
||||
margin-top: 0.9rem;
|
||||
@@ -1130,4 +1205,13 @@ onMounted(loadList)
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.action-row .el-button--danger {
|
||||
--el-button-bg-color: transparent;
|
||||
--el-button-border-color: transparent;
|
||||
}
|
||||
.action-row .el-button--danger:hover {
|
||||
--el-button-bg-color: var(--el-color-danger-light-8, #fef0f0);
|
||||
--el-button-border-color: var(--el-color-danger-light-5, #fbc4c4);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user