feat: 二次元前端改版 + 后端账号plan探测/调度修复

This commit is contained in:
2026-08-06 10:11:05 +08:00
parent 7f4701d362
commit f76ecd5769
37 changed files with 2484 additions and 813 deletions
+44 -9
View File
@@ -35,6 +35,7 @@ function applyEdit(payload) {
const typeFilter = ref('') // '' | 'openai' | 'adobe' | 'runway' | 'leonardo'
const statusFilter = ref('') // '' | 'active' | 'quota' | 'disabled'
const planFilter = ref('') // '' | 'vip' | 'free' | 'sub' | 'master'
const search = ref('')
const page = ref(1)
@@ -70,6 +71,14 @@ function typePill(t) {
imagine: 'bg-teal-500/10 text-teal-300 ring-teal-400/30',
}[t] || 'bg-white/[0.06] text-white/70 ring-white/15'
}
// 与后端 accountConcurrency 保持一致:adobe 普号固定 1 并发,会员号取配置值
// (未配置时默认 5);custom 上游取配置值 (默认 1);grok 固定 10,其余 1。
function acctConcurrency(a) {
if (a.type === 'adobe') return a.plan === 'free' ? 1 : (Number(a.concurrency) || 5)
if (a.type === 'custom') return Number(a.concurrency) || 1
return a.type === 'grok' ? 10 : 1
}
const STATUS_LABEL = { active: '正常', quota: '额度耗尽', disabled: '已禁用', pending: '检测中' }
// Server-side pagination: rows IS the current page, already filtered/sorted
@@ -112,6 +121,7 @@ function buildQs() {
})
if (typeFilter.value) qs.set('type', typeFilter.value)
if (statusFilter.value) qs.set('status', statusFilter.value)
if (planFilter.value && typeFilter.value === 'adobe') qs.set('plan', planFilter.value)
if (search.value.trim()) qs.set('q', search.value.trim())
return qs.toString()
}
@@ -250,6 +260,9 @@ function applyQuota(row, result) {
row._unknown = false
row.remaining = result.remaining
row.reset_after = result.reset_after
// 会员身份随额度一起回写:改过会员/掉了会员的号,查一次额度徽章就跟着更新。
if (result.plan !== undefined) row.plan = result.plan
if (result.sub_account !== undefined) row.sub_account = result.sub_account
}
async function toggleAccountStatus(pool, id, current) {
@@ -372,6 +385,15 @@ onMounted(() => { loadAccounts(); loadModelList() })
<span class="w-1.5 h-1.5 rounded-full bg-rose-400"></span>已禁用
</button>
</div>
<template v-if="typeFilter === 'adobe'">
<div class="w-px h-5 bg-white/10"></div>
<div class="flex items-center gap-1">
<button @click="setFilter(() => planFilter = '')" class="fp" :class="planFilter === '' && 'fp-on'">全部账号</button>
<button @click="setFilter(() => planFilter = 'free')" class="fp" :class="planFilter === 'free' && 'fp-slate'">普号</button>
<button @click="setFilter(() => planFilter = 'sub')" class="fp" :class="planFilter === 'sub' && 'fp-sky'">子号</button>
<button @click="setFilter(() => planFilter = 'master')" class="fp" :class="planFilter === 'master' && 'fp-purple'">母号</button>
</div>
</template>
<div class="flex-1 min-w-[200px]">
<input v-model="search" class="field !py-1.5 text-xs" placeholder="搜索 邮箱 / ID / 类型…" />
</div>
@@ -453,14 +475,17 @@ onMounted(() => { loadAccounts(); loadModelList() })
case. -->
<div class="flex items-center gap-2 min-w-0">
<span class="text-sm text-white/90 truncate" :title="a.email || '-'">{{ a.email || '-' }}</span>
<span v-if="a.sub_account"
class="shrink-0 inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-medium bg-sky-500/15 text-sky-300 ring-1 ring-sky-400/20"
:title="'子号 (剩余 ' + (a.remaining ?? '—') + ' 积分)'">子号</span>
<span v-if="a.image_limited && a.status !== 'quota'"
class="shrink-0 inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-medium bg-amber-500/15 text-amber-300 ring-1 ring-amber-400/20"
<!-- 身份互斥:子号必然是会员号,所以 子号 > 母号 > 普号 只取一个。
plan 未知(还没查过额度)时不猜,先不显示。 -->
<span v-if="a.sub_account" class="acct-tag acct-tag-sub"
:title="'子号 · 会员 (剩余 ' + (a.remaining ?? '—') + ' 积分)'">子号</span>
<span v-else-if="a.plan && a.plan !== 'free'" class="acct-tag acct-tag-master"
:title="'母号 · 会员 (' + a.plan + ')'">母号</span>
<span v-else-if="a.plan === 'free'" class="acct-tag acct-tag-free"
title="普号 (无会员)">普号</span>
<span v-if="a.image_limited && a.status !== 'quota' && a.plan !== 'free'" class="acct-tag acct-tag-limit"
title="图片额度耗尽,仅视频可用">图片限额</span>
<span v-if="a.video_limited && a.status !== 'quota'"
class="shrink-0 inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-medium bg-amber-500/15 text-amber-300 ring-1 ring-amber-400/20"
<span v-if="a.video_limited && a.status !== 'quota' && a.plan !== 'free'" class="acct-tag acct-tag-limit"
title="视频额度耗尽,仅图片可用">视频限额</span>
</div>
</td>
@@ -484,8 +509,8 @@ onMounted(() => { loadAccounts(); loadModelList() })
</td>
<!-- concurrency (custom = configured value; others = system fixed) -->
<td class="px-3 py-3.5 align-middle text-center whitespace-nowrap text-xs tabular-nums">
<span v-if="a.type === 'custom'" class="text-white/70">{{ a.concurrency || 1 }}</span>
<span v-else class="text-white/25" title="系统固定">{{ a.type === 'grok' ? 10 : 1 }}</span>
<span v-if="a.type === 'custom'" class="text-white/70">{{ acctConcurrency(a) }}</span>
<span v-else class="text-white/25" title="系统固定">{{ acctConcurrency(a) }}</span>
</td>
<!-- reset_after -->
<td class="px-3 py-3.5 align-middle text-xs whitespace-nowrap">
@@ -622,6 +647,16 @@ onMounted(() => { loadAccounts(); loadModelList() })
color: rgb(94 234 212);
box-shadow: inset 0 0 0 1px rgb(94 234 212 / 0.45);
}
.fp-purple {
background: rgb(168 85 247 / 0.22);
color: rgb(216 180 254);
box-shadow: inset 0 0 0 1px rgb(216 180 254 / 0.45);
}
.fp-slate {
background: rgb(148 163 184 / 0.22);
color: rgb(203 213 225);
box-shadow: inset 0 0 0 1px rgb(203 213 225 / 0.4);
}
/* --- icon-only action buttons */
.act {