style(ui): 表格配色收敛到全局墨色基调

- 统计卡:大数字统一墨色、标签统一次要灰,去掉绿/红/橙彩虹数字
- 所有语义色文字 300/200 浅色加深为 600/500(淡底深字,适配亮色主题)
- 徽章/胶囊 ring 与 border 统一为 500/25 细描边
- 滑动开关开态由绿改墨黑(对齐 vivid switch.on)
- .fp-* 徽章改为 vivid 同款淡底深字
- 跳过媒体覆盖层胶囊、暗色代码块等本就属于暗底的元素

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
2026-10-27 17:44:57 +08:00
co-authored by Devin
parent df6ef5ad22
commit b700294ad3
20 changed files with 143 additions and 182 deletions
+6 -6
View File
@@ -225,7 +225,7 @@ async function save() {
<SelectMenu v-if="!isEdit" v-model="selectedId" :options="addOptions"
placeholder="选择一个支持的模型" />
<div v-else class="field font-mono !cursor-default opacity-90">{{ entry?.id }}</div>
<p v-if="!isEdit && !addOptions.length" class="text-[11px] text-amber-300/80 mt-1.5">
<p v-if="!isEdit && !addOptions.length" class="text-[11px] text-amber-600 mt-1.5">
模型经「渠道管理 → 拉取上游模型」导入。
</p>
</div>
@@ -240,8 +240,8 @@ async function save() {
<div v-if="entry" class="rounded-xl bg-white/[0.03] ring-1 ring-white/[0.06] p-3.5 space-y-2.5">
<div class="flex items-center gap-2">
<span class="inline-flex items-center gap-1 rounded-full px-2.5 py-1 text-[11px] font-medium ring-1"
:class="isVideo ? 'bg-rose-500/10 text-rose-300 ring-rose-400/30'
: 'bg-sky-500/10 text-sky-300 ring-sky-400/30'">
:class="isVideo ? 'bg-rose-500/10 text-rose-600 ring-rose-500/25'
: 'bg-sky-500/10 text-sky-600 ring-sky-500/25'">
{{ isVideo ? '生视频' : '生图' }}
</span>
<span class="text-[11px] text-white/45 capitalize">{{ entry.provider }}</span>
@@ -306,7 +306,7 @@ async function save() {
</div>
<div class="relative flex-1">
<input v-model="imagePricesAgent[r]" type="number" min="0" step="1" class="field !pr-10" placeholder="跟随普通" />
<span class="absolute right-2.5 top-1/2 -translate-y-1/2 text-amber-300/40 text-[10px]">代理</span>
<span class="absolute right-2.5 top-1/2 -translate-y-1/2 text-amber-600 text-[10px]">代理</span>
</div>
</div>
<p v-if="!resolutions.length" class="text-xs text-white/35">该模型未声明分辨率档位</p>
@@ -329,7 +329,7 @@ async function save() {
</div>
<div class="relative flex-1">
<input v-model="videoPricesAgent[d]" type="number" min="0" step="1" class="field !pr-10" placeholder="跟随普通" />
<span class="absolute right-2.5 top-1/2 -translate-y-1/2 text-amber-300/40 text-[10px]">代理</span>
<span class="absolute right-2.5 top-1/2 -translate-y-1/2 text-amber-600 text-[10px]">代理</span>
</div>
</div>
</div>
@@ -347,7 +347,7 @@ async function save() {
</div>
</template>
<p v-if="error" class="text-xs text-rose-300">{{ error }}</p>
<p v-if="error" class="text-xs text-rose-600">{{ error }}</p>
<div class="flex justify-end gap-2 pt-1">
<button @click="emit('close')" class="btn-soft">取消</button>
+2 -2
View File
@@ -258,7 +258,7 @@ async function recover() {
<div v-if="maxRefs > 0">
<label class="lbl">
{{ refsLabel }}
<span v-if="refsRequired" class="text-rose-300">*</span>
<span v-if="refsRequired" class="text-rose-600">*</span>
</label>
<div class="flex gap-2 flex-wrap items-start">
<div v-for="(img, i) in refImages" :key="i"
@@ -286,7 +286,7 @@ async function recover() {
</button>
<p v-if="status" class="text-xs text-white/55">{{ status }}</p>
<p v-if="error" class="text-xs text-rose-300 break-all">{{ error }}</p>
<p v-if="error" class="text-xs text-rose-600 break-all">{{ error }}</p>
<div v-if="resultUrl" class="rounded-xl ring-1 ring-white/10 bg-white/[0.03] overflow-hidden grid place-items-center min-h-[220px]">
<video v-if="resultKind === 'video'" :src="resultUrl" controls autoplay
+1 -1
View File
@@ -44,7 +44,7 @@ watch(() => route.path, () => {
</script>
<template>
<div class="theme-x h-screen flex bg-[var(--app-bg)] text-[color:var(--fg-2)] selection:bg-orange-400/30 overflow-hidden">
<div class="theme-x h-screen flex bg-[var(--app-bg)] text-[color:var(--fg-2)] selection:bg-orange-500/30 overflow-hidden">
<!-- ===== Sidebar ===== -->
<aside class="w-60 shrink-0 border-r border-r-[color:var(--ink-soft)] bg-white flex flex-col">
<router-link to="/" class="h-16 flex items-center gap-2.5 px-5 border-b border-b-[color:var(--ink-soft)] group">
+1 -1
View File
@@ -68,7 +68,7 @@ async function onLogout() {
</script>
<template>
<div class="theme-x min-h-screen bg-[var(--app-bg)] text-[color:var(--fg-2)] selection:bg-orange-400/30"
<div class="theme-x min-h-screen bg-[var(--app-bg)] text-[color:var(--fg-2)] selection:bg-orange-500/30"
:class="{ 'home-bg': isHome }">
<!-- ===== Top bar:悬浮胶囊,滚动展开为通栏 ===== -->
<header class="top-nav fixed top-0 inset-x-0 z-40" :class="{ scrolled }">
+3 -3
View File
@@ -121,7 +121,7 @@ function goPage(n) {
<td class="px-3 py-3.5 align-middle text-xs text-white/55 whitespace-nowrap">{{ fmt(o.created_at) }}</td>
<td class="px-3 py-3.5 align-middle text-xs text-white/55 whitespace-nowrap">{{ fmt(o.paid_at) }}</td>
<td class="px-3 py-3.5 align-middle text-right tabular-nums text-white/85">{{ o.source && o.source !== 'epay' ? '—' : '¥' + o.amount }}</td>
<td class="px-3 py-3.5 align-middle text-right tabular-nums" :class="o.points < 0 ? 'text-rose-300' : 'text-orange-300'">{{ o.points }}</td>
<td class="px-3 py-3.5 align-middle text-right tabular-nums" :class="o.points < 0 ? 'text-rose-600' : 'text-orange-600'">{{ o.points }}</td>
<td class="px-3 py-3.5 align-middle">
<span class="chip" :class="chipClass(o.status)">{{ STATUS[o.status] }}<span class="opacity-50 ml-1">· {{ METHOD[o.pay_type] || o.pay_type }}</span></span>
</td>
@@ -147,8 +147,8 @@ function goPage(n) {
.fp { display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.35rem 0.7rem; font-size: 0.72rem; border-radius: 0.55rem; color: var(--fg-2); background: var(--surface-2); box-shadow: inset 0 0 0 1px var(--hairline); transition: background 0.15s, color 0.15s; }
.fp:hover { background: var(--hover); color: var(--fg); }
.fp-on { background: var(--surface-solid); color: rgb(15 23 42); box-shadow: none; }
.fp-emerald { background: rgb(16 185 129 / 0.22); color: rgb(110 231 183); box-shadow: inset 0 0 0 1px rgb(110 231 183 / 0.45); }
.fp-amber { background: rgb(245 158 11 / 0.22); color: rgb(252 211 77); box-shadow: inset 0 0 0 1px rgb(252 211 77 / 0.45); }
.fp-emerald { background: rgb(16 185 129 / 0.1); color: #059669; box-shadow: inset 0 0 0 1px rgb(16 185 129 / 0.22); }
.fp-amber { background: rgb(245 158 11 / 0.12); color: #b45309; box-shadow: inset 0 0 0 1px rgb(245 158 11 / 0.25); }
.chip { display: inline-flex; align-items: center; gap: 0.3rem; padding: 0.18rem 0.55rem; font-size: 0.7rem; font-weight: 500; border-radius: 9999px; white-space: nowrap; background: var(--surface-2); color: var(--fg-3); box-shadow: inset 0 0 0 1px var(--hairline); }
.log-table { border-collapse: separate; border-spacing: 0; }
.log-row td { border-bottom: 1px solid var(--hairline); transition: background-color 0.15s ease, box-shadow 0.15s ease; }
+1 -1
View File
@@ -91,7 +91,7 @@ function goPage(n) {
<span class="empty-text">{{ search.trim() ? '没有匹配的记录' : '还没有触发记录' }}</span>
</div></td></tr>
<tr v-for="h in items" :key="h.id" class="border-b border-white/[0.04] hover:bg-white/[0.03] transition-colors align-top">
<td class="px-5 py-3.5 text-sm font-medium text-rose-300">{{ h.word }}</td>
<td class="px-5 py-3.5 text-sm font-medium text-rose-600">{{ h.word }}</td>
<td class="px-3 py-3.5">
<div class="text-sm text-white/85 truncate" :title="h.user_name">{{ h.user_name || '—' }}</div>
</td>
+1 -1
View File
@@ -162,7 +162,7 @@ onMounted(load)
<input type="checkbox" :checked="selected.has(w.id)" @change="toggleSelect(w.id)" @click.stop class="chk" />
</td>
<td class="px-5 py-3.5 align-middle text-sm font-medium text-white/90">{{ w.word }}</td>
<td class="px-3 py-3.5 align-middle text-right tabular-nums" :class="w.hits > 0 ? 'text-rose-300' : 'text-white/50'">{{ w.hits }}</td>
<td class="px-3 py-3.5 align-middle text-right tabular-nums" :class="w.hits > 0 ? 'text-rose-600' : 'text-white/50'">{{ w.hits }}</td>
<td class="px-3 py-3.5 align-middle text-xs text-white/50">{{ new Date(w.created_at).toLocaleString() }}</td>
<td class="px-3 py-3.5 align-middle text-right">
<button @click="del(w)" class="act danger" title="删除"><Icon name="trash" class="w-3.5 h-3.5" /></button>
+12 -20
View File
@@ -150,20 +150,20 @@ const pageNumbers = computed(() => {
<!-- KPI strip — same shape as LogsView / InvitesAdminView -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
<div class="card p-4">
<div class="text-[11px] uppercase tracking-wider text-white/45">总数</div>
<div class="text-[11px] uppercase tracking-wider text-[color:var(--fg-3)]">总数</div>
<div class="text-2xl font-semibold mt-1 tabular-nums">{{ stats.total }}</div>
</div>
<div class="card p-4">
<div class="text-[11px] uppercase tracking-wider text-emerald-300/80">未使用</div>
<div class="text-2xl font-semibold mt-1 tabular-nums text-emerald-300">{{ stats.active }}</div>
<div class="text-[11px] uppercase tracking-wider text-[color:var(--fg-3)]">未使用</div>
<div class="text-2xl font-semibold mt-1 tabular-nums">{{ stats.active }}</div>
</div>
<div class="card p-4">
<div class="text-[11px] uppercase tracking-wider text-white/35">已使用</div>
<div class="text-[11px] uppercase tracking-wider text-[color:var(--fg-3)]">已使用</div>
<div class="text-2xl font-semibold mt-1 tabular-nums text-white/60">{{ stats.redeemed }}</div>
</div>
<div class="card p-4">
<div class="text-[11px] uppercase tracking-wider text-rose-300/80">未使用面额</div>
<div class="text-2xl font-semibold mt-1 tabular-nums text-rose-300">{{ Number(stats.active_amount || 0).toLocaleString('en-US') }}</div>
<div class="text-[11px] uppercase tracking-wider text-[color:var(--fg-3)]">未使用面额</div>
<div class="text-2xl font-semibold mt-1 tabular-nums">{{ Number(stats.active_amount || 0).toLocaleString('en-US') }}</div>
</div>
</div>
@@ -190,7 +190,7 @@ const pageNumbers = computed(() => {
</div>
<button @click="generate" class="btn-primary"><Icon name="plus" class="w-3.5 h-3.5" /> 生成</button>
</div>
<p v-if="form.type === 'marketing'" class="text-[11px] text-rose-300/80 mt-2">营销兑换码:同一批次每个用户只能兑换一次。</p>
<p v-if="form.type === 'marketing'" class="text-[11px] text-rose-600 mt-2">营销兑换码:同一批次每个用户只能兑换一次。</p>
<!-- last batch -->
<div v-if="lastBatch.length" class="mt-4 rounded-xl bg-white/[0.04] ring-1 ring-white/10 p-4">
@@ -264,7 +264,7 @@ const pageNumbers = computed(() => {
<td class="px-5 py-3.5 align-middle font-mono text-xs text-white/90 truncate" :title="c.code">
<span class="inline-flex items-center gap-2">
<span class="truncate">{{ c.code }}</span>
<span v-if="c.type === 'marketing'" class="shrink-0 inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-sans font-medium bg-rose-500/15 text-rose-300 ring-1 ring-rose-400/25">营销</span>
<span v-if="c.type === 'marketing'" class="shrink-0 inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-sans font-medium bg-rose-500/15 text-rose-600 ring-1 ring-rose-500/25">营销</span>
</span>
</td>
<td class="px-3 py-3.5 align-middle text-right tabular-nums text-white/85 whitespace-nowrap">
@@ -273,10 +273,10 @@ const pageNumbers = computed(() => {
<td class="px-3 py-3.5 align-middle">
<span class="inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-medium ring-1"
:class="c.status === 'active'
? 'bg-emerald-500/10 text-emerald-300 ring-emerald-400/30'
? 'bg-emerald-500/10 text-emerald-600 ring-emerald-500/25'
: 'bg-white/[0.06] text-white/55 ring-white/15'">
<span class="w-1.5 h-1.5 rounded-full"
:class="c.status === 'active' ? 'bg-emerald-400' : 'bg-slate-400'"></span>
:class="c.status === 'active' ? 'bg-emerald-500' : 'bg-slate-400'"></span>
{{ c.status === 'active' ? '未使用' : '已使用' }}
</span>
</td>
@@ -358,16 +358,8 @@ const pageNumbers = computed(() => {
}
.fp:hover { background: var(--hover); color: var(--fg); }
.fp-on { background: var(--surface-solid); color: rgb(15 23 42); box-shadow: none; }
.fp-emerald {
background: rgb(16 185 129 / 0.22);
color: rgb(110 231 183);
box-shadow: inset 0 0 0 1px rgb(110 231 183 / 0.45);
}
.fp-rose {
background: rgb(244 63 94 / 0.22);
color: rgb(254 205 211);
box-shadow: inset 0 0 0 1px rgb(254 205 211 / 0.45);
}
.fp-emerald { background: rgb(16 185 129 / 0.1); color: #059669; box-shadow: inset 0 0 0 1px rgb(16 185 129 / 0.22); }
.fp-rose { background: rgb(239 68 68 / 0.09); color: #dc2626; box-shadow: inset 0 0 0 1px rgb(239 68 68 / 0.2); }
.pg {
min-width: 1.75rem;
+13 -13
View File
@@ -155,9 +155,9 @@ onMounted(load)
<div class="page-bar"><button @click="openAdd" class="btn-primary"><Icon name="plus" class="w-4 h-4" /> 添加渠道</button></div>
<p v-if="err" class="text-xs text-rose-300">{{ err }}</p>
<p v-if="warn" class="text-xs text-amber-300">{{ warn }}</p>
<p v-if="okMsg" class="text-xs text-emerald-300">{{ okMsg }}</p>
<p v-if="err" class="text-xs text-rose-600">{{ err }}</p>
<p v-if="warn" class="text-xs text-amber-600">{{ warn }}</p>
<p v-if="okMsg" class="text-xs text-emerald-600">{{ okMsg }}</p>
<div class="card overflow-hidden">
<div class="overflow-x-auto">
@@ -185,15 +185,15 @@ onMounted(load)
</td>
<td class="tabular-nums">{{ c.concurrency || '不限' }}</td>
<td class="tabular-nums">{{ c.weight || 1 }}</td>
<td class="text-right tabular-nums" :class="c.running_count > 0 ? 'text-emerald-300' : ''">{{ c.running_count || 0 }}</td>
<td class="text-right tabular-nums" :class="c.running_count > 0 ? 'text-emerald-600' : ''">{{ c.running_count || 0 }}</td>
<td class="text-right tabular-nums">{{ (c.success_total || 0).toLocaleString() }}</td>
<td class="text-right tabular-nums" :class="c.fail_count > 0 ? 'text-rose-300' : ''">{{ (c.fail_count || 0).toLocaleString() }}</td>
<td class="text-right tabular-nums" :class="c.fail_count > 0 ? 'text-rose-600' : ''">{{ (c.fail_count || 0).toLocaleString() }}</td>
<td class="text-right tabular-nums">{{ (c.used_credits || 0).toLocaleString() }}</td>
<td class="text-right tabular-nums">{{ c.upstream_balance == null ? '-' : c.upstream_balance.toLocaleString() }}</td>
<td>
<button @click="toggle(c)" class="inline-flex items-center gap-1.5 text-xs"
:class="c.enabled ? 'text-emerald-300' : 'text-white/40'">
<span class="w-1.5 h-1.5 rounded-full" :class="c.enabled ? 'bg-emerald-400' : 'bg-white/25'"></span>
:class="c.enabled ? 'text-emerald-600' : 'text-white/40'">
<span class="w-1.5 h-1.5 rounded-full" :class="c.enabled ? 'bg-emerald-500' : 'bg-white/25'"></span>
{{ c.enabled ? '启用' : '停用' }}
</button>
</td>
@@ -228,12 +228,12 @@ onMounted(load)
<input v-model.trim="form.name" type="text" maxlength="64" class="field" placeholder="例如:主站" />
</div>
<div>
<label class="lbl">上游地址 <span class="text-rose-300">*</span></label>
<label class="lbl">上游地址 <span class="text-rose-600">*</span></label>
<input v-model.trim="form.base_url" type="text" maxlength="300" class="field" placeholder="https://example.com" />
<p class="mt-1 text-[11px] text-white/35">同构 /v1 协议实例的站点地址,不带路径</p>
</div>
<div>
<label class="lbl">API Key <span v-if="!form.id" class="text-rose-300">*</span></label>
<label class="lbl">API Key <span v-if="!form.id" class="text-rose-600">*</span></label>
<input v-model.trim="form.api_key" type="text" class="field" :placeholder="form.id ? '留空保持不变' : 'vk_…'" autocomplete="off" />
<p class="mt-1 text-[11px] text-white/35">上游站点的用户 API Key(vk_ 前缀),密文存储,不回显</p>
</div>
@@ -256,7 +256,7 @@ onMounted(load)
</button>
<span class="text-xs" :class="form.enabled ? 'text-emerald-600' : 'text-[var(--fg-3)]'">{{ form.enabled ? '已启用' : '已停用' }}</span>
</div>
<p v-if="formErr" class="text-xs text-rose-300">{{ formErr }}</p>
<p v-if="formErr" class="text-xs text-rose-600">{{ formErr }}</p>
</div>
<div class="px-5 py-4 border-t border-white/[0.06] flex justify-end gap-2">
<button @click="form = null" class="btn-ghost">取消</button>
@@ -276,7 +276,7 @@ onMounted(load)
<div v-if="fetching" class="empty !py-10"><span class="empty-text">正在从上游拉取模型列表…</span></div>
<template v-else>
<p class="text-xs text-white/45 mb-3 leading-relaxed">开启的模型会同步到本站模型列表,参数随上游带入;到「模型管理」配价后上架。别名即本站模型标识,留空 = 与上游同名。</p>
<p v-if="fetchErr" class="text-xs text-rose-300 mb-3">{{ fetchErr }}</p>
<p v-if="fetchErr" class="text-xs text-rose-600 mb-3">{{ fetchErr }}</p>
<div v-if="!specs.length" class="empty !py-10"><span class="empty-text">上游没有可售模型(模型需在上游已配价)</span></div>
<div v-else class="space-y-2 max-h-[400px] overflow-y-auto p-0.5">
<div v-for="s in specs" :key="s.id" @click="togglePick(s.id)"
@@ -284,7 +284,7 @@ onMounted(load)
:class="!picked.has(s.id) && 'opacity-45'">
<div class="flex items-center gap-2.5 min-w-0">
<span class="w-2 h-2 rounded-full shrink-0"
:class="s.kind === 'video' ? 'bg-sky-400' : s.kind === 'text' ? 'bg-amber-400' : 'bg-zinc-500'"></span>
:class="s.kind === 'video' ? 'bg-sky-500' : s.kind === 'text' ? 'bg-amber-500' : 'bg-zinc-500'"></span>
<code class="font-mono text-[13px] font-semibold text-white/90 truncate">{{ s.id }}</code>
<span class="text-[11px] font-semibold text-white/40 shrink-0">{{ KIND_LABEL[s.kind] || s.kind }}</span>
<button type="button" class="ml-auto sw" :class="picked.has(s.id) && 'sw-on'"
@@ -331,6 +331,6 @@ onMounted(load)
/* iOS-style switch for 模型开关 in the sync modal */
.sw { position: relative; width: 2rem; height: 1.15rem; border-radius: 9999px; background: var(--hover); box-shadow: inset 0 0 0 1px var(--hairline); transition: background 0.18s ease; flex-shrink: 0; }
.sw-thumb { position: absolute; top: 2px; left: 2px; width: calc(1.15rem - 4px); height: calc(1.15rem - 4px); border-radius: 9999px; background: var(--surface-solid); box-shadow: 0 1px 2px rgb(15 23 42 / 0.3); transition: transform 0.18s ease; }
.sw-on { background: rgb(16 185 129 / 0.7); box-shadow: inset 0 0 0 1px rgb(16 185 129 / 0.5); }
.sw-on { background: #18181b; box-shadow: inset 0 0 0 1px #18181b; }
.sw-on .sw-thumb { transform: translateX(calc(2rem - 1.15rem)); }
</style>
+3 -3
View File
@@ -84,12 +84,12 @@ onMounted(load)
<td class="px-5 py-3.5 align-middle text-sm font-medium text-white/90">{{ g.name }}</td>
<td class="px-3 py-3.5 align-middle text-right tabular-nums">
<span v-if="g.max_concurrency > 0" class="text-white/85">{{ g.max_concurrency }}</span>
<span v-else class="text-emerald-300/90">不限制</span>
<span v-else class="text-emerald-600">不限制</span>
</td>
<td class="px-3 py-3.5 align-middle text-right tabular-nums text-white/70">{{ g.user_count }}</td>
<td class="px-3 py-3.5 align-middle">
<span v-if="g.is_default" class="inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-medium bg-rose-500/10 text-rose-300 ring-1 ring-rose-400/30">
<span class="w-1.5 h-1.5 rounded-full bg-rose-400"></span> 默认
<span v-if="g.is_default" class="inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-medium bg-rose-500/10 text-rose-600 ring-1 ring-rose-500/25">
<span class="w-1.5 h-1.5 rounded-full bg-rose-500"></span> 默认
</span>
<button v-else @click="setDefault(g)" class="btn-soft text-xs">设为默认</button>
</td>
+11 -11
View File
@@ -306,7 +306,7 @@ onMounted(() => { loadSite(); loadReg(); loadSmtp(); loadCredits(); loadAnnounce
<div class="card p-5">
<div class="flex items-center justify-between mb-4">
<h2 class="text-sm font-semibold">网站</h2>
<span v-if="siteSaved" class="text-xs text-emerald-300">已保存 ✓</span>
<span v-if="siteSaved" class="text-xs text-emerald-600">已保存 ✓</span>
</div>
<div class="space-y-3">
<label class="row">
@@ -319,7 +319,7 @@ onMounted(() => { loadSite(); loadReg(); loadSmtp(); loadCredits(); loadAnnounce
<div @click="pickLogo" @drop="onLogoDrop" @dragover.prevent="logoDragOver = true" @dragleave="logoDragOver = false"
title="点击或拖拽图片替换"
class="w-16 h-16 rounded-xl grid place-items-center overflow-hidden shrink-0 cursor-pointer transition-all"
:class="logoDragOver ? 'ring-2 ring-sky-400 bg-sky-50/40' : ''">
:class="logoDragOver ? 'ring-2 ring-sky-500/25 bg-sky-50/40' : ''">
<img v-if="logoPreview" :src="logoPreview" class="w-full h-full object-cover" />
<Logo v-else :size="64" class="w-full h-full" />
</div>
@@ -366,7 +366,7 @@ onMounted(() => { loadSite(); loadReg(); loadSmtp(); loadCredits(); loadAnnounce
<div class="card p-5">
<div class="flex items-center justify-between mb-4">
<h2 class="text-sm font-semibold">注册与登录</h2>
<span v-if="regSaved" class="text-xs text-emerald-300">已保存 ✓</span>
<span v-if="regSaved" class="text-xs text-emerald-600">已保存 ✓</span>
</div>
<div class="space-y-3">
<label class="row">
@@ -409,7 +409,7 @@ onMounted(() => { loadSite(); loadReg(); loadSmtp(); loadCredits(); loadAnnounce
<div class="card p-5">
<div class="flex items-center justify-between mb-4">
<h2 class="text-sm font-semibold">邮件服务 (SMTP)</h2>
<span v-if="smtpSaved" class="text-xs text-emerald-300">已保存 ✓</span>
<span v-if="smtpSaved" class="text-xs text-emerald-600">已保存 ✓</span>
</div>
<p class="text-xs text-slate-400 mb-4">用于发送注册 / 找回密码的验证码邮件。465 端口自动用 SSL,其余端口可选 STARTTLS。</p>
<div class="grid sm:grid-cols-2 gap-3">
@@ -440,7 +440,7 @@ onMounted(() => { loadSite(); loadReg(); loadSmtp(); loadCredits(); loadAnnounce
</button>
</div>
<p v-if="!smtpConfigured" class="text-xs text-slate-400 mt-1.5">请先保存 SMTP 配置后再测试。</p>
<p v-else-if="testMsg" class="text-xs mt-1.5" :class="testOk ? 'text-emerald-300' : 'text-rose-300'">{{ testMsg }}</p>
<p v-else-if="testMsg" class="text-xs mt-1.5" :class="testOk ? 'text-emerald-600' : 'text-rose-600'">{{ testMsg }}</p>
</div>
</div>
@@ -448,7 +448,7 @@ onMounted(() => { loadSite(); loadReg(); loadSmtp(); loadCredits(); loadAnnounce
<div class="card p-5">
<div class="flex items-center justify-between mb-4">
<h2 class="text-sm font-semibold">代理 (生图请求)</h2>
<span v-if="proxySaved" class="text-xs text-emerald-300">已保存 ✓</span>
<span v-if="proxySaved" class="text-xs text-emerald-600">已保存 ✓</span>
</div>
<p class="text-xs text-slate-400 mb-4">调用上游生成图片/视频时统一使用的 HTTP 代理,留空 = 直连。格式如 <code class="px-1 bg-slate-100 rounded">http://127.0.0.1:7890</code>。修改即时生效,无需重启。</p>
<input v-model="proxy.proxy" placeholder="留空 = 直连,如 http://127.0.0.1:7890" class="field" />
@@ -456,14 +456,14 @@ onMounted(() => { loadSite(); loadReg(); loadSmtp(); loadCredits(); loadAnnounce
<button @click="saveProxy" :disabled="proxyBusy" class="btn-primary">{{ proxyBusy ? '保存中…' : '保存设置' }}</button>
<button @click="testProxy" :disabled="proxyTestBusy || !proxy.proxy.trim()" class="btn-ghost">{{ proxyTestBusy ? '测试中…' : '代理测试' }}</button>
</div>
<p v-if="proxyTest.msg" class="text-xs mt-2" :class="proxyTest.ok ? 'text-emerald-300' : 'text-rose-300'">{{ proxyTest.msg }}</p>
<p v-if="proxyTest.msg" class="text-xs mt-2" :class="proxyTest.ok ? 'text-emerald-600' : 'text-rose-600'">{{ proxyTest.msg }}</p>
</div>
<!-- rewards -->
<div class="card p-5">
<div class="flex items-center justify-between mb-4">
<h2 class="text-sm font-semibold">积分奖励</h2>
<span v-if="credSaved" class="text-xs text-emerald-300">已保存 ✓</span>
<span v-if="credSaved" class="text-xs text-emerald-600">已保存 ✓</span>
</div>
<div class="space-y-3">
<label class="row">
@@ -494,7 +494,7 @@ onMounted(() => { loadSite(); loadReg(); loadSmtp(); loadCredits(); loadAnnounce
<div class="card p-5">
<div class="flex items-center justify-between mb-4">
<h2 class="text-sm font-semibold">去AI特征</h2>
<span v-if="deaiSaved" class="text-xs text-emerald-300">已保存 ✓</span>
<span v-if="deaiSaved" class="text-xs text-emerald-600">已保存 ✓</span>
</div>
<p class="text-xs text-slate-400 mb-4">画图台开启「去AI特征」后,按画质档位在模型价格之上额外扣除的积分。仅对图片生成生效。</p>
<div class="space-y-3">
@@ -580,7 +580,7 @@ onMounted(() => { loadSite(); loadReg(); loadSmtp(); loadCredits(); loadAnnounce
<div class="card p-5">
<div class="flex items-center justify-between mb-4">
<h2 class="text-sm font-semibold">日志</h2>
<span v-if="logsSaved" class="text-xs text-emerald-300">已保存 ✓</span>
<span v-if="logsSaved" class="text-xs text-emerald-600">已保存 ✓</span>
</div>
<div class="space-y-3">
<label class="row">
@@ -601,7 +601,7 @@ onMounted(() => { loadSite(); loadReg(); loadSmtp(); loadCredits(); loadAnnounce
<div class="card p-5">
<div class="flex items-center justify-between mb-4">
<h2 class="text-sm font-semibold">生成文件 (图片 / 视频)</h2>
<span v-if="mediaSaved" class="text-xs text-emerald-300">
<span v-if="mediaSaved" class="text-xs text-emerald-600">
已保存 ✓<span v-if="mediaRemoved" class="text-white/45"> · 立即清理 {{ mediaRemoved }} 个文件</span>
</span>
</div>
+4 -4
View File
@@ -209,7 +209,7 @@ async function copy(text) {
<dt class="text-white/45">你的 Key</dt><dd class="font-mono text-white/70">{{ keyHint }}</dd>
</div>
</dl>
<p class="text-[11px] text-white/40 mt-4">还没有 Key?去 <router-link to="/settings" class="text-orange-300 underline">设置 → API Key</router-link> 生成。</p>
<p class="text-[11px] text-white/40 mt-4">还没有 Key?去 <router-link to="/settings" class="text-orange-600 underline">设置 → API Key</router-link> 生成。</p>
</div>
<div class="card p-6">
@@ -229,7 +229,7 @@ async function copy(text) {
<div class="card overflow-hidden">
<table class="w-full text-sm">
<thead>
<tr class="text-left text-[11px] uppercase tracking-wider text-white/40 border-b border-white/[0.08]">
<tr class="text-left text-[11px] uppercase tracking-wider text-[color:var(--fg-3)] border-b border-white/[0.08]">
<th class="px-4 py-3 font-medium whitespace-nowrap">model</th>
<th class="px-4 py-3 font-medium whitespace-nowrap">类型</th>
<th class="px-4 py-3 font-medium whitespace-nowrap" title="视频价 = 分辨率价 + 时长价">定价 <span class="normal-case text-white/30">积分</span></th>
@@ -290,7 +290,7 @@ async function copy(text) {
<h2 class="text-lg font-semibold mb-3">生成参数 <span class="text-xs font-normal text-white/40">POST /v1/generate</span></h2>
<div class="card overflow-hidden">
<table class="w-full text-sm">
<thead><tr class="text-left text-[11px] uppercase tracking-wider text-white/40 border-b border-white/[0.08]">
<thead><tr class="text-left text-[11px] uppercase tracking-wider text-[color:var(--fg-3)] border-b border-white/[0.08]">
<th class="px-4 py-2.5 font-medium">参数</th><th class="px-4 py-2.5 font-medium">类型</th><th class="px-4 py-2.5 font-medium">必填</th><th class="px-4 py-2.5 font-medium">说明</th>
</tr></thead>
<tbody>
@@ -309,7 +309,7 @@ async function copy(text) {
<h2 class="text-lg font-semibold mb-3">上传参数 <span class="text-xs font-normal text-white/40">POST /v1/uploads</span></h2>
<div class="card overflow-hidden">
<table class="w-full text-sm">
<thead><tr class="text-left text-[11px] uppercase tracking-wider text-white/40 border-b border-white/[0.08]">
<thead><tr class="text-left text-[11px] uppercase tracking-wider text-[color:var(--fg-3)] border-b border-white/[0.08]">
<th class="px-4 py-2.5 font-medium">参数</th><th class="px-4 py-2.5 font-medium">类型</th><th class="px-4 py-2.5 font-medium">必填</th><th class="px-4 py-2.5 font-medium">说明</th>
</tr></thead>
<tbody>
+9 -9
View File
@@ -219,20 +219,20 @@ onUnmounted(() => window.removeEventListener('keydown', onKey))
consistent. /images returns all four numbers in one payload. -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
<div class="card p-4">
<div class="text-[11px] uppercase tracking-wider text-white/45">总计</div>
<div class="text-[11px] uppercase tracking-wider text-[color:var(--fg-3)]">总计</div>
<div class="text-2xl font-semibold mt-1 tabular-nums">{{ stats.total }}</div>
</div>
<div class="card p-4">
<div class="text-[11px] uppercase tracking-wider text-sky-300/80">图像</div>
<div class="text-2xl font-semibold mt-1 tabular-nums text-sky-300">{{ stats.image }}</div>
<div class="text-[11px] uppercase tracking-wider text-[color:var(--fg-3)]">图像</div>
<div class="text-2xl font-semibold mt-1 tabular-nums">{{ stats.image }}</div>
</div>
<div class="card p-4">
<div class="text-[11px] uppercase tracking-wider text-rose-300/80">视频</div>
<div class="text-2xl font-semibold mt-1 tabular-nums text-rose-300">{{ stats.video }}</div>
<div class="text-[11px] uppercase tracking-wider text-[color:var(--fg-3)]">视频</div>
<div class="text-2xl font-semibold mt-1 tabular-nums">{{ stats.video }}</div>
</div>
<div class="card p-4">
<div class="text-[11px] uppercase tracking-wider text-amber-300/80">存储</div>
<div class="text-2xl font-semibold mt-1 tabular-nums text-amber-300">{{ fmtSize(stats.size_bytes) }}</div>
<div class="text-[11px] uppercase tracking-wider text-[color:var(--fg-3)]">存储</div>
<div class="text-2xl font-semibold mt-1 tabular-nums">{{ fmtSize(stats.size_bytes) }}</div>
</div>
</div>
@@ -330,9 +330,9 @@ onUnmounted(() => window.removeEventListener('keydown', onKey))
<div class="text-[10px] text-white/55 flex items-center justify-between gap-2 tabular-nums">
<span class="truncate" :title="f.model || ''">{{ f.model || '—' }}</span>
<span class="shrink-0 flex items-center gap-1">
<span v-if="f.resolution" class="text-emerald-300/90">{{ f.resolution }}</span>
<span v-if="f.resolution" class="text-emerald-600">{{ f.resolution }}</span>
<span v-if="f.ratio" class="text-white/40">{{ f.ratio }}</span>
<span v-if="f.kind === 'video' && f.duration" class="text-rose-300/80">{{ f.duration }}</span>
<span v-if="f.kind === 'video' && f.duration" class="text-rose-600">{{ f.duration }}</span>
<span v-if="f.deai" class="text-[#71717a]">去AI</span>
</span>
</div>
+4 -4
View File
@@ -165,7 +165,7 @@ function toast(m) {
class="border-b border-white/[0.04] hover:bg-white/[0.03] transition-colors">
<td class="px-5 py-3.5 align-middle font-medium text-white/90 truncate">{{ r.name }}</td>
<td class="px-3 py-3.5 align-middle text-right tabular-nums whitespace-nowrap"
:class="r.reward ? 'text-emerald-300' : 'text-white/25'">
:class="r.reward ? 'text-emerald-600' : 'text-white/25'">
{{ r.reward ? '+' + r.reward : '—' }}
</td>
<td class="px-3 py-3.5 align-middle text-xs text-white/55 tabular-nums whitespace-nowrap">
@@ -177,10 +177,10 @@ function toast(m) {
<td class="px-5 py-3.5 align-middle text-right">
<span class="inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-medium ring-1"
:class="r.status === 'completed'
? 'bg-emerald-500/10 text-emerald-300 ring-emerald-400/30'
: 'bg-amber-500/10 text-amber-300 ring-amber-400/30'">
? 'bg-emerald-500/10 text-emerald-600 ring-emerald-500/25'
: 'bg-amber-500/10 text-amber-600 ring-amber-500/25'">
<span class="w-1.5 h-1.5 rounded-full"
:class="r.status === 'completed' ? 'bg-emerald-400' : 'bg-amber-400'"></span>
:class="r.status === 'completed' ? 'bg-emerald-500' : 'bg-amber-500'"></span>
{{ r.status === 'completed' ? '已完成' : '待生图' }}
</span>
</td>
+11 -11
View File
@@ -33,20 +33,20 @@ onMounted(load)
consistent. /invites already returns the stats payload. -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
<div class="card p-4">
<div class="text-[11px] uppercase tracking-wider text-white/45">总邀请</div>
<div class="text-[11px] uppercase tracking-wider text-[color:var(--fg-3)]">总邀请</div>
<div class="text-2xl font-semibold mt-1 tabular-nums">{{ stats.total }}</div>
</div>
<div class="card p-4">
<div class="text-[11px] uppercase tracking-wider text-emerald-300/80">已完成</div>
<div class="text-2xl font-semibold mt-1 tabular-nums text-emerald-300">{{ stats.completed }}</div>
<div class="text-[11px] uppercase tracking-wider text-[color:var(--fg-3)]">已完成</div>
<div class="text-2xl font-semibold mt-1 tabular-nums">{{ stats.completed }}</div>
</div>
<div class="card p-4">
<div class="text-[11px] uppercase tracking-wider text-amber-300/80">待生图</div>
<div class="text-2xl font-semibold mt-1 tabular-nums text-amber-300">{{ stats.pending }}</div>
<div class="text-[11px] uppercase tracking-wider text-[color:var(--fg-3)]">待生图</div>
<div class="text-2xl font-semibold mt-1 tabular-nums">{{ stats.pending }}</div>
</div>
<div class="card p-4">
<div class="text-[11px] uppercase tracking-wider text-rose-300/80">已发奖励</div>
<div class="text-2xl font-semibold mt-1 tabular-nums text-rose-300">{{ Number(stats.reward_paid || 0).toLocaleString('en-US') }}</div>
<div class="text-[11px] uppercase tracking-wider text-[color:var(--fg-3)]">已发奖励</div>
<div class="text-2xl font-semibold mt-1 tabular-nums">{{ Number(stats.reward_paid || 0).toLocaleString('en-US') }}</div>
</div>
</div>
@@ -83,7 +83,7 @@ onMounted(load)
<td class="px-5 py-3.5 align-middle font-medium text-white/90 truncate">{{ r.inviter }}</td>
<td class="px-3 py-3.5 align-middle text-white/75 truncate">{{ r.invitee }}</td>
<td class="px-3 py-3.5 align-middle text-right tabular-nums whitespace-nowrap"
:class="r.reward ? 'text-emerald-300' : 'text-white/25'">
:class="r.reward ? 'text-emerald-600' : 'text-white/25'">
{{ r.reward ? '+' + r.reward : '—' }}
</td>
<td class="px-3 py-3.5 align-middle text-xs text-white/55 tabular-nums whitespace-nowrap">
@@ -95,10 +95,10 @@ onMounted(load)
<td class="px-5 py-3.5 align-middle text-right">
<span class="inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-medium ring-1"
:class="r.status === 'completed'
? 'bg-emerald-500/10 text-emerald-300 ring-emerald-400/30'
: 'bg-amber-500/10 text-amber-300 ring-amber-400/30'">
? 'bg-emerald-500/10 text-emerald-600 ring-emerald-500/25'
: 'bg-amber-500/10 text-amber-600 ring-amber-500/25'">
<span class="w-1.5 h-1.5 rounded-full"
:class="r.status === 'completed' ? 'bg-emerald-400' : 'bg-amber-400'"></span>
:class="r.status === 'completed' ? 'bg-emerald-500' : 'bg-amber-500'"></span>
{{ r.status === 'completed' ? '已完成' : '待生图' }}
</span>
</td>
+26 -38
View File
@@ -134,23 +134,23 @@ onUnmounted(() => {
// ---- chip helpers ----
const statusLabel = (s) => ({ success: '成功', failed: '失败', pending: '进行中' }[s] || s)
const statusPill = (s) => ({
success: 'bg-emerald-500/10 text-emerald-300 ring-emerald-400/30',
failed: 'bg-rose-500/10 text-rose-300 ring-rose-400/30',
pending: 'bg-amber-500/10 text-amber-300 ring-amber-400/30',
success: 'bg-emerald-500/10 text-emerald-600 ring-emerald-500/25',
failed: 'bg-rose-500/10 text-rose-600 ring-rose-500/25',
pending: 'bg-amber-500/10 text-amber-600 ring-amber-500/25',
}[s] || 'bg-white/[0.06] text-white/65 ring-white/15')
const statusDot = (s) => ({
success: 'bg-emerald-400',
failed: 'bg-rose-400',
pending: 'bg-amber-400',
success: 'bg-emerald-500',
failed: 'bg-rose-500',
pending: 'bg-amber-500',
}[s] || 'bg-white/40')
// Source: backend stamps "v1" (API key), "user" (画图台), "admin" (后台测试模型).
const sourceLabel = (s) => ({ v1: 'API', user: '画图台', admin: '测试' }[s] || '画图台')
const sourcePill = (s) => ({
v1: 'bg-orange-500/15 text-orange-300 ring-orange-400/30',
admin: 'bg-amber-500/15 text-amber-300 ring-amber-400/30',
user: 'bg-sky-500/15 text-sky-300 ring-sky-400/30',
}[s] || 'bg-sky-500/15 text-sky-300 ring-sky-400/30')
v1: 'bg-orange-500/15 text-orange-600 ring-orange-500/25',
admin: 'bg-amber-500/15 text-amber-600 ring-amber-500/25',
user: 'bg-sky-500/15 text-sky-600 ring-sky-500/25',
}[s] || 'bg-sky-500/15 text-sky-600 ring-sky-500/25')
</script>
<template>
@@ -162,20 +162,20 @@ const sourcePill = (s) => ({
<!-- KPI strip — dense pills aligned with the dashboard tints -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
<div class="card p-4">
<div class="text-[11px] uppercase tracking-wider text-white/45">总计</div>
<div class="text-[11px] uppercase tracking-wider text-[color:var(--fg-3)]">总计</div>
<div class="text-2xl font-semibold mt-1 tabular-nums">{{ stats.total }}</div>
</div>
<div class="card p-4">
<div class="text-[11px] uppercase tracking-wider text-emerald-300/80">成功</div>
<div class="text-2xl font-semibold mt-1 tabular-nums text-emerald-300">{{ stats.success }}</div>
<div class="text-[11px] uppercase tracking-wider text-[color:var(--fg-3)]">成功</div>
<div class="text-2xl font-semibold mt-1 tabular-nums">{{ stats.success }}</div>
</div>
<div class="card p-4">
<div class="text-[11px] uppercase tracking-wider text-rose-300/80">失败</div>
<div class="text-2xl font-semibold mt-1 tabular-nums text-rose-300">{{ stats.failed }}</div>
<div class="text-[11px] uppercase tracking-wider text-[color:var(--fg-3)]">失败</div>
<div class="text-2xl font-semibold mt-1 tabular-nums">{{ stats.failed }}</div>
</div>
<div class="card p-4">
<div class="text-[11px] uppercase tracking-wider text-amber-300/80">进行中</div>
<div class="text-2xl font-semibold mt-1 tabular-nums text-amber-300">{{ stats.pending }}</div>
<div class="text-[11px] uppercase tracking-wider text-[color:var(--fg-3)]">进行中</div>
<div class="text-2xl font-semibold mt-1 tabular-nums">{{ stats.pending }}</div>
</div>
</div>
@@ -231,7 +231,7 @@ const sourcePill = (s) => ({
<td class="px-4 py-3.5 align-middle text-center">
<button v-if="e.status === 'success' && e.file && e.source !== 'v1'"
@click="openPreview(e)"
class="block w-12 h-12 mx-auto rounded-lg overflow-hidden ring-1 ring-white/10 hover:ring-rose-400/60 transition-all">
class="block w-12 h-12 mx-auto rounded-lg overflow-hidden ring-1 ring-white/10 hover:ring-rose-500/25 transition-all">
<img v-if="e.kind !== 'video' || !thumbFail[e.id]" :src="thumbUrl(e.file)" loading="lazy"
class="w-full h-full object-cover"
@error="e.kind === 'video' && (thumbFail[e.id] = true)" />
@@ -240,8 +240,8 @@ const sourcePill = (s) => ({
@mouseenter="$event.target.play && $event.target.play()"
@mouseleave="$event.target.pause && $event.target.pause()" />
</button>
<div v-else-if="e.status === 'pending'" class="w-12 h-12 mx-auto rounded-lg bg-amber-500/10 ring-1 ring-amber-400/30 grid place-items-center">
<span class="w-2 h-2 rounded-full bg-amber-400 animate-pulse"></span>
<div v-else-if="e.status === 'pending'" class="w-12 h-12 mx-auto rounded-lg bg-amber-500/10 ring-1 ring-amber-500/25 grid place-items-center">
<span class="w-2 h-2 rounded-full bg-amber-500 animate-pulse"></span>
</div>
<!-- failed (and any non-success/non-pending) rows: no thumbnail, just a dash.
The 状态 column already flags the failure. -->
@@ -268,7 +268,7 @@ const sourcePill = (s) => ({
<div class="font-mono text-xs text-white/90 break-all" :title="e.model">{{ e.model }}</div>
<div class="mt-1 flex items-center gap-1.5 min-w-0">
<span class="text-[10px] uppercase tracking-wider font-medium truncate min-w-0"
:class="e.kind === 'video' ? 'text-rose-300/80' : 'text-sky-300/80'">
:class="e.kind === 'video' ? 'text-rose-600' : 'text-sky-600'">
{{ e.kind === 'video' ? '视频' : '图像' }}
<span v-if="e.provider" class="text-white/30 ml-1">· {{ e.provider }}</span>
</span>
@@ -283,7 +283,7 @@ const sourcePill = (s) => ({
:class="e.prompt ? 'cursor-pointer hover:text-white' : ''"
:title="e.prompt ? '点击复制提示词' : ''"
@click="e.prompt && copyPrompt(e)">{{ e.prompt || '—' }}</div>
<div v-if="e.error" class="mt-1 text-[11px] text-rose-300/85 truncate flex items-center gap-1.5 cursor-pointer hover:text-rose-200 transition-colors"
<div v-if="e.error" class="mt-1 text-[11px] text-rose-600 truncate flex items-center gap-1.5 cursor-pointer hover:text-rose-500 transition-colors"
:title="e.error + ' — 点击复制'"
@click.stop="copyError(e)">
{{ e.error }}
@@ -308,7 +308,7 @@ const sourcePill = (s) => ({
</template>
</td>
<td class="px-3 py-3.5 text-right text-xs tabular-nums align-middle whitespace-nowrap">
<span v-if="e.cost > 0" class="text-amber-300 font-medium">{{ e.cost }}</span>
<span v-if="e.cost > 0" class="text-amber-600 font-medium">{{ e.cost }}</span>
<span v-else class="text-white/25">0</span>
</td>
<td class="px-4 py-3.5 text-right text-xs tabular-nums align-middle whitespace-nowrap text-white/85">
@@ -371,21 +371,9 @@ const sourcePill = (s) => ({
color: rgb(15 23 42);
box-shadow: none;
}
.fp-emerald {
background: rgb(16 185 129 / 0.22);
color: rgb(110 231 183);
box-shadow: inset 0 0 0 1px rgb(110 231 183 / 0.45);
}
.fp-rose {
background: rgb(244 63 94 / 0.22);
color: rgb(253 164 175);
box-shadow: inset 0 0 0 1px rgb(253 164 175 / 0.45);
}
.fp-amber {
background: rgb(245 158 11 / 0.22);
color: rgb(252 211 77);
box-shadow: inset 0 0 0 1px rgb(252 211 77 / 0.45);
}
.fp-emerald { background: rgb(16 185 129 / 0.1); color: #059669; box-shadow: inset 0 0 0 1px rgb(16 185 129 / 0.22); }
.fp-rose { background: rgb(239 68 68 / 0.09); color: #dc2626; box-shadow: inset 0 0 0 1px rgb(239 68 68 / 0.2); }
.fp-amber { background: rgb(245 158 11 / 0.12); color: #b45309; box-shadow: inset 0 0 0 1px rgb(245 158 11 / 0.25); }
/* --- type / status chip used inside table rows --- */
.chip {
+8 -8
View File
@@ -82,20 +82,20 @@ onMounted(loadModels)
<!-- KPI strip — same shape as LogsView / InvitesAdminView -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
<div class="card p-4">
<div class="text-[11px] uppercase tracking-wider text-white/45">总数</div>
<div class="text-[11px] uppercase tracking-wider text-[color:var(--fg-3)]">总数</div>
<div class="text-2xl font-semibold mt-1 tabular-nums">{{ stats.total }}</div>
</div>
<div class="card p-4">
<div class="text-[11px] uppercase tracking-wider text-sky-300/80">图像</div>
<div class="text-2xl font-semibold mt-1 tabular-nums text-sky-300">{{ stats.image }}</div>
<div class="text-[11px] uppercase tracking-wider text-[color:var(--fg-3)]">图像</div>
<div class="text-2xl font-semibold mt-1 tabular-nums">{{ stats.image }}</div>
</div>
<div class="card p-4">
<div class="text-[11px] uppercase tracking-wider text-rose-300/80">视频</div>
<div class="text-2xl font-semibold mt-1 tabular-nums text-rose-300">{{ stats.video }}</div>
<div class="text-[11px] uppercase tracking-wider text-[color:var(--fg-3)]">视频</div>
<div class="text-2xl font-semibold mt-1 tabular-nums">{{ stats.video }}</div>
</div>
<div class="card p-4">
<div class="text-[11px] uppercase tracking-wider text-emerald-300/80">上架</div>
<div class="text-2xl font-semibold mt-1 tabular-nums text-emerald-300">{{ stats.sellable }}<span class="text-white/35 text-lg ml-1">/ {{ stats.total }}</span></div>
<div class="text-[11px] uppercase tracking-wider text-[color:var(--fg-3)]">上架</div>
<div class="text-2xl font-semibold mt-1 tabular-nums">{{ stats.sellable }}<span class="text-white/35 text-lg ml-1">/ {{ stats.total }}</span></div>
</div>
</div>
@@ -207,7 +207,7 @@ onMounted(loadModels)
<td class="px-3 py-3.5 align-middle">
<span class="inline-flex items-center gap-1 rounded-full px-2.5 py-1 text-[11px] font-medium ring-1"
:class="m.sellable
? 'bg-emerald-500/10 text-emerald-300 ring-emerald-400/30'
? 'bg-emerald-500/10 text-emerald-600 ring-emerald-500/25'
: 'bg-white/[0.05] text-white/40 ring-white/10'"
:title="m.sellable ? '全部档位有价且有启用渠道,对前台可见' : '配齐全部档位价格并绑定启用渠道后自动上架'">
{{ m.sellable ? '已上架' : '未上架' }}
+6 -6
View File
@@ -113,15 +113,15 @@ const recentLogs = computed(() => logs.value.slice(0, 12))
// ---- formatters ----
function statusLabel(s) { return s === 'healthy' ? '健康' : s === 'warning' ? '告警' : '未配置' }
function statusDot(s) { return s === 'healthy' ? 'bg-emerald-400' : s === 'warning' ? 'bg-amber-400' : 'bg-rose-500' }
function statusDot(s) { return s === 'healthy' ? 'bg-emerald-500' : s === 'warning' ? 'bg-amber-500' : 'bg-rose-500' }
function statusPill(s) {
if (s === 'healthy') return 'bg-emerald-500/10 text-emerald-300 ring-emerald-400/30'
if (s === 'warning') return 'bg-amber-500/10 text-amber-300 ring-amber-400/30'
return 'bg-rose-500/10 text-rose-300 ring-rose-400/30'
if (s === 'healthy') return 'bg-emerald-500/10 text-emerald-600 ring-emerald-500/25'
if (s === 'warning') return 'bg-amber-500/10 text-amber-600 ring-amber-500/25'
return 'bg-rose-500/10 text-rose-600 ring-rose-500/25'
}
function logDot(status) {
if (status === 'success') return 'bg-emerald-400'
if (status === 'pending') return 'bg-amber-400'
if (status === 'success') return 'bg-emerald-500'
if (status === 'pending') return 'bg-amber-500'
return 'bg-rose-500'
}
function fmtMs(ms) {
+3 -3
View File
@@ -312,7 +312,7 @@ onUnmounted(() => {
<div>
<Icon name="close" class="w-6 h-6 mx-auto mb-2 opacity-60" />
<div>生成失败</div>
<div v-if="e.error" class="text-[10px] text-rose-400 line-clamp-2 mt-1">{{ e.error }}</div>
<div v-if="e.error" class="text-[10px] text-rose-600 line-clamp-2 mt-1">{{ e.error }}</div>
</div>
</div>
@@ -355,9 +355,9 @@ onUnmounted(() => {
<div class="text-[10px] text-white/55 flex items-center justify-between gap-2 tabular-nums">
<span class="break-all" :title="e.model || ''">{{ e.model || '—' }}</span>
<span class="shrink-0 flex items-center gap-1">
<span v-if="e.resolution" class="text-emerald-300/90">{{ e.resolution }}</span>
<span v-if="e.resolution" class="text-emerald-600">{{ e.resolution }}</span>
<span v-if="e.ratio" class="text-white/40">{{ e.ratio }}</span>
<span v-if="e.kind === 'video' && e.duration" class="text-rose-300/80">{{ e.duration }}</span>
<span v-if="e.kind === 'video' && e.duration" class="text-rose-600">{{ e.duration }}</span>
<span v-if="e.deai" class="text-[#71717a]">去AI</span>
</span>
</div>
+18 -37
View File
@@ -224,20 +224,20 @@ async function doRecharge() {
<!-- KPI strip — same shape as LogsView / ModelsView -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
<div class="card p-4">
<div class="text-[11px] uppercase tracking-wider text-white/45">用户总数</div>
<div class="text-[11px] uppercase tracking-wider text-[color:var(--fg-3)]">用户总数</div>
<div class="text-2xl font-semibold mt-1 tabular-nums">{{ stats.total }}</div>
</div>
<div class="card p-4">
<div class="text-[11px] uppercase tracking-wider text-emerald-300/80">正常</div>
<div class="text-2xl font-semibold mt-1 tabular-nums text-emerald-300">{{ stats.active }}</div>
<div class="text-[11px] uppercase tracking-wider text-[color:var(--fg-3)]">正常</div>
<div class="text-2xl font-semibold mt-1 tabular-nums">{{ stats.active }}</div>
</div>
<div class="card p-4">
<div class="text-[11px] uppercase tracking-wider text-rose-300/80">管理员</div>
<div class="text-2xl font-semibold mt-1 tabular-nums text-rose-300">{{ stats.admins }}</div>
<div class="text-[11px] uppercase tracking-wider text-[color:var(--fg-3)]">管理员</div>
<div class="text-2xl font-semibold mt-1 tabular-nums">{{ stats.admins }}</div>
</div>
<div class="card p-4">
<div class="text-[11px] uppercase tracking-wider text-amber-300/80">总积分</div>
<div class="text-2xl font-semibold mt-1 tabular-nums text-amber-300">{{ points(stats.credits_total).toLocaleString('en-US') }}</div>
<div class="text-[11px] uppercase tracking-wider text-[color:var(--fg-3)]">总积分</div>
<div class="text-2xl font-semibold mt-1 tabular-nums">{{ points(stats.credits_total).toLocaleString('en-US') }}</div>
</div>
</div>
@@ -331,12 +331,12 @@ async function doRecharge() {
<td class="px-3 py-3.5 align-middle">
<span class="inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-medium ring-1 whitespace-nowrap"
:class="u.role === 'admin'
? 'bg-rose-500/10 text-rose-300 ring-rose-400/30'
? 'bg-rose-500/10 text-rose-600 ring-rose-500/25'
: u.role === 'agent'
? 'bg-amber-500/10 text-amber-300 ring-amber-400/30'
? 'bg-amber-500/10 text-amber-600 ring-amber-500/25'
: 'bg-white/[0.06] text-white/70 ring-white/15'">
<span class="w-1.5 h-1.5 rounded-full"
:class="u.role === 'admin' ? 'bg-rose-400' : u.role === 'agent' ? 'bg-amber-400' : 'bg-slate-400'"></span>
:class="u.role === 'admin' ? 'bg-rose-500' : u.role === 'agent' ? 'bg-amber-500' : 'bg-slate-400'"></span>
{{ roleLabel(u.role) }}
</span>
</td>
@@ -352,7 +352,7 @@ async function doRecharge() {
{{ points(u.credits).toLocaleString('en-US') }}
</td>
<td class="px-3 py-3.5 align-middle text-right tabular-nums whitespace-nowrap"
:class="u.recharge_total > 0 ? 'text-emerald-300' : 'text-white/25'">
:class="u.recharge_total > 0 ? 'text-emerald-600' : 'text-white/25'">
¥{{ (u.recharge_total || 0).toLocaleString('en-US') }}
</td>
<td class="px-3 py-3.5 align-middle text-right tabular-nums whitespace-nowrap"
@@ -360,7 +360,7 @@ async function doRecharge() {
{{ (u.generation_count || 0).toLocaleString('en-US') }}
</td>
<td class="px-3 py-3.5 align-middle text-right tabular-nums whitespace-nowrap"
:class="u.banned_word_hits > 0 ? 'text-rose-300' : 'text-white/25'">
:class="u.banned_word_hits > 0 ? 'text-rose-600' : 'text-white/25'">
{{ (u.banned_word_hits || 0).toLocaleString('en-US') }}
</td>
<td class="px-3 py-3.5 align-middle text-xs whitespace-nowrap">
@@ -427,7 +427,7 @@ async function doRecharge() {
</div>
<div class="p-5 space-y-3">
<div>
<label class="lbl">邮箱 <span class="text-rose-300">*</span></label>
<label class="lbl">邮箱 <span class="text-rose-600">*</span></label>
<input v-model="addForm.email" class="field" placeholder="user@example.com" />
</div>
<div>
@@ -577,26 +577,10 @@ async function doRecharge() {
}
.fp:hover { background: var(--hover); color: var(--fg); }
.fp-on { background: var(--surface-solid); color: rgb(15 23 42); box-shadow: none; }
.fp-emerald {
background: rgb(16 185 129 / 0.22);
color: rgb(110 231 183);
box-shadow: inset 0 0 0 1px rgb(110 231 183 / 0.45);
}
.fp-rose {
background: rgb(244 63 94 / 0.22);
color: rgb(253 164 175);
box-shadow: inset 0 0 0 1px rgb(253 164 175 / 0.45);
}
.fp-orange {
background: rgb(249 115 22 / 0.22);
color: rgb(253 186 116);
box-shadow: inset 0 0 0 1px rgb(253 186 116 / 0.45);
}
.fp-amber {
background: rgb(245 158 11 / 0.22);
color: rgb(252 211 77);
box-shadow: inset 0 0 0 1px rgb(252 211 77 / 0.45);
}
.fp-emerald { background: rgb(16 185 129 / 0.1); color: #059669; box-shadow: inset 0 0 0 1px rgb(16 185 129 / 0.22); }
.fp-rose { background: rgb(239 68 68 / 0.09); color: #dc2626; box-shadow: inset 0 0 0 1px rgb(239 68 68 / 0.2); }
.fp-orange { background: rgb(249 115 22 / 0.12); color: #c2410c; box-shadow: inset 0 0 0 1px rgb(249 115 22 / 0.25); }
.fp-amber { background: rgb(245 158 11 / 0.12); color: #b45309; box-shadow: inset 0 0 0 1px rgb(245 158 11 / 0.25); }
/* --- icon-only action buttons */
.act {
@@ -642,10 +626,7 @@ async function doRecharge() {
box-shadow: 0 1px 2px rgb(15 23 42 / 0.3);
transition: transform 0.18s ease;
}
.sw-on {
background: rgb(16 185 129 / 0.7);
box-shadow: inset 0 0 0 1px rgb(16 185 129 / 0.5);
}
.sw-on { background: #18181b; box-shadow: inset 0 0 0 1px #18181b; }
.sw-on .sw-thumb { transform: translateX(calc(2.25rem - 1.3rem)); }
/* --- numbered pagination buttons */