Files
image2api/frontend/src/layouts/AdminLayout.vue
T
chiyiandClaude Opus 4.8 5cf6206ee9 feat: 并发分组系统 + 品牌/Logo 上传 + 兑换码开关 + 文档分辨率表
并发分组(新功能):
- 新表 concurrency_groups(名称/上限/默认),用户加 concurrency_group_id
- 启动自动建「默认并发」组(上限10、默认),老用户回填、新注册自动绑定
- 并发计数改用 Redis(自愈 sorted-set + TTL + fail-open):
  · 用户并发(画图台 + API key 合计)受其分组上限限制,0=不限制 → 超返回 429
  · 账号级并发也从内存 gate 换成同一套 Redis(6 处调用点)
  · 移除旧的「已有正在生成的任务」单任务锁
- 后台「并发分组」新菜单:增删改、设默认、用户数;默认组不可删(删别的组成员转默认)
- 用户管理:并发列 + 新建/编辑可选分组
- 个人设置页:账户信息卡(用户名/邮箱/角色/余额/并发);/me 暴露 concurrency_group/limit

品牌 / Logo(上传到 RustFS):
- Logo 改成拖拽/点击上传,点保存才上传;替换自动删旧;branding/ 设为公开且被清理任务 pin 住(永不删)
- 有自定义就用:前台左侧 nav + 后台侧栏 + favicon(浏览器标签);没有则默认 V 图标
- 前台页头还原成文字;首页 Hero 子标题用 site.subtitle(默认那句宣传语,设置页预填)
- 邮件验证码标题用站点名;新增 POST/DELETE /settings/logo + POST /settings/asset(首页底图上传)

兑换码开关:
- 系统设置→积分 新增「开启兑换码」(默认开);关闭后后端拒绝兑换、前台隐藏兑换入口(/site 暴露 cdk_redeem_enabled)

文档 / 分辨率:
- 去掉 quality 参数:size(宽x高)同时决定比例 + 分辨率档(长边映射 1K/2K/4K)
- 文档加「分辨率对照表」(14 个比例 × 1K/2K/4K → size 该传的值);guessRatio 与自定义模型 RATIO_OPTS 对齐到 14 个

其它:
- 删模型时同步清掉各上游账号「支持模型」里的该 id
- 首页设置/兑换码弹窗去固定高度滚动条;展示位弹窗浅色主题适配

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 13:52:31 +08:00

119 lines
6.2 KiB
Vue

<script setup>
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import Icon from '../components/Icon.vue'
import Logo from '../components/Logo.vue'
import { site } from '../site'
import { isDark, toggleTheme } from '../theme'
const route = useRoute()
const tabs = [
{ label: '概览', to: '/admin/overview', icon: 'overview' },
{ label: '模型管理', to: '/admin/models', icon: 'models' },
{ label: '账号管理', to: '/admin/accounts', icon: 'plug' },
{ label: '用户管理', to: '/admin/users', icon: 'accounts' },
{ label: '并发分组', to: '/admin/concurrency', icon: 'shield' },
{ label: '兑换码', to: '/admin/cdks', icon: 'spark' },
{ label: '邀请日志', to: '/admin/invites', icon: 'accounts' },
{ label: '图片管理', to: '/admin/images', icon: 'files' },
{ label: '首页内容', to: '/admin/showcase', icon: 'spark' },
{ label: '日志', to: '/admin/logs', icon: 'log' },
{ label: '配置', to: '/admin/config', icon: 'config' },
]
const currentLabel = computed(() => route.meta?.label || '')
</script>
<template>
<div class="theme-x h-screen flex bg-[var(--app-bg)] text-[color:var(--fg-2)] selection:bg-violet-400/30 overflow-hidden">
<!-- ===== Sidebar ===== -->
<aside class="w-60 shrink-0 border-r border-[color:var(--hairline)] bg-[var(--surface)] backdrop-blur-md flex flex-col">
<router-link to="/" class="h-16 flex items-center gap-2.5 px-5 border-b border-[color:var(--hairline)] group">
<img v-if="site.logo" :src="site.logo" :alt="site.title" class="w-8 h-8 rounded-[10px] object-contain shadow-lg shadow-violet-500/20 ring-1 ring-white/10" />
<Logo v-else :size="32" class="rounded-[10px] shadow-lg shadow-violet-500/20 ring-1 ring-white/10" />
<div class="leading-tight min-w-0">
<div class="text-sm font-semibold truncate tracking-tight text-[color:var(--fg)]">{{ site.title }}</div>
<div class="text-[11px] text-[color:var(--fg-3)] truncate">Admin</div>
</div>
</router-link>
<nav class="flex-1 px-3 py-4 space-y-1">
<router-link
v-for="t in tabs" :key="t.to" :to="t.to"
class="admin-link group"
active-class="active">
<span class="absolute left-0 top-1/2 -translate-y-1/2 w-[3px] h-5 rounded-r-full opacity-0 transition-opacity"
style="background: linear-gradient(180deg, #f0abfc, #a78bfa)"></span>
<Icon :name="t.icon" class="w-4 h-4 shrink-0 opacity-70 group-hover:opacity-100 transition-opacity" />
<span class="text-sm">{{ t.label }}</span>
</router-link>
</nav>
<div class="p-3 border-t border-[color:var(--hairline)] space-y-1">
<button type="button" @click="toggleTheme"
class="w-full flex items-center gap-2.5 rounded-lg px-3 py-2 text-xs text-[color:var(--fg-2)] hover:bg-[var(--hover)] hover:text-[color:var(--fg)] transition-colors">
<svg v-if="isDark" class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
<svg v-else class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>
{{ isDark ? '亮色模式' : '暗色模式' }}
</button>
<router-link to="/user"
class="flex items-center gap-2.5 rounded-lg px-3 py-2 text-xs text-[color:var(--fg-2)] hover:bg-[var(--hover)] hover:text-[color:var(--fg)] transition-colors">
<Icon name="spark" class="w-3.5 h-3.5" /> 用户端
<Icon name="open" class="w-3 h-3 ml-auto" />
</router-link>
</div>
</aside>
<!-- ===== Main ===== -->
<div class="flex-1 min-w-0 flex flex-col relative">
<!-- soft background mesh, mirrors the public shell -->
<div aria-hidden="true" class="pointer-events-none absolute inset-0 overflow-hidden">
<div class="absolute -top-32 left-1/3 w-[40rem] h-[40rem] rounded-full opacity-[0.12]"
style="background: radial-gradient(circle, #a855f7, transparent 60%); filter: blur(110px)"></div>
<div class="absolute top-1/2 -right-40 w-[36rem] h-[36rem] rounded-full opacity-[0.10]"
style="background: radial-gradient(circle, #06b6d4, transparent 60%); filter: blur(110px)"></div>
<div class="absolute bottom-0 left-0 w-[32rem] h-[32rem] rounded-full opacity-[0.08]"
style="background: radial-gradient(circle, #f43f5e, transparent 60%); filter: blur(110px)"></div>
</div>
<header class="relative z-10 h-14 shrink-0 border-b border-[color:var(--hairline)] bg-[var(--app-bg)]/70 backdrop-blur-md flex items-center px-8">
<div class="text-[10px] uppercase tracking-[0.25em] text-[color:var(--fg-3)] font-medium mr-3">Admin</div>
<div class="text-[color:var(--fg-faint)] mr-3">/</div>
<h1 class="text-sm font-semibold tracking-tight text-[color:var(--fg)]">{{ currentLabel }}</h1>
</header>
<main :class="['theme-text flex-1 overflow-y-auto overscroll-y-none relative z-10', { 'public-dark': isDark }]">
<div class="px-8 py-7">
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</div>
</main>
</div>
</div>
</template>
<style scoped>
.admin-link {
position: relative;
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.55rem 0.875rem;
border-radius: 0.625rem;
color: var(--fg-2);
font-weight: 500;
transition: background 0.15s ease, color 0.15s ease;
}
.admin-link:hover { background: var(--hover); color: var(--fg); }
.admin-link.active { color: var(--fg); background: var(--hover); }
.admin-link.active > span:first-child { opacity: 1; }
.fade-enter-active, .fade-leave-active { transition: opacity 0.15s ease, transform 0.15s ease; }
.fade-enter-from { opacity: 0; transform: translateY(4px); }
.fade-leave-to { opacity: 0; }
</style>