@@ -1,5 +1,6 @@
< script setup lang = "ts" >
/** 历史 生成记录页( module 13 task 259 对齐 admin panel-history):指定用户下拉 + 起止时间筛选、类型中文、≤3 缩略图、大图预览、空态。 */
/** 生成记录页 · 像素复刻旧版 admin.html panel-history(自绘:form-box 筛选 + panel-box 表格 + 旧式分页 + thumb 缩略图)。
* script 逻辑沿用现有 Vue 实现(查询/重置/翻页/预览/清理数据)。 */
import { formatDateTime } from '@/utils/datetime'
import { computed , onMounted , reactive , ref } from 'vue'
import { fetchHistoryList , fetchHistoryUserOptions , clearHistory } from './history-api.ts'
@@ -21,9 +22,18 @@ const filter = reactive({ userId: '', timeStart: '', timeEnd: '' })
const previewVisible = ref ( false )
const previewItem = ref < HistoryRecordItem | null > ( null )
const jumpPage = ref ( '' )
const hasFilter = computed ( ( ) => Boolean ( filter . userId || filter . timeStart || filter . timeEnd ) )
const emptyText = computed ( ( ) => historyEmptyText ( { hasFilter : hasFilter . value , total : total . value } ) )
const totalPages = computed ( ( ) => Math . max ( 1 , Math . ceil ( total . value / pageSize ) ) )
/** datetime-local 原生值(YYYY-MM-DDTHH:mm) 转后端兼容格式。 */
function toApiTime ( value : string ) : string | undefined {
const t = ( value || '' ) . trim ( )
if ( ! t ) return undefined
return ` ${ t . replace ( 'T' , ' ' ) } :00 `
}
/** 预览图顺序:长图在前 + 结果图去重(对齐 admin 缩略图 [long,...result])。 */
function previewUrls ( item : HistoryRecordItem ) : string [ ] {
@@ -48,8 +58,8 @@ async function load() {
page : page . value ,
pageSize ,
userId : filter . userId ? Number ( filter . userId ) : undefined ,
timeStart : filter . timeStart || undefined ,
timeEnd : filter . timeEnd || undefined ,
timeStart : toApiTime ( filter . timeStart ) ,
timeEnd : toApiTime ( filter . timeEnd ) ,
} )
rows . value = result . items
total . value = result . total
@@ -90,10 +100,20 @@ function retry() {
}
function changePage ( next : number ) {
if ( next < 1 || next > totalPages . value ) return
page . value = next
load ( )
}
function goJump ( ) {
const n = Number . parseInt ( jumpPage . value , 10 )
if ( Number . isNaN ( n ) ) {
ElMessage . warning ( '请输入页码' )
return
}
changePage ( n )
}
function openPreview ( item : HistoryRecordItem ) {
previewItem . value = item
previewVisible . value = true
@@ -101,7 +121,7 @@ function openPreview(item: HistoryRecordItem) {
const clearing = ref ( false )
/** 清空全部生成记录:二次确认后调后端 DELETE /api/admin/history。 */
/** 清空全部生成记录:二次确认后调后端 DELETE /api/admin/history(新版增强,保留) 。 */
async function clearAll ( ) {
try {
await ElMessageBox . confirm ( '确定清空全部生成记录吗?该操作会删除所有历史数据,无法恢复。' , '清理生成记录' , {
@@ -132,143 +152,485 @@ onMounted(() => {
< / script >
< template >
< div class = "page-stack " >
< div class = "page-heading " >
< div >
< h2 > 生成记录 < / h2 >
< p > 查看各用户在后台生成的历史记录与结果图 。 < / p >
< div class = "history-view " >
< section class = "form-box " >
< h3 > 筛选条件 < / h3 >
< div class = "form-row" >
< div class = "form-group" style = "min-width: 150px" >
< label > 指定用户 < / label >
< select v-model = "filter.userId" :disabled="userLoading" >
< option value = "" > 全部用户 < / option >
< option v-for = "u in userOptions" :key="u.id" :value="String(u.id)" > {{ u.username }} < / option >
< / select >
< / div >
< div class = "form-group" style = "min-width: 176px" >
< label > 开始时间 < / label >
< input v-model = "filter.timeStart" type="datetime-local" / >
< / div >
< div class = "form-group" style = "min-width: 176px" >
< label > 结束时间 < / label >
< input v-model = "filter.timeEnd" type="datetime-local" / >
< / div >
< div class = "form-actions" >
< button class = "btn" type = "button" @click ="apply" > 查询 < / button >
< button class = "btn btn-secondary" type = "button" @click ="reset" > 重置 < / button >
< / div >
< / div >
< / section >
< section class = "panel-box" >
< div class = "panel-head" >
< h3 > 生成记录 < / h3 >
< button class = "btn btn-danger" type = "button" :disabled = "clearing" :aria-busy = "clearing" @click ="clearAll" >
{{ clearing ? ' 清理中... ' : ' 清理数据 ' }}
< / button >
< / div >
< div v-if = "errorText" class="msg err" >
< span > 加载失败 : { { errorText } } < / span >
< button class = "btn btn-sm btn-secondary" type = "button" @click ="retry" > 重试 < / button >
< / div >
< div class = "table-scroll history-table-scroll" >
< table >
< thead >
< tr >
< th style = "width: 70px" > ID < / th >
< th style = "width: 130px" > 用户 < / th >
< th style = "width: 160px" > 类型 < / th >
< th style = "width: 170px" > 创建时间 < / th >
< th > 结果预览 < / th >
< th style = "width: 96px" > 操作 < / th >
< / tr >
< / thead >
< tbody >
< template v-if = "rows.length" >
< tr v-for = "h in rows" :key="h.id" >
< td > { { h . id } } < / td >
< td > { { h . username || '-' } } < / td >
< td > { { historyPanelTypeLabel ( h . panelType ) } } < / td >
< td > { { formatDateTime ( h . createdAt ) } } < / td >
< td >
< div class = "thumb-wrap" >
< img
v-for = "url in previewUrls(h).slice(0, 3)"
:key = "url"
class = "thumb"
:src = "url"
alt = ""
title = "点击查看大图"
@click ="openPreview(h)"
/ >
< span v-if = "!previewUrls(h).length" class="empty-mini" > 暂无图片 < / span >
< / div >
< / td >
< td >
< button class = "btn btn-sm" type = "button" @click ="openPreview(h)" > 查看大图 < / button >
< / td >
< / tr >
< / template >
< tr v-else-if = "loading" >
< td colspan = "6" class = "empty-tip" > 加载中 ... < / td >
< / tr >
< tr v-else >
< td colspan = "6" class = "empty-tip" > { { emptyText } } < / td >
< / tr >
< / tbody >
< / table >
< / div >
< div class = "pagination" >
< span class = "page-total" > 共 { { total } } 条 < / span >
< button type = "button" : disabled = "page <= 1" @click ="changePage(page - 1)" > 上一页 < / button >
< span class = "page-cur" > 第 { { page } } / { { totalPages } } 页 < / span >
< button type = "button" : disabled = "page >= totalPages" @click ="changePage(page + 1)" > 下一页 < / button >
< span class = "page-jump" >
跳至
< input v-model = "jumpPage" type="text" inputmode="numeric" @keyup.enter="goJump" / >
页
< button type = "button" @click ="goJump" > 跳转 < / button >
< / span >
< / div >
< / section >
< div v-if = "previewVisible" class="modal-mask" @click.self="previewVisible = false" >
< div class = "modal" >
< div class = "modal-head" >
< h3 v-if = "previewItem" > 记录 # {{ previewItem.id }} 结果图 < / h3 >
< button class = "modal-close" type = "button" aria -label = " 关闭 " @click ="previewVisible = false" > × < / button >
< / div >
< div v-if = "previewItem" class="preview-meta" >
< span > { { previewItem . username || '-' } } < / span >
< span > · < / span >
< span > { { historyPanelTypeLabel ( previewItem . panelType ) } } < / span >
< span > · < / span >
< span > { { formatDateTime ( previewItem . createdAt ) } } < / span >
< / div >
< div v-if = "previewItem && previewUrls(previewItem).length" class="preview-grid" >
< a
v-for = "url in previewUrls(previewItem)"
:key = "url"
:href = "url"
target = "_blank"
rel = "noopener"
title = "新标签打开原图"
>
< img :src = "url" alt = "" / >
< / a >
< / div >
< div v-else class = "modal-empty" > 无结果图片 < / div >
< div class = "modal-foot" >
< button class = "btn btn-secondary" type = "button" @click ="previewVisible = false" > 关闭 < / button >
< / div >
< / div >
< / div >
< el-card shadow = "never" class = "filter-card" >
< div class = "filter-grid" >
< div class = "f-item" >
< label > 指定用户 < / label >
< el-select v-model = "filter.userId" clearable filterable placeholder="全部用户" style="width: 100%" :loading="userLoading" >
< el -option label = "全部用户" value = "" / >
< el-option v-for = "u in userOptions" :key="u.id" :label="u.username" :value="String(u.id)" / >
< / el-select >
< / div >
< div class = "f-item" >
< label > 开始时间 < / label >
< el-date-picker
v-model = "filter.timeStart"
type = "datetime"
placeholder = "开始时间"
value -format = " YYYY -MM -DD HH : mm : ss "
style = "width: 100%"
/ >
< / div >
< div class = "f-item" >
< label > 结束时间 < / label >
< el-date-picker
v-model = "filter.timeEnd"
type = "datetime"
placeholder = "结束时间"
value -format = " YYYY -MM -DD HH : mm : ss "
style = "width: 100%"
/ >
< / div >
< div class = "f-item btn-row" >
< el-button type = "primary" @click ="apply" > 查询 < / el -button >
< el-button @click ="reset" > 重置 < / el -button >
< / div >
< / div >
< / el-card >
< el-card shadow = "never" >
< div class = "table-toolbar" >
< el-button type = "danger" plain :loading = "clearing" @click ="clearAll" > 清理数据 < / el -button >
< / div >
< el-alert
v-if = "errorText"
:title = "errorText"
type = "error"
show -icon
:closable = "false"
style = "margin-bottom: 12px"
>
< template # default >
< el-button link type = "primary" @click ="retry" > 重试 < / el -button >
< / template >
< / el-alert >
< el-table v-loading = "loading" :data="rows" stripe :empty-text="emptyText" >
< el -table -column prop = "id" label = "ID" min -width = " 90 " / >
< el-table-column prop = "username" label = "用户" min -width = " 140 " / >
< el-table-column label = "类型" min -width = " 150 " >
< template # default = "{ row }" >
{ { historyPanelTypeLabel ( ( row as HistoryRecordItem ) . panelType ) } }
< / template >
< / el-table-column >
< el-table-column prop = "createdAt" label = "创建时间" min -width = " 180 " >
< template # default = "{ row }" > { { formatDateTime ( ( row as HistoryRecordItem ) . createdAt ) } } < / template >
< / el-table-column >
< el-table-column label = "结果预览" min -width = " 220 " >
< template # default = "{ row }" >
< div v-if = "previewUrls(row as HistoryRecordItem).length" class="thumbs" >
< el -image
v-for = "(url, index) in previewUrls(row as HistoryRecordItem).slice(0, 3)"
:key = "url"
:src = "url"
: preview -src -list = " previewUrls ( row as HistoryRecordItem ) "
:initial-index = "index"
preview -teleported
fit = "cover"
class = "thumb"
/ >
< span v-if = "previewUrls(row as HistoryRecordItem).length > 3" class="dim" > + {{ previewUrls ( row as HistoryRecordItem ) .length - 3 }} < / span >
< / div >
< span v-else class = "dim" > — < / span >
< / template >
< / el-table-column >
< el-table-column label = "操作" min -width = " 110 " fixed = "right" >
< template # default = "{ row }" >
< el-button type = "primary" size = "small" @click ="openPreview(row as HistoryRecordItem)" > 查看大图 < / el -button >
< / template >
< / el-table-column >
< / el-table >
< div class = "table-footer" >
< span > 共 < b > { { total . toLocaleString ( ) } } < / b > 条 < / span >
< el-pagination background layout = "prev, pager, next, jumper" :total = "total" :page-size = "pageSize" :current-page = "page" @ current -change = " changePage " / >
< / div >
< / el-card >
< el-dialog v-model = "previewVisible" :title="previewItem ? `记录 #${previewItem.id} 结果图` : '结果图'" width="820px" top="6vh" >
< div v-if = "previewItem" class="preview-meta" >
< el -tag size = "small" > { { previewItem . username } } < / el-tag >
< span > { { historyPanelTypeLabel ( previewItem . panelType ) } } < / span >
< span > { { formatDateTime ( previewItem . createdAt ) } } < / span >
< / div >
< div v-if = "previewItem && previewUrls(previewItem).length" class="preview-grid" >
< el -image
v-for = "(url, index) in previewUrls(previewItem)"
:key = "url"
:src = "url"
:preview-src-list = "previewUrls(previewItem)"
:initial-index = "index"
preview -teleported
fit = "contain"
class = "preview-img"
/ >
< / div >
< el-empty v-else description = "无结果图片" / >
< / el-dialog >
< / div >
< / template >
< style scoped >
. filter - card { margin - bottom : 14 px ; }
. filter - grid { display : flex ; flex - wrap : wrap ; gap : 12 px 18 px ; }
. f - item { display : flex ; flex - direction : column ; gap : 6 px ; width : 190 px ; }
. f - item label { color : var ( -- el - text - color - secondary ) ; font - size : 12 px ; }
. f - item . btn - row { flex - direction : row ; align - items : flex - end ; gap : 8 px ; width : auto ; }
. thumbs { display : flex ; align - items : center ; gap : 6 px ; }
. thumb { width : 56 px ; height : 56 px ; border - radius : 4 px ; border : 1 px solid var ( -- el - border - color - lighter ) ; flex : none ; }
. table - footer { display : flex ; justify - content : space - between ; align - items : center ; padding - top : 14 px ; }
. table - footer span { color : var ( -- el - text - color - secondary ) ; font - size : 12.5 px ; }
. table - footer b { color : var ( -- el - text - color - primary ) ; }
. dim { color : var ( -- el - text - color - secondary ) ; font - size : 12 px ; }
. preview - meta { display : flex ; align - items : center ; gap : 12 px ; color : var ( -- el - text - color - secondary ) ; font - size : 12 px ; margin - bottom : 12 px ; }
. preview - grid { display : flex ; flex - direction : column ; gap : 12 px ; }
. preview - img { width : 100 % ; border : 1 px solid var ( -- el - border - color - lighter ) ; border - radius : 6 px ; }
/* 像素复刻旧版 admin.html 蓝白末层样式:form-box/panel-box 双卡 + table-scroll 表格 + 旧式分页 + thumb 缩略图。 */
. history - view {
font - family : inherit ;
color : var ( -- c - text , # 24384 d ) ;
display : flex ;
flex - direction : column ;
gap : 18 px ;
}
. form - box ,
. panel - box {
width : 100 % ;
min - width : 0 ;
padding : 20 px 22 px 24 px ;
border : 1 px solid # d8e3ee ;
border - radius : 14 px ;
background : linear - gradient ( 145 deg , # ffffff , # f9fbfd ) ;
box - shadow : 0 1 px 2 px rgba ( 39 , 67 , 94 , 0.04 ) , 0 14 px 30 px - 24 px rgba ( 39 , 67 , 94 , 0.28 ) ;
}
h3 {
margin : 0 0 16 px ;
font - size : 15 px ;
font - weight : 650 ;
color : # 24384 d ;
letter - spacing : 0.2 px ;
}
. panel - head {
display : flex ;
align - items : center ;
justify - content : space - between ;
gap : 12 px ;
}
. panel - head h3 {
margin - bottom : 16 px ;
}
. form - row {
display : flex ;
flex - wrap : wrap ;
align - items : flex - end ;
gap : 14 px 18 px ;
}
. form - group {
display : flex ;
flex - direction : column ;
gap : 7 px ;
}
. form - group label {
color : # 5 b6f83 ;
font - size : 12.5 px ;
font - weight : 600 ;
}
. form - group input ,
. form - group select {
min - width : 0 ;
min - height : 42 px ;
padding : 8 px 12 px ;
border : 1 px solid # cbd9e6 ;
border - radius : 9 px ;
background : # f8fbfd ;
color : # 24384 d ;
font - size : 13.5 px ;
font - family : inherit ;
color - scheme : light ;
outline : none ;
transition : border - color 160 ms ease , box - shadow 160 ms ease , background 160 ms ease ;
}
. form - group input : hover ,
. form - group select : hover {
border - color : # 9 fb7cd ;
}
. form - group input : focus ,
. form - group select : focus {
background : # ffffff ;
border - color : # 5 f85ad ;
box - shadow : 0 0 0 3 px rgba ( 95 , 133 , 173 , 0.16 ) ;
}
. form - actions {
display : inline - flex ;
align - items : center ;
gap : 8 px ;
padding - bottom : 1 px ;
}
. btn {
display : inline - flex ;
align - items : center ;
justify - content : center ;
gap : 6 px ;
min - height : 42 px ;
padding : 9 px 18 px ;
border : 1 px solid # 4 f78a5 ;
border - radius : 9 px ;
background : linear - gradient ( 135 deg , # 5 f85ad , # 4 f78a5 ) ;
color : # ffffff ;
font - family : inherit ;
font - size : 13.5 px ;
cursor : pointer ;
box - shadow : 0 8 px 18 px - 14 px rgba ( 79 , 120 , 165 , 0.72 ) ;
transition : background 160 ms ease , box - shadow 160 ms ease , transform 120 ms ease ;
}
. btn : hover : not ( : disabled ) {
background : linear - gradient ( 135 deg , # 7094 ba , # 5 d83ac ) ;
box - shadow : 0 11 px 22 px - 14 px rgba ( 79 , 120 , 165 , 0.8 ) ;
}
. btn : disabled {
cursor : not - allowed ;
opacity : 0.6 ;
}
. btn - secondary {
background : # ffffff ;
color : # 5 b6f83 ;
border - color : # c7d7e5 ;
}
. btn - secondary : hover : not ( : disabled ) {
color : # 2 f5d8b ;
border - color : # 95 b1cb ;
background : # edf5fb ;
}
. btn - danger {
background : linear - gradient ( 135 deg , # c06d77 , # b35f6a ) ;
border - color : # b35f6a ;
}
. btn - danger : hover : not ( : disabled ) {
background : linear - gradient ( 135 deg , # cb7c84 , # b96570 ) ;
}
. btn - sm {
min - height : 36 px ;
padding : 7 px 12 px ;
}
. msg {
display : flex ;
align - items : center ;
justify - content : space - between ;
gap : 12 px ;
margin - bottom : 14 px ;
padding : 10 px 12 px ;
border : 1 px solid ;
border - radius : 9 px ;
font - size : 13 px ;
}
. msg . err {
color : # 91474 f ;
background : # f8ebeb ;
border - color : # e4c2c5 ;
}
. table - scroll {
width : 100 % ;
min - width : 0 ;
overflow - x : auto ;
border : 1 px solid # dbe5ee ;
border - radius : 10 px ;
background : # ffffff ;
}
. table - scroll table {
width : 100 % ;
border - collapse : collapse ;
table - layout : fixed ;
}
. history - table - scroll > table {
min - width : 900 px ;
}
. table - scroll th ,
. table - scroll td {
padding : 10 px 12 px ;
text - align : left ;
font - size : 13.5 px ;
line - height : 1.5 ;
border - bottom : 1 px solid # e0e8ef ;
vertical - align : middle ;
overflow : hidden ;
text - overflow : ellipsis ;
}
. table - scroll th {
background : # edf4fa ;
color : # 4 e6479 ;
border - bottom - color : # d5e1eb ;
font - size : 12.5 px ;
font - weight : 600 ;
letter - spacing : 0.4 px ;
white - space : nowrap ;
}
. table - scroll tbody tr : hover td {
background : # f1f7fb ;
}
. table - scroll tbody tr : last - child td {
border - bottom : 0 ;
}
. empty - tip {
padding : 44 px 24 px ;
text - align : center ;
color : # 8293 a5 ;
font - size : 13.5 px ;
}
. thumb - wrap {
display : flex ;
flex - wrap : wrap ;
gap : 6 px ;
align - items : center ;
}
. thumb {
width : 56 px ;
height : 56 px ;
object - fit : cover ;
border - radius : 6 px ;
border : 1 px solid # d8e3ee ;
cursor : zoom - in ;
}
. empty - mini {
color : # 8293 a5 ;
font - size : 12.5 px ;
}
. pagination {
display : flex ;
flex - wrap : wrap ;
align - items : center ;
gap : 8 px ;
margin - top : 18 px ;
color : # 5 b6f83 ;
font - size : 13 px ;
}
. pagination button {
min - height : 30 px ;
padding : 4 px 12 px ;
border : 1 px solid # c7d7e5 ;
border - radius : 8 px ;
background : # ffffff ;
color : # 5 b6f83 ;
font - family : inherit ;
font - size : 13 px ;
cursor : pointer ;
}
. pagination button : hover : not ( : disabled ) {
background : # edf5fb ;
border - color : # 95 b1cb ;
color : # 2 f5d8b ;
}
. pagination button : disabled {
background : # eef3f7 ;
color : # 9 baaba ;
cursor : not - allowed ;
}
. page - total {
margin - right : 4 px ;
}
. page - jump {
display : inline - flex ;
align - items : center ;
gap : 6 px ;
}
. page - jump input {
width : 56 px ;
min - height : 30 px ;
padding : 4 px 8 px ;
border : 1 px solid # cbd9e6 ;
border - radius : 8 px ;
background : # f8fbfd ;
color : # 24384 d ;
font - size : 13 px ;
font - family : inherit ;
}
. modal - mask {
position : fixed ;
inset : 0 ;
z - index : 3000 ;
display : flex ;
align - items : center ;
justify - content : center ;
padding : 40 px 20 px ;
background : rgba ( 45 , 66 , 86 , 0.36 ) ;
backdrop - filter : blur ( 6 px ) ;
}
. modal {
display : flex ;
flex - direction : column ;
width : min ( 880 px , 100 % ) ;
max - height : 84 vh ;
padding : 20 px 22 px ;
border : 1 px solid # c7d7e5 ;
border - radius : 14 px ;
background : # ffffff ;
box - shadow : 0 22 px 60 px rgba ( 39 , 67 , 94 , 0.2 ) ;
}
. modal - head {
display : flex ;
align - items : center ;
justify - content : space - between ;
}
. modal - head h3 {
margin : 0 ;
}
. modal - close {
width : 30 px ;
height : 30 px ;
border : 1 px solid transparent ;
border - radius : 9 px ;
background : none ;
color : # 5 b6f83 ;
font - size : 20 px ;
line - height : 1 ;
cursor : pointer ;
}
. modal - close : hover {
border - color : # cbd9e6 ;
background : # eef4fa ;
color : # 24384 d ;
}
. preview - meta {
display : flex ;
align - items : center ;
gap : 8 px ;
margin : 12 px 0 10 px ;
color : # 5 b6f83 ;
font - size : 12.5 px ;
}
. preview - grid {
flex : 1 ;
min - height : 0 ;
overflow - y : auto ;
display : flex ;
flex - direction : column ;
gap : 12 px ;
border : 1 px solid # dbe5ee ;
border - radius : 10 px ;
padding : 12 px ;
background : # f8fbfd ;
}
. preview - grid a {
display : block ;
}
. preview - grid img {
display : block ;
width : 100 % ;
border - radius : 6 px ;
border : 1 px solid # d8e3ee ;
background : # fff ;
}
. modal - empty {
padding : 48 px 20 px ;
text - align : center ;
color : # 8293 a5 ;
}
. modal - foot {
display : flex ;
justify - content : flex - end ;
gap : 8 px ;
margin - top : 14 px ;
}
< / style >