feat(ui): 设置页重设计 + 全站表格配色统一为亮色变量
- 设置页:移除暗色残留,统一页头/卡片/图标块/按钮样式 - 模型管理:照 vivid 重排——模型名等宽显示,子行展示渠道名+权重, 定价胶囊「档位+普通价+代理价」,配置列统一灰色等宽标签, 操作列加宽修复删除按钮被挤 - 后端 ModelsView 返回每个模型绑定的渠道名列表(channel_models JOIN) - 全站 scoped 样式里的硬编码白值(rgb(255 255 255/white)批量替换为 主题变量,修复亮色模式下表头/开关/操作按钮不可见(如渠道表头) - 关于页:联系卡片统一等高布局,链接/非链接卡片对齐一致 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -10,24 +10,31 @@ const powers = [
|
||||
{ icon: 't2v', title: '文字生视频', desc: '一段描述生成短片,时长分辨率可选。', tint: 'linear-gradient(135deg,#38bdf8,#52525b)' },
|
||||
{ icon: 'i2v', title: '图生视频', desc: '给张参考图,让静态画面动起来。', tint: 'linear-gradient(135deg,#18181b,#3f3f46)' },
|
||||
]
|
||||
|
||||
const contactCards = computed(() => {
|
||||
const c = contact.value || {}
|
||||
const list = []
|
||||
if (c.shop) list.push({ label: '商店', value: '前往商店', href: c.shop })
|
||||
if (c.email) list.push({ label: '邮箱', value: c.email, href: `mailto:${c.email}` })
|
||||
if (c.qq) list.push({ label: 'QQ', value: c.qq, href: c.qq_link || '' })
|
||||
if (c.qq_group) list.push({ label: 'QQ 群', value: c.qq_group, href: c.qq_group_link || '' })
|
||||
return list
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-16 pb-8">
|
||||
<div class="space-y-14 pb-8">
|
||||
<!-- ===== hero ===== -->
|
||||
<section class="relative overflow-hidden rounded-[1.4rem] anime-card px-6 py-10 md:px-12 md:py-14">
|
||||
<div class="memphis-dots absolute inset-0 opacity-40 pointer-events-none"></div>
|
||||
<section class="ab-hero">
|
||||
<div class="relative grid md:grid-cols-[1.1fr_0.9fr] gap-8 items-center">
|
||||
<div>
|
||||
<span class="sticker">✦ 关于我们</span>
|
||||
<h1 class="mt-4 text-4xl md:text-5xl font-black tracking-tight text-[color:var(--fg)]">
|
||||
把创作交给 <span class="anime-grad-text">{{ site.title }}</span>
|
||||
</h1>
|
||||
<p class="text-[color:var(--fg-2)] mt-5 text-base md:text-lg leading-relaxed max-w-xl">
|
||||
{{ site.title }} 是一个聚合 GPT、Gemini、Firefly、Flux 等顶级模型的 AI 生图与生视频平台。
|
||||
<span class="ab-eyebrow">✦ 关于我们</span>
|
||||
<h1 class="ab-title">把创作交给 <span class="ab-brand">{{ site.title }}</span></h1>
|
||||
<p class="ab-lead">
|
||||
{{ site.title }} 是一个 AI 生图与生视频平台,汇集多家顶级模型,按实际生成量计费。
|
||||
把脑海里的画面写成一句话,剩下的交给我们变成图像与视频。
|
||||
</p>
|
||||
<router-link to="/user" class="bubble-btn mt-7">开始创作 →</router-link>
|
||||
<router-link to="/user" class="btn-primary ab-cta">开始创作 <span aria-hidden="true">→</span></router-link>
|
||||
</div>
|
||||
<!-- floating glass tile cluster standing in for a mascot illustration -->
|
||||
<div class="ab-cluster" aria-hidden="true">
|
||||
@@ -42,9 +49,9 @@ const powers = [
|
||||
|
||||
<!-- ===== powers ===== -->
|
||||
<section>
|
||||
<div class="anime-eyebrow text-[var(--m-orange)]">能做什么</div>
|
||||
<h2 class="mt-3 text-2xl md:text-3xl font-black tracking-tight text-[color:var(--fg)]">四种方式,把想象变成画面</h2>
|
||||
<div class="mt-7 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
<div class="ab-eyebrow">能做什么</div>
|
||||
<h2 class="ab-h2">四种方式,把想象变成画面</h2>
|
||||
<div class="mt-6 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
<div v-for="p in powers" :key="p.title" class="power-card p-5">
|
||||
<div class="power-ico" :style="{ background: p.tint }">
|
||||
<svg v-if="p.icon === 't2i'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 5h16v14H4z"/><path d="M4 15l4.5-4.5 3 3L15 10l5 5"/><circle cx="9" cy="9" r="1.3"/></svg>
|
||||
@@ -60,60 +67,45 @@ const powers = [
|
||||
|
||||
<!-- ===== contact ===== -->
|
||||
<section>
|
||||
<div class="anime-eyebrow text-sky-600">联系我们</div>
|
||||
<h2 class="mt-3 text-2xl md:text-3xl font-black tracking-tight text-[color:var(--fg)]">有问题?来找我们玩~</h2>
|
||||
<p class="text-[color:var(--fg-3)] mt-2 mb-6">合作、反馈,或想加入交流群,随时招呼。</p>
|
||||
<div class="ab-eyebrow">联系我们</div>
|
||||
<h2 class="ab-h2">有问题?来找我们玩~</h2>
|
||||
<p class="ab-sub">合作、反馈,或想加入交流群,随时招呼。</p>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 max-w-3xl">
|
||||
<a v-if="contact.shop" :href="contact.shop" target="_blank" rel="noopener"
|
||||
class="group anime-card flex items-center justify-between gap-3 p-5">
|
||||
<div>
|
||||
<div class="sticker">商店</div>
|
||||
<div class="text-base font-semibold text-[color:var(--fg)] mt-2 group-hover:text-[var(--m-orange)] transition-colors">前往商店</div>
|
||||
<div class="mt-6 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
<component :is="c.href ? 'a' : 'div'" v-for="c in contactCards" :key="c.label"
|
||||
:href="c.href || undefined" :target="c.href ? '_blank' : undefined" rel="noopener"
|
||||
class="ct-card" :class="c.href && 'ct-link'">
|
||||
<div class="ct-top">
|
||||
<span class="ct-label">{{ c.label }}</span>
|
||||
<span v-if="c.href" class="ct-arrow" aria-hidden="true">↗</span>
|
||||
</div>
|
||||
<span class="text-[color:var(--fg-faint)] group-hover:translate-x-1 transition-transform">→</span>
|
||||
</a>
|
||||
|
||||
<a v-if="contact.email" :href="`mailto:${contact.email}`"
|
||||
class="group anime-card p-5">
|
||||
<div class="sticker">邮箱</div>
|
||||
<div class="text-base font-semibold text-[color:var(--fg)] mt-2 break-all group-hover:text-sky-600 transition-colors">{{ contact.email }}</div>
|
||||
</a>
|
||||
|
||||
<a v-if="contact.qq && contact.qq_link" :href="contact.qq_link" target="_blank" rel="noopener"
|
||||
class="group anime-card flex items-center justify-between gap-3 p-5">
|
||||
<div>
|
||||
<div class="sticker">QQ</div>
|
||||
<div class="text-base font-semibold text-[color:var(--fg)] mt-2 tabular-nums break-all group-hover:text-sky-600 transition-colors">{{ contact.qq }}</div>
|
||||
</div>
|
||||
<span class="text-[color:var(--fg-faint)] group-hover:translate-x-1 transition-transform">→</span>
|
||||
</a>
|
||||
<div v-else-if="contact.qq" class="anime-card p-5">
|
||||
<div class="sticker">QQ</div>
|
||||
<div class="text-base font-semibold text-[color:var(--fg)] mt-2 tabular-nums break-all">{{ contact.qq }}</div>
|
||||
</div>
|
||||
|
||||
<a v-if="contact.qq_group && contact.qq_group_link" :href="contact.qq_group_link" target="_blank" rel="noopener"
|
||||
class="group anime-card flex items-center justify-between gap-3 p-5">
|
||||
<div>
|
||||
<div class="sticker">QQ 群</div>
|
||||
<div class="text-base font-semibold text-[color:var(--fg)] mt-2 tabular-nums break-all group-hover:text-emerald-600 transition-colors">{{ contact.qq_group }}</div>
|
||||
</div>
|
||||
<span class="text-[color:var(--fg-faint)] group-hover:translate-x-1 transition-transform">→</span>
|
||||
</a>
|
||||
<div v-else-if="contact.qq_group" class="anime-card p-5">
|
||||
<div class="sticker">QQ 群</div>
|
||||
<div class="text-base font-semibold text-[color:var(--fg)] mt-2 tabular-nums break-all">{{ contact.qq_group }}</div>
|
||||
</div>
|
||||
<div class="ct-value">{{ c.value }}</div>
|
||||
</component>
|
||||
</div>
|
||||
|
||||
<p v-if="!contact.shop && !contact.email && !contact.qq && !contact.qq_group"
|
||||
class="text-sm text-[color:var(--fg-3)]">管理员尚未配置联系方式。</p>
|
||||
<p v-if="!contactCards.length" class="ab-sub">管理员尚未配置联系方式。</p>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ab-hero { position: relative; overflow: hidden; border-radius: 24px; padding: 48px 52px; background: var(--surface-solid); border: 1px solid rgb(24 24 27 / .07); box-shadow: 0 1px 2px rgb(24 24 27 / .03), 0 18px 40px -22px rgb(24 24 27 / .16); }
|
||||
.ab-hero::before { content: ''; position: absolute; inset: 0; pointer-events: none; background: radial-gradient(520px 300px at 92% 0%, rgba(251, 146, 60, .1), transparent 70%); }
|
||||
.ab-eyebrow { display: inline-flex; align-items: center; gap: 6px; font-size: 12.5px; font-weight: 800; letter-spacing: .08em; color: var(--fg-3); }
|
||||
.ab-title { margin-top: 14px; font-size: clamp(32px, 4vw, 48px); font-weight: 800; line-height: 1.15; letter-spacing: -0.04em; color: var(--fg); }
|
||||
.ab-brand { background: linear-gradient(100deg, #18181b, #71717a); -webkit-background-clip: text; background-clip: text; color: transparent; }
|
||||
.ab-lead { margin-top: 18px; max-width: 34em; font-size: 16px; line-height: 1.8; color: var(--fg-2); }
|
||||
.ab-cta { margin-top: 26px; height: 44px; padding: 0 24px !important; font-size: 14px !important; border-radius: 999px !important; }
|
||||
.ab-h2 { margin-top: 8px; font-size: 26px; font-weight: 800; letter-spacing: -0.03em; color: var(--fg); }
|
||||
.ab-sub { margin-top: 6px; font-size: 14px; color: var(--fg-3); }
|
||||
.ct-card { display: flex; flex-direction: column; justify-content: space-between; gap: 18px; min-height: 108px; padding: 18px 20px; border-radius: 16px; background: var(--surface-solid); border: 1px solid rgb(24 24 27 / .07); text-decoration: none; transition: transform .18s ease, border-color .18s ease, box-shadow .18s ease; }
|
||||
.ct-link:hover { transform: translateY(-3px); border-color: rgb(24 24 27 / .3); box-shadow: 0 14px 30px -16px rgb(24 24 27 / .25); }
|
||||
.ct-top { display: flex; align-items: center; justify-content: space-between; }
|
||||
.ct-label { font-size: 12px; font-weight: 800; letter-spacing: .06em; color: var(--fg-3); }
|
||||
.ct-arrow { font-size: 15px; color: var(--fg-faint); transition: transform .18s ease, color .18s ease; }
|
||||
.ct-link:hover .ct-arrow { transform: translate(2px, -2px); color: var(--fg); }
|
||||
.ct-value { font-size: 17px; font-weight: 800; letter-spacing: -0.01em; color: var(--fg); font-variant-numeric: tabular-nums; word-break: break-all; }
|
||||
@media (max-width: 640px) { .ab-hero { padding: 30px 22px; } }
|
||||
/* Floating glass tile cluster — a loose collage of rotated frosted squares. */
|
||||
.ab-cluster {
|
||||
position: relative;
|
||||
@@ -125,19 +117,17 @@ const powers = [
|
||||
place-items: center;
|
||||
width: 5.5rem;
|
||||
height: 5.5rem;
|
||||
border: 1px solid var(--glass-border);
|
||||
border: 1px solid rgb(24 24 27 / .07);
|
||||
border-radius: 1.2rem;
|
||||
background: var(--surface);
|
||||
-webkit-backdrop-filter: blur(14px);
|
||||
backdrop-filter: blur(14px);
|
||||
box-shadow: var(--glass-shadow-lg);
|
||||
background: var(--surface-solid);
|
||||
box-shadow: 0 14px 30px -14px rgb(24 24 27 / .25);
|
||||
color: var(--m-orange);
|
||||
}
|
||||
.ab-tile svg { width: 2.3rem; height: 2.3rem; }
|
||||
.ab-1 { left: 12%; top: 6%; transform: rotate(-9deg); animation: memphis-float 6s ease-in-out infinite; }
|
||||
.ab-2 { right: 14%; top: 0; color: var(--m-orange-2); transform: rotate(7deg); animation: memphis-float 7s ease-in-out infinite reverse; }
|
||||
.ab-3 { left: 26%; bottom: 4%; color: var(--m-pink); transform: rotate(5deg); animation: memphis-bounce 4.5s ease-in-out infinite; }
|
||||
.ab-4 { right: 8%; bottom: 14%; color: var(--m-mint); transform: rotate(-6deg); animation: memphis-float 5.5s ease-in-out infinite; }
|
||||
.ab-2 { right: 14%; top: 0; color: #18181b; transform: rotate(7deg); animation: memphis-float 7s ease-in-out infinite reverse; }
|
||||
.ab-3 { left: 26%; bottom: 4%; color: #52525b; transform: rotate(5deg); animation: memphis-bounce 4.5s ease-in-out infinite; }
|
||||
.ab-4 { right: 8%; bottom: 14%; color: #18181b; transform: rotate(-6deg); animation: memphis-float 5.5s ease-in-out infinite; }
|
||||
.ab-star {
|
||||
position: absolute;
|
||||
left: 50%; top: 44%;
|
||||
@@ -146,12 +136,10 @@ const powers = [
|
||||
animation: memphis-wiggle 3.5s ease-in-out infinite;
|
||||
}
|
||||
.power-card {
|
||||
border: 1px solid var(--glass-border);
|
||||
border: 1px solid rgb(24 24 27 / .07);
|
||||
border-radius: 1.1rem;
|
||||
background: var(--surface);
|
||||
-webkit-backdrop-filter: blur(14px);
|
||||
backdrop-filter: blur(14px);
|
||||
box-shadow: var(--glass-shadow);
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 2px rgb(24 24 27 / .03), 0 10px 28px -16px rgb(24 24 27 / .12);
|
||||
transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
|
||||
}
|
||||
.power-card:hover { transform: translateY(-3px); border-color: rgb(24 24 27 / 0.45); box-shadow: var(--glass-shadow-lg); }
|
||||
|
||||
@@ -141,18 +141,18 @@ function goPage(n) {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fp { display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.35rem 0.7rem; font-size: 0.72rem; border-radius: 0.55rem; color: rgb(255 255 255 / 0.65); background: rgb(255 255 255 / 0.05); box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.06); transition: background 0.15s, color 0.15s; }
|
||||
.fp:hover { background: rgb(255 255 255 / 0.09); color: white; }
|
||||
.fp-on { background: rgb(255 255 255 / 0.92); color: rgb(15 23 42); box-shadow: none; }
|
||||
.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); }
|
||||
.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: rgb(255 255 255 / 0.06); color: rgb(255 255 255 / 0.55); box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.1); }
|
||||
.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 rgb(255 255 255 / 0.04); transition: background-color 0.15s ease, box-shadow 0.15s ease; }
|
||||
.log-row:hover td { background: rgb(255 255 255 / 0.025); }
|
||||
.log-row td { border-bottom: 1px solid var(--hairline); transition: background-color 0.15s ease, box-shadow 0.15s ease; }
|
||||
.log-row:hover td { background: var(--surface-2); }
|
||||
.log-row:hover td:first-child { box-shadow: inset 2px 0 0 rgb(253 186 116 / 0.55); }
|
||||
.log-row:last-child td { border-bottom: none; }
|
||||
.pg { min-width: 1.75rem; padding: 0.3rem 0.55rem; font-size: 0.72rem; font-weight: 500; text-align: center; border-radius: 0.45rem; color: rgb(255 255 255 / 0.7); background: rgb(255 255 255 / 0.04); box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.08); transition: background 0.15s, color 0.15s; }
|
||||
.pg:hover:not(.pg-on) { background: rgb(255 255 255 / 0.1); color: white; }
|
||||
.pg-on { background: rgb(255 255 255 / 0.92); color: rgb(15 23 42); box-shadow: none; }
|
||||
.pg { min-width: 1.75rem; padding: 0.3rem 0.55rem; font-size: 0.72rem; font-weight: 500; text-align: center; border-radius: 0.45rem; color: var(--fg-2); background: var(--surface-2); box-shadow: inset 0 0 0 1px var(--hairline); transition: background 0.15s, color 0.15s; }
|
||||
.pg:hover:not(.pg-on) { background: var(--hover); color: var(--fg); }
|
||||
.pg-on { background: var(--surface-solid); color: rgb(15 23 42); box-shadow: none; }
|
||||
</style>
|
||||
|
||||
@@ -113,8 +113,8 @@ function goPage(n) {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.pg { min-width: 1.75rem; padding: 0.3rem 0.55rem; font-size: 0.72rem; font-weight: 500; text-align: center; border-radius: 0.45rem; color: rgb(255 255 255 / 0.7); background: rgb(255 255 255 / 0.04); box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.08); transition: background 0.15s, color 0.15s; }
|
||||
.pg:hover:not(.pg-on) { background: rgb(255 255 255 / 0.1); color: white; }
|
||||
.pg-on { background: rgb(255 255 255 / 0.92); color: rgb(15 23 42); box-shadow: none; }
|
||||
.pg { min-width: 1.75rem; padding: 0.3rem 0.55rem; font-size: 0.72rem; font-weight: 500; text-align: center; border-radius: 0.45rem; color: var(--fg-2); background: var(--surface-2); box-shadow: inset 0 0 0 1px var(--hairline); transition: background 0.15s, color 0.15s; }
|
||||
.pg:hover:not(.pg-on) { background: var(--hover); color: var(--fg); }
|
||||
.pg-on { background: var(--surface-solid); color: rgb(15 23 42); box-shadow: none; }
|
||||
.line-clamp-2 { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
||||
</style>
|
||||
|
||||
@@ -200,11 +200,11 @@ onMounted(load)
|
||||
.act {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
width: 1.9rem; height: 1.9rem; border-radius: 0.5rem;
|
||||
color: rgb(255 255 255 / 0.7); background: rgb(255 255 255 / 0.04);
|
||||
box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.08);
|
||||
color: var(--fg-2); background: var(--surface-2);
|
||||
box-shadow: inset 0 0 0 1px var(--hairline);
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
.act:hover { background: rgb(255 255 255 / 0.1); color: white; }
|
||||
.act:hover { background: var(--hover); color: var(--fg); }
|
||||
.act.danger { color: rgb(253 164 175); background: rgb(244 63 94 / 0.12); box-shadow: inset 0 0 0 1px rgb(244 63 94 / 0.3); }
|
||||
.act.danger:hover { color: white; background: rgb(244 63 94 / 0.25); }
|
||||
.btn-soft.danger {
|
||||
@@ -217,9 +217,9 @@ onMounted(load)
|
||||
background: rgb(244 63 94 / 0.25);
|
||||
}
|
||||
.chk { accent-color: rgb(244 63 94); width: 0.9rem; height: 0.9rem; cursor: pointer; }
|
||||
.pg { min-width: 1.75rem; padding: 0.3rem 0.55rem; font-size: 0.72rem; font-weight: 500; text-align: center; border-radius: 0.45rem; color: rgb(255 255 255 / 0.7); background: rgb(255 255 255 / 0.04); box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.08); transition: background 0.15s, color 0.15s; }
|
||||
.pg:hover:not(.pg-on) { background: rgb(255 255 255 / 0.1); color: white; }
|
||||
.pg-on { background: rgb(255 255 255 / 0.92); color: rgb(15 23 42); box-shadow: none; }
|
||||
.pg { min-width: 1.75rem; padding: 0.3rem 0.55rem; font-size: 0.72rem; font-weight: 500; text-align: center; border-radius: 0.45rem; color: var(--fg-2); background: var(--surface-2); box-shadow: inset 0 0 0 1px var(--hairline); transition: background 0.15s, color 0.15s; }
|
||||
.pg:hover:not(.pg-on) { background: var(--hover); color: var(--fg); }
|
||||
.pg-on { background: var(--surface-solid); color: rgb(15 23 42); box-shadow: none; }
|
||||
.fade-enter-active, .fade-leave-active { transition: opacity 0.15s ease; }
|
||||
.fade-enter-from, .fade-leave-to { opacity: 0; }
|
||||
</style>
|
||||
|
||||
@@ -1142,7 +1142,7 @@ const saveLabel = computed(() => ({
|
||||
box-shadow: 0 4px 14px rgb(229 72 77 / 0.25);
|
||||
}
|
||||
.cv-btn--accent {
|
||||
background: #fff;
|
||||
background: var(--surface-solid);
|
||||
border-color: rgb(24 24 27 / 0.28);
|
||||
color: #18181b;
|
||||
}
|
||||
@@ -1193,7 +1193,7 @@ const saveLabel = computed(() => ({
|
||||
padding: 6px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgb(24 24 27 / 0.08);
|
||||
background: rgb(255 255 255 / 0.92);
|
||||
background: var(--surface-solid);
|
||||
-webkit-backdrop-filter: blur(14px);
|
||||
backdrop-filter: blur(14px);
|
||||
box-shadow: 0 1px 2px rgb(24 24 27 / 0.04), 0 12px 32px -12px rgb(24 24 27 / 0.2);
|
||||
@@ -1221,7 +1221,7 @@ const saveLabel = computed(() => ({
|
||||
padding: 6px;
|
||||
border-radius: 18px;
|
||||
border: 1px solid rgb(24 24 27 / 0.08);
|
||||
background: rgb(255 255 255 / 0.92);
|
||||
background: var(--surface-solid);
|
||||
-webkit-backdrop-filter: blur(14px);
|
||||
backdrop-filter: blur(14px);
|
||||
box-shadow: 0 1px 2px rgb(24 24 27 / 0.04), 0 12px 32px -12px rgb(24 24 27 / 0.2);
|
||||
|
||||
@@ -327,12 +327,12 @@ const pageNumbers = computed(() => {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
width: 1.9rem; height: 1.9rem;
|
||||
border-radius: 0.5rem;
|
||||
color: rgb(255 255 255 / 0.7);
|
||||
background: rgb(255 255 255 / 0.04);
|
||||
box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.08);
|
||||
color: var(--fg-2);
|
||||
background: var(--surface-2);
|
||||
box-shadow: inset 0 0 0 1px var(--hairline);
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
.act:hover { background: rgb(255 255 255 / 0.1); color: white; }
|
||||
.act:hover { background: var(--hover); color: var(--fg); }
|
||||
.act.danger {
|
||||
color: rgb(253 164 175);
|
||||
background: rgb(244 63 94 / 0.12);
|
||||
@@ -344,13 +344,13 @@ const pageNumbers = computed(() => {
|
||||
display: inline-flex; align-items: center; gap: 0.35rem;
|
||||
padding: 0.35rem 0.7rem; font-size: 0.72rem;
|
||||
border-radius: 0.55rem;
|
||||
color: rgb(255 255 255 / 0.65);
|
||||
background: rgb(255 255 255 / 0.05);
|
||||
box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.06);
|
||||
color: var(--fg-2);
|
||||
background: var(--surface-2);
|
||||
box-shadow: inset 0 0 0 1px var(--hairline);
|
||||
transition: background 0.15s, color 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
.fp:hover { background: rgb(255 255 255 / 0.09); color: white; }
|
||||
.fp-on { background: rgb(255 255 255 / 0.92); color: rgb(15 23 42); box-shadow: none; }
|
||||
.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);
|
||||
@@ -369,14 +369,14 @@ const pageNumbers = computed(() => {
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
border-radius: 0.45rem;
|
||||
color: rgb(255 255 255 / 0.7);
|
||||
background: rgb(255 255 255 / 0.04);
|
||||
box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.08);
|
||||
color: var(--fg-2);
|
||||
background: var(--surface-2);
|
||||
box-shadow: inset 0 0 0 1px var(--hairline);
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
.pg:hover:not(.pg-on) { background: rgb(255 255 255 / 0.1); color: white; }
|
||||
.pg:hover:not(.pg-on) { background: var(--hover); color: var(--fg); }
|
||||
.pg-on {
|
||||
background: rgb(255 255 255 / 0.92);
|
||||
background: var(--surface-solid);
|
||||
color: rgb(15 23 42);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@@ -314,20 +314,20 @@ onMounted(load)
|
||||
|
||||
<style scoped>
|
||||
.tbl { width: 100%; border-collapse: collapse; min-width: 1000px; }
|
||||
.tbl thead th { padding: 10px 14px; text-align: left; font-size: 11px; font-weight: 600; color: rgb(255 255 255 / 0.45); border-bottom: 1px solid rgb(255 255 255 / 0.06); white-space: nowrap; }
|
||||
.tbl tbody td { padding: 10px 14px; border-bottom: 1px solid rgb(255 255 255 / 0.04); font-size: 12.5px; color: rgb(255 255 255 / 0.7); vertical-align: middle; }
|
||||
.tbl thead th { padding: 10px 14px; text-align: left; font-size: 11px; font-weight: 600; color: var(--fg-3); border-bottom: 1px solid var(--hairline); white-space: nowrap; }
|
||||
.tbl tbody td { padding: 10px 14px; border-bottom: 1px solid var(--hairline); font-size: 12.5px; color: var(--fg-2); vertical-align: middle; }
|
||||
.tbl tbody tr:last-child td { border-bottom: none; }
|
||||
.tbl tbody tr:hover { background: rgb(255 255 255 / 0.02); }
|
||||
.tbl tbody tr:hover { background: var(--surface-2); }
|
||||
|
||||
.lbl { display: block; font-size: 0.72rem; font-weight: 500; color: rgb(255 255 255 / 0.55); margin-bottom: 0.4rem; }
|
||||
.lbl { display: block; font-size: 0.72rem; font-weight: 500; color: var(--fg-3); margin-bottom: 0.4rem; }
|
||||
|
||||
.op { display: inline-grid; place-items: center; min-width: 28px; height: 26px; padding: 0 7px; margin-left: 4px; border-radius: 7px; background: rgb(255 255 255 / 0.05); color: rgb(255 255 255 / 0.6); }
|
||||
.op:hover { background: rgb(255 255 255 / 0.1); color: white; }
|
||||
.op { display: inline-grid; place-items: center; min-width: 28px; height: 26px; padding: 0 7px; margin-left: 4px; border-radius: 7px; background: var(--surface-2); color: var(--fg-2); }
|
||||
.op:hover { background: var(--hover); color: var(--fg); }
|
||||
.op.danger:hover { background: rgb(244 63 94 / 0.15); color: rgb(251 113 133); }
|
||||
|
||||
/* iOS-style switch for 模型开关 in the sync modal */
|
||||
.sw { position: relative; width: 2rem; height: 1.15rem; border-radius: 9999px; background: rgb(255 255 255 / 0.12); box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.08); 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: white; box-shadow: 0 1px 2px rgb(15 23 42 / 0.3); transition: transform 0.18s ease; }
|
||||
.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 .sw-thumb { transform: translateX(calc(2rem - 1.15rem)); }
|
||||
</style>
|
||||
|
||||
@@ -135,11 +135,11 @@ onMounted(load)
|
||||
.act {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
width: 1.9rem; height: 1.9rem; border-radius: 0.5rem;
|
||||
color: rgb(255 255 255 / 0.7); background: rgb(255 255 255 / 0.04);
|
||||
box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.08);
|
||||
color: var(--fg-2); background: var(--surface-2);
|
||||
box-shadow: inset 0 0 0 1px var(--hairline);
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
.act:hover { background: rgb(255 255 255 / 0.1); color: white; }
|
||||
.act:hover { background: var(--hover); color: var(--fg); }
|
||||
.act.danger { color: rgb(253 164 175); background: rgb(244 63 94 / 0.12); box-shadow: inset 0 0 0 1px rgb(244 63 94 / 0.3); }
|
||||
.act.danger:hover { color: white; background: rgb(244 63 94 / 0.25); }
|
||||
.fade-enter-active, .fade-leave-active { transition: opacity 0.15s ease; }
|
||||
|
||||
@@ -661,7 +661,7 @@ html.dark .hint b { color: rgb(253 164 175); } /* rose — visible warning, no
|
||||
width: calc(1.3rem - 4px);
|
||||
height: calc(1.3rem - 4px);
|
||||
border-radius: 9999px;
|
||||
background: white;
|
||||
background: var(--surface-solid);
|
||||
box-shadow: 0 1px 2px rgb(15 23 42 / 0.2);
|
||||
transition: transform 0.18s ease;
|
||||
}
|
||||
|
||||
@@ -238,7 +238,7 @@ function toast(m) {
|
||||
.inv-k { margin-top: 8px; font-size: 12.5px; font-weight: 600; color: #a1a1aa; }
|
||||
|
||||
.steps { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
|
||||
.step { display: flex; align-items: center; gap: 14px; padding: 16px 18px; background: #fff; border: 1px solid rgb(24 24 27 / .07); border-radius: 16px; }
|
||||
.step { display: flex; align-items: center; gap: 14px; padding: 16px 18px; background: var(--surface-solid); border: 1px solid rgb(24 24 27 / .07); border-radius: 16px; }
|
||||
.step-n { flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; display: grid; place-items: center; background: #f4f4f5; color: #18181b; font-size: 14px; font-weight: 800; }
|
||||
.step-t { font-size: 14px; font-weight: 800; color: var(--fg); }
|
||||
.step-d { margin-top: 2px; font-size: 12px; color: var(--fg-3); }
|
||||
@@ -259,9 +259,9 @@ function toast(m) {
|
||||
box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.08);
|
||||
transition: background 0.15s, color 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
.pg:hover:not(.pg-on) { background: rgb(255 255 255 / 0.1); color: white; }
|
||||
.pg:hover:not(.pg-on) { background: var(--hover); color: var(--fg); }
|
||||
.pg-on {
|
||||
background: rgb(255 255 255 / 0.92);
|
||||
background: var(--surface-solid);
|
||||
color: rgb(15 23 42);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@@ -360,14 +360,14 @@ const sourcePill = (s) => ({
|
||||
padding: 0.35rem 0.7rem;
|
||||
font-size: 0.72rem;
|
||||
border-radius: 0.55rem;
|
||||
color: rgb(255 255 255 / 0.65);
|
||||
background: rgb(255 255 255 / 0.05);
|
||||
box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.06);
|
||||
color: var(--fg-2);
|
||||
background: var(--surface-2);
|
||||
box-shadow: inset 0 0 0 1px var(--hairline);
|
||||
transition: background 0.15s, color 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
.fp:hover { background: rgb(255 255 255 / 0.09); color: white; }
|
||||
.fp:hover { background: var(--hover); color: var(--fg); }
|
||||
.fp-on {
|
||||
background: rgb(255 255 255 / 0.92);
|
||||
background: var(--surface-solid);
|
||||
color: rgb(15 23 42);
|
||||
box-shadow: none;
|
||||
}
|
||||
@@ -418,10 +418,10 @@ const sourcePill = (s) => ({
|
||||
focus indicator without being noisy). --- */
|
||||
.log-table { border-collapse: separate; border-spacing: 0; }
|
||||
.log-row td {
|
||||
border-bottom: 1px solid rgb(255 255 255 / 0.04);
|
||||
border-bottom: 1px solid var(--hairline);
|
||||
transition: background-color 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
.log-row:hover td { background: rgb(255 255 255 / 0.025); }
|
||||
.log-row:hover td { background: var(--surface-2); }
|
||||
.log-row:hover td:first-child {
|
||||
box-shadow: inset 2px 0 0 rgb(253 186 116 / 0.55);
|
||||
}
|
||||
@@ -435,14 +435,14 @@ const sourcePill = (s) => ({
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
border-radius: 0.45rem;
|
||||
color: rgb(255 255 255 / 0.7);
|
||||
background: rgb(255 255 255 / 0.04);
|
||||
box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.08);
|
||||
color: var(--fg-2);
|
||||
background: var(--surface-2);
|
||||
box-shadow: inset 0 0 0 1px var(--hairline);
|
||||
transition: background 0.15s, color 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
.pg:hover:not(.pg-on) { background: rgb(255 255 255 / 0.1); color: white; }
|
||||
.pg:hover:not(.pg-on) { background: var(--hover); color: var(--fg); }
|
||||
.pg-on {
|
||||
background: rgb(255 255 255 / 0.92);
|
||||
background: var(--surface-solid);
|
||||
color: rgb(15 23 42);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@@ -117,25 +117,23 @@ onMounted(loadModels)
|
||||
<router-link v-else to="/admin/channels" class="text-sm text-sky-300 underline underline-offset-2">还没有模型,先到「渠道管理」添加渠道,保存后会自动同步上游模型</router-link>
|
||||
</div>
|
||||
|
||||
<table v-else class="w-full text-sm table-fixed">
|
||||
<table v-else class="w-full text-sm table-fixed min-w-[1080px]">
|
||||
<colgroup>
|
||||
<col /> <!-- model id + provider -->
|
||||
<col class="w-52" /> <!-- model name + provider -->
|
||||
<col class="w-20" /> <!-- type -->
|
||||
<col /> <!-- pricing -->
|
||||
<col /> <!-- capability -->
|
||||
<col class="w-20" /> <!-- weight -->
|
||||
<col class="w-24" /> <!-- generation count -->
|
||||
<col /> <!-- config -->
|
||||
<col class="w-24" /> <!-- success count -->
|
||||
<col class="w-20" /> <!-- sellable badge -->
|
||||
<col class="w-36" /> <!-- actions -->
|
||||
<col class="w-32" /> <!-- actions -->
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr class="text-[10px] uppercase tracking-[0.2em] text-white/40 border-b border-white/[0.06]">
|
||||
<th class="text-left px-5 py-3 font-medium">模型</th>
|
||||
<th class="text-left px-3 py-3 font-medium">类型</th>
|
||||
<th class="text-left px-3 py-3 font-medium">定价</th>
|
||||
<th class="text-left px-3 py-3 font-medium">能力</th>
|
||||
<th class="text-right px-3 py-3 font-medium">权重</th>
|
||||
<th class="text-right px-3 py-3 font-medium">生图次数</th>
|
||||
<th class="text-left px-3 py-3 font-medium">配置</th>
|
||||
<th class="text-right px-3 py-3 font-medium">成功次数</th>
|
||||
<th class="text-left px-3 py-3 font-medium">上架</th>
|
||||
<th class="text-right px-5 py-3 font-medium">操作</th>
|
||||
</tr>
|
||||
@@ -143,103 +141,65 @@ onMounted(loadModels)
|
||||
<tbody>
|
||||
<tr v-for="m in filtered" :key="m.id"
|
||||
class="border-b border-white/[0.04] hover:bg-white/[0.03] transition-colors">
|
||||
<!-- Model id + provider underneath -->
|
||||
<!-- Model name (mono) + provider / weight subline -->
|
||||
<td class="px-5 py-3.5 align-middle min-w-0">
|
||||
<div class="flex items-start gap-2 min-w-0">
|
||||
<div class="font-mono text-xs text-white/90 truncate" :title="m.id">{{ m.id }}</div>
|
||||
<span v-if="m.alias" class="inline-flex items-center rounded-full px-2 py-0.5 text-[10px] bg-sky-500/10 text-sky-300 ring-1 ring-sky-400/20 shrink-0">{{ m.alias }}</span>
|
||||
<div class="font-mono text-[12.5px] text-white/90 truncate" :title="m.id">{{ m.alias || m.name || m.id }}</div>
|
||||
<div class="mt-1 text-[10.5px] text-white/45 truncate" :title="(m.channels || []).join('、')">
|
||||
{{ (m.channels || []).length ? m.channels.join('、') : (m.provider === 'channel' ? '渠道导入' : (m.provider || '—')) }} · 权重 {{ m.weight || 0 }}
|
||||
</div>
|
||||
<div class="mt-1 text-[10px] text-white/45 truncate">{{ m.provider === 'channel' ? '渠道导入' : (m.provider || '—') }}</div>
|
||||
</td>
|
||||
|
||||
<!-- Type chip with the same shape as Logs/Provider 健康 -->
|
||||
<!-- Type tag -->
|
||||
<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.type === 'video'
|
||||
? 'bg-rose-500/10 text-rose-300 ring-rose-400/30'
|
||||
: 'bg-sky-500/10 text-sky-300 ring-sky-400/30'">
|
||||
<span class="kind-tag" :class="m.type === 'video' ? 'kind-video' : 'kind-image'">
|
||||
{{ TYPE_LABEL[m.type] || m.type }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<!-- Pricing block — image shows 1K/2K/4K + price, video shows
|
||||
duration + price; each as a small price-chip. -->
|
||||
<!-- Pricing — compact chips: 档位 + 普通价 + 代理价 -->
|
||||
<td class="px-3 py-3.5 align-middle">
|
||||
<!-- Each tier shows a pair: 普通价 (emerald) + 代理价 (amber).
|
||||
代理价未设置时回退普通价数值。 -->
|
||||
<div v-if="m.type === 'image'" class="flex flex-wrap gap-1">
|
||||
<span v-for="r in (m.resolutions || [])" :key="r" class="price-chip">
|
||||
<span class="text-white/85">{{ r }}</span>
|
||||
<span class="text-white/30 mx-1">普通</span>
|
||||
<span class="text-emerald-300 tabular-nums">{{ points(m.prices?.[r]) }}</span>
|
||||
<span class="text-amber-300/40 ml-1.5">代理</span>
|
||||
<span class="text-amber-300 tabular-nums">{{ points(m.prices_agent?.[r] ?? m.prices?.[r]) }}</span>
|
||||
<div v-if="m.type === 'image'" class="flex flex-wrap gap-1.5">
|
||||
<span v-for="r in (m.resolutions || [])" :key="r" class="p-chip">
|
||||
<b>{{ r }}</b><em>{{ points(m.prices?.[r]) }}</em><em class="ag">{{ points(m.prices_agent?.[r] ?? m.prices?.[r]) }}</em>
|
||||
</span>
|
||||
<span v-if="!(m.resolutions || []).length" class="text-white/30 text-xs">—</span>
|
||||
</div>
|
||||
<div v-else class="flex flex-wrap gap-1">
|
||||
<span v-for="r in (m.resolutions || [])" :key="'r'+r" class="price-chip">
|
||||
<span class="text-white/85">{{ r }}</span>
|
||||
<span class="text-white/30 mx-1">普通</span>
|
||||
<span class="text-emerald-300 tabular-nums">{{ points(m.prices?.[r]) }}</span>
|
||||
<span class="text-amber-300/40 ml-1.5">代理</span>
|
||||
<span class="text-amber-300 tabular-nums">{{ points(m.prices_agent?.[r] ?? m.prices?.[r]) }}</span>
|
||||
<div v-else class="flex flex-wrap gap-1.5">
|
||||
<span v-for="r in (m.resolutions || [])" :key="'r'+r" class="p-chip">
|
||||
<b>{{ r }}</b><em>{{ points(m.prices?.[r]) }}</em><em class="ag">{{ points(m.prices_agent?.[r] ?? m.prices?.[r]) }}</em>
|
||||
</span>
|
||||
<span v-if="m.duration_prices?.per_second != null" class="price-chip">
|
||||
<span class="text-white/85">/s</span>
|
||||
<span class="text-white/30 mx-1">普通</span>
|
||||
<span class="text-sky-300 tabular-nums">{{ points(m.duration_prices.per_second) }}</span>
|
||||
<span class="text-amber-300/40 ml-1.5">代理</span>
|
||||
<span class="text-amber-300 tabular-nums">{{ points(m.duration_prices_agent?.per_second ?? m.duration_prices.per_second) }}</span>
|
||||
<span v-if="m.duration_prices?.per_second != null" class="p-chip">
|
||||
<b>/s</b><em>{{ points(m.duration_prices.per_second) }}</em><em class="ag">{{ points(m.duration_prices_agent?.per_second ?? m.duration_prices.per_second) }}</em>
|
||||
</span>
|
||||
<span v-else v-for="d in (m.durations || [])" :key="'d'+d" class="price-chip">
|
||||
<span class="text-white/85">{{ d }}</span>
|
||||
<span class="text-white/30 mx-1">普通</span>
|
||||
<span class="text-sky-300 tabular-nums">+{{ points(m.duration_prices?.[d]) }}</span>
|
||||
<span class="text-amber-300/40 ml-1.5">代理</span>
|
||||
<span class="text-amber-300 tabular-nums">+{{ points(m.duration_prices_agent?.[d] ?? m.duration_prices?.[d]) }}</span>
|
||||
<span v-else v-for="d in (m.durations || [])" :key="'d'+d" class="p-chip">
|
||||
<b>{{ d }}</b><em>+{{ points(m.duration_prices?.[d]) }}</em><em class="ag">+{{ points(m.duration_prices_agent?.[d] ?? m.duration_prices?.[d]) }}</em>
|
||||
</span>
|
||||
<span v-if="!(m.resolutions || []).length && !(m.durations || []).length && m.duration_prices?.per_second == null" class="text-white/30 text-xs">—</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Capability — duration range, frame/ref modes, media support -->
|
||||
<!-- 配置 — uniform mono tags: duration, resolutions, refs, ratios -->
|
||||
<td class="px-3 py-3.5 align-middle">
|
||||
<div class="flex flex-wrap items-center gap-1 text-[11px]">
|
||||
<!-- Duration range (4s–15s) -->
|
||||
<span v-if="m.type === 'video' && (m.durations || []).length >= 2" class="cap-chip cap-dur">{{ m.durations[0] }}–{{ m.durations[m.durations.length - 1] }}</span>
|
||||
<span v-else-if="m.type === 'video' && (m.durations || []).length === 1" class="cap-chip cap-dur">{{ m.durations[0] }}</span>
|
||||
<!-- Resolutions for image -->
|
||||
<span v-if="m.type === 'image'" v-for="r in (m.resolutions || [])" :key="'ir'+r" class="cap-chip cap-slate">{{ r }}</span>
|
||||
<!-- Resolutions for video -->
|
||||
<span v-if="m.type === 'video'" v-for="r in (m.resolutions || [])" :key="'vr'+r" class="cap-chip cap-slate">{{ r }}</span>
|
||||
<!-- Frame mode (首尾帧 2) -->
|
||||
<span v-if="m.reference_mode === 'frame' && m.max_reference_images > 0" class="cap-chip cap-frame">首尾帧 {{ Math.min(m.max_reference_images, 2) }}</span>
|
||||
<!-- Reference image mode (参考图 9) -->
|
||||
<span v-if="m.reference_mode === 'frame' && m.max_reference_images > 2" class="cap-chip cap-ref">参考图 {{ refImages(m) }}</span>
|
||||
<span v-else-if="m.reference_mode && m.reference_mode !== 'none' && m.reference_mode !== 'frame' && m.max_reference_images > 0" class="cap-chip cap-ref">参考图 {{ refImages(m) }}</span>
|
||||
<span v-else-if="m.type === 'image' && m.image_to_image" class="cap-chip cap-ref">参考图</span>
|
||||
<!-- 视频/音频参考上限,来自 /video-presets -->
|
||||
<div class="flex flex-wrap items-center gap-1">
|
||||
<span v-if="m.type === 'video' && (m.durations || []).length >= 2" class="cfg-tag">{{ m.durations[0] }}–{{ m.durations[m.durations.length - 1] }}</span>
|
||||
<span v-else-if="m.type === 'video' && (m.durations || []).length === 1" class="cfg-tag">{{ m.durations[0] }}</span>
|
||||
<span v-if="m.type === 'image'" v-for="r in (m.resolutions || [])" :key="'ir'+r" class="cfg-tag">{{ r }}</span>
|
||||
<span v-if="m.reference_mode === 'frame' && m.max_reference_images > 0" class="cfg-tag">首尾帧{{ Math.min(m.max_reference_images, 2) }}</span>
|
||||
<span v-if="m.reference_mode === 'frame' && m.max_reference_images > 2" class="cfg-tag">参考图{{ refImages(m) }}</span>
|
||||
<span v-else-if="m.reference_mode && m.reference_mode !== 'none' && m.reference_mode !== 'frame' && m.max_reference_images > 0" class="cfg-tag">参考图{{ refImages(m) }}</span>
|
||||
<span v-else-if="m.type === 'image' && m.image_to_image" class="cfg-tag">参考图</span>
|
||||
<template v-if="caps(m)">
|
||||
<span v-if="caps(m).videos" class="cap-chip cap-media">视频 {{ caps(m).videos }}</span>
|
||||
<span v-if="caps(m).audios" class="cap-chip cap-media">音频 {{ caps(m).audios }}</span>
|
||||
<span v-if="caps(m).videos" class="cfg-tag">视频{{ caps(m).videos }}</span>
|
||||
<span v-if="caps(m).audios" class="cfg-tag">音频{{ caps(m).audios }}</span>
|
||||
</template>
|
||||
<!-- Ratios -->
|
||||
<span v-for="r in (m.ratios || [])" :key="'rt'+r" class="cap-chip cap-mono">{{ r.replace(':', '×') }}</span>
|
||||
<span v-for="r in (m.ratios || [])" :key="'rt'+r" class="cfg-tag">{{ r }}</span>
|
||||
<span v-if="capEmpty(m)" class="text-white/30 text-xs">—</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Display weight — higher floats to the top of the dropdown/list -->
|
||||
<!-- Successful generations to date -->
|
||||
<td class="px-3 py-3.5 align-middle text-right tabular-nums whitespace-nowrap"
|
||||
:class="(m.weight || 0) !== 0 ? 'text-white/85' : 'text-white/30'"
|
||||
title="展示权重(越大越靠前)">
|
||||
{{ m.weight || 0 }}
|
||||
</td>
|
||||
|
||||
<!-- Successful generations to date, from event_log via /managed-models -->
|
||||
<td class="px-3 py-3.5 align-middle text-right tabular-nums whitespace-nowrap"
|
||||
:class="m.generation_count > 0 ? 'text-white/85' : 'text-white/25'">
|
||||
:class="m.generation_count > 0 ? 'text-white/85 font-medium' : 'text-white/25'">
|
||||
{{ (m.generation_count || 0).toLocaleString('en-US') }}
|
||||
</td>
|
||||
|
||||
@@ -279,96 +239,39 @@ onMounted(loadModels)
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* --- filter pills (mirrors LogsView so the admin shell stays consistent) */
|
||||
.fp {
|
||||
display: inline-flex; align-items: center; gap: 0.35rem;
|
||||
padding: 0.35rem 0.7rem; font-size: 0.72rem;
|
||||
border-radius: 0.55rem;
|
||||
color: rgb(255 255 255 / 0.65);
|
||||
background: rgb(255 255 255 / 0.05);
|
||||
box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.06);
|
||||
transition: background 0.15s, color 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
.fp:hover { background: rgb(255 255 255 / 0.09); color: white; }
|
||||
.fp-on { background: rgb(255 255 255 / 0.92); 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-white {
|
||||
background: rgb(255 255 255 / 0.18);
|
||||
color: white;
|
||||
box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.35);
|
||||
/* --- type tag (vivid 同款:图像蓝底 / 视频墨底) */
|
||||
.kind-tag {
|
||||
display: inline-flex; padding: 3px 10px; border-radius: 999px;
|
||||
font-size: 12px; font-weight: 700; white-space: nowrap;
|
||||
}
|
||||
.kind-image { background: rgb(14 165 233 / 0.12); color: rgb(2 132 199); }
|
||||
.kind-video { background: rgb(24 24 27 / 0.1); color: rgb(24 24 27); }
|
||||
|
||||
/* --- price chips: tight pill showing a key (resolution or duration) plus
|
||||
its emerald-tinted price. */
|
||||
.price-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.18rem 0.55rem;
|
||||
font-size: 0.7rem;
|
||||
border-radius: 9999px;
|
||||
background: rgb(255 255 255 / 0.04);
|
||||
box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.08);
|
||||
white-space: nowrap;
|
||||
/* --- pricing chip: 档位 + 普通价(绿) + 代理价(琥珀) --- */
|
||||
.p-chip {
|
||||
display: inline-flex; align-items: baseline; gap: 6px;
|
||||
padding: 3px 9px; border-radius: 8px;
|
||||
background: var(--surface-2);
|
||||
box-shadow: inset 0 0 0 1px var(--hairline);
|
||||
font-size: 12px; white-space: nowrap;
|
||||
}
|
||||
.p-chip b { color: var(--fg); font-weight: 600; }
|
||||
.p-chip em { font-style: normal; font-weight: 700; color: rgb(5 150 105); font-variant-numeric: tabular-nums; }
|
||||
.p-chip em.ag { color: rgb(180 83 9); font-weight: 600; }
|
||||
.p-chip em.ag::before { content: '代理 '; font-size: 10px; font-weight: 500; opacity: 0.75; }
|
||||
|
||||
/* --- capability chips: smaller, monochrome variants */
|
||||
.cap-chip {
|
||||
/* --- config tags: uniform neutral mono chips (时长 / 分辨率 / 参考 / 比例) --- */
|
||||
.cfg-tag {
|
||||
display: inline-flex; align-items: center;
|
||||
padding: 0.15rem 0.5rem;
|
||||
font-size: 0.68rem; font-weight: 500;
|
||||
border-radius: 9999px;
|
||||
padding: 2px 8px; border-radius: 6px;
|
||||
background: var(--surface-2);
|
||||
box-shadow: inset 0 0 0 1px var(--hairline);
|
||||
font-size: 11px; font-weight: 600; color: var(--fg-2);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.cap-dur {
|
||||
background: rgb(14 165 233 / 0.12);
|
||||
color: rgb(147 197 253);
|
||||
box-shadow: inset 0 0 0 1px rgb(129 140 248 / 0.3);
|
||||
}
|
||||
.cap-frame {
|
||||
background: rgb(251 113 133 / 0.12);
|
||||
color: rgb(244 114 182);
|
||||
box-shadow: inset 0 0 0 1px rgb(244 114 182 / 0.35);
|
||||
}
|
||||
.cap-ref {
|
||||
background: rgb(16 185 129 / 0.12);
|
||||
color: rgb(110 231 183);
|
||||
box-shadow: inset 0 0 0 1px rgb(52 211 153 / 0.3);
|
||||
}
|
||||
.cap-media {
|
||||
background: rgb(245 158 11 / 0.12);
|
||||
color: rgb(252 211 77);
|
||||
box-shadow: inset 0 0 0 1px rgb(252 211 77 / 0.35);
|
||||
}
|
||||
.cap-slate {
|
||||
background: rgb(255 255 255 / 0.05);
|
||||
color: rgb(255 255 255 / 0.7);
|
||||
box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.08);
|
||||
}
|
||||
.cap-mono {
|
||||
background: transparent;
|
||||
color: rgb(255 255 255 / 0.5);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.08);
|
||||
}
|
||||
|
||||
/* --- danger variant for 删除 (kept for any other .btn-soft callers) */
|
||||
.btn-soft.danger {
|
||||
color: rgb(253 164 175);
|
||||
background: rgb(244 63 94 / 0.12);
|
||||
box-shadow: inset 0 0 0 1px rgb(244 63 94 / 0.3);
|
||||
}
|
||||
.btn-soft.danger:hover {
|
||||
color: white;
|
||||
background: rgb(244 63 94 / 0.25);
|
||||
}
|
||||
|
||||
/* --- compact square icon button for the actions column. Predictable
|
||||
width (1.9rem × 3 + gaps) keeps the row inside its 9rem slot, so
|
||||
the last button never gets clipped. */
|
||||
/* --- compact square icon button for the actions column --- */
|
||||
.act {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -376,19 +279,19 @@ onMounted(loadModels)
|
||||
width: 1.9rem;
|
||||
height: 1.9rem;
|
||||
border-radius: 0.5rem;
|
||||
color: rgb(255 255 255 / 0.7);
|
||||
background: rgb(255 255 255 / 0.04);
|
||||
box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.08);
|
||||
color: var(--fg-2);
|
||||
background: var(--surface-2);
|
||||
box-shadow: inset 0 0 0 1px var(--hairline);
|
||||
transition: background 0.15s, color 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
.act:hover { background: rgb(255 255 255 / 0.1); color: white; }
|
||||
.act:hover { background: var(--hover); color: var(--fg); }
|
||||
.act.danger {
|
||||
color: rgb(253 164 175);
|
||||
background: rgb(244 63 94 / 0.12);
|
||||
box-shadow: inset 0 0 0 1px rgb(244 63 94 / 0.3);
|
||||
color: rgb(225 29 72);
|
||||
background: rgb(244 63 94 / 0.08);
|
||||
box-shadow: inset 0 0 0 1px rgb(244 63 94 / 0.22);
|
||||
}
|
||||
.act.danger:hover {
|
||||
color: white;
|
||||
background: rgb(244 63 94 / 0.25);
|
||||
color: #fff;
|
||||
background: rgb(225 29 72);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -378,10 +378,10 @@ onUnmounted(() => clearInterval(timer))
|
||||
.ov-title { font-size: 20px; font-weight: 800; letter-spacing: -0.03em; color: var(--fg); }
|
||||
.ov-title--sm { font-size: 16px; }
|
||||
.ov-sub { margin-top: 2px; font-size: 12px; color: var(--fg-3); }
|
||||
.ov-btn { display: inline-flex; align-items: center; gap: 6px; height: 34px; padding: 0 14px; border-radius: 999px; border: 1px solid rgb(24 24 27 / 0.12); background: #fff; font-size: 13px; font-weight: 600; color: var(--fg); cursor: pointer; transition: border-color .15s, box-shadow .15s; }
|
||||
.ov-btn { display: inline-flex; align-items: center; gap: 6px; height: 34px; padding: 0 14px; border-radius: 999px; border: 1px solid rgb(24 24 27 / 0.12); background: var(--surface-solid); font-size: 13px; font-weight: 600; color: var(--fg); cursor: pointer; transition: border-color .15s, box-shadow .15s; }
|
||||
.ov-btn:hover { border-color: rgb(24 24 27 / 0.3); box-shadow: 0 4px 12px rgb(24 24 27 / 0.08); }
|
||||
|
||||
.panel { background: #fff; border: 1px solid rgb(24 24 27 / 0.07); border-radius: 18px; box-shadow: 0 1px 2px rgb(24 24 27 / 0.03), 0 10px 28px -16px rgb(24 24 27 / 0.12); }
|
||||
.panel { background: var(--surface-solid); border: 1px solid rgb(24 24 27 / 0.07); border-radius: 18px; box-shadow: 0 1px 2px rgb(24 24 27 / 0.03), 0 10px 28px -16px rgb(24 24 27 / 0.12); }
|
||||
.panel-h { display: flex; align-items: center; justify-content: space-between; padding: 16px 20px 0; }
|
||||
.panel-h h3 { font-size: 14px; font-weight: 800; color: var(--fg); letter-spacing: -0.01em; }
|
||||
.more { font-size: 12px; font-weight: 600; color: var(--fg-3); text-decoration: none; }
|
||||
@@ -405,7 +405,7 @@ onUnmounted(() => clearInterval(timer))
|
||||
.delta--up { color: #15803d; } .delta--down { color: #dc2626; } .delta--flat { color: var(--fg-3); }
|
||||
|
||||
.strip { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
|
||||
.chip { display: flex; align-items: center; gap: 12px; padding: 14px 16px; background: #fff; border: 1px solid rgb(24 24 27 / 0.07); border-radius: 16px; }
|
||||
.chip { display: flex; align-items: center; gap: 12px; padding: 14px 16px; background: var(--surface-solid); border: 1px solid rgb(24 24 27 / 0.07); border-radius: 16px; }
|
||||
.chip-ic { width: 38px; height: 38px; border-radius: 12px; display: grid; place-items: center; background: #f4f4f5; color: #18181b; flex-shrink: 0; }
|
||||
.chip-num { font-size: 20px; font-weight: 800; letter-spacing: -0.03em; color: var(--fg); font-variant-numeric: tabular-nums; line-height: 1.1; }
|
||||
.chip-num small { font-size: 13px; font-weight: 600; color: var(--fg-3); }
|
||||
@@ -447,7 +447,7 @@ onUnmounted(() => clearInterval(timer))
|
||||
|
||||
.seg { display: inline-flex; padding: 3px; border-radius: 999px; background: #f0f0f2; }
|
||||
.seg button { padding: 5px 14px; border: 0; border-radius: 999px; background: transparent; font-size: 12px; font-weight: 700; color: var(--fg-3); cursor: pointer; transition: all .15s; }
|
||||
.seg button.on { background: #fff; color: var(--fg); box-shadow: 0 1px 3px rgb(24 24 27 / 0.12); }
|
||||
.seg button.on { background: var(--surface-solid); color: var(--fg); box-shadow: 0 1px 3px rgb(24 24 27 / 0.12); }
|
||||
|
||||
.bar-top { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin-bottom: 6px; font-size: 13px; color: var(--fg); }
|
||||
.bar-top > span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
+173
-216
@@ -218,253 +218,185 @@ async function recharge() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="theme-text space-y-12">
|
||||
<!-- header -->
|
||||
<header class="flex items-end justify-between flex-wrap gap-4">
|
||||
<div class="space-y-5">
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<div class="text-[10px] uppercase tracking-[0.3em] text-orange-300/70 font-medium">账户</div>
|
||||
<h1 class="mt-2 text-4xl md:text-5xl font-bold tracking-tight">设置</h1>
|
||||
<p class="text-white/45 mt-2">API Key、密码、积分、登录状态 — 都在这里。</p>
|
||||
<h2 class="page-title">设置</h2>
|
||||
<p class="page-sub">API Key、密码、积分、签到 — 账户全在这里。</p>
|
||||
</div>
|
||||
<button @click="logout"
|
||||
class="inline-flex items-center gap-2 rounded-full bg-rose-500/15 text-rose-300 hover:bg-rose-500/25 hover:text-rose-200 ring-1 ring-rose-500/30 px-4 py-2 text-sm font-medium transition-all">
|
||||
<Icon name="close" class="w-3.5 h-3.5" /> 退出登录
|
||||
</button>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<!-- ===== Account info ===== -->
|
||||
<!-- ===== 账户信息 ===== -->
|
||||
<section class="card p-6">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<Icon name="accounts" class="w-4 h-4 text-orange-300" />
|
||||
<h2 class="text-sm font-semibold">账户信息</h2>
|
||||
<div class="sec-head">
|
||||
<span class="sec-ico"><Icon name="accounts" :size="16" /></span>
|
||||
<h3>账户信息</h3>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 lg:grid-cols-5 gap-4 text-sm">
|
||||
<div>
|
||||
<div class="text-[11px] text-white/40 uppercase tracking-wider mb-1">用户名</div>
|
||||
<div class="text-white/90 font-medium truncate" :title="auth.user?.name || ''">{{ auth.user?.name || '—' }}</div>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 gap-x-6 gap-y-5">
|
||||
<div class="st-item">
|
||||
<div class="st-label">用户名</div>
|
||||
<div class="st-value truncate" :title="auth.user?.name || ''">{{ auth.user?.name || '—' }}</div>
|
||||
</div>
|
||||
<div class="col-span-2 lg:col-span-1">
|
||||
<div class="text-[11px] text-white/40 uppercase tracking-wider mb-1">绑定邮箱</div>
|
||||
<div class="text-white/90 font-mono text-xs break-all" :title="auth.user?.email || ''">{{ auth.user?.email || '—' }}</div>
|
||||
<div class="st-item">
|
||||
<div class="st-label">绑定邮箱</div>
|
||||
<div class="st-value break-all" :title="auth.user?.email || ''">{{ auth.user?.email || '—' }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-[11px] text-white/40 uppercase tracking-wider mb-1">角色</div>
|
||||
<div class="text-white/90">{{ roleLabel }}</div>
|
||||
<div class="st-item">
|
||||
<div class="st-label">角色</div>
|
||||
<div class="st-value">{{ roleLabel }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-[11px] text-white/40 uppercase tracking-wider mb-1">积分余额</div>
|
||||
<div class="text-amber-300 font-semibold tabular-nums">{{ pointsLabel(balance) }}</div>
|
||||
<div class="st-item">
|
||||
<div class="st-label">积分余额</div>
|
||||
<div class="st-value st-num text-amber-600">{{ pointsLabel(balance) }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-[11px] text-white/40 uppercase tracking-wider mb-1">累计充值</div>
|
||||
<div class="text-emerald-300 font-semibold tabular-nums">¥{{ rechargeTotal }}</div>
|
||||
<div class="st-item">
|
||||
<div class="st-label">累计充值</div>
|
||||
<div class="st-value st-num text-emerald-600">¥{{ rechargeTotal }}</div>
|
||||
</div>
|
||||
<div class="st-item">
|
||||
<div class="st-label">并发上限</div>
|
||||
<div class="st-value truncate" :title="concurrencyLabel">{{ concurrencyLabel }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ===== API Key ===== -->
|
||||
<section class="card p-6">
|
||||
<div class="grid md:grid-cols-[220px_1fr] gap-6 items-start">
|
||||
<div>
|
||||
<div class="text-[11px] text-white/40 uppercase tracking-wider mb-1">并发上限</div>
|
||||
<div class="text-white/90 truncate" :title="concurrencyLabel">{{ concurrencyLabel }}</div>
|
||||
<div class="sec-head">
|
||||
<span class="sec-ico"><Icon name="plug" :size="16" /></span>
|
||||
<h3>API Key</h3>
|
||||
</div>
|
||||
<p class="text-[13px] text-[color:var(--fg-3)] leading-relaxed mt-1">
|
||||
调用 <code class="key-code">/v1/*</code> 接口的访问密钥。完整密钥不保存,仅生成时显示一次。
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<div v-if="hasKey" class="key-box">
|
||||
<code class="flex-1 font-mono text-[13px] text-[color:var(--fg)] break-all">{{ apiKeyRevealed && apiKey ? apiKey : maskedKey }}</code>
|
||||
<button v-if="apiKey" @click="apiKeyRevealed = !apiKeyRevealed" class="btn-soft !h-8">{{ apiKeyRevealed ? '隐藏' : '显示' }}</button>
|
||||
<button @click="copyKey" :title="apiKey ? '复制完整 Key' : '完整 Key 仅生成时显示一次,已无法找回'"
|
||||
class="btn-soft !h-8" :class="!apiKey && 'opacity-40'">复制</button>
|
||||
</div>
|
||||
<div v-else class="rounded-xl border border-dashed border-[color:var(--hairline)] px-4 py-5 text-center text-xs text-[color:var(--fg-faint)]">
|
||||
还没有 Key — 点下面的「生成」按钮自动生成
|
||||
</div>
|
||||
<p v-if="apiKey" class="text-[11px] text-amber-600 mt-2">⚠ 完整 Key 仅显示这一次,请立刻复制保存。</p>
|
||||
<div class="mt-3 flex gap-2">
|
||||
<button @click="generateKey" class="btn-primary">{{ hasKey ? '重新生成' : '生成 Key' }}</button>
|
||||
<button v-if="hasKey" @click="clearKey" class="btn-danger-soft">清除</button>
|
||||
</div>
|
||||
<p class="text-[11px] text-[color:var(--fg-faint)] mt-3">
|
||||
Key 由系统随机生成,不能手动填写。重新生成会让旧 Key 立刻失效。完整调用示例见
|
||||
<router-link to="/docs" class="underline underline-offset-2">接口文档</router-link>。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ===== Two-column grid ===== -->
|
||||
<div class="grid lg:grid-cols-2 gap-5">
|
||||
|
||||
<!-- API KEY (full width) -->
|
||||
<section class="lg:col-span-2 card overflow-hidden">
|
||||
<div class="p-7 md:p-8 relative">
|
||||
<div class="absolute -top-16 -right-16 w-48 h-48 rounded-full opacity-30 blur-3xl"
|
||||
style="background: radial-gradient(circle,#27272a,transparent 60%)"></div>
|
||||
<div class="relative grid md:grid-cols-[260px_1fr] gap-8 items-start">
|
||||
<div>
|
||||
<div class="inline-grid w-10 h-10 rounded-xl bg-orange-500/15 ring-1 ring-orange-400/30 grid place-items-center text-orange-300">
|
||||
<Icon name="plug" class="w-4 h-4" />
|
||||
</div>
|
||||
<h2 class="text-xl font-bold mt-4">API Key</h2>
|
||||
<p class="text-sm text-white/50 mt-2 leading-relaxed">
|
||||
调用 <code class="bg-white/10 text-white/90 px-1 py-0.5 rounded text-xs">/v1/*</code> 接口需要的访问密钥。完整密钥不保存,仅生成时显示一次。
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-white/50 mb-2">当前密钥</label>
|
||||
|
||||
<!-- has a key -->
|
||||
<div v-if="hasKey" class="rounded-xl bg-white/[0.05] ring-1 ring-white/10 px-4 py-3 flex items-center gap-3">
|
||||
<code class="flex-1 font-mono text-sm text-white/90 break-all">{{ apiKeyRevealed && apiKey ? apiKey : maskedKey }}</code>
|
||||
<button v-if="apiKey" @click="apiKeyRevealed = !apiKeyRevealed"
|
||||
class="text-xs rounded-lg px-2.5 py-1.5 ring-1 ring-white/10 hover:bg-white/[0.06] hover:ring-white/20 transition-all whitespace-nowrap">
|
||||
{{ apiKeyRevealed ? '隐藏' : '显示' }}
|
||||
</button>
|
||||
<button @click="copyKey"
|
||||
:title="apiKey ? '复制完整 Key' : '完整 Key 仅生成时显示一次,已无法找回'"
|
||||
class="text-xs rounded-lg px-2.5 py-1.5 ring-1 ring-white/10 transition-all hover:bg-white/[0.06] hover:ring-white/20"
|
||||
:class="!apiKey && 'opacity-40'">
|
||||
复制
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- no key -->
|
||||
<div v-else class="rounded-xl border border-dashed border-white/15 px-4 py-5 text-center text-xs text-white/40">
|
||||
还没有 Key — 点下面的「生成」按钮自动生成
|
||||
</div>
|
||||
|
||||
<p v-if="apiKey" class="text-[11px] text-amber-300/80 mt-2">⚠ 完整 Key 仅显示这一次,请立刻复制保存。</p>
|
||||
|
||||
<!-- actions -->
|
||||
<div class="mt-3 flex gap-2">
|
||||
<button @click="generateKey"
|
||||
class="rounded-xl bg-white text-black hover:bg-white/90 px-5 py-2.5 text-sm font-semibold transition-colors">
|
||||
{{ hasKey ? '重新生成' : '生成 Key' }}
|
||||
</button>
|
||||
<button v-if="hasKey" @click="clearKey"
|
||||
class="rounded-xl ring-1 ring-rose-500/30 text-rose-300 hover:bg-rose-500/15 px-4 py-2.5 text-sm transition-colors">
|
||||
清除
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="text-[11px] text-white/40 mt-3">
|
||||
Key 由系统随机生成,不能手动填写。重新生成会让旧 Key 立刻失效。完整调用示例见
|
||||
<router-link to="/docs" class="text-orange-300 underline">接口文档</router-link>。
|
||||
</p>
|
||||
</div>
|
||||
<!-- ===== 每日签到 ===== -->
|
||||
<section v-if="checkinEnabled" class="card p-6">
|
||||
<div class="sec-head">
|
||||
<span class="sec-ico"><Icon name="refresh" :size="16" /></span>
|
||||
<h3>每日签到</h3>
|
||||
</div>
|
||||
<p class="text-[13px] text-[color:var(--fg-3)] mt-1">每天签到 +{{ checkinReward }} 积分。</p>
|
||||
<div class="mt-4 flex items-baseline gap-2">
|
||||
<span class="text-3xl font-extrabold tabular-nums tracking-tight">{{ streak }}</span>
|
||||
<span class="text-xs text-[color:var(--fg-3)]">天连续</span>
|
||||
</div>
|
||||
<div class="mt-4 flex items-center gap-1.5">
|
||||
<div v-for="(d, i) in last7" :key="d.ds" class="ck-day"
|
||||
:class="d.lit || (d.isToday && checkedToday) ? 'ck-lit' : d.isToday ? 'ck-today' : ''">
|
||||
<Icon v-if="d.lit || (d.isToday && checkedToday)" name="spark" :size="12" />
|
||||
<span v-else class="text-[10px]">{{ d.isToday ? '今' : i + 1 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CHECK-IN -->
|
||||
<section v-if="checkinEnabled" class="relative card p-7 md:p-8 overflow-hidden">
|
||||
<div class="inline-grid w-10 h-10 rounded-xl bg-sky-500/15 ring-1 ring-sky-400/30 grid place-items-center text-sky-300">
|
||||
<Icon name="refresh" class="w-4 h-4" />
|
||||
</div>
|
||||
<h2 class="text-xl font-bold mt-4">每日签到</h2>
|
||||
<p class="text-sm text-white/50 mt-2">每天签到 +{{ checkinReward }} 积分。</p>
|
||||
|
||||
<!-- streak -->
|
||||
<div class="mt-5 flex items-baseline gap-2">
|
||||
<span class="text-3xl font-bold tabular-nums">{{ streak }}</span>
|
||||
<span class="text-xs text-white/40 uppercase tracking-widest">天连续</span>
|
||||
</div>
|
||||
|
||||
<!-- 7-day dots -->
|
||||
<div class="mt-5 flex items-center gap-1.5">
|
||||
<div v-for="(d, i) in last7" :key="d.ds"
|
||||
class="flex-1 h-12 rounded-xl ring-1 transition-all flex flex-col items-center justify-center"
|
||||
:class="d.lit
|
||||
? 'bg-sky-500 ring-sky-400 text-white'
|
||||
: d.isToday
|
||||
? (checkedToday ? 'bg-sky-500 ring-sky-400 text-white' : 'bg-white/[0.05] ring-white/15 text-white/60')
|
||||
: 'bg-white/[0.02] ring-white/[0.06] text-white/30'">
|
||||
<Icon v-if="d.lit || (d.isToday && checkedToday)" name="spark" class="w-3 h-3" />
|
||||
<span v-else class="text-[10px] uppercase">{{ d.isToday ? '今' : i + 1 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button @click="checkin" :disabled="checkedToday"
|
||||
class="mt-5 w-full rounded-xl bg-white text-black hover:bg-white/90 disabled:bg-white/10 disabled:text-white/40 disabled:cursor-not-allowed py-3 text-sm font-semibold transition-colors">
|
||||
{{ checkedToday ? `今日已签到 · 明天再来` : `立即签到 +${checkinReward} 积分` }}
|
||||
<button @click="checkin" :disabled="checkedToday" class="btn-primary w-full mt-5 !py-2.5">
|
||||
{{ checkedToday ? '今日已签到 · 明天再来' : `立即签到 +${checkinReward} 积分` }}
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<!-- RECHARGE (易支付) — hidden unless the admin enabled 充值 -->
|
||||
<section v-if="payCfg.enabled" class="relative card p-7 md:p-8 overflow-hidden">
|
||||
<div class="inline-grid w-10 h-10 rounded-xl bg-emerald-500/15 ring-1 ring-emerald-400/30 grid place-items-center text-emerald-300">
|
||||
<Icon name="spark" class="w-4 h-4" />
|
||||
<!-- ===== 积分充值 ===== -->
|
||||
<section v-if="payCfg.enabled" class="card p-6">
|
||||
<div class="sec-head">
|
||||
<span class="sec-ico"><Icon name="spark" :size="16" /></span>
|
||||
<h3>积分充值</h3>
|
||||
</div>
|
||||
<h2 class="text-xl font-bold mt-4">积分充值</h2>
|
||||
<p class="text-sm text-white/50 mt-2">累计充值 <strong class="text-emerald-300">¥{{ rechargeTotal }}</strong> · {{ payCfg.points_ratio }} 积分 / 元</p>
|
||||
|
||||
<div class="grid grid-cols-3 sm:grid-cols-5 gap-2 mt-5">
|
||||
<p class="text-[13px] text-[color:var(--fg-3)] mt-1">累计充值 <strong class="text-emerald-600">¥{{ rechargeTotal }}</strong> · {{ payCfg.points_ratio }} 积分 / 元</p>
|
||||
<div class="grid grid-cols-3 sm:grid-cols-5 gap-2 mt-4">
|
||||
<button v-for="a in AMOUNTS" :key="a" @click="picked = a" class="amt" :class="picked === a && 'amt-on'">{{ a }}元</button>
|
||||
<button @click="picked = 'custom'" class="amt" :class="picked === 'custom' && 'amt-on'">自定义</button>
|
||||
</div>
|
||||
<input v-if="picked === 'custom'" v-model="customAmount" type="number" min="1" step="1" placeholder="输入金额(元)"
|
||||
class="amt-input mt-3 w-full px-4 py-2.5 text-sm" />
|
||||
|
||||
<div class="flex gap-2 mt-4">
|
||||
<input v-if="picked === 'custom'" v-model="customAmount" type="number" min="1" step="1" placeholder="输入金额(元)"
|
||||
class="field mt-3" />
|
||||
<div class="flex gap-2 mt-3">
|
||||
<button v-for="m in payCfg.methods" :key="m" @click="payMethod = m" class="amt flex-1" :class="payMethod === m && 'amt-on'">{{ methodName(m) }}</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 flex items-center justify-between gap-3">
|
||||
<span class="text-sm text-white/60">到账 <strong class="text-orange-300 text-base tabular-nums">{{ pointsPreview }}</strong> 积分</span>
|
||||
<button @click="recharge" :disabled="recharging"
|
||||
class="rounded-xl bg-white text-black hover:bg-white/90 disabled:opacity-60 disabled:cursor-not-allowed px-6 py-2.5 text-sm font-semibold transition-colors inline-flex items-center gap-2">
|
||||
<span v-if="recharging" class="w-3.5 h-3.5 rounded-full border-2 border-black/30 border-t-black animate-spin"></span>
|
||||
<span class="text-sm text-[color:var(--fg-2)]">到账 <strong class="text-amber-600 text-base tabular-nums">{{ pointsPreview }}</strong> 积分</span>
|
||||
<button @click="recharge" :disabled="recharging" class="btn-primary">
|
||||
<span v-if="recharging" class="w-3.5 h-3.5 rounded-full border-2 border-white/30 border-t-white animate-spin"></span>
|
||||
{{ recharging ? '下单中…' : '立即充值' }}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CDK REDEEM — hidden when the admin turns the 兑换码 switch off -->
|
||||
<section v-if="site.cdkRedeemEnabled" class="relative card p-7 md:p-8 overflow-hidden">
|
||||
<div class="inline-grid w-10 h-10 rounded-xl bg-emerald-500/15 ring-1 ring-emerald-400/30 grid place-items-center text-emerald-300">
|
||||
<Icon name="spark" class="w-4 h-4" />
|
||||
<!-- ===== 兑换码 ===== -->
|
||||
<section v-if="site.cdkRedeemEnabled" class="card p-6">
|
||||
<div class="sec-head">
|
||||
<span class="sec-ico"><Icon name="spark" :size="16" /></span>
|
||||
<h3>兑换码充值</h3>
|
||||
</div>
|
||||
<h2 class="text-xl font-bold mt-4">兑换码充值</h2>
|
||||
<div class="mt-2 flex items-baseline gap-2">
|
||||
<span class="text-3xl font-bold tabular-nums">{{ points(balance).toLocaleString('en-US') }}</span>
|
||||
<span class="text-xs text-white/40 uppercase tracking-widest">积分余额</span>
|
||||
<div class="mt-3 flex items-baseline gap-2">
|
||||
<span class="text-3xl font-extrabold tabular-nums tracking-tight">{{ points(balance).toLocaleString('en-US') }}</span>
|
||||
<span class="text-xs text-[color:var(--fg-3)]">积分余额</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 flex gap-2">
|
||||
<input v-model="cdkCode" @keyup.enter="redeemCdk" placeholder="输入兑换码 (CDK)"
|
||||
class="flex-1 rounded-xl bg-white/[0.05] ring-1 ring-white/10 focus:ring-white/30 px-4 py-3 text-sm font-mono uppercase tracking-wider outline-none transition-colors placeholder:text-white/30 placeholder:normal-case placeholder:tracking-normal" />
|
||||
<button @click="redeemCdk" :disabled="cdkBusy"
|
||||
class="rounded-xl bg-white text-black hover:bg-white/90 disabled:opacity-40 px-5 py-3 text-sm font-semibold transition-colors">
|
||||
{{ cdkBusy ? '兑换中…' : '兑换' }}
|
||||
</button>
|
||||
<div class="mt-4 flex gap-2">
|
||||
<input v-model="cdkCode" @keyup.enter="redeemCdk" placeholder="输入兑换码(CDK)"
|
||||
class="field flex-1 font-mono uppercase tracking-wider" />
|
||||
<button @click="redeemCdk" :disabled="cdkBusy" class="btn-primary shrink-0">{{ cdkBusy ? '兑换中…' : '兑换' }}</button>
|
||||
</div>
|
||||
<p v-if="cdkError" class="text-xs text-rose-400 mt-2">{{ cdkError }}</p>
|
||||
<p class="text-[11px] text-white/35 mt-3">兑换码每个仅可使用一次。还没有兑换码?到商店购买后回此处充值。</p>
|
||||
<a v-if="site.contact?.shop" :href="site.contact.shop" target="_blank" rel="noopener"
|
||||
class="mt-3 group inline-flex items-center gap-2 rounded-xl bg-emerald-500/15 ring-1 ring-emerald-400/30 text-emerald-300 hover:bg-emerald-500/25 px-4 py-2.5 text-sm font-semibold transition-colors">
|
||||
<Icon name="spark" class="w-4 h-4" />
|
||||
前往商店购买兑换码
|
||||
<span class="group-hover:translate-x-0.5 transition-transform">→</span>
|
||||
<p v-if="cdkError" class="text-xs text-rose-600 mt-2">{{ cdkError }}</p>
|
||||
<p class="text-[11px] text-[color:var(--fg-faint)] mt-3">兑换码每个仅可使用一次。还没有兑换码?到商店购买后回此处充值。</p>
|
||||
<a v-if="site.contact?.shop" :href="site.contact.shop" target="_blank" rel="noopener" class="btn-soft mt-3">
|
||||
前往商店购买兑换码 <span aria-hidden="true">→</span>
|
||||
</a>
|
||||
</section>
|
||||
|
||||
<!-- PASSWORD -->
|
||||
<section class="card p-7 md:p-8">
|
||||
<div class="inline-grid w-10 h-10 rounded-xl bg-amber-500/15 ring-1 ring-amber-400/30 grid place-items-center text-amber-300">
|
||||
<Icon name="refresh" class="w-4 h-4" />
|
||||
<!-- ===== 修改密码 ===== -->
|
||||
<section class="card p-6">
|
||||
<div class="sec-head">
|
||||
<span class="sec-ico"><Icon name="config" :size="16" /></span>
|
||||
<h3>修改密码</h3>
|
||||
</div>
|
||||
<h2 class="text-xl font-bold mt-4">修改密码</h2>
|
||||
<p class="text-sm text-white/50 mt-2">8-24 位,必须包含大写字母、小写字母、数字和符号。</p>
|
||||
|
||||
<div class="mt-6 space-y-3">
|
||||
<input v-model="pwdForm.current" type="password" placeholder="当前密码" autocomplete="current-password"
|
||||
class="w-full rounded-xl bg-white/[0.05] ring-1 ring-white/10 focus:ring-white/30 px-4 py-3 text-sm outline-none transition-colors placeholder:text-white/30" />
|
||||
<input v-model="pwdForm.next" type="password" placeholder="新密码(8-24位,含大小写/数字/符号)" autocomplete="new-password"
|
||||
class="w-full rounded-xl bg-white/[0.05] ring-1 ring-white/10 focus:ring-white/30 px-4 py-3 text-sm outline-none transition-colors placeholder:text-white/30" />
|
||||
<input v-model="pwdForm.confirm" type="password" placeholder="再次输入新密码" autocomplete="new-password"
|
||||
class="w-full rounded-xl bg-white/[0.05] ring-1 ring-white/10 focus:ring-white/30 px-4 py-3 text-sm outline-none transition-colors placeholder:text-white/30" />
|
||||
<p v-if="pwdError" class="text-xs text-rose-400">{{ pwdError }}</p>
|
||||
<button @click="changePwd" :disabled="pwdSubmitting"
|
||||
class="w-full rounded-xl bg-white text-black hover:bg-white/90 disabled:opacity-40 py-3 text-sm font-semibold transition-colors">
|
||||
{{ pwdSubmitting ? '提交中…' : '更新密码' }}
|
||||
</button>
|
||||
<p class="text-[13px] text-[color:var(--fg-3)] mt-1">8-24 位,必须包含大写字母、小写字母、数字和符号。</p>
|
||||
<div class="mt-4 space-y-3">
|
||||
<input v-model="pwdForm.current" type="password" placeholder="当前密码" autocomplete="current-password" class="field" />
|
||||
<input v-model="pwdForm.next" type="password" placeholder="新密码(8-24 位,含大小写 / 数字 / 符号)" autocomplete="new-password" class="field" />
|
||||
<input v-model="pwdForm.confirm" type="password" placeholder="再次输入新密码" autocomplete="new-password" class="field" />
|
||||
<p v-if="pwdError" class="text-xs text-rose-600">{{ pwdError }}</p>
|
||||
<button @click="changePwd" :disabled="pwdSubmitting" class="btn-primary w-full !py-2.5">{{ pwdSubmitting ? '提交中…' : '更新密码' }}</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- SIGN OUT (full width) -->
|
||||
<section class="lg:col-span-2 card p-6 md:p-7 flex items-center gap-5 flex-wrap">
|
||||
<div class="inline-grid w-10 h-10 rounded-xl bg-rose-500/15 ring-1 ring-rose-400/30 grid place-items-center text-rose-300">
|
||||
<Icon name="close" class="w-4 h-4" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-base font-semibold">退出登录</div>
|
||||
<p class="text-sm text-white/50 mt-1">会清除本地 API Key、余额缓存。下次进来要重新设置。</p>
|
||||
</div>
|
||||
<button @click="logout"
|
||||
class="rounded-xl bg-rose-500/20 text-rose-200 hover:bg-rose-500/30 ring-1 ring-rose-500/30 px-5 py-2.5 text-sm font-semibold transition-colors">
|
||||
退出
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- ===== 退出登录 ===== -->
|
||||
<section class="card p-5 flex items-center gap-4 flex-wrap">
|
||||
<span class="sec-ico"><Icon name="close" :size="16" /></span>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-sm font-bold">退出登录</div>
|
||||
<p class="text-xs text-[color:var(--fg-3)] mt-0.5">会清除本地 API Key 与缓存,下次进入需要重新登录。</p>
|
||||
</div>
|
||||
<button @click="logout" class="btn-danger-soft">退出</button>
|
||||
</section>
|
||||
|
||||
<!-- toast -->
|
||||
<transition name="fade">
|
||||
<div v-if="toastMsg"
|
||||
class="fixed bottom-8 left-1/2 -translate-x-1/2 z-50 bg-white text-black text-sm font-medium px-5 py-2.5 rounded-full shadow-2xl">
|
||||
class="fixed bottom-8 left-1/2 -translate-x-1/2 z-50 bg-zinc-900 text-white text-sm font-medium px-5 py-2.5 rounded-full shadow-2xl">
|
||||
{{ toastMsg }}
|
||||
</div>
|
||||
</transition>
|
||||
@@ -475,8 +407,45 @@ async function recharge() {
|
||||
.fade-enter-active, .fade-leave-active { transition: opacity 0.15s ease, transform 0.15s ease; }
|
||||
.fade-enter-from, .fade-leave-to { opacity: 0; transform: translateY(8px); }
|
||||
|
||||
/* Recharge amount / method buttons — theme-aware (clean in light AND dark).
|
||||
Selected uses the inverted solid-button color, not a harsh orange. */
|
||||
.sec-head { display: flex; align-items: center; gap: 8px; }
|
||||
.sec-head h3 { font-size: 15px; font-weight: 800; letter-spacing: -0.01em; color: var(--fg); }
|
||||
.sec-ico {
|
||||
width: 34px; height: 34px; flex: none; border-radius: 10px;
|
||||
display: grid; place-items: center;
|
||||
background: var(--surface-2);
|
||||
box-shadow: inset 0 0 0 1px var(--hairline);
|
||||
color: var(--fg-2);
|
||||
}
|
||||
.st-label { font-size: 11px; font-weight: 700; letter-spacing: .05em; color: var(--fg-faint); margin-bottom: 4px; }
|
||||
.st-value { font-size: 14px; font-weight: 600; color: var(--fg); }
|
||||
.st-num { font-variant-numeric: tabular-nums; }
|
||||
.key-code { background: var(--surface-2); padding: 1px 6px; border-radius: 5px; font-size: 12px; }
|
||||
.key-box {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 8px 8px 8px 14px; border-radius: 12px;
|
||||
background: var(--surface-2);
|
||||
box-shadow: inset 0 0 0 1px var(--hairline);
|
||||
}
|
||||
.btn-danger-soft {
|
||||
display: inline-flex; align-items: center; justify-content: center; gap: 6px;
|
||||
height: 36px; padding: 0 16px; border-radius: 10px;
|
||||
font-size: 13px; font-weight: 700;
|
||||
color: rgb(190 18 60); background: rgb(244 63 94 / 0.08);
|
||||
box-shadow: inset 0 0 0 1px rgb(244 63 94 / 0.22);
|
||||
transition: background .15s ease, color .15s ease;
|
||||
}
|
||||
.btn-danger-soft:hover { color: #fff; background: rgb(225 29 72); }
|
||||
.ck-day {
|
||||
flex: 1; height: 46px; border-radius: 10px;
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
font-weight: 600;
|
||||
background: var(--surface-2); color: var(--fg-faint);
|
||||
box-shadow: inset 0 0 0 1px var(--hairline);
|
||||
}
|
||||
.ck-today { color: var(--fg-2); box-shadow: inset 0 0 0 1.5px rgb(24 24 27 / 0.4); }
|
||||
.ck-lit { background: #18181b; color: #fff; box-shadow: none; }
|
||||
|
||||
/* Recharge amount / method buttons */
|
||||
.amt {
|
||||
border-radius: 0.6rem;
|
||||
padding: 0.6rem 0;
|
||||
@@ -488,17 +457,5 @@ async function recharge() {
|
||||
transition: background 0.15s, color 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
.amt:hover { color: var(--fg); background: var(--hover); }
|
||||
.amt-on {
|
||||
background: var(--btn-solid-bg);
|
||||
color: var(--btn-solid-fg);
|
||||
box-shadow: none;
|
||||
}
|
||||
.amt-input {
|
||||
border-radius: 0.7rem;
|
||||
color: var(--fg);
|
||||
background: var(--surface-2);
|
||||
box-shadow: inset 0 0 0 1px var(--hairline);
|
||||
outline: none;
|
||||
}
|
||||
.amt-input:focus { box-shadow: inset 0 0 0 1px var(--fg-3); }
|
||||
</style>
|
||||
.amt-on { background: var(--btn-solid-bg); color: var(--btn-solid-fg); box-shadow: none; }
|
||||
</style>
|
||||
@@ -560,7 +560,7 @@ async function doRecharge() {
|
||||
display: block;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 500;
|
||||
color: rgb(255 255 255 / 0.55);
|
||||
color: var(--fg-3);
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
@@ -569,13 +569,13 @@ async function doRecharge() {
|
||||
display: inline-flex; align-items: center; gap: 0.35rem;
|
||||
padding: 0.35rem 0.7rem; font-size: 0.72rem;
|
||||
border-radius: 0.55rem;
|
||||
color: rgb(255 255 255 / 0.65);
|
||||
background: rgb(255 255 255 / 0.05);
|
||||
box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.06);
|
||||
color: var(--fg-2);
|
||||
background: var(--surface-2);
|
||||
box-shadow: inset 0 0 0 1px var(--hairline);
|
||||
transition: background 0.15s, color 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
.fp:hover { background: rgb(255 255 255 / 0.09); color: white; }
|
||||
.fp-on { background: rgb(255 255 255 / 0.92); color: rgb(15 23 42); box-shadow: none; }
|
||||
.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);
|
||||
@@ -602,12 +602,12 @@ async function doRecharge() {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
width: 1.9rem; height: 1.9rem;
|
||||
border-radius: 0.5rem;
|
||||
color: rgb(255 255 255 / 0.7);
|
||||
background: rgb(255 255 255 / 0.04);
|
||||
box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.08);
|
||||
color: var(--fg-2);
|
||||
background: var(--surface-2);
|
||||
box-shadow: inset 0 0 0 1px var(--hairline);
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
.act:hover { background: rgb(255 255 255 / 0.1); color: white; }
|
||||
.act:hover { background: var(--hover); color: var(--fg); }
|
||||
.act.danger {
|
||||
color: rgb(253 164 175);
|
||||
background: rgb(244 63 94 / 0.12);
|
||||
@@ -620,7 +620,7 @@ async function doRecharge() {
|
||||
.field:disabled {
|
||||
opacity: 0.65;
|
||||
cursor: not-allowed;
|
||||
background: rgb(255 255 255 / 0.025);
|
||||
background: var(--surface-2);
|
||||
}
|
||||
|
||||
/* iOS-style switch for the 状态 column — mirrors the one in ModelsView. */
|
||||
@@ -628,8 +628,8 @@ async function doRecharge() {
|
||||
position: relative;
|
||||
width: 2.25rem; height: 1.3rem;
|
||||
border-radius: 9999px;
|
||||
background: rgb(255 255 255 / 0.12);
|
||||
box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.08);
|
||||
background: var(--hover);
|
||||
box-shadow: inset 0 0 0 1px var(--hairline);
|
||||
transition: background 0.18s ease;
|
||||
}
|
||||
.sw-thumb {
|
||||
@@ -637,7 +637,7 @@ async function doRecharge() {
|
||||
top: 2px; left: 2px;
|
||||
width: calc(1.3rem - 4px); height: calc(1.3rem - 4px);
|
||||
border-radius: 9999px;
|
||||
background: white;
|
||||
background: var(--surface-solid);
|
||||
box-shadow: 0 1px 2px rgb(15 23 42 / 0.3);
|
||||
transition: transform 0.18s ease;
|
||||
}
|
||||
@@ -655,13 +655,13 @@ async function doRecharge() {
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
border-radius: 0.45rem;
|
||||
color: rgb(255 255 255 / 0.7);
|
||||
background: rgb(255 255 255 / 0.04);
|
||||
box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.08);
|
||||
color: var(--fg-2);
|
||||
background: var(--surface-2);
|
||||
box-shadow: inset 0 0 0 1px var(--hairline);
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
.pg:hover:not(.pg-on) { background: rgb(255 255 255 / 0.1); color: white; }
|
||||
.pg-on { background: rgb(255 255 255 / 0.92); color: rgb(15 23 42); box-shadow: none; }
|
||||
.pg:hover:not(.pg-on) { background: var(--hover); color: var(--fg); }
|
||||
.pg-on { background: var(--surface-solid); color: rgb(15 23 42); box-shadow: none; }
|
||||
|
||||
.fade-enter-active, .fade-leave-active { transition: opacity 0.15s ease; }
|
||||
.fade-enter-from, .fade-leave-to { opacity: 0; }
|
||||
|
||||
Reference in New Issue
Block a user