|
- <template>
- <Teleport to="body">
- <Transition name="wh-fade">
- <div v-if="modelValue" class="fixed inset-0 z-[110]">
- <div class="absolute inset-0 bg-[var(--c-0f172a)]/45 backdrop-blur-sm" @click="close" />
-
- <!-- PC:右侧抽屉;移动端:底部上滑面板(响应式) -->
- <Transition name="wh-panel">
- <aside
- v-if="modelValue"
- class="absolute bottom-0 left-0 right-0 flex max-h-[86vh] flex-col rounded-t-2xl bg-[var(--c-ffffff)] shadow-2xl sm:left-auto sm:top-0 sm:h-full sm:max-h-none sm:w-[420px] sm:rounded-none"
- >
- <!-- 头部 -->
- <div class="flex shrink-0 items-center justify-between gap-3 border-b border-[var(--c-eef2f7)] px-5 py-4">
- <div>
- <h3 class="text-base font-extrabold text-[var(--c-172033)]">历史记录</h3>
- <div class="mt-0.5 text-xs text-[var(--c-8a97a8)]">{{ typeLabel }} · 共 {{ total }} 条</div>
- </div>
- <div class="flex items-center gap-1">
- <button
- class="flex size-8 items-center justify-center rounded-full text-[var(--c-a0aabb)] transition-colors hover:bg-[var(--c-f1f5fa)] hover:text-[var(--c-172033)]"
- title="刷新"
- @click="fetchList(true)"
- >
- <UIcon name="i-lucide-refresh-cw" class="size-4" :class="loading && 'animate-spin'" />
- </button>
- <button
- class="flex size-8 items-center justify-center rounded-full text-[var(--c-a0aabb)] transition-colors hover:bg-[var(--c-f1f5fa)] hover:text-[var(--c-172033)]"
- @click="close"
- >
- <UIcon name="i-lucide-x" class="size-5" />
- </button>
- </div>
- </div>
-
- <!-- 列表 -->
- <div class="flex-1 overflow-y-auto px-4 py-4">
- <!-- 未登录 -->
- <div v-if="!isLogin" class="py-16 text-center">
- <UIcon name="i-lucide-lock" class="mx-auto mb-3 size-10 text-[var(--c-a0aabb)]" />
- <div class="text-sm text-[var(--c-8a95a6)]">登录后查看历史记录</div>
- </div>
-
- <template v-else>
- <div v-if="list.length" class="space-y-2.5">
- <button
- v-for="item in list"
- :key="item.id"
- type="button"
- class="flex w-full items-center gap-3 rounded-2xl border border-[var(--c-eef2f7)] p-3 text-left transition-colors hover:border-[var(--c-0b8cff)]/40 hover:bg-[var(--c-f7fbff)]"
- @click="select(item)"
- >
- <!-- 缩略图 / 名字预览 -->
- <div class="size-14 shrink-0 overflow-hidden rounded-xl bg-[var(--c-eef3f8)]">
- <img
- v-if="item.cover"
- :src="item.cover"
- class="size-full object-cover"
- :alt="typeLabel"
- loading="lazy"
- >
- <div v-else class="flex size-full items-center justify-center text-[11px] font-bold text-[var(--c-8a95a8)]">
- <span v-if="Number(item.status) === 0" class="size-5 animate-spin rounded-full border-2 border-[var(--c-0b8cff)]/25 border-t-[var(--c-0b8cff)]" />
- <span v-else-if="item.names && item.names.length" class="px-1 text-center text-[var(--c-0b8cff)]">{{ item.names[0] }}</span>
- <span v-else>无图</span>
- </div>
- </div>
-
- <!-- 文案 -->
- <div class="min-w-0 flex-1">
- <div class="flex items-center gap-2">
- <span class="truncate text-sm font-bold text-[var(--c-172033)]">
- {{ item.names?.length ? item.names.join('、') : (item.summary || item.subtitle || typeLabel) }}
- </span>
- </div>
- <div class="mt-1.5 flex items-center gap-2">
- <span class="rounded-full px-2 py-0.5 text-[10px] font-extrabold" :class="statusClass(item)">
- {{ item.status_text }}
- </span>
- <span class="text-[11px] text-[var(--c-a0a8b8)]">{{ formatTime(item.add_time) }}</span>
- </div>
- </div>
-
- <UIcon name="i-lucide-chevron-right" class="size-4 shrink-0 text-[var(--c-c3cddb)]" />
- </button>
- </div>
-
- <!-- 空态 -->
- <div v-else-if="!loading" class="py-16 text-center">
- <UIcon name="i-lucide-history" class="mx-auto mb-3 size-10 text-[var(--c-a0aabb)]" />
- <div class="text-sm font-bold text-[var(--c-172033)]">暂无历史记录</div>
- <div class="mt-1 text-xs text-[var(--c-8a95a6)]">生成后可在这里回看</div>
- </div>
-
- <!-- 加载 / 更多 -->
- <div v-if="loading" class="py-5 text-center text-xs text-[var(--c-9aa4b2)]">
- {{ list.length ? '加载更多...' : '加载中...' }}
- </div>
- <div v-else-if="hasMore" class="mt-4 flex justify-center">
- <button
- class="rounded-full bg-[var(--c-eef7ff)] px-6 py-2 text-xs font-bold text-[var(--c-0b8cff)] transition-colors hover:bg-[var(--c-e0f0ff)]"
- @click="loadMore"
- >
- 加载更多
- </button>
- </div>
- </template>
- </div>
-
- <!-- 底部:查看全部 -->
- <div class="shrink-0 border-t border-[var(--c-eef2f7)] px-5 py-3">
- <NuxtLink
- to="/works"
- class="flex items-center justify-center gap-1 text-sm font-bold text-[var(--c-0b8cff)] hover:underline"
- @click="close"
- >
- 查看我的全部作品
- <UIcon name="i-lucide-arrow-right" class="size-4" />
- </NuxtLink>
- </div>
- </aside>
- </Transition>
- </div>
- </Transition>
- </Teleport>
- </template>
-
- <script setup>
- import dayjs from 'dayjs'
-
- const props = defineProps({
- modelValue: { type: Boolean, default: false },
- /** 作品类型:ainame / anime / photo / oldphoto / portrait */
- type: { type: String, default: 'all' },
- })
- const emit = defineEmits(['update:modelValue', 'select'])
-
- const { post } = useApi()
- const { isLogin } = useUser()
- const toast = useToast()
-
- const list = ref([])
- const total = ref(0)
- const page = ref(1)
- const pageSize = 10
- const loading = ref(false)
- let timer = null
-
- const hasMore = computed(() => list.value.length < total.value)
- const typeLabel = computed(() => {
- const map = { ainame: 'AI 取名', anime: '动漫头像', photo: '证件照', oldphoto: '老照片', portrait: '形象照' }
- return map[props.type] || '全部作品'
- })
-
- function formatTime(value) {
- return value ? dayjs(Number(value) * 1000).format('MM-DD HH:mm') : ''
- }
-
- function statusClass(item) {
- const s = Number(item.status)
- if (s === 0) return 'bg-[var(--c-fff5e8)] text-[var(--c-ff8a00)]'
- if (s < 0) return 'bg-[var(--c-fff0ef)] text-[var(--c-f04438)]'
- return 'bg-[var(--c-ecfbf3)] text-[var(--c-16a15d)]'
- }
-
- async function fetchList(reset = false, silent = false) {
- if (!isLogin.value || loading.value) return
- loading.value = true
- if (reset) page.value = 1
- try {
- const res = await post('/work/getlist', {
- type: props.type,
- page_no: page.value,
- page_size: pageSize,
- })
- const rows = res.list || []
- list.value = reset ? rows : list.value.concat(rows)
- total.value = Number(res.data?.total || rows.length)
- scheduleRefresh()
- } catch (e) {
- if (!silent) toast.add({ title: e.message || '历史记录加载失败', color: 'error' })
- } finally {
- loading.value = false
- }
- }
-
- // 有生成中的记录时自动刷新
- function scheduleRefresh() {
- clearTimer()
- if (props.modelValue && list.value.some(i => Number(i.status) === 0)) {
- timer = setTimeout(() => fetchList(true, true), 3000)
- }
- }
- function clearTimer() {
- if (timer) clearTimeout(timer)
- timer = null
- }
-
- function loadMore() {
- if (loading.value || !hasMore.value) return
- page.value += 1
- fetchList()
- }
-
- function select(item) {
- emit('select', item)
- }
-
- function close() {
- emit('update:modelValue', false)
- }
-
- watch(() => props.modelValue, (open) => {
- if (open) fetchList(true)
- else clearTimer()
- })
-
- onUnmounted(() => clearTimer())
-
- // 供父组件在生成成功后主动刷新
- defineExpose({ refresh: () => fetchList(true, true) })
- </script>
-
- <style scoped>
- .wh-fade-enter-active,
- .wh-fade-leave-active { transition: opacity 0.2s ease; }
- .wh-fade-enter-from,
- .wh-fade-leave-to { opacity: 0; }
-
- .wh-panel-enter-active,
- .wh-panel-leave-active { transition: transform 0.24s ease; }
- .wh-panel-enter-from,
- .wh-panel-leave-to { transform: translateY(100%); }
-
- @media (min-width: 640px) {
- .wh-panel-enter-from,
- .wh-panel-leave-to { transform: translateX(100%); }
- }
- </style>
|