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.
 
 
 

498 regels
11 KiB

  1. <template>
  2. <view class="page">
  3. <scroll-view
  4. class="scroll"
  5. scroll-y
  6. refresher-enabled
  7. :refresher-triggered="refreshing"
  8. @refresherrefresh="refresh"
  9. @scrolltolower="loadMore"
  10. >
  11. <view class="wrap">
  12. <view class="studio-head">
  13. <view>
  14. <text class="eyebrow">AVATAR GALLERY</text>
  15. <text class="page-title">我的动漫头像</text>
  16. <text class="page-desc">保存每一次生成结果,完成后可查看大图、下载或继续创作。</text>
  17. </view>
  18. <view class="create-btn" @click="goCreate">生成</view>
  19. </view>
  20. <view class="stats-row">
  21. <view class="stat-card">
  22. <text class="stat-num">{{ total || list.length }}</text>
  23. <text class="stat-label">全部作品</text>
  24. </view>
  25. <view class="stat-card">
  26. <text class="stat-num">{{ pendingCount }}</text>
  27. <text class="stat-label">生成中</text>
  28. </view>
  29. <view class="stat-card">
  30. <text class="stat-num">{{ successCount }}</text>
  31. <text class="stat-label">已完成</text>
  32. </view>
  33. </view>
  34. <view v-if="list.length" class="gallery">
  35. <view v-for="item in list" :key="item.chat_open_id" class="work-card">
  36. <view class="poster" @click="openItem(item)">
  37. <image v-if="item.content && Number(item.is_suc) > -1" class="poster-img" :src="item.content" mode="aspectFill" />
  38. <view v-else class="poster-empty"></view>
  39. <view class="status-chip" :class="statusClass(item)">
  40. {{ statusText(item) }}
  41. </view>
  42. <view v-if="Number(item.is_suc) === 0" class="progress-mask">
  43. <view class="spinner"></view>
  44. <text>正在生成</text>
  45. </view>
  46. <view v-if="Number(item.is_suc) < 0" class="failed-mask">
  47. <text>{{ item.content || '生成失败' }}</text>
  48. </view>
  49. </view>
  50. <view class="card-info">
  51. <view class="time-line">
  52. <text>{{ formatTime(item.add_time) }}</text>
  53. <view class="delete-btn" @click.stop="deleteItem(item)">删除</view>
  54. </view>
  55. </view>
  56. </view>
  57. </view>
  58. <view v-if="!list.length && !loading" class="empty-state">
  59. <view class="empty-preview">
  60. <view class="empty-face"></view>
  61. </view>
  62. <text class="empty-title">还没有动漫头像</text>
  63. <text class="empty-desc">上传一张清晰自拍,生成第一张专属头像。</text>
  64. <view class="empty-action" @click="goCreate">去生成</view>
  65. </view>
  66. <view v-if="loading" class="loading">{{ list.length ? '加载更多...' : '加载中...' }}</view>
  67. <view v-if="list.length && list.length >= total && !loading" class="end-text">已经到底了</view>
  68. </view>
  69. </scroll-view>
  70. </view>
  71. </template>
  72. <script setup>
  73. import { computed, ref } from 'vue'
  74. import { onLoad, onUnload } from '@dcloudio/uni-app'
  75. import dayjs from 'dayjs'
  76. import { toolApi } from '@/api/index.js'
  77. import { useApp } from '@/utils/useApp.js'
  78. const { toast } = useApp()
  79. const list = ref([])
  80. const page = ref(1)
  81. const total = ref(0)
  82. const loading = ref(false)
  83. const refreshing = ref(false)
  84. let timer = 0
  85. const pendingCount = computed(() => list.value.filter((item) => Number(item.is_suc) === 0).length)
  86. const successCount = computed(() => list.value.filter((item) => Number(item.is_suc) > 0).length)
  87. onLoad(() => refresh())
  88. onUnload(() => clearTimeout(timer))
  89. function formatTime(value) {
  90. if (!value) return ''
  91. return dayjs(Number(value) * 1000).format('YYYY-MM-DD HH:mm')
  92. }
  93. async function fetchList(reset = false) {
  94. if (loading.value) return
  95. loading.value = true
  96. try {
  97. const res = await toolApi.getAnimeList({
  98. page_no: page.value,
  99. page_size: 10,
  100. role: 2
  101. })
  102. const rows = res.list || []
  103. total.value = Number((res.data && res.data.total) || rows.length)
  104. list.value = reset ? rows : list.value.concat(rows)
  105. scheduleRefresh()
  106. } catch (e) {
  107. toast(e.msg || '历史记录加载失败')
  108. } finally {
  109. loading.value = false
  110. refreshing.value = false
  111. }
  112. }
  113. function scheduleRefresh() {
  114. clearTimeout(timer)
  115. if (list.value.some((item) => Number(item.is_suc) === 0)) {
  116. timer = setTimeout(() => refreshSilent(), 2500)
  117. }
  118. }
  119. function refresh() {
  120. refreshing.value = true
  121. refreshSilent()
  122. }
  123. function refreshSilent() {
  124. page.value = 1
  125. fetchList(true)
  126. }
  127. function loadMore() {
  128. if (loading.value) return
  129. if (list.value.length >= total.value) return
  130. page.value += 1
  131. fetchList()
  132. }
  133. function openItem(item) {
  134. if (!item.content || Number(item.is_suc) < 0) return
  135. uni.navigateTo({ url: `/pages/tool/work-detail?id=${item.chat_open_id}_0&from=anime` })
  136. }
  137. function statusText(item) {
  138. if (Number(item.is_suc) === 0) return '生成中'
  139. if (Number(item.is_suc) < 0) return '失败'
  140. return '完成'
  141. }
  142. function statusClass(item) {
  143. if (Number(item.is_suc) === 0) return 'pending'
  144. if (Number(item.is_suc) < 0) return 'failed'
  145. return 'done'
  146. }
  147. function goCreate() {
  148. const delta = getPageStackDelta('pages/tool/anime-avatar')
  149. if (delta > 0) {
  150. uni.navigateBack({ delta })
  151. return
  152. }
  153. uni.navigateTo({ url: '/pages/tool/anime-avatar' })
  154. }
  155. function getPageStackDelta(routeName) {
  156. const pages = getCurrentPages()
  157. const index = pages.findIndex((page) => page.route === routeName)
  158. return index >= 0 ? pages.length - 1 - index : -1
  159. }
  160. function deleteItem(item) {
  161. uni.showModal({
  162. title: '温馨提示',
  163. content: '是否确认删除该内容?',
  164. confirmText: '确认删除',
  165. success: async (res) => {
  166. if (!res.confirm) return
  167. try {
  168. await toolApi.deleteAnime({ chat_open_id: item.chat_open_id })
  169. toast('删除成功')
  170. refresh()
  171. } catch (e) {
  172. toast(e.msg || '删除失败')
  173. }
  174. }
  175. })
  176. }
  177. </script>
  178. <style lang="scss" scoped>
  179. .page,
  180. .scroll {
  181. height: 100vh;
  182. background: #f6f8fc;
  183. }
  184. .wrap {
  185. padding: 24rpx 28rpx 70rpx;
  186. }
  187. .studio-head {
  188. padding: 30rpx;
  189. border-radius: 26rpx;
  190. background: #ffffff;
  191. display: flex;
  192. align-items: center;
  193. gap: 24rpx;
  194. box-shadow: 0 10rpx 28rpx rgba(31, 52, 86, 0.05);
  195. }
  196. .studio-head > view:first-child {
  197. flex: 1;
  198. min-width: 0;
  199. }
  200. .eyebrow {
  201. display: block;
  202. color: #1f8cff;
  203. font-size: 20rpx;
  204. font-weight: 800;
  205. letter-spacing: 0;
  206. }
  207. .page-title {
  208. display: block;
  209. margin-top: 10rpx;
  210. color: #172033;
  211. font-size: 40rpx;
  212. line-height: 1.2;
  213. font-weight: 900;
  214. }
  215. .page-desc {
  216. display: block;
  217. margin-top: 12rpx;
  218. color: #64748b;
  219. font-size: 24rpx;
  220. line-height: 1.5;
  221. }
  222. .create-btn {
  223. width: 112rpx;
  224. height: 68rpx;
  225. border-radius: 999rpx;
  226. color: #ffffff;
  227. background: #1f8cff;
  228. font-size: 26rpx;
  229. line-height: 68rpx;
  230. text-align: center;
  231. font-weight: 800;
  232. box-shadow: 0 12rpx 24rpx rgba(31, 140, 255, 0.22);
  233. flex-shrink: 0;
  234. }
  235. .stats-row {
  236. margin-top: 18rpx;
  237. display: grid;
  238. grid-template-columns: repeat(3, 1fr);
  239. gap: 14rpx;
  240. }
  241. .stat-card {
  242. padding: 20rpx 10rpx;
  243. border-radius: 20rpx;
  244. background: #ffffff;
  245. text-align: center;
  246. box-shadow: 0 8rpx 20rpx rgba(31, 52, 86, 0.04);
  247. }
  248. .stat-num {
  249. display: block;
  250. color: #172033;
  251. font-size: 34rpx;
  252. line-height: 1;
  253. font-weight: 900;
  254. }
  255. .stat-label {
  256. display: block;
  257. margin-top: 10rpx;
  258. color: #7c8a9b;
  259. font-size: 22rpx;
  260. }
  261. .gallery {
  262. margin-top: 24rpx;
  263. display: grid;
  264. grid-template-columns: repeat(2, minmax(0, 1fr));
  265. gap: 22rpx;
  266. }
  267. .work-card {
  268. border-radius: 24rpx;
  269. background: #ffffff;
  270. overflow: hidden;
  271. box-shadow: 0 10rpx 28rpx rgba(31, 52, 86, 0.06);
  272. }
  273. .poster {
  274. position: relative;
  275. width: 100%;
  276. height: 440rpx;
  277. background: #dfe7f2;
  278. overflow: hidden;
  279. }
  280. .poster-img,
  281. .poster-empty {
  282. width: 100%;
  283. height: 100%;
  284. }
  285. .poster-empty {
  286. background: linear-gradient(135deg, #eef4fb, #dbe6f4);
  287. }
  288. .status-chip {
  289. position: absolute;
  290. left: 14rpx;
  291. top: 14rpx;
  292. height: 42rpx;
  293. padding: 0 16rpx;
  294. border-radius: 999rpx;
  295. color: #ffffff;
  296. font-size: 22rpx;
  297. line-height: 42rpx;
  298. font-weight: 800;
  299. background: rgba(15, 23, 42, 0.48);
  300. backdrop-filter: blur(8rpx);
  301. }
  302. .status-chip.done {
  303. background: rgba(31, 140, 255, 0.9);
  304. }
  305. .status-chip.pending {
  306. background: rgba(249, 115, 22, 0.92);
  307. }
  308. .status-chip.failed {
  309. background: rgba(239, 68, 68, 0.9);
  310. }
  311. .progress-mask,
  312. .failed-mask {
  313. position: absolute;
  314. left: 0;
  315. right: 0;
  316. top: 0;
  317. bottom: 0;
  318. display: flex;
  319. flex-direction: column;
  320. align-items: center;
  321. justify-content: center;
  322. padding: 24rpx;
  323. color: #ffffff;
  324. font-size: 26rpx;
  325. text-align: center;
  326. background: rgba(15, 23, 42, 0.52);
  327. }
  328. .failed-mask {
  329. background: rgba(127, 29, 29, 0.72);
  330. }
  331. .spinner {
  332. width: 48rpx;
  333. height: 48rpx;
  334. margin-bottom: 18rpx;
  335. border: 6rpx solid rgba(255, 255, 255, 0.45);
  336. border-top-color: #ffffff;
  337. border-radius: 50%;
  338. animation: spin 0.8s linear infinite;
  339. }
  340. .card-info {
  341. padding: 18rpx;
  342. }
  343. .time-line {
  344. display: flex;
  345. align-items: center;
  346. gap: 12rpx;
  347. color: #64748b;
  348. font-size: 22rpx;
  349. }
  350. .time-line text {
  351. flex: 1;
  352. min-width: 0;
  353. }
  354. .delete-btn {
  355. width: 74rpx;
  356. height: 44rpx;
  357. border-radius: 999rpx;
  358. color: #ef4444;
  359. background: #fff1f2;
  360. font-size: 22rpx;
  361. line-height: 44rpx;
  362. text-align: center;
  363. flex-shrink: 0;
  364. }
  365. .empty-state {
  366. margin-top: 86rpx;
  367. padding: 56rpx 34rpx;
  368. border-radius: 28rpx;
  369. background: #ffffff;
  370. display: flex;
  371. flex-direction: column;
  372. align-items: center;
  373. text-align: center;
  374. box-shadow: 0 10rpx 28rpx rgba(31, 52, 86, 0.05);
  375. }
  376. .empty-preview {
  377. width: 150rpx;
  378. height: 200rpx;
  379. border-radius: 32rpx;
  380. background: linear-gradient(135deg, #edf6ff, #fff4f0);
  381. display: flex;
  382. align-items: center;
  383. justify-content: center;
  384. }
  385. .empty-face {
  386. width: 76rpx;
  387. height: 76rpx;
  388. border-radius: 50%;
  389. border: 6rpx solid #1f8cff;
  390. position: relative;
  391. }
  392. .empty-face::after {
  393. content: '';
  394. position: absolute;
  395. left: 16rpx;
  396. right: 16rpx;
  397. bottom: 18rpx;
  398. height: 6rpx;
  399. border-radius: 999rpx;
  400. background: #1f8cff;
  401. }
  402. .empty-title {
  403. margin-top: 28rpx;
  404. color: #172033;
  405. font-size: 32rpx;
  406. font-weight: 900;
  407. }
  408. .empty-desc {
  409. margin-top: 12rpx;
  410. color: #64748b;
  411. font-size: 25rpx;
  412. line-height: 1.5;
  413. }
  414. .empty-action {
  415. margin-top: 28rpx;
  416. width: 220rpx;
  417. height: 72rpx;
  418. border-radius: 999rpx;
  419. color: #ffffff;
  420. background: #1f8cff;
  421. font-size: 28rpx;
  422. line-height: 72rpx;
  423. font-weight: 800;
  424. }
  425. .loading,
  426. .end-text {
  427. padding: 46rpx 0 20rpx;
  428. color: #7c8a9b;
  429. font-size: 24rpx;
  430. text-align: center;
  431. }
  432. @keyframes spin {
  433. to {
  434. transform: rotate(360deg);
  435. }
  436. }
  437. </style>