feat(user): 创作记录/生成日志/订单页统一页头与工具栏

页头改为标题+说明+圆形刷新按钮;类型/状态筛选改下拉;
全选/下载/删除/去画图等操作收进工具栏右侧,样式与后台一致。

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:01:34 +08:00
co-authored by Devin
parent af7e09cf21
commit c49125b38f
3 changed files with 42 additions and 46 deletions
+11 -12
View File
@@ -8,6 +8,7 @@ import { api, jsonBody } from '../api'
import { openPayment } from '../payment' import { openPayment } from '../payment'
import { refreshMe } from '../auth' import { refreshMe } from '../auth'
import Icon from '../components/Icon.vue' import Icon from '../components/Icon.vue'
import SelectMenu from '../components/SelectMenu.vue'
const router = useRouter() const router = useRouter()
const items = ref([]) const items = ref([])
@@ -34,6 +35,7 @@ function fmt(unix) {
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}` 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: '已取消' }]
async function load() { async function load() {
loading.value = true loading.value = true
const qs = new URLSearchParams({ limit: String(pageSize), offset: String((page.value - 1) * pageSize) }) const qs = new URLSearchParams({ limit: String(pageSize), offset: String((page.value - 1) * pageSize) })
@@ -91,27 +93,24 @@ async function cont(o) {
<template> <template>
<section class="space-y-5 log-page"> <section class="space-y-5 log-page">
<div class="flex items-end justify-between flex-wrap gap-3"> <div class="page-head">
<div> <div>
<h1 class="text-2xl font-semibold tracking-tight text-slate-900">订单</h1> <h2 class="page-title">订单</h2>
<p class="text-sm text-slate-500 mt-1">{{ total }} 笔充值订单 · 未支付可继续支付</p> <p class="page-sub">{{ total }} 笔充值订单 · 未支付可继续支付</p>
</div>
<div class="page-actions">
<button @click="load" class="refresh-btn" title="刷新" aria-label="刷新"><Icon name="refresh" :size="18" /></button>
</div> </div>
<button @click="router.push('/settings')" class="btn-primary"><Icon name="spark" class="w-4 h-4" /> 去充值</button>
</div> </div>
<!-- Filter bar --> <!-- Filter bar -->
<div class="card toolbar"> <div class="card toolbar">
<div class="flex items-center gap-1.5"> <SelectMenu :model-value="status" compact :options="STATUS_OPTS" @update:model-value="setStatus" />
<button v-for="s in [['','全部'],['pending','待支付'],['paid','已支付'],['cancelled','已取消']]" :key="s[0]"
@click="setStatus(s[0])"
class="text-xs rounded-lg px-2.5 py-1.5 transition-colors"
:class="status === s[0] ? 'chip-on' : 'bg-slate-100 text-slate-600 hover:bg-slate-200'">{{ s[1] }}</button>
</div>
<div class="flex-1 min-w-[180px]"> <div class="flex-1 min-w-[180px]">
<input v-model="search" @keyup.enter="doSearch" @change="doSearch" <input v-model="search" @keyup.enter="doSearch" @change="doSearch"
class="field !py-1.5 text-xs" placeholder="搜索 订单号 / 金额 / 方式…" /> class="field" placeholder="搜索 订单号 / 金额 / 方式…" />
</div> </div>
<button @click="load" class="btn-soft"><Icon name="refresh" class="w-3.5 h-3.5" /> 刷新</button> <button @click="router.push('/settings')" class="btn-primary ml-auto"><Icon name="spark" class="w-3.5 h-3.5" /> 去充值</button>
</div> </div>
<div v-if="loading && !items.length" class="card text-center text-sm text-slate-400 py-24">加载中…</div> <div v-if="loading && !items.length" class="card text-center text-sm text-slate-400 py-24">加载中…</div>
+7 -7
View File
@@ -130,14 +130,14 @@ const params = (e) => {
<template> <template>
<section class="space-y-5 log-page"> <section class="space-y-5 log-page">
<!-- Header --> <!-- Header -->
<div class="flex items-end justify-between flex-wrap gap-3"> <div class="page-head">
<div> <div>
<h1 class="text-2xl font-semibold tracking-tight text-slate-900">生成日志</h1> <h2 class="page-title">生成日志</h2>
<p class="text-sm text-slate-500 mt-1">{{ total }} 条记录 · 含失败原因</p> <p class="page-sub">{{ total }} 条记录 · 含失败原因</p>
</div>
<div class="page-actions">
<button @click="load" class="refresh-btn" title="刷新" aria-label="刷新"><Icon name="refresh" :size="18" /></button>
</div> </div>
<button @click="router.push('/user')" class="btn-primary">
<Icon name="spark" class="w-4 h-4" /> 去画图
</button>
</div> </div>
<!-- KPI 统计(本人累计) --> <!-- KPI 统计(本人累计) -->
@@ -168,7 +168,7 @@ const params = (e) => {
<input v-model="search" @keyup.enter="doSearch" @change="doSearch" <input v-model="search" @keyup.enter="doSearch" @change="doSearch"
class="field !py-1.5 text-xs" placeholder="搜索 提示词 / 模型 / 错误…" /> class="field !py-1.5 text-xs" placeholder="搜索 提示词 / 模型 / 错误…" />
</div> </div>
<button @click="load" class="btn-soft"><Icon name="refresh" class="w-3.5 h-3.5" /> 刷新</button> <button @click="router.push('/user')" class="btn-primary ml-auto"><Icon name="spark" class="w-3.5 h-3.5" /> 去画图</button>
</div> </div>
<!-- States --> <!-- States -->
+22 -25
View File
@@ -6,6 +6,7 @@ import { fmtTs } from '../utils/format'
import { copyText } from '../utils/clipboard' import { copyText } from '../utils/clipboard'
import { zipSync } from 'fflate' import { zipSync } from 'fflate'
import Icon from '../components/Icon.vue' import Icon from '../components/Icon.vue'
import SelectMenu from '../components/SelectMenu.vue'
import MediaLightbox from '../components/MediaLightbox.vue' import MediaLightbox from '../components/MediaLightbox.vue'
const router = useRouter() const router = useRouter()
@@ -46,6 +47,7 @@ const filtered = computed(() => items.value)
function doSearch() { page.value = 1; load() } function doSearch() { page.value = 1; load() }
const totalPages = computed(() => Math.max(1, Math.ceil(total.value / pageSize))) const totalPages = computed(() => Math.max(1, Math.ceil(total.value / pageSize)))
const KIND_OPTS = [{ value: '', label: '全部类型' }, { value: 'image', label: '图像' }, { value: 'video', label: '视频' }]
function setKind(v) { kindFilter.value = v; page.value = 1; load() } function setKind(v) { kindFilter.value = v; page.value = 1; load() }
function goPage(n) { function goPage(n) {
const target = Math.max(1, Math.min(totalPages.value, n)) const target = Math.max(1, Math.min(totalPages.value, n))
@@ -231,44 +233,39 @@ onUnmounted(() => {
<template> <template>
<section class="space-y-5"> <section class="space-y-5">
<!-- Header --> <!-- Header -->
<div class="flex items-end justify-between flex-wrap gap-3"> <div class="page-head">
<div> <div>
<h1 class="text-2xl font-semibold tracking-tight text-slate-900">我的创作记录</h1> <h2 class="page-title">我的创作记录</h2>
<p class="text-sm text-slate-500 mt-1"> <p class="page-sub">{{ total }} 条作品</p>
{{ total }} 条作品 </div>
</p> <div class="page-actions">
<button @click="load" class="refresh-btn" title="刷新" aria-label="刷新"><Icon name="refresh" :size="18" /></button>
</div> </div>
<button @click="router.push('/user')" class="btn-primary">
<Icon name="spark" class="w-4 h-4" /> 去画图
</button>
</div> </div>
<!-- Filter bar --> <!-- Filter bar -->
<div class="card toolbar"> <div class="card toolbar">
<div class="flex items-center gap-1.5"> <SelectMenu :model-value="kindFilter" compact :options="KIND_OPTS" @update:model-value="setKind" />
<button @click="setKind('')" class="text-xs rounded-lg px-2.5 py-1.5 transition-colors"
:class="kindFilter === '' ? 'chip-on' : 'bg-slate-100 text-slate-600 hover:bg-slate-200'">全部</button>
<button @click="setKind('image')" class="text-xs rounded-lg px-2.5 py-1.5 transition-colors"
:class="kindFilter === 'image' ? 'chip-on' : 'bg-slate-100 text-slate-600 hover:bg-slate-200'">图像</button>
<button @click="setKind('video')" class="text-xs rounded-lg px-2.5 py-1.5 transition-colors"
:class="kindFilter === 'video' ? 'chip-on' : 'bg-slate-100 text-slate-600 hover:bg-slate-200'">视频</button>
</div>
<div class="flex-1 min-w-[180px]"> <div class="flex-1 min-w-[180px]">
<input v-model="search" @keyup.enter="doSearch" @change="doSearch" <input v-model="search" @keyup.enter="doSearch" @change="doSearch"
class="field !py-1.5 text-xs" placeholder="搜索提示词或模型…" /> class="field" placeholder="搜索提示词或模型…" />
</div> </div>
<button @click="togglePickAll" class="text-xs rounded-lg px-2.5 py-1.5 transition-colors inline-flex items-center gap-1" <div class="ml-auto flex items-center gap-2 flex-wrap">
:class="pageAllPicked ? 'chip-on' : 'bg-slate-100 text-slate-600 hover:bg-slate-200'">
<Icon name="check" class="w-3.5 h-3.5" /> 全选本页
</button>
<template v-if="picked.size"> <template v-if="picked.size">
<button @click="downloadPicked" :disabled="zipping" class="text-xs rounded-lg px-2.5 py-1.5 bg-slate-900 text-white hover:bg-slate-700 inline-flex items-center gap-1 disabled:opacity-50"> <button @click="deletePicked" class="btn-soft danger">
<Icon name="download" class="w-3.5 h-3.5" /> {{ zipping ? '打包中…' : `下载选中 (${picked.size})` }}
</button>
<button @click="deletePicked" class="text-xs rounded-lg px-2.5 py-1.5 bg-rose-600 text-white hover:bg-rose-500 inline-flex items-center gap-1">
<Icon name="trash" class="w-3.5 h-3.5" /> 删除选中 ({{ picked.size }}) <Icon name="trash" class="w-3.5 h-3.5" /> 删除选中 ({{ picked.size }})
</button> </button>
<button @click="downloadPicked" :disabled="zipping" class="btn-soft disabled:opacity-50">
<Icon name="download" class="w-3.5 h-3.5" /> {{ zipping ? '打包中…' : `下载选中 (${picked.size})` }}
</button>
</template> </template>
<button @click="togglePickAll" class="btn-soft">
<Icon name="check" class="w-3.5 h-3.5" /> {{ pageAllPicked ? '取消全选' : '全选本页' }}
</button>
<button @click="router.push('/user')" class="btn-primary">
<Icon name="spark" class="w-3.5 h-3.5" /> 去画图
</button>
</div>
</div> </div>
<!-- Empty --> <!-- Empty -->