style(ui): 全站空占位统一为图标块 + 文案

- 新增全局 .empty/.empty-ico/.empty-text/.empty-sub
- 渠道、并发分组、违禁词、触发记录、兑换码、订单、邀请日志、
  文档模型表、作品管理的空态全部改为图标块样式
- 渠道表单状态已另改滑动开关

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

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
2026-10-27 17:37:27 +08:00
co-authored by Devin
parent 682960de26
commit df6ef5ad22
13 changed files with 70 additions and 36 deletions
+6
View File
@@ -359,6 +359,12 @@ button:not(:disabled), [role="button"]:not([aria-disabled="true"]) { cursor: poi
/* 表体单元格基线:没显式设颜色的 td 统一次要色,数字/名称列各自覆盖 */ /* 表体单元格基线:没显式设颜色的 td 统一次要色,数字/名称列各自覆盖 */
.theme-text .card table tbody td { color: var(--fg-2); } .theme-text .card table tbody td { color: var(--fg-2); }
.theme-text .card table tbody tr.row-on td:first-child { box-shadow: inset 2px 0 0 #18181b; } .theme-text .card table tbody tr.row-on td:first-child { box-shadow: inset 2px 0 0 #18181b; }
/* ── 空占位:图标块 + 文案,全站表格/卡片共用 ── */
.empty { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 0.75rem; padding: 4.5rem 1.5rem; color: var(--fg-3); text-align: center; }
.empty-ico { width: 3.5rem; height: 3.5rem; border-radius: 1rem; background: var(--surface-2); border: 1px solid var(--hairline); display: grid; place-items: center; color: var(--fg-3); }
.empty-text { font-size: 0.875rem; }
.empty-sub { font-size: 0.75rem; color: var(--fg-3); margin-top: -0.375rem; }
.public-dark .hover\:bg-slate-50:hover { background: rgb(255 255 255 / 0.04); } .public-dark .hover\:bg-slate-50:hover { background: rgb(255 255 255 / 0.04); }
.public-dark .hover\:bg-slate-100:hover { background: rgb(255 255 255 / 0.06); } .public-dark .hover\:bg-slate-100:hover { background: rgb(255 255 255 / 0.06); }
+5 -2
View File
@@ -95,8 +95,11 @@ function goPage(n) {
</div> </div>
</div> </div>
<div v-if="loading && !items.length" class="card text-center text-sm text-white/40 py-20">加载中…</div> <div v-if="loading && !items.length" class="card empty"><span class="empty-text">加载中…</span></div>
<div v-else-if="!total" class="card text-center text-sm text-white/40 py-20">暂无订单</div> <div v-else-if="!total" class="card empty">
<span class="empty-ico"><Icon name="receipt" class="w-6 h-6" /></span>
<span class="empty-text">暂无订单</span>
</div>
<div v-else class="card overflow-x-auto !p-0"> <div v-else class="card overflow-x-auto !p-0">
<table class="w-full text-sm log-table min-w-[820px]"> <table class="w-full text-sm log-table min-w-[820px]">
+5 -2
View File
@@ -85,8 +85,11 @@ function goPage(n) {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-if="loading && !items.length"><td colspan="4" class="text-center text-xs text-white/40 py-10">加载中…</td></tr> <tr v-if="loading && !items.length"><td colspan="4"><div class="empty"><span class="empty-text">加载中…</span></div></td></tr>
<tr v-else-if="!items.length"><td colspan="4" class="text-center text-xs text-white/40 py-10">{{ search.trim() ? '没有匹配的记录' : '还没有触发记录' }}</td></tr> <tr v-else-if="!items.length"><td colspan="4"><div class="empty">
<span class="empty-ico"><Icon name="ban" class="w-6 h-6" /></span>
<span class="empty-text">{{ search.trim() ? '没有匹配的记录' : '还没有触发记录' }}</span>
</div></td></tr>
<tr v-for="h in items" :key="h.id" class="border-b border-white/[0.04] hover:bg-white/[0.03] transition-colors align-top"> <tr v-for="h in items" :key="h.id" class="border-b border-white/[0.04] hover:bg-white/[0.03] transition-colors align-top">
<td class="px-5 py-3.5 text-sm font-medium text-rose-300">{{ h.word }}</td> <td class="px-5 py-3.5 text-sm font-medium text-rose-300">{{ h.word }}</td>
<td class="px-3 py-3.5"> <td class="px-3 py-3.5">
+5 -2
View File
@@ -152,8 +152,11 @@ onMounted(load)
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-if="loading && !items.length"><td colspan="5" class="text-center text-xs text-white/40 py-10">加载中…</td></tr> <tr v-if="loading && !items.length"><td colspan="5"><div class="empty"><span class="empty-text">加载中…</span></div></td></tr>
<tr v-else-if="!items.length"><td colspan="5" class="text-center text-xs text-white/40 py-10">还没有违禁词</td></tr> <tr v-else-if="!items.length"><td colspan="5"><div class="empty">
<span class="empty-ico"><Icon name="ban" class="w-6 h-6" /></span>
<span class="empty-text">还没有违禁词</span>
</div></td></tr>
<tr v-for="w in pagedItems" :key="w.id" class="border-b border-white/[0.04] hover:bg-white/[0.03] transition-colors" :class="selected.has(w.id) && 'row-on'"> <tr v-for="w in pagedItems" :key="w.id" class="border-b border-white/[0.04] hover:bg-white/[0.03] transition-colors" :class="selected.has(w.id) && 'row-on'">
<td class="px-3 py-3.5 align-middle text-center"> <td class="px-3 py-3.5 align-middle text-center">
<input type="checkbox" :checked="selected.has(w.id)" @change="toggleSelect(w.id)" @click.stop class="chk" /> <input type="checkbox" :checked="selected.has(w.id)" @change="toggleSelect(w.id)" @click.stop class="chk" />
+9 -3
View File
@@ -218,9 +218,15 @@ const pageNumbers = computed(() => {
<!-- table --> <!-- table -->
<div class="card overflow-hidden"> <div class="card overflow-hidden">
<div v-if="loading && !items.length" class="text-center text-sm text-white/40 py-16">加载中…</div> <div v-if="loading && !items.length" class="empty"><span class="empty-text">加载中…</span></div>
<div v-else-if="!items.length && !stats.total" class="text-center text-sm text-white/40 py-16">还没有兑换码</div> <div v-else-if="!items.length && !stats.total" class="empty">
<div v-else-if="!items.length" class="text-center text-sm text-white/40 py-16">没有匹配的兑换码</div> <span class="empty-ico"><Icon name="key" class="w-6 h-6" /></span>
<span class="empty-text">还没有兑换码</span>
</div>
<div v-else-if="!items.length" class="empty">
<span class="empty-ico"><Icon name="key" class="w-6 h-6" /></span>
<span class="empty-text">没有匹配的兑换码</span>
</div>
<table v-else class="w-full text-sm"> <table v-else class="w-full text-sm">
<colgroup> <colgroup>
<col class="w-9" /> <col class="w-9" />
+8 -4
View File
@@ -204,8 +204,12 @@ onMounted(load)
<button @click="del(c)" class="op danger" title="删除"><Icon name="trash" class="w-3.5 h-3.5" /></button> <button @click="del(c)" class="op danger" title="删除"><Icon name="trash" class="w-3.5 h-3.5" /></button>
</td> </td>
</tr> </tr>
<tr v-if="loading"><td colspan="11" class="py-10 text-center text-white/40 text-sm">正在加载渠道…</td></tr> <tr v-if="loading"><td colspan="11"><div class="empty"><span class="empty-text">正在加载渠道…</span></div></td></tr>
<tr v-else-if="!channels.length"><td colspan="11" class="py-10 text-center text-white/40 text-sm">没有渠道,点上方「添加渠道」接入上游实例</td></tr> <tr v-else-if="!channels.length"><td colspan="11"><div class="empty">
<span class="empty-ico"><Icon name="plug" class="w-6 h-6" /></span>
<span class="empty-text">还没有渠道</span>
<span class="empty-sub">点上方「添加渠道」接入上游实例</span>
</div></td></tr>
</tbody> </tbody>
</table> </table>
</div> </div>
@@ -269,11 +273,11 @@ onMounted(load)
<button @click="fetchTarget = null" class="text-white/40 hover:text-white transition-colors"><Icon name="close" class="w-5 h-5" /></button> <button @click="fetchTarget = null" class="text-white/40 hover:text-white transition-colors"><Icon name="close" class="w-5 h-5" /></button>
</div> </div>
<div class="p-5"> <div class="p-5">
<div v-if="fetching" class="py-10 text-center text-white/40 text-sm">正在从上游拉取模型列表…</div> <div v-if="fetching" class="empty !py-10"><span class="empty-text">正在从上游拉取模型列表…</span></div>
<template v-else> <template v-else>
<p class="text-xs text-white/45 mb-3 leading-relaxed">开启的模型会同步到本站模型列表,参数随上游带入;到「模型管理」配价后上架。别名即本站模型标识,留空 = 与上游同名。</p> <p class="text-xs text-white/45 mb-3 leading-relaxed">开启的模型会同步到本站模型列表,参数随上游带入;到「模型管理」配价后上架。别名即本站模型标识,留空 = 与上游同名。</p>
<p v-if="fetchErr" class="text-xs text-rose-300 mb-3">{{ fetchErr }}</p> <p v-if="fetchErr" class="text-xs text-rose-300 mb-3">{{ fetchErr }}</p>
<div v-if="!specs.length" class="py-10 text-center text-white/40 text-sm">上游没有可售模型(模型需在上游已配价)</div> <div v-if="!specs.length" class="empty !py-10"><span class="empty-text">上游没有可售模型(模型需在上游已配价)</span></div>
<div v-else class="space-y-2 max-h-[400px] overflow-y-auto p-0.5"> <div v-else class="space-y-2 max-h-[400px] overflow-y-auto p-0.5">
<div v-for="s in specs" :key="s.id" @click="togglePick(s.id)" <div v-for="s in specs" :key="s.id" @click="togglePick(s.id)"
class="p-3 rounded-xl ring-1 ring-white/10 bg-white/[0.02] hover:bg-white/[0.04] hover:ring-white/20 cursor-pointer transition-all space-y-2" class="p-3 rounded-xl ring-1 ring-white/10 bg-white/[0.02] hover:bg-white/[0.04] hover:ring-white/20 cursor-pointer transition-all space-y-2"
+5 -2
View File
@@ -75,8 +75,11 @@ onMounted(load)
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-if="loading"><td colspan="5" class="text-center text-xs text-white/40 py-10">加载中…</td></tr> <tr v-if="loading"><td colspan="5"><div class="empty"><span class="empty-text">加载中…</span></div></td></tr>
<tr v-else-if="!items.length"><td colspan="5" class="text-center text-xs text-white/40 py-10">还没有分组</td></tr> <tr v-else-if="!items.length"><td colspan="5"><div class="empty">
<span class="empty-ico"><Icon name="layers" class="w-6 h-6" /></span>
<span class="empty-text">还没有分组</span>
</div></td></tr>
<tr v-for="g in items" :key="g.id" class="border-b border-white/[0.04] hover:bg-white/[0.03] transition-colors"> <tr v-for="g in items" :key="g.id" class="border-b border-white/[0.04] hover:bg-white/[0.03] transition-colors">
<td class="px-5 py-3.5 align-middle text-sm font-medium text-white/90">{{ g.name }}</td> <td class="px-5 py-3.5 align-middle text-sm font-medium text-white/90">{{ g.name }}</td>
<td class="px-3 py-3.5 align-middle text-right tabular-nums"> <td class="px-3 py-3.5 align-middle text-right tabular-nums">
+4 -1
View File
@@ -275,7 +275,10 @@ async function copy(text) {
</div> </div>
</td> </td>
</tr> </tr>
<tr v-if="!models.length"><td colspan="4" class="px-4 py-10 text-center text-white/35">暂无可用模型</td></tr> <tr v-if="!models.length"><td colspan="4"><div class="empty">
<span class="empty-ico"><Icon name="models" class="w-6 h-6" /></span>
<span class="empty-text">暂无可用模型</span>
</div></td></tr>
</tbody> </tbody>
</table> </table>
</div> </div>
+4 -4
View File
@@ -259,12 +259,12 @@ onUnmounted(() => window.removeEventListener('keydown', onKey))
</div> </div>
<!-- grid --> <!-- grid -->
<div v-if="loading && !items.length" class="text-center text-sm text-white/40 py-20">加载中…</div> <div v-if="loading && !items.length" class="empty"><span class="empty-text">加载中…</span></div>
<div v-else-if="!items.length" class="card flex flex-col items-center gap-3 text-white/40 py-20"> <div v-else-if="!items.length" class="card empty">
<span class="w-14 h-14 rounded-2xl bg-white/[0.04] grid place-items-center"> <span class="empty-ico">
<Icon name="files" class="w-6 h-6" /> <Icon name="files" class="w-6 h-6" />
</span> </span>
<span class="text-sm">还没有生成过任何图片</span> <span class="empty-text">还没有生成过任何图片</span>
</div> </div>
<div v-else class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-3"> <div v-else class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-3">
+5 -2
View File
@@ -52,8 +52,11 @@ onMounted(load)
<div class="card overflow-hidden"> <div class="card overflow-hidden">
<div v-if="loading && !items.length" class="text-center text-sm text-white/40 py-16">加载中…</div> <div v-if="loading && !items.length" class="empty"><span class="empty-text">加载中…</span></div>
<div v-else-if="!items.length" class="text-center text-sm text-white/40 py-16">还没有邀请记录</div> <div v-else-if="!items.length" class="empty">
<span class="empty-ico"><Icon name="team" class="w-6 h-6" /></span>
<span class="empty-text">还没有邀请记录</span>
</div>
<table v-else class="w-full text-sm"> <table v-else class="w-full text-sm">
<colgroup> <colgroup>
+4 -4
View File
@@ -192,10 +192,10 @@ const sourcePill = (s) => ({
<!-- Table --> <!-- Table -->
<div class="card overflow-hidden"> <div class="card overflow-hidden">
<div v-if="loading && !items.length" class="text-center text-sm text-white/40 py-20">加载中…</div> <div v-if="loading && !items.length" class="empty"><span class="empty-text">加载中…</span></div>
<div v-else-if="!filtered.length" class="flex flex-col items-center gap-3 text-white/40 py-20"> <div v-else-if="!filtered.length" class="empty">
<span class="w-14 h-14 rounded-2xl bg-white/[0.04] grid place-items-center"><Icon name="files" class="w-6 h-6" /></span> <span class="empty-ico"><Icon name="files" class="w-6 h-6" /></span>
<span class="text-sm">{{ search.trim() ? '没有匹配的记录' : '还没有日志' }}</span> <span class="empty-text">{{ search.trim() ? '没有匹配的记录' : '还没有日志' }}</span>
</div> </div>
<!-- Each row is a thumbnail + a stack of model/prompt + a meta line. <!-- Each row is a thumbnail + a stack of model/prompt + a meta line.
+5 -5
View File
@@ -110,11 +110,11 @@ onMounted(loadModels)
<!-- Table --> <!-- Table -->
<div class="card overflow-hidden"> <div class="card overflow-hidden">
<div v-if="loading && !models.length" class="text-center text-sm text-white/40 py-20">加载中…</div> <div v-if="loading && !models.length" class="empty"><span class="empty-text">加载中…</span></div>
<div v-else-if="!filtered.length" class="flex flex-col items-center gap-3 text-white/40 py-20"> <div v-else-if="!filtered.length" class="empty">
<span class="w-14 h-14 rounded-2xl bg-white/[0.04] grid place-items-center"><Icon name="models" class="w-6 h-6" /></span> <span class="empty-ico"><Icon name="models" class="w-6 h-6" /></span>
<span v-if="models.length" class="text-sm">没有匹配的模型</span> <span v-if="models.length" class="empty-text">没有匹配的模型</span>
<router-link v-else to="/admin/channels" class="text-sm text-sky-300 underline underline-offset-2">还没有模型,先到「渠道管理」添加渠道,保存后会自动同步上游模型</router-link> <router-link v-else to="/admin/channels" class="empty-text underline underline-offset-2">还没有模型,先到「渠道管理」添加渠道,保存后会自动同步上游模型</router-link>
</div> </div>
<table v-else class="w-full text-sm table-fixed min-w-[1080px]"> <table v-else class="w-full text-sm table-fixed min-w-[1080px]">
+5 -5
View File
@@ -258,13 +258,13 @@ async function doRecharge() {
<!-- Table --> <!-- Table -->
<div class="card overflow-hidden"> <div class="card overflow-hidden">
<div v-if="loading && !items.length" class="text-center text-sm text-white/40 py-20">加载中…</div> <div v-if="loading && !items.length" class="empty"><span class="empty-text">加载中…</span></div>
<div v-else-if="!filtered.length" class="flex flex-col items-center gap-3 text-white/40 py-20"> <div v-else-if="!filtered.length" class="empty">
<span class="w-14 h-14 rounded-2xl bg-white/[0.04] grid place-items-center"> <span class="empty-ico">
<Icon name="accounts" class="w-6 h-6" /> <Icon name="accounts" class="w-6 h-6" />
</span> </span>
<span class="text-sm">{{ stats.total ? '没有匹配的用户' : '还没有用户' }}</span> <span class="empty-text">{{ stats.total ? '没有匹配的用户' : '还没有用户' }}</span>
<button v-if="!stats.total" @click="showAdd = true" class="btn-soft mt-1">新建第一个</button> <button v-if="!stats.total" @click="showAdd = true" class="btn-soft">新建第一个</button>
</div> </div>
<div v-else class="overflow-x-auto"> <div v-else class="overflow-x-auto">