feat(admin): 所有后台页统一页头(标题+说明+刷新+主操作),新默认 logo 与站名字体

- 13 个后台页统一 page-head:左标题与一行说明,右侧刷新与主操作,刷新位置一致
- 渠道/并发/违禁词/订单/邀请日志/作品/首页内容/系统配置补齐或改写标题
- 违禁词与订单页改用统一 toolbar,订单筛选胶囊改下拉
- 站名字体改站酷快乐体(英文数字 5KB 子集 + 汉字按需加载)并加粗描边
- 默认 logo 重画:墨黑圆角方块 + 白色星芒 + 暖橙点,favicon 同步

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 16:53:26 +08:00
co-authored by Devin
parent d49207b8f2
commit 98d1124f66
15 changed files with 143 additions and 89 deletions
+16 -14
View File
@@ -4,6 +4,7 @@
import { ref, computed, onMounted } from 'vue'
import { api } from '../api'
import Icon from '../components/Icon.vue'
import SelectMenu from '../components/SelectMenu.vue'
const items = ref([])
const total = ref(0)
@@ -27,6 +28,8 @@ function fmt(unix) {
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`
}
const STATUS_OPTS = [{ value: '', label: '全部状态' }, { value: 'pending', label: '待支付' }, { value: 'paid', label: '已支付' }, { value: 'cancelled', label: '已取消' }]
const SOURCE_OPTS = [{ value: '', label: '全部来源' }, { value: 'epay', label: '在线充值' }, { value: 'admin', label: '后台充值' }, { value: 'cdk', label: '兑换码' }]
async function load() {
loading.value = true
const qs = new URLSearchParams({ limit: String(pageSize), offset: String((page.value - 1) * pageSize) })
@@ -73,26 +76,25 @@ function goPage(n) {
<template>
<section class="theme-text space-y-4">
<div class="card p-4 flex items-center justify-between gap-3 flex-wrap">
<div class="page-head">
<div>
<h2 class="text-sm font-semibold">订单管理</h2>
<p class="text-xs text-white/45 mt-0.5">{{ total }} 笔充值订单</p>
<h2 class="page-title">订单管理</h2>
<p class="page-sub">{{ total }} 笔充值订单</p>
</div>
<div class="flex items-center gap-3 flex-wrap">
<div class="flex items-center gap-1.5">
<button v-for="s in [['','全部'],['pending','待支付'],['paid','已支付'],['cancelled','已取消']]" :key="s[0]"
@click="setStatus(s[0])" class="fp" :class="status === s[0] && 'fp-on'">{{ s[1] }}</button>
</div>
<div class="flex items-center gap-1.5">
<button v-for="s in [['','全部来源'],['epay','在线充值'],['admin','后台充值'],['cdk','兑换码']]" :key="s[0]"
@click="setSource(s[0])" class="fp" :class="source === s[0] && 'fp-on'">{{ s[1] }}</button>
</div>
<input v-model="search" @keyup.enter="doSearch" @change="doSearch"
class="field !py-1.5 text-xs !w-52" placeholder="搜索 订单号 / 用户名 / 金额…" />
<div class="page-actions">
<button @click="load" class="btn-soft"><Icon name="refresh" class="w-3.5 h-3.5" /> 刷新</button>
</div>
</div>
<div class="card toolbar">
<SelectMenu :model-value="status" compact :options="STATUS_OPTS" @update:model-value="setStatus" />
<SelectMenu :model-value="source" compact :options="SOURCE_OPTS" @update:model-value="setSource" />
<div class="flex-1 min-w-[200px]">
<input v-model="search" @keyup.enter="doSearch" @change="doSearch"
class="field" placeholder="搜索 订单号 / 用户名 / 金额…" />
</div>
</div>
<div v-if="loading && !items.length" class="card text-center text-sm text-white/40 py-20">加载中…</div>
<div v-else-if="!total" class="card text-center text-sm text-white/40 py-20">暂无订单</div>
+11 -6
View File
@@ -51,18 +51,23 @@ function goPage(n) {
<template>
<section class="theme-text space-y-4">
<div class="card p-4 flex items-center justify-between gap-3 flex-wrap">
<div class="page-head">
<div>
<h2 class="text-sm font-semibold">违禁词触发列表</h2>
<p class="text-xs text-white/45 mt-0.5">{{ total }} 条触发记录 · 每次拦截记一条(违禁词 / 用户 / 时间 / 提示词)</p>
<h2 class="page-title">违禁词触发列表</h2>
<p class="page-sub">{{ total }} 条触发记录 · 每次拦截记一条(违禁词 / 用户 / 时间 / 提示词)</p>
</div>
<div class="flex items-center gap-2">
<input v-model="search" @keyup.enter="doSearch" @change="doSearch"
class="field !py-1.5 text-xs !w-56" placeholder="搜索 违禁词 / 用户名 / 提示词…" />
<div class="page-actions">
<button @click="load" class="btn-soft"><Icon name="refresh" class="w-3.5 h-3.5" /> 刷新</button>
</div>
</div>
<div class="card toolbar">
<div class="flex-1 min-w-[200px]">
<input v-model="search" @keyup.enter="doSearch" @change="doSearch"
class="field" placeholder="搜索 违禁词 / 用户名 / 提示词…" />
</div>
</div>
<div class="card overflow-hidden">
<table class="w-full text-sm table-fixed">
<colgroup>
+14 -10
View File
@@ -117,21 +117,25 @@ onMounted(load)
<template>
<section class="theme-text space-y-4">
<div class="card p-4 flex items-center justify-between gap-3 flex-wrap">
<div class="page-head">
<div>
<h2 class="text-sm font-semibold">违禁词管理</h2>
<p class="text-xs text-white/45 mt-0.5">提示词包含违禁词的生成请求(画图台 + API)会被<strong class="text-white/70">直接拦截</strong>,并累计触发次数(见用户管理)。匹配不区分大小写。</p>
<h2 class="page-title">违禁词管理</h2>
<p class="page-sub">提示词包含违禁词的生成请求(画图台 + API)会被<strong>直接拦截</strong>,并累计触发次数(见用户管理)。匹配不区分大小写。</p>
</div>
<div class="flex items-center gap-2">
<button v-if="selected.size" @click="delSelected" class="btn-soft danger shrink-0" title="删除选中的违禁词">
<Icon name="trash" class="w-3.5 h-3.5" /> 删除选中 ({{ selected.size }})
</button>
<input v-model="newWord" @keyup.enter="add" class="field !py-1.5 text-xs w-52" placeholder="输入违禁词后回车" />
<button @click="add" class="btn-primary shrink-0">+ 添加</button>
<button @click="importOpen = true" class="btn-soft shrink-0">批量导入</button>
<div class="page-actions">
<button @click="load" class="btn-soft"><Icon name="refresh" class="w-3.5 h-3.5" /> 刷新</button>
</div>
</div>
<div class="card toolbar">
<input v-model="newWord" @keyup.enter="add" class="field w-72" placeholder="输入违禁词后回车添加" />
<button @click="add" class="btn-primary shrink-0">+ 添加</button>
<button @click="importOpen = true" class="btn-soft shrink-0">批量导入</button>
<button v-if="selected.size" @click="delSelected" class="btn-soft danger shrink-0 ml-auto" title="删除选中的违禁词">
<Icon name="trash" class="w-3.5 h-3.5" /> 删除选中 ({{ selected.size }})
</button>
</div>
<div class="card overflow-hidden">
<table class="w-full text-sm">
<thead>
+7 -6
View File
@@ -143,14 +143,15 @@ onMounted(load)
<template>
<section class="space-y-4">
<div class="card p-4 flex items-center justify-between gap-3 flex-wrap">
<div class="page-head">
<div>
<div class="text-sm font-medium">渠道</div>
<p class="text-xs text-white/45 mt-0.5">同构 /v1 协议上游实例 · 保存时自动同步上游模型 · 余额实时探测</p>
<h2 class="page-title">渠道管理</h2>
<p class="page-sub">同构 /v1 协议上游实例 · 保存时自动同步上游模型 · 余额实时探测</p>
</div>
<div class="page-actions">
<button @click="load" class="btn-soft"><Icon name="refresh" class="w-3.5 h-3.5" /> 刷新</button>
<button @click="openAdd" class="btn-primary"><Icon name="plus" class="w-4 h-4" /> 添加渠道</button>
</div>
<button @click="openAdd" class="btn-primary">
<Icon name="plus" class="w-4 h-4" /> 添加渠道
</button>
</div>
<p v-if="err" class="text-xs text-rose-300">{{ err }}</p>
+7 -4
View File
@@ -51,12 +51,15 @@ onMounted(load)
<template>
<section class="theme-text space-y-4">
<div class="card p-4 flex items-center justify-between gap-3 flex-wrap">
<div class="page-head">
<div>
<h2 class="text-sm font-semibold">并发分组</h2>
<p class="text-xs text-white/45 mt-0.5">每个分组限制成员用户的<strong class="text-white/70">同时生成数</strong>(画图台 + API key 合计)。<strong class="text-white/70">0 = 不限制</strong>。新用户自动进入「默认注册分组」。</p>
<h2 class="page-title">并发分组</h2>
<p class="page-sub">每个分组限制成员用户的<strong>同时生成数</strong>(画图台 + API key 合计)。<strong>0 = 不限制</strong>。新用户自动进入「默认注册分组」。</p>
</div>
<div class="page-actions">
<button @click="load" class="btn-soft"><Icon name="refresh" class="w-3.5 h-3.5" /> 刷新</button>
<button @click="openNew" class="btn-primary shrink-0">+ 新增分组</button>
</div>
<button @click="openNew" class="btn-primary shrink-0">+ 新增分组</button>
</div>
<div class="card overflow-hidden">
+9
View File
@@ -293,6 +293,15 @@ onMounted(() => { loadSite(); loadReg(); loadSmtp(); loadCredits(); loadAnnounce
<template>
<section class="space-y-5">
<div class="page-head">
<div>
<h2 class="page-title">系统配置</h2>
<p class="page-sub">网站、支付、注册、邮件、代理等全站设置</p>
</div>
<div class="page-actions">
</div>
</div>
<!-- site -->
<div class="card p-5">
<div class="flex items-center justify-between mb-4">
+8 -3
View File
@@ -207,6 +207,14 @@ onUnmounted(() => window.removeEventListener('keydown', onKey))
<template>
<section class="space-y-4">
<div class="page-head">
<div>
<h2 class="page-title">作品管理</h2>
</div>
<div class="page-actions">
<button @click="load" class="btn-soft"><Icon name="refresh" class="w-3.5 h-3.5" /> 刷新</button>
</div>
</div>
<!-- KPI strip — same shape as the LogsView so the admin shell stays
consistent. /images returns all four numbers in one payload. -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
@@ -247,9 +255,6 @@ onUnmounted(() => window.removeEventListener('keydown', onKey))
<Icon name="trash" class="w-3.5 h-3.5" /> 删除选中 ({{ picked.size }})
</button>
</template>
<button @click="load" class="btn-soft">
<Icon name="refresh" class="w-3.5 h-3.5" /> 刷新
</button>
</div>
</div>
+10 -5
View File
@@ -20,6 +20,15 @@ onMounted(load)
<template>
<section class="space-y-4">
<div class="page-head">
<div>
<h2 class="page-title">邀请日志</h2>
<p class="page-sub">全站邀请记录:谁邀请了谁、注册与完成时间、奖励状态</p>
</div>
<div class="page-actions">
<button @click="load" class="btn-soft"><Icon name="refresh" class="w-3.5 h-3.5" /> 刷新</button>
</div>
</div>
<!-- KPI strip — same shape as the LogsView so the admin shell stays
consistent. /invites already returns the stats payload. -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
@@ -42,11 +51,7 @@ onMounted(load)
</div>
<div class="card overflow-hidden">
<div class="px-5 py-3 border-b border-white/[0.06] flex items-center justify-between">
<div class="text-xs text-white/55">全站邀请记录:谁邀请了谁、注册与完成时间、奖励状态。</div>
<button @click="load" class="btn-soft"><Icon name="refresh" class="w-3.5 h-3.5" /> 刷新</button>
</div>
<div v-if="loading && !items.length" class="text-center text-sm text-white/40 py-16">加载中…</div>
<div v-else-if="!items.length" class="text-center text-sm text-white/40 py-16">还没有邀请记录</div>
+1 -1
View File
@@ -375,7 +375,7 @@ onUnmounted(() => clearInterval(timer))
.ov-bar { display: flex; align-items: flex-end; justify-content: space-between; gap: 12px; }
.ov-bar--sub { margin-top: 8px; align-items: center; }
.ov-bar__right { display: flex; align-items: center; gap: 10px; }
.ov-title { font-size: 22px; font-weight: 800; letter-spacing: -0.03em; color: var(--fg); }
.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; }
+10 -1
View File
@@ -179,7 +179,16 @@ onMounted(refresh)
<template>
<section class="space-y-4">
<!-- header / filter / new -->
<div class="page-head">
<div>
<h2 class="page-title">首页内容</h2>
<p class="page-sub">首页首屏拼贴(Hero)与作品墙(Work)的展示图</p>
</div>
<div class="page-actions">
<button @click="refresh" class="btn-soft"><Icon name="refresh" class="w-3.5 h-3.5" /> 刷新</button>
</div>
</div>
<!-- filter / new -->
<div class="card p-4 flex items-center justify-between gap-3 flex-wrap">
<div class="flex items-center gap-1.5">
<button @click="setFilter('all')" class="filter-pill" :class="filter === 'all' && 'on'">全部</button>