feat(ui): 用户表重构 + 全站表头加高加大

- 用户列:头像块 + 用户名/邮箱/备注三行合并(vivid 同款)
- 最近登录与登录 IP 合并为两行单元格,注册时间单行日期
- 15 列平铺收敛为 12 列,最小宽 1500→1060,1440 视口免横滚
- 全站表头 11px→13px、上下留白 16px,行更高更舒展
- 表体基线字号统一 13px
- 账户信息卡片标签与值间距加大

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:52:30 +08:00
co-authored by Devin
parent aeb94dd11c
commit ab9b63f5cb
2 changed files with 55 additions and 50 deletions
+8 -7
View File
@@ -342,12 +342,13 @@ button:not(:disabled), [role="button"]:not([aria-disabled="true"]) { cursor: poi
} }
/* 表头统一:各页曾分别写 10px/11px、tracking 0.2em/0.04em——收拢到同一规格。 */ /* 表头统一:各页曾分别写 10px/11px、tracking 0.2em/0.04em——收拢到同一规格。 */
.theme-text .card table thead :is(th, td) { .theme-text .card table thead :is(th, td) {
font-size: 11px; font-size: 13px;
font-weight: 700; font-weight: 650;
letter-spacing: 0.07em; letter-spacing: 0.01em;
color: var(--fg-3); color: var(--fg-3);
padding-top: 11px; padding-top: 16px;
padding-bottom: 11px; padding-bottom: 16px;
line-height: 1.4;
white-space: nowrap; white-space: nowrap;
} }
.theme-text .card table tbody tr { transition: background-color 0.12s ease; } .theme-text .card table tbody tr { transition: background-color 0.12s ease; }
@@ -356,8 +357,8 @@ button:not(:disabled), [role="button"]:not([aria-disabled="true"]) { cursor: poi
/* 多选表格的选中行高亮(行加 .row-on) */ /* 多选表格的选中行高亮(行加 .row-on) */
.theme-text .card table tbody tr.row-on { background-color: rgb(24 24 27 / 0.035); } .theme-text .card table tbody tr.row-on { background-color: rgb(24 24 27 / 0.035); }
.theme-text .card table tbody tr.row-on:hover { background-color: #f0f0f2; } .theme-text .card table tbody tr.row-on:hover { background-color: #f0f0f2; }
/* 表体单元格基线:没显式设颜色的 td 统一次要色,数字/名称列各自覆盖 */ /* 表体单元格基线:统一 13px 字号 + 次要色,数字/名称列各自覆盖 */
.theme-text .card table tbody td { color: var(--fg-2); } .theme-text .card table tbody td { color: var(--fg-2); font-size: 13px; }
.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; }
/* ── 空占位:图标块 + 文案,全站表格/卡片共用 ── */ /* ── 空占位:图标块 + 文案,全站表格/卡片共用 ── */
+47 -43
View File
@@ -268,23 +268,20 @@ async function doRecharge() {
</div> </div>
<div v-else class="overflow-x-auto"> <div v-else class="overflow-x-auto">
<table class="w-full text-sm table-fixed min-w-[1500px]"> <table class="w-full text-sm table-fixed min-w-[1060px]">
<colgroup> <colgroup>
<col class="w-9" /> <!-- select --> <col class="w-10" /> <!-- select -->
<col class="w-36" /> <!-- username --> <col /> <!-- user (avatar+name+email+notes) -->
<col /> <!-- email (flex) -->
<col class="w-24" /> <!-- notes -->
<col class="w-28" /> <!-- concurrency -->
<col class="w-20" /> <!-- role --> <col class="w-20" /> <!-- role -->
<col class="w-16" /> <!-- status switch --> <col class="w-14" /> <!-- status switch -->
<col class="w-24" /> <!-- credits --> <col class="w-20" /> <!-- credits -->
<col class="w-24" /> <!-- recharge total --> <col class="w-24" /> <!-- recharge total -->
<col class="w-20" /> <!-- generation count --> <col class="w-24" /> <!-- concurrency -->
<col class="w-20" /> <!-- banned word hits --> <col class="w-16" /> <!-- generation count -->
<col class="w-28" /> <!-- registered --> <col class="w-16" /> <!-- banned word hits -->
<col class="w-28" /> <!-- last login --> <col class="w-24" /> <!-- registered -->
<col class="w-32" /> <!-- login IP --> <col class="w-36" /> <!-- last login + ip -->
<col class="w-32" /> <!-- actions --> <col class="w-28" /> <!-- actions -->
</colgroup> </colgroup>
<thead> <thead>
<tr class="text-[10px] uppercase tracking-[0.2em] text-white/40 border-b border-white/[0.06]"> <tr class="text-[10px] uppercase tracking-[0.2em] text-white/40 border-b border-white/[0.06]">
@@ -292,19 +289,16 @@ async function doRecharge() {
<input type="checkbox" :checked="allSelected" @change="toggleSelectAll" <input type="checkbox" :checked="allSelected" @change="toggleSelectAll"
class="chk" title="全选" /> class="chk" title="全选" />
</th> </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-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-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-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-right 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-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-left px-3 py-3 font-medium">登录 IP</th>
<th class="text-right px-3 py-3 font-medium">操作</th> <th class="text-right px-3 py-3 font-medium">操作</th>
</tr> </tr>
</thead> </thead>
@@ -316,17 +310,15 @@ async function doRecharge() {
<input type="checkbox" :checked="selected.has(u.id)" @change="toggleSelect(u.id)" @click.stop <input type="checkbox" :checked="selected.has(u.id)" @change="toggleSelect(u.id)" @click.stop
class="chk" /> class="chk" />
</td> </td>
<td class="px-3 py-3.5 align-middle text-sm font-medium text-white/90 truncate" :title="u.name || '—'"> <td class="px-3 py-3.5 align-middle">
{{ u.name || '—' }} <div class="flex items-center gap-2.5 min-w-0">
</td> <span class="uavatar">{{ (u.name || u.email || '?').slice(0, 1).toUpperCase() }}</span>
<td class="px-3 py-3.5 align-middle text-xs text-white/75 truncate" :title="u.email"> <div class="min-w-0">
{{ u.email || '—' }} <div class="text-[13.5px] font-semibold text-white/90 truncate" :title="u.name || '—'">{{ u.name || '—' }}</div>
</td> <div class="text-xs text-white/50 truncate" :title="u.email">{{ u.email || '—' }}</div>
<td class="px-3 py-3.5 align-middle text-xs truncate" :class="u.notes ? 'text-white/70' : 'text-white/25'" :title="u.notes || ''"> <div v-if="u.notes" class="text-[11px] text-white/35 truncate" :title="u.notes">{{ u.notes }}</div>
{{ u.notes || '—' }} </div>
</td> </div>
<td class="px-3 py-3.5 align-middle text-xs truncate text-white/70" :title="cgroupLabel(u.concurrency_group_id)">
{{ cgroupLabel(u.concurrency_group_id) }}
</td> </td>
<td class="px-3 py-3.5 align-middle"> <td class="px-3 py-3.5 align-middle">
<span class="inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-medium ring-1 whitespace-nowrap" <span class="inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-medium ring-1 whitespace-nowrap"
@@ -355,6 +347,9 @@ async function doRecharge() {
:class="u.recharge_total > 0 ? 'text-emerald-600' : 'text-white/25'"> :class="u.recharge_total > 0 ? 'text-emerald-600' : 'text-white/25'">
¥{{ (u.recharge_total || 0).toLocaleString('en-US') }} ¥{{ (u.recharge_total || 0).toLocaleString('en-US') }}
</td> </td>
<td class="px-3 py-3.5 align-middle text-xs truncate text-white/60" :title="cgroupLabel(u.concurrency_group_id)">
{{ cgroupLabel(u.concurrency_group_id) }}
</td>
<td class="px-3 py-3.5 align-middle text-right tabular-nums whitespace-nowrap" <td class="px-3 py-3.5 align-middle text-right tabular-nums whitespace-nowrap"
:class="u.generation_count > 0 ? 'text-white/85' : 'text-white/25'"> :class="u.generation_count > 0 ? 'text-white/85' : 'text-white/25'">
{{ (u.generation_count || 0).toLocaleString('en-US') }} {{ (u.generation_count || 0).toLocaleString('en-US') }}
@@ -363,22 +358,16 @@ async function doRecharge() {
:class="u.banned_word_hits > 0 ? 'text-rose-600' : 'text-white/25'"> :class="u.banned_word_hits > 0 ? 'text-rose-600' : 'text-white/25'">
{{ (u.banned_word_hits || 0).toLocaleString('en-US') }} {{ (u.banned_word_hits || 0).toLocaleString('en-US') }}
</td> </td>
<td class="px-3 py-3.5 align-middle text-xs whitespace-nowrap"> <td class="px-3 py-3.5 align-middle whitespace-nowrap">
<div v-if="u.created_at" class="leading-tight" :title="fmtTs(u.created_at)"> <span v-if="u.created_at" class="text-[13px] text-white/65 tabular-nums" :title="fmtTs(u.created_at)">{{ fmtDate(u.created_at) }}</span>
<div class="text-white/65 tabular-nums">{{ fmtDate(u.created_at) }}</div>
<div class="text-white/35 tabular-nums">{{ fmtClock(u.created_at) }}</div>
</div>
<span v-else class="text-white/25">—</span> <span v-else class="text-white/25">—</span>
</td> </td>
<td class="px-3 py-3.5 align-middle text-xs whitespace-nowrap"> <td class="px-3 py-3.5 align-middle whitespace-nowrap">
<div v-if="u.last_login_at" class="leading-tight" :title="fmtTs(u.last_login_at)"> <div v-if="u.last_login_at" class="leading-snug" :title="fmtTs(u.last_login_at)">
<div class="text-white/65 tabular-nums">{{ fmtDate(u.last_login_at) }}</div> <div class="text-[13px] text-white/65 tabular-nums">{{ fmtDate(u.last_login_at) }} {{ fmtClock(u.last_login_at) }}</div>
<div class="text-white/35 tabular-nums">{{ fmtClock(u.last_login_at) }}</div> <div class="text-[11px] text-white/40 font-mono truncate" :title="u.last_login_ip || ''">{{ u.last_login_ip || '—' }}</div>
</div> </div>
<span v-else class="text-white/25">从未登录</span> <span v-else class="text-[13px] text-white/25">从未登录</span>
</td>
<td class="px-3 py-3.5 align-middle text-xs font-mono text-white/55 truncate" :title="u.last_login_ip || ''">
{{ u.last_login_ip || '—' }}
</td> </td>
<td class="px-3 py-3.5 align-middle text-right whitespace-nowrap"> <td class="px-3 py-3.5 align-middle text-right whitespace-nowrap">
<div class="inline-flex items-center gap-1"> <div class="inline-flex items-center gap-1">
@@ -582,6 +571,21 @@ async function doRecharge() {
.fp-orange { background: rgb(249 115 22 / 0.12); color: #c2410c; box-shadow: inset 0 0 0 1px rgb(249 115 22 / 0.25); } .fp-orange { background: rgb(249 115 22 / 0.12); color: #c2410c; box-shadow: inset 0 0 0 1px rgb(249 115 22 / 0.25); }
.fp-amber { background: rgb(245 158 11 / 0.12); color: #b45309; box-shadow: inset 0 0 0 1px rgb(245 158 11 / 0.25); } .fp-amber { background: rgb(245 158 11 / 0.12); color: #b45309; box-shadow: inset 0 0 0 1px rgb(245 158 11 / 0.25); }
/* --- user avatar tile in table */
.uavatar {
display: grid;
place-items: center;
width: 34px;
height: 34px;
border-radius: 10px;
background: var(--surface-2);
box-shadow: inset 0 0 0 1px var(--hairline);
color: var(--fg-2);
font-size: 13px;
font-weight: 800;
flex-shrink: 0;
}
/* --- icon-only action buttons */ /* --- icon-only action buttons */
.act { .act {
display: inline-flex; align-items: center; justify-content: center; display: inline-flex; align-items: center; justify-content: center;