|
- <template>
- <div>
- <AppHero
- kicker="MY CREATIONS"
- title="我的作品"
- desc="AI 取名、动漫头像、证件照等全部创作记录,生成中的作品会自动刷新。"
- />
-
- <div class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8 lg:py-8">
- <!-- 未登录 -->
- <div v-if="!isLogin" class="rounded-2xl bg-[var(--c-ffffff)] py-20 text-center shadow-[0_8px_28px_var(--sh-34-68-112-50)]">
- <UIcon name="i-lucide-image" class="mx-auto mb-4 size-12 text-[var(--c-a0aabb)]" />
- <div class="text-base font-extrabold text-[var(--c-172033)]">登录后查看作品</div>
- <div class="mx-auto mt-2 max-w-sm text-sm text-[var(--c-8a95a6)]">登录后会同步你的作品、点数和创作记录。</div>
- <button
- class="mt-5 h-10 rounded-full bg-[var(--c-0b8cff)] px-7 text-sm font-extrabold text-white transition-colors hover:bg-[var(--c-0a7ce0)]"
- @click="showLogin = true"
- >
- 去登录
- </button>
- </div>
-
- <template v-else>
- <!-- 分类 Tabs(吸顶) -->
- <div class="sticky top-16 z-20 -mx-4 mb-5 flex gap-2.5 overflow-x-auto border-b border-[var(--c-eef1f6)] bg-[var(--c-f3f7fb)]/95 px-4 py-3 backdrop-blur sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8">
- <button
- v-for="item in tabs"
- :key="item.key"
- type="button"
- class="flex h-9 shrink-0 items-center gap-2 rounded-full px-4 transition-colors"
- :class="activeTab === item.key ? 'bg-[var(--c-eaf5ff)] text-[var(--c-0b8cff)]' : 'bg-[var(--c-ffffff)] text-[var(--c-687386)] hover:bg-[var(--c-f4f7fb)]'"
- @click="changeTab(item.key)"
- >
- <span class="text-sm font-extrabold">{{ item.name }}</span>
- <span
- class="min-w-[20px] rounded-full px-1.5 py-0.5 text-center text-[10px] font-extrabold"
- :class="activeTab === item.key ? 'bg-[var(--c-ffffff)] text-[var(--c-0b8cff)]' : 'bg-[var(--c-f4f7fb)] text-[var(--c-8a95a8)]'"
- >{{ counts[item.key] || 0 }}</span>
- </button>
- </div>
-
- <!-- 作品网格 -->
- <div v-if="list.length" class="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
- <div
- v-for="item in list"
- :key="item.id"
- class="group cursor-pointer overflow-hidden rounded-2xl bg-[var(--c-ffffff)] shadow-[0_10px_28px_var(--sh-34-68-112-60)] transition-shadow hover:shadow-[0_14px_34px_var(--sh-34-68-112-120)]"
- @click="openWork(item)"
- >
- <!-- AI 取名卡片 -->
- <div
- v-if="item.work_type === 'ainame'"
- class="min-h-[190px] bg-gradient-to-b from-[var(--c-f8fbff)] to-[var(--c-ffffff)] p-3.5"
- >
- <div class="flex items-center justify-between gap-2">
- <span class="rounded-full bg-[var(--c-eaf5ff)] px-2 py-0.5 text-[10px] font-extrabold text-[var(--c-0b8cff)]">AI取名</span>
- <span class="rounded-full px-2 py-0.5 text-[10px] font-extrabold" :class="statusClass(item)">
- {{ item.status_text }}
- </span>
- </div>
- <div class="mt-3 flex min-h-[76px] flex-wrap content-start gap-1.5">
- <span
- v-for="name in item.names"
- :key="name"
- class="rounded-full bg-[var(--c-eaf5ff)] px-2.5 py-1 text-xs font-extrabold text-[var(--c-0b8cff)]"
- >{{ name }}</span>
- <span v-if="!item.names || !item.names.length" class="text-xs text-[var(--c-8a95a8)]">名字整理中</span>
- </div>
- <div class="mt-1 line-clamp-2 text-[11px] leading-relaxed text-[var(--c-687386)]">
- {{ item.summary || item.subtitle }}
- </div>
- </div>
-
- <!-- 图片类作品 -->
- <div v-else class="relative aspect-square overflow-hidden bg-[var(--c-eef3f8)]">
- <img
- v-if="item.cover"
- :src="item.cover"
- class="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
- :alt="typeText(item.work_type)"
- loading="lazy"
- >
- <div v-else class="flex h-full w-full flex-col items-center justify-center gap-2 text-xs font-bold text-[var(--c-8a95a8)]">
- <span v-if="Number(item.status) === 0" class="size-8 animate-spin rounded-full border-[3px] border-[var(--c-0b8cff)]/20 border-t-[var(--c-0b8cff)]" />
- <span>{{ Number(item.status) === 0 ? '生成中' : '暂无封面' }}</span>
- </div>
-
- <!-- 顶部渐变 + 标签 -->
- <div class="pointer-events-none absolute inset-x-0 top-0 h-14 bg-gradient-to-b from-black/35 to-transparent" />
- <div class="absolute inset-x-3 top-3 flex items-center justify-between gap-2">
- <span class="rounded-full bg-[var(--c-ffffff)]/90 px-2 py-0.5 text-[10px] font-extrabold text-[var(--c-172033)]">
- {{ typeText(item.work_type) }}
- </span>
- <span class="rounded-full px-2 py-0.5 text-[10px] font-extrabold" :class="statusClass(item)">
- {{ item.status_text }}
- </span>
- </div>
-
- <!-- 生成中遮罩 -->
- <div
- v-if="Number(item.status) === 0"
- class="absolute inset-0 flex flex-col items-center justify-center gap-3 text-xs font-bold text-white"
- style="background-color: var(--sh-9-16-28-420)"
- >
- <span class="size-8 animate-spin rounded-full border-[3px] border-white/35 border-t-white" />
- <span>正在创作</span>
- </div>
- </div>
-
- <div class="px-3.5 py-3">
- <div class="text-[11px] text-[var(--c-a0a8b8)]">{{ formatTime(item.add_time) }}</div>
- </div>
- </div>
- </div>
-
- <!-- 空态 -->
- <div v-if="!list.length && !loading" class="rounded-2xl bg-[var(--c-ffffff)] py-20 text-center shadow-[0_8px_28px_var(--sh-34-68-112-50)]">
- <div class="mx-auto mb-5 grid size-14 grid-cols-2 gap-1.5 rounded-2xl bg-[var(--c-ffffff)] p-2.5 shadow-[0_10px_28px_var(--sh-34-68-112-60)]">
- <span class="rounded-md bg-[var(--c-dceeff)]" />
- <span class="rounded-md bg-[var(--c-b9ddff)]" />
- <span class="rounded-md bg-[var(--c-b9ddff)]" />
- <span class="rounded-md bg-[var(--c-dceeff)]" />
- </div>
- <div class="text-base font-extrabold text-[var(--c-172033)]">还没有作品</div>
- <div class="mx-auto mt-2 max-w-sm text-sm text-[var(--c-7f8896)]">去工具箱生成第一张头像、照片或名字方案。</div>
- <NuxtLink
- to="/"
- class="mt-5 inline-flex h-10 items-center rounded-full bg-[var(--c-0b8cff)] px-7 text-sm font-extrabold text-white transition-colors hover:bg-[var(--c-0a7ce0)]"
- >
- 去创作
- </NuxtLink>
- </div>
-
- <!-- 加载 / 加载更多 -->
- <div v-if="loading" class="py-8 text-center text-sm text-[var(--c-9aa4b2)]">
- {{ list.length ? '加载更多...' : '加载中...' }}
- </div>
- <div v-else-if="hasMore" class="mt-8 flex justify-center">
- <button
- class="rounded-full bg-[var(--c-eef7ff)] px-7 py-2.5 text-sm font-bold text-[var(--c-0b8cff)] transition-colors hover:bg-[var(--c-e0f0ff)]"
- @click="loadMore"
- >
- 加载更多
- </button>
- </div>
- <div v-else-if="list.length" class="py-8 text-center text-sm text-[var(--c-9aa4b2)]">没有更多了</div>
- </template>
- </div>
-
- <!-- 作品详情弹窗 -->
- <WorkDetailModal v-model="detailOpen" :work="currentWork" />
- </div>
- </template>
-
- <script setup>
- import dayjs from 'dayjs'
-
- useSeoMeta({
- title: '我的作品 - AI在线',
- description: '查看 AI 取名、动漫头像、证件照等全部创作记录',
- })
-
- const { isLogin, showLogin } = useUser()
- const { post } = useApi()
- const toast = useToast()
-
- const tabs = [
- { key: 'all', name: '全部' },
- { key: 'ainame', name: 'AI取名' },
- { key: 'anime', name: '动漫头像' },
- { key: 'photo', name: '证件照' },
- { key: 'oldphoto', name: '老照片' },
- { key: 'portrait', name: '形象照' },
- ]
-
- const activeTab = ref('all')
- const list = ref([])
- const total = ref(0)
- const page = ref(1)
- const pageSize = 20
- const loading = ref(false)
- const counts = ref({ all: 0, ainame: 0, anime: 0, image: 0, photo: 0, oldphoto: 0, portrait: 0 })
- let timer = null
-
- const hasMore = computed(() => list.value.length < total.value)
-
- function formatTime(value) {
- if (!value) return ''
- return dayjs(Number(value) * 1000).format('YYYY-MM-DD HH:mm')
- }
-
- async function fetchList(reset = false, silent = false) {
- if (!isLogin.value) {
- list.value = []
- total.value = 0
- return
- }
- if (loading.value) return
- loading.value = true
- if (reset) page.value = 1
- try {
- const res = await post('/work/getlist', {
- type: activeTab.value,
- page_no: page.value,
- page_size: pageSize,
- })
- const rows = res.list || []
- const data = res.data || {}
- list.value = reset ? rows : list.value.concat(rows)
- total.value = Number(data.total || rows.length)
- counts.value = normalizeCounts(data.counts || {})
- scheduleRefresh()
- } catch (e) {
- if (!silent) toast.add({ title: e.message || '作品加载失败', color: 'error' })
- } finally {
- loading.value = false
- }
- }
-
- function normalizeCounts(data = {}) {
- return {
- all: Number(data.all || 0),
- ainame: Number(data.ainame || 0),
- anime: Number(data.anime || 0),
- image: Number(data.image || 0),
- photo: Number(data.photo || 0),
- oldphoto: Number(data.oldphoto || 0),
- portrait: Number(data.portrait || 0),
- }
- }
-
- // 有生成中的作品时自动轮询刷新
- function scheduleRefresh() {
- clearTimer()
- if (list.value.some(item => Number(item.status) === 0)) {
- timer = setTimeout(() => fetchList(true, true), 3000)
- }
- }
- function clearTimer() {
- if (timer) clearTimeout(timer)
- timer = null
- }
-
- function changeTab(key) {
- if (activeTab.value === key) return
- activeTab.value = key
- fetchList(true)
- }
-
- function loadMore() {
- if (loading.value || !hasMore.value) return
- page.value += 1
- fetchList()
- }
-
- // 点击作品用弹窗查看详情(不再跳回创作页)
- const detailOpen = ref(false)
- const currentWork = ref({})
-
- function openWork(item) {
- // 生成中的作品也可打开,弹窗内会显示进度状态
- currentWork.value = item
- detailOpen.value = true
- }
-
- function statusClass(item) {
- const status = Number(item.status)
- if (status === 0) return 'bg-[var(--c-fff5e8)] text-[var(--c-ff8a00)]'
- if (status < 0) return 'bg-[var(--c-fff0ef)] text-[var(--c-f04438)]'
- return 'bg-[var(--c-ecfbf3)] text-[var(--c-16a15d)]'
- }
-
- function typeText(type) {
- const map = {
- ainame: 'AI取名',
- anime: '动漫头像',
- image: 'AI作品',
- photo: '证件照',
- oldphoto: '老照片',
- portrait: '形象照',
- }
- return map[type] || 'AI作品'
- }
-
- onMounted(() => fetchList(true))
- onUnmounted(() => clearTimer())
- watch(isLogin, (v) => { if (v) fetchList(true) })
- </script>
|