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>
This commit is contained in:
@@ -141,6 +141,28 @@ function pickImage(file) {
|
||||
picking.value = false
|
||||
}
|
||||
|
||||
// Upload a NEW image as 底图 — click/drag the preview. Stored public under
|
||||
// branding/ in RustFS; form.image is set to the returned path immediately.
|
||||
const scImgInput = ref(null)
|
||||
const scDragOver = ref(false)
|
||||
const uploadingImg = ref(false)
|
||||
function pickShowcaseImg() { scImgInput.value && scImgInput.value.click() }
|
||||
async function uploadShowcaseImg(f) {
|
||||
if (!f || !f.type || !f.type.startsWith('image/')) return
|
||||
if (f.size > 8 * 1024 * 1024) { error.value = '图片不能超过 8MB'; return }
|
||||
uploadingImg.value = true; error.value = ''
|
||||
const dataUrl = await new Promise((res, rej) => {
|
||||
const r = new FileReader(); r.onload = () => res(r.result); r.onerror = rej; r.readAsDataURL(f)
|
||||
}).catch(() => '')
|
||||
if (!dataUrl) { uploadingImg.value = false; error.value = '读取图片失败'; return }
|
||||
const r = await api('/settings/asset', jsonBody('POST', { data: dataUrl }))
|
||||
uploadingImg.value = false
|
||||
if (r.ok && r.data?.path) form.image = r.data.path
|
||||
else error.value = r.data?.detail || '上传失败'
|
||||
}
|
||||
function onScImgInput(ev) { uploadShowcaseImg((ev.target.files || [])[0]); if (ev.target) ev.target.value = '' }
|
||||
function onScDrop(ev) { ev.preventDefault(); scDragOver.value = false; uploadShowcaseImg((ev.dataTransfer?.files || [])[0]) }
|
||||
|
||||
function bgFor(image) {
|
||||
if (!image) return {}
|
||||
const src = /^https?:\/\//i.test(image) ? image : generatedUrl(image)
|
||||
@@ -240,19 +262,30 @@ onMounted(refresh)
|
||||
</div>
|
||||
|
||||
<div class="p-5 space-y-4">
|
||||
<!-- live preview -->
|
||||
<div class="relative rounded-2xl overflow-hidden ring-1 ring-white/10 aspect-[5/2] bg-white/[0.04]"
|
||||
:style="bgFor(form.image)">
|
||||
<!-- live preview — click or drag an image here to upload as 底图 -->
|
||||
<div class="relative rounded-2xl overflow-hidden ring-1 ring-white/10 aspect-[5/2] bg-white/[0.04] cursor-pointer transition-all"
|
||||
:class="scDragOver ? 'ring-2 ring-indigo-400' : ''"
|
||||
:style="bgFor(form.image)"
|
||||
@click="pickShowcaseImg" @drop="onScDrop" @dragover.prevent="scDragOver = true" @dragleave="scDragOver = false">
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/85 via-black/30 to-transparent"></div>
|
||||
<div v-if="!form.image" class="absolute inset-0 grid place-items-center text-xs text-white/40">
|
||||
未选择底图
|
||||
<button type="button" @click.stop="openPicker"
|
||||
class="absolute top-2 right-2 z-10 inline-flex items-center gap-1 rounded-lg bg-black/55 ring-1 ring-white/15 hover:bg-black/75 text-white text-[11px] px-2.5 py-1.5 transition-colors">
|
||||
<Icon name="files" class="w-3.5 h-3.5" /> 选择已生成
|
||||
</button>
|
||||
<div v-if="!form.image" class="absolute inset-0 grid place-items-center text-xs text-white/60">
|
||||
<div class="text-center">
|
||||
<Icon name="plus" class="w-6 h-6 mx-auto mb-1 opacity-80" />
|
||||
{{ uploadingImg ? '上传中…' : '点击或拖拽图片上传底图' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute inset-x-0 bottom-0 p-5">
|
||||
<div v-else-if="uploadingImg" class="absolute inset-0 grid place-items-center bg-black/40 text-xs text-white">上传中…</div>
|
||||
<div class="absolute inset-x-0 bottom-0 p-5 pointer-events-none">
|
||||
<div v-if="form.subtitle" class="text-[10px] uppercase tracking-[0.3em] text-white/55">{{ form.subtitle }}</div>
|
||||
<div v-if="form.title" class="text-xl font-bold text-white mt-1">{{ form.title }}</div>
|
||||
<div v-if="form.prompt" class="text-xs text-white/65 mt-1 line-clamp-2">{{ form.prompt }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<input ref="scImgInput" type="file" accept="image/*" class="hidden" @change="onScImgInput" />
|
||||
|
||||
<div class="grid sm:grid-cols-2 gap-3">
|
||||
<div>
|
||||
@@ -269,17 +302,6 @@ onMounted(refresh)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- image picker (the central change — admins pick a real image) -->
|
||||
<div>
|
||||
<label class="block text-xs text-[color:var(--fg-3)] mb-1.5">底图</label>
|
||||
<div class="flex gap-2">
|
||||
<input v-model="form.image" class="field font-mono text-[11px]"
|
||||
placeholder="user/abc.png 或 https://…" />
|
||||
<button type="button" @click="openPicker" class="btn-soft shrink-0">选择已生成</button>
|
||||
</div>
|
||||
<p class="text-[11px] text-[color:var(--fg-faint)] mt-1">填写 /generated 下的相对路径,或粘贴一个外链 URL。</p>
|
||||
</div>
|
||||
|
||||
<template v-if="form.kind !== 'work'">
|
||||
<div class="grid sm:grid-cols-2 gap-3">
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user