Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 
 

240 linhas
9.1 KiB

  1. <template>
  2. <Teleport to="body">
  3. <Transition name="wh-fade">
  4. <div v-if="modelValue" class="fixed inset-0 z-[110]">
  5. <div class="absolute inset-0 bg-[var(--c-0f172a)]/45 backdrop-blur-sm" @click="close" />
  6. <!-- PC:右侧抽屉;移动端:底部上滑面板(响应式) -->
  7. <Transition name="wh-panel">
  8. <aside
  9. v-if="modelValue"
  10. 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"
  11. >
  12. <!-- 头部 -->
  13. <div class="flex shrink-0 items-center justify-between gap-3 border-b border-[var(--c-eef2f7)] px-5 py-4">
  14. <div>
  15. <h3 class="text-base font-extrabold text-[var(--c-172033)]">历史记录</h3>
  16. <div class="mt-0.5 text-xs text-[var(--c-8a97a8)]">{{ typeLabel }} · 共 {{ total }} 条</div>
  17. </div>
  18. <div class="flex items-center gap-1">
  19. <button
  20. 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)]"
  21. title="刷新"
  22. @click="fetchList(true)"
  23. >
  24. <UIcon name="i-lucide-refresh-cw" class="size-4" :class="loading && 'animate-spin'" />
  25. </button>
  26. <button
  27. 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)]"
  28. @click="close"
  29. >
  30. <UIcon name="i-lucide-x" class="size-5" />
  31. </button>
  32. </div>
  33. </div>
  34. <!-- 列表 -->
  35. <div class="flex-1 overflow-y-auto px-4 py-4">
  36. <!-- 未登录 -->
  37. <div v-if="!isLogin" class="py-16 text-center">
  38. <UIcon name="i-lucide-lock" class="mx-auto mb-3 size-10 text-[var(--c-a0aabb)]" />
  39. <div class="text-sm text-[var(--c-8a95a6)]">登录后查看历史记录</div>
  40. </div>
  41. <template v-else>
  42. <div v-if="list.length" class="space-y-2.5">
  43. <button
  44. v-for="item in list"
  45. :key="item.id"
  46. type="button"
  47. 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)]"
  48. @click="select(item)"
  49. >
  50. <!-- 缩略图 / 名字预览 -->
  51. <div class="size-14 shrink-0 overflow-hidden rounded-xl bg-[var(--c-eef3f8)]">
  52. <img
  53. v-if="item.cover"
  54. :src="item.cover"
  55. class="size-full object-cover"
  56. :alt="typeLabel"
  57. loading="lazy"
  58. >
  59. <div v-else class="flex size-full items-center justify-center text-[11px] font-bold text-[var(--c-8a95a8)]">
  60. <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)]" />
  61. <span v-else-if="item.names && item.names.length" class="px-1 text-center text-[var(--c-0b8cff)]">{{ item.names[0] }}</span>
  62. <span v-else>无图</span>
  63. </div>
  64. </div>
  65. <!-- 文案 -->
  66. <div class="min-w-0 flex-1">
  67. <div class="flex items-center gap-2">
  68. <span class="truncate text-sm font-bold text-[var(--c-172033)]">
  69. {{ item.names?.length ? item.names.join('、') : (item.summary || item.subtitle || typeLabel) }}
  70. </span>
  71. </div>
  72. <div class="mt-1.5 flex items-center gap-2">
  73. <span class="rounded-full px-2 py-0.5 text-[10px] font-extrabold" :class="statusClass(item)">
  74. {{ item.status_text }}
  75. </span>
  76. <span class="text-[11px] text-[var(--c-a0a8b8)]">{{ formatTime(item.add_time) }}</span>
  77. </div>
  78. </div>
  79. <UIcon name="i-lucide-chevron-right" class="size-4 shrink-0 text-[var(--c-c3cddb)]" />
  80. </button>
  81. </div>
  82. <!-- 空态 -->
  83. <div v-else-if="!loading" class="py-16 text-center">
  84. <UIcon name="i-lucide-history" class="mx-auto mb-3 size-10 text-[var(--c-a0aabb)]" />
  85. <div class="text-sm font-bold text-[var(--c-172033)]">暂无历史记录</div>
  86. <div class="mt-1 text-xs text-[var(--c-8a95a6)]">生成后可在这里回看</div>
  87. </div>
  88. <!-- 加载 / 更多 -->
  89. <div v-if="loading" class="py-5 text-center text-xs text-[var(--c-9aa4b2)]">
  90. {{ list.length ? '加载更多...' : '加载中...' }}
  91. </div>
  92. <div v-else-if="hasMore" class="mt-4 flex justify-center">
  93. <button
  94. 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)]"
  95. @click="loadMore"
  96. >
  97. 加载更多
  98. </button>
  99. </div>
  100. </template>
  101. </div>
  102. <!-- 底部:查看全部 -->
  103. <div class="shrink-0 border-t border-[var(--c-eef2f7)] px-5 py-3">
  104. <NuxtLink
  105. to="/works"
  106. class="flex items-center justify-center gap-1 text-sm font-bold text-[var(--c-0b8cff)] hover:underline"
  107. @click="close"
  108. >
  109. 查看我的全部作品
  110. <UIcon name="i-lucide-arrow-right" class="size-4" />
  111. </NuxtLink>
  112. </div>
  113. </aside>
  114. </Transition>
  115. </div>
  116. </Transition>
  117. </Teleport>
  118. </template>
  119. <script setup>
  120. import dayjs from 'dayjs'
  121. const props = defineProps({
  122. modelValue: { type: Boolean, default: false },
  123. /** 作品类型:ainame / anime / photo / oldphoto / portrait */
  124. type: { type: String, default: 'all' },
  125. })
  126. const emit = defineEmits(['update:modelValue', 'select'])
  127. const { post } = useApi()
  128. const { isLogin } = useUser()
  129. const toast = useToast()
  130. const list = ref([])
  131. const total = ref(0)
  132. const page = ref(1)
  133. const pageSize = 10
  134. const loading = ref(false)
  135. let timer = null
  136. const hasMore = computed(() => list.value.length < total.value)
  137. const typeLabel = computed(() => {
  138. const map = { ainame: 'AI 取名', anime: '动漫头像', photo: '证件照', oldphoto: '老照片', portrait: '形象照' }
  139. return map[props.type] || '全部作品'
  140. })
  141. function formatTime(value) {
  142. return value ? dayjs(Number(value) * 1000).format('MM-DD HH:mm') : ''
  143. }
  144. function statusClass(item) {
  145. const s = Number(item.status)
  146. if (s === 0) return 'bg-[var(--c-fff5e8)] text-[var(--c-ff8a00)]'
  147. if (s < 0) return 'bg-[var(--c-fff0ef)] text-[var(--c-f04438)]'
  148. return 'bg-[var(--c-ecfbf3)] text-[var(--c-16a15d)]'
  149. }
  150. async function fetchList(reset = false, silent = false) {
  151. if (!isLogin.value || loading.value) return
  152. loading.value = true
  153. if (reset) page.value = 1
  154. try {
  155. const res = await post('/work/getlist', {
  156. type: props.type,
  157. page_no: page.value,
  158. page_size: pageSize,
  159. })
  160. const rows = res.list || []
  161. list.value = reset ? rows : list.value.concat(rows)
  162. total.value = Number(res.data?.total || rows.length)
  163. scheduleRefresh()
  164. } catch (e) {
  165. if (!silent) toast.add({ title: e.message || '历史记录加载失败', color: 'error' })
  166. } finally {
  167. loading.value = false
  168. }
  169. }
  170. // 有生成中的记录时自动刷新
  171. function scheduleRefresh() {
  172. clearTimer()
  173. if (props.modelValue && list.value.some(i => Number(i.status) === 0)) {
  174. timer = setTimeout(() => fetchList(true, true), 3000)
  175. }
  176. }
  177. function clearTimer() {
  178. if (timer) clearTimeout(timer)
  179. timer = null
  180. }
  181. function loadMore() {
  182. if (loading.value || !hasMore.value) return
  183. page.value += 1
  184. fetchList()
  185. }
  186. function select(item) {
  187. emit('select', item)
  188. }
  189. function close() {
  190. emit('update:modelValue', false)
  191. }
  192. watch(() => props.modelValue, (open) => {
  193. if (open) fetchList(true)
  194. else clearTimer()
  195. })
  196. onUnmounted(() => clearTimer())
  197. // 供父组件在生成成功后主动刷新
  198. defineExpose({ refresh: () => fetchList(true, true) })
  199. </script>
  200. <style scoped>
  201. .wh-fade-enter-active,
  202. .wh-fade-leave-active { transition: opacity 0.2s ease; }
  203. .wh-fade-enter-from,
  204. .wh-fade-leave-to { opacity: 0; }
  205. .wh-panel-enter-active,
  206. .wh-panel-leave-active { transition: transform 0.24s ease; }
  207. .wh-panel-enter-from,
  208. .wh-panel-leave-to { transform: translateY(100%); }
  209. @media (min-width: 640px) {
  210. .wh-panel-enter-from,
  211. .wh-panel-leave-to { transform: translateX(100%); }
  212. }
  213. </style>