You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

161 lines
4.9 KiB

  1. <template>
  2. <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 sm:py-12">
  3. <h1 class="text-2xl sm:text-3xl font-bold text-slate-900 mb-2">我的作品</h1>
  4. <p class="text-sm text-slate-500 mb-8">查看你所有的 AI 创作成果</p>
  5. <!-- Tab 分类 -->
  6. <div class="flex gap-2 overflow-x-auto pb-2 mb-6">
  7. <button
  8. v-for="tab in tabs"
  9. :key="tab.key"
  10. :class="[
  11. 'shrink-0 px-4 py-2 text-sm rounded-lg transition-colors cursor-pointer',
  12. activeTab === tab.key
  13. ? 'bg-indigo-500 text-white font-medium'
  14. : 'bg-white text-slate-600 border border-slate-200 hover:bg-slate-50',
  15. ]"
  16. @click="activeTab = tab.key"
  17. >
  18. {{ tab.label }}
  19. </button>
  20. </div>
  21. <!-- 加载中 -->
  22. <div v-if="loading" class="flex justify-center py-20">
  23. <svg class="w-8 h-8 text-indigo-500 animate-spin" fill="none" viewBox="0 0 24 24">
  24. <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
  25. <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
  26. </svg>
  27. </div>
  28. <!-- 作品网格 -->
  29. <div v-else-if="works.length > 0" class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-4">
  30. <div
  31. v-for="work in works"
  32. :key="work.id"
  33. class="group bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden hover:shadow-md transition-all duration-200"
  34. >
  35. <div class="aspect-square bg-slate-100 relative overflow-hidden">
  36. <img
  37. v-if="work.cover_url || work.url"
  38. :src="work.cover_url || work.url"
  39. :alt="work.name"
  40. class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
  41. />
  42. <div
  43. v-else-if="work.status === 'processing'"
  44. class="w-full h-full flex items-center justify-center bg-indigo-50"
  45. >
  46. <svg class="w-8 h-8 text-indigo-400 animate-spin" fill="none" viewBox="0 0 24 24">
  47. <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
  48. <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
  49. </svg>
  50. </div>
  51. <div v-else class="w-full h-full flex items-center justify-center text-slate-300 text-sm">
  52. {{ work.type_name || '作品' }}
  53. </div>
  54. </div>
  55. <div class="p-3">
  56. <p class="text-sm font-medium text-slate-800 truncate">{{ work.name || '未命名' }}</p>
  57. <p class="mt-1 text-xs text-slate-400">{{ formatDate(work.created_at) }}</p>
  58. </div>
  59. </div>
  60. </div>
  61. <!-- 空状态 -->
  62. <div v-else class="text-center py-20">
  63. <svg class="w-16 h-16 mx-auto text-slate-200 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  64. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
  65. </svg>
  66. <p class="text-sm text-slate-500">暂无作品,快去创作吧!</p>
  67. </div>
  68. <!-- 加载更多 -->
  69. <div v-if="hasMore" class="flex justify-center mt-8">
  70. <button
  71. class="px-6 py-2.5 text-sm text-indigo-600 bg-indigo-50 hover:bg-indigo-100 rounded-lg transition-colors cursor-pointer"
  72. @click="loadMore"
  73. >
  74. 加载更多
  75. </button>
  76. </div>
  77. </div>
  78. </template>
  79. <script setup>
  80. useSeoMeta({
  81. title: '我的作品 - 奇想宇宙',
  82. description: '查看AI创作的所有作品',
  83. })
  84. const { isLogin } = useUser()
  85. const { post } = useApi()
  86. const tabs = [
  87. { label: '全部', key: 'all' },
  88. { label: 'AI取名', key: 'name' },
  89. { label: '动漫头像', key: 'avatar' },
  90. { label: 'AI绘画', key: 'draw' },
  91. ]
  92. const activeTab = ref('all')
  93. const loading = ref(true)
  94. const works = ref([])
  95. const page = ref(1)
  96. const hasMore = ref(false)
  97. // 格式化日期
  98. function formatDate(dateStr) {
  99. if (!dateStr) return ''
  100. return String(dateStr).substring(0, 10)
  101. }
  102. // 加载作品
  103. async function loadWorks(reset = false) {
  104. if (reset) {
  105. page.value = 1
  106. works.value = []
  107. }
  108. loading.value = true
  109. try {
  110. const res = await post('/tool/getworks', {
  111. type: activeTab.value === 'all' ? '' : activeTab.value,
  112. page: page.value,
  113. page_size: 20,
  114. client: 1,
  115. })
  116. if (res.code === 0) {
  117. const list = res.list || res.data || []
  118. if (reset) {
  119. works.value = list
  120. } else {
  121. works.value.push(...list)
  122. }
  123. hasMore.value = list.length >= 20
  124. }
  125. } catch {
  126. // 忽略
  127. } finally {
  128. loading.value = false
  129. }
  130. }
  131. // 加载更多
  132. function loadMore() {
  133. page.value++
  134. loadWorks(false)
  135. }
  136. // 切换分类
  137. watch(activeTab, () => {
  138. loadWorks(true)
  139. })
  140. onMounted(() => {
  141. if (isLogin.value) {
  142. loadWorks(true)
  143. }
  144. })
  145. </script>