|
|
|
@@ -1,49 +1,76 @@
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
/** 历史生成记录页(module 13 task 259 对齐 admin panel-history):指定用户下拉 + 起止时间筛选、类型中文、≤3 缩略图、大图预览、空态。 */
|
|
|
|
|
import { formatDateTime } from '@/utils/datetime'
|
|
|
|
|
/** 历史生成记录页:分页 + 时间/用户筛选 + 结果大图预览。 */
|
|
|
|
|
import { onMounted, reactive, ref } from 'vue'
|
|
|
|
|
import { ElMessage } from 'element-plus'
|
|
|
|
|
import { fetchHistoryList } from './history-api.ts'
|
|
|
|
|
import { computed, onMounted, reactive, ref } from 'vue'
|
|
|
|
|
import { fetchHistoryList, fetchHistoryUserOptions } from './history-api.ts'
|
|
|
|
|
import type { HistoryUserOption } from './history-user-option.ts'
|
|
|
|
|
import type { HistoryRecordItem } from './history-dto.ts'
|
|
|
|
|
import { historyEmptyText, historyErrorText } from './history-feedback.ts'
|
|
|
|
|
import { historyPanelTypeLabel } from './history-type.ts'
|
|
|
|
|
|
|
|
|
|
const loading = ref(false)
|
|
|
|
|
const userLoading = ref(false)
|
|
|
|
|
const rows = ref<HistoryRecordItem[]>([])
|
|
|
|
|
const total = ref(0)
|
|
|
|
|
const page = ref(1)
|
|
|
|
|
const pageSize = 15
|
|
|
|
|
const filter = reactive({ userId: '', dateRange: [] as string[] })
|
|
|
|
|
const userOptions = ref<HistoryUserOption[]>([])
|
|
|
|
|
const errorText = ref('')
|
|
|
|
|
const filter = reactive({ userId: '', timeStart: '', timeEnd: '' })
|
|
|
|
|
|
|
|
|
|
const previewVisible = ref(false)
|
|
|
|
|
const previewItem = ref<HistoryRecordItem | null>(null)
|
|
|
|
|
|
|
|
|
|
function imageUrls(item: HistoryRecordItem): string[] {
|
|
|
|
|
const urls: string[] = []
|
|
|
|
|
for (const url of item.resultUrls || []) urls.push(url)
|
|
|
|
|
if (item.longImageUrl) urls.push(item.longImageUrl)
|
|
|
|
|
return urls
|
|
|
|
|
const hasFilter = computed(() => Boolean(filter.userId || filter.timeStart || filter.timeEnd))
|
|
|
|
|
const emptyText = computed(() => historyEmptyText({ hasFilter: hasFilter.value, total: total.value }))
|
|
|
|
|
|
|
|
|
|
/** 预览图顺序:长图在前 + 结果图去重(对齐 admin 缩略图 [long,...result])。 */
|
|
|
|
|
function previewUrls(item: HistoryRecordItem): string[] {
|
|
|
|
|
const out: string[] = []
|
|
|
|
|
const seen = new Set<string>()
|
|
|
|
|
const push = (url: string) => {
|
|
|
|
|
const clean = typeof url === 'string' ? url.trim() : ''
|
|
|
|
|
if (!clean || seen.has(clean)) return
|
|
|
|
|
seen.add(clean)
|
|
|
|
|
out.push(clean)
|
|
|
|
|
}
|
|
|
|
|
if (item.longImageUrl) push(item.longImageUrl)
|
|
|
|
|
for (const url of item.resultUrls || []) push(url)
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function load() {
|
|
|
|
|
loading.value = true
|
|
|
|
|
errorText.value = ''
|
|
|
|
|
try {
|
|
|
|
|
const userId = filter.userId.trim()
|
|
|
|
|
const result = await fetchHistoryList({
|
|
|
|
|
page: page.value,
|
|
|
|
|
pageSize,
|
|
|
|
|
userId: userId ? Number(userId) : undefined,
|
|
|
|
|
timeStart: filter.dateRange?.[0] || '',
|
|
|
|
|
timeEnd: filter.dateRange?.[1] || '',
|
|
|
|
|
userId: filter.userId ? Number(filter.userId) : undefined,
|
|
|
|
|
timeStart: filter.timeStart || undefined,
|
|
|
|
|
timeEnd: filter.timeEnd || undefined,
|
|
|
|
|
})
|
|
|
|
|
rows.value = result.items
|
|
|
|
|
total.value = result.total
|
|
|
|
|
page.value = result.page
|
|
|
|
|
} catch (error) {
|
|
|
|
|
ElMessage.error(error instanceof Error ? error.message : '历史记录加载失败')
|
|
|
|
|
errorText.value = historyErrorText(error)
|
|
|
|
|
} finally {
|
|
|
|
|
loading.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadUserOptions() {
|
|
|
|
|
userLoading.value = true
|
|
|
|
|
try {
|
|
|
|
|
userOptions.value = await fetchHistoryUserOptions()
|
|
|
|
|
} catch {
|
|
|
|
|
userOptions.value = []
|
|
|
|
|
} finally {
|
|
|
|
|
userLoading.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function apply() {
|
|
|
|
|
page.value = 1
|
|
|
|
|
load()
|
|
|
|
@@ -51,17 +78,30 @@ function apply() {
|
|
|
|
|
|
|
|
|
|
function reset() {
|
|
|
|
|
filter.userId = ''
|
|
|
|
|
filter.dateRange = []
|
|
|
|
|
filter.timeStart = ''
|
|
|
|
|
filter.timeEnd = ''
|
|
|
|
|
page.value = 1
|
|
|
|
|
load()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function retry() {
|
|
|
|
|
load()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function changePage(next: number) {
|
|
|
|
|
page.value = next
|
|
|
|
|
load()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openPreview(item: HistoryRecordItem) {
|
|
|
|
|
previewItem.value = item
|
|
|
|
|
previewVisible.value = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onMounted(load)
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
load()
|
|
|
|
|
loadUserOptions()
|
|
|
|
|
})
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
@@ -73,15 +113,34 @@ onMounted(load)
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<el-card shadow="never" style="margin-bottom: 14px">
|
|
|
|
|
<el-card shadow="never" class="filter-card">
|
|
|
|
|
<div class="filter-grid">
|
|
|
|
|
<div class="f-item">
|
|
|
|
|
<label>用户ID</label>
|
|
|
|
|
<el-input v-model="filter.userId" placeholder="按用户ID筛选" clearable @keyup.enter="apply" />
|
|
|
|
|
<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 wide">
|
|
|
|
|
<label>生成时间</label>
|
|
|
|
|
<el-date-picker v-model="filter.dateRange" type="daterange" range-separator="至" start-placeholder="开始" end-placeholder="结束" value-format="YYYY-MM-DD" unlink-panels />
|
|
|
|
|
<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>
|
|
|
|
@@ -91,28 +150,45 @@ onMounted(load)
|
|
|
|
|
</el-card>
|
|
|
|
|
|
|
|
|
|
<el-card shadow="never">
|
|
|
|
|
<el-table v-loading="loading" :data="rows" stripe border>
|
|
|
|
|
<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" width="90" />
|
|
|
|
|
<el-table-column prop="username" label="用户" width="140" />
|
|
|
|
|
<el-table-column prop="panelType" label="记录类型" min-width="150" />
|
|
|
|
|
<el-table-column label="结果数" width="90" align="center">
|
|
|
|
|
<template #default="{ row }">{{ (row as HistoryRecordItem).resultUrls.length }}</template>
|
|
|
|
|
<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.createdAt) }}</template>
|
|
|
|
|
<template #default="{ row }">{{ formatDateTime((row as HistoryRecordItem).createdAt) }}</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column label="缩略图" min-width="200">
|
|
|
|
|
<el-table-column label="结果预览" min-width="220">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<el-image
|
|
|
|
|
v-for="(url, index) in ((row as HistoryRecordItem).resultUrls.slice(0, 4))"
|
|
|
|
|
:key="index"
|
|
|
|
|
:src="url"
|
|
|
|
|
:preview-src-list="(row as HistoryRecordItem).resultUrls"
|
|
|
|
|
preview-teleported
|
|
|
|
|
fit="cover"
|
|
|
|
|
style="width: 56px; height: 56px; margin-right: 6px; border-radius: 4px"
|
|
|
|
|
/>
|
|
|
|
|
<span v-if="(row as HistoryRecordItem).resultUrls.length > 4" class="dim">+{{ (row as HistoryRecordItem).resultUrls.length - 4 }}</span>
|
|
|
|
|
<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="操作" width="110" fixed="right">
|
|
|
|
@@ -123,18 +199,27 @@ onMounted(load)
|
|
|
|
|
</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="(p: number) => { page = p; load() }" />
|
|
|
|
|
<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>{{ previewItem.panelType }}</span>
|
|
|
|
|
<span>{{ historyPanelTypeLabel(previewItem.panelType) }}</span>
|
|
|
|
|
<span>{{ formatDateTime(previewItem.createdAt) }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-if="previewItem && imageUrls(previewItem).length" class="preview-grid">
|
|
|
|
|
<el-image v-for="(url, index) in imageUrls(previewItem)" :key="index" :src="url" :preview-src-list="imageUrls(previewItem)" :initial-index="index" preview-teleported fit="contain" style="max-width: 100%" class="preview-img" />
|
|
|
|
|
<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>
|
|
|
|
@@ -142,11 +227,13 @@ onMounted(load)
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.filter-card { margin-bottom: 14px; }
|
|
|
|
|
.filter-grid { display: flex; flex-wrap: wrap; gap: 12px 18px; }
|
|
|
|
|
.f-item { display: flex; flex-direction: column; gap: 6px; width: 180px; }
|
|
|
|
|
.f-item { display: flex; flex-direction: column; gap: 6px; width: 190px; }
|
|
|
|
|
.f-item label { color: var(--el-text-color-secondary); font-size: 12px; }
|
|
|
|
|
.f-item.wide { width: 320px; }
|
|
|
|
|
.f-item.btn-row { flex-direction: row; align-items: flex-end; gap: 8px; width: auto; }
|
|
|
|
|
.thumbs { display: flex; align-items: center; gap: 6px; }
|
|
|
|
|
.thumb { width: 56px; height: 56px; border-radius: 4px; border: 1px solid var(--el-border-color-lighter); flex: none; }
|
|
|
|
|
.table-footer { display: flex; justify-content: space-between; align-items: center; padding-top: 14px; }
|
|
|
|
|
.table-footer span { color: var(--el-text-color-secondary); font-size: 12.5px; }
|
|
|
|
|
.table-footer b { color: var(--el-text-color-primary); }
|
|
|
|
|