Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.
 
 
 

394 Zeilen
10 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">OLD PHOTO 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.photo_open_id" class="work-card">
  36. <view class="poster" @click="openItem(item)">
  37. <image v-if="item.cover" class="poster-img" :src="item.cover" mode="aspectFill" />
  38. <view v-else class="poster-empty"></view>
  39. <view class="status-chip" :class="statusClass(item)">{{ item.status_text }}</view>
  40. <view v-if="isPending(item)" class="progress-mask">
  41. <view class="spinner"></view>
  42. <text>{{ Number(item.status) === 10 ? '排队中' : '正在修复' }}</text>
  43. </view>
  44. <view v-else-if="Number(item.status) < 0" class="failed-mask">
  45. <text>{{ item.error_msg || '修复失败' }}</text>
  46. </view>
  47. </view>
  48. <view class="card-info">
  49. <text class="mode-name">{{ item.mode_name || '老照片修复' }}</text>
  50. <view class="time-line">
  51. <text>{{ formatTime(item.add_time) }}</text>
  52. <view class="delete-btn" @click.stop="deleteItem(item)">删除</view>
  53. </view>
  54. </view>
  55. </view>
  56. </view>
  57. <view v-if="!list.length && !loading" class="empty-state">
  58. <view class="empty-preview">
  59. <view class="empty-photo"></view>
  60. </view>
  61. <text class="empty-title">还没有修复记录</text>
  62. <text class="empty-desc">上传一张老照片,试试修复和上色的效果。</text>
  63. <view class="empty-action" @click="goCreate">去修复</view>
  64. </view>
  65. <view v-if="loading" class="loading">{{ list.length ? '加载更多...' : '加载中...' }}</view>
  66. <view v-if="list.length && list.length >= total && !loading" class="end-text">已经到底了</view>
  67. </view>
  68. </scroll-view>
  69. </view>
  70. </template>
  71. <script setup>
  72. import { computed, ref } from 'vue'
  73. import { onLoad, onUnload } from '@dcloudio/uni-app'
  74. import dayjs from 'dayjs'
  75. import { oldPhotoApi } from '@/api/index.js'
  76. import { useApp } from '@/utils/useApp.js'
  77. const { toast } = useApp()
  78. const list = ref([])
  79. const page = ref(1)
  80. const total = ref(0)
  81. const loading = ref(false)
  82. const refreshing = ref(false)
  83. let timer = 0
  84. const pendingCount = computed(() => list.value.filter((item) => isPending(item)).length)
  85. const successCount = computed(() => list.value.filter((item) => Number(item.status) === 30).length)
  86. onLoad(() => refresh())
  87. onUnload(() => clearTimeout(timer))
  88. function isPending(item) {
  89. return [10, 20].includes(Number(item.status))
  90. }
  91. function formatTime(value) {
  92. if (!value) return ''
  93. return dayjs(Number(value) * 1000).format('YYYY-MM-DD HH:mm')
  94. }
  95. async function fetchList(reset = false) {
  96. if (loading.value) return
  97. loading.value = true
  98. try {
  99. const res = await oldPhotoApi.getList({ page_no: page.value, page_size: 10 })
  100. const rows = res.list || []
  101. total.value = Number((res.data && res.data.total) || rows.length)
  102. list.value = reset ? rows : list.value.concat(rows)
  103. scheduleRefresh()
  104. } catch (e) {
  105. toast((e && e.msg) || '历史记录加载失败')
  106. } finally {
  107. loading.value = false
  108. refreshing.value = false
  109. }
  110. }
  111. // 有进行中的任务就轮询:后端 getlist 会顺带推进任务状态
  112. function scheduleRefresh() {
  113. clearTimeout(timer)
  114. if (list.value.some((item) => isPending(item))) {
  115. timer = setTimeout(() => refreshSilent(), 3000)
  116. }
  117. }
  118. function refresh() {
  119. refreshing.value = true
  120. refreshSilent()
  121. }
  122. function refreshSilent() {
  123. page.value = 1
  124. fetchList(true)
  125. }
  126. function loadMore() {
  127. if (loading.value) return
  128. if (list.value.length >= total.value) return
  129. page.value += 1
  130. fetchList()
  131. }
  132. // 进行中的也能进详情页看进度
  133. function openItem(item) {
  134. if (Number(item.status) < 0) return toast(item.error_msg || '该记录修复失败')
  135. uni.navigateTo({ url: `/pages/tool/old-photo-detail?id=${item.photo_open_id}` })
  136. }
  137. function statusClass(item) {
  138. if (isPending(item)) return 'pending'
  139. if (Number(item.status) < 0) return 'failed'
  140. return 'done'
  141. }
  142. function goCreate() {
  143. const delta = getPageStackDelta('pages/tool/old-photo')
  144. if (delta > 0) {
  145. uni.navigateBack({ delta })
  146. return
  147. }
  148. uni.navigateTo({ url: '/pages/tool/old-photo' })
  149. }
  150. function getPageStackDelta(routeName) {
  151. const pages = getCurrentPages()
  152. const index = pages.findIndex((item) => item.route === routeName)
  153. return index >= 0 ? pages.length - 1 - index : -1
  154. }
  155. function deleteItem(item) {
  156. uni.showModal({
  157. title: '温馨提示',
  158. content: '是否确认删除该记录?',
  159. confirmText: '确认删除',
  160. success: async (res) => {
  161. if (!res.confirm) return
  162. try {
  163. await oldPhotoApi.delete({ photo_open_id: item.photo_open_id })
  164. toast('删除成功')
  165. refresh()
  166. } catch (e) {
  167. toast((e && e.msg) || '删除失败')
  168. }
  169. }
  170. })
  171. }
  172. </script>
  173. <style lang="scss" scoped>
  174. .page,
  175. .scroll {
  176. height: 100vh;
  177. background: #f6f8fc;
  178. }
  179. .wrap { padding: 24rpx 28rpx 70rpx; }
  180. .studio-head {
  181. padding: 30rpx;
  182. border-radius: 26rpx;
  183. background: #ffffff;
  184. display: flex;
  185. align-items: center;
  186. gap: 24rpx;
  187. box-shadow: 0 10rpx 28rpx rgba(31, 52, 86, 0.05);
  188. }
  189. .studio-head > view:first-child { flex: 1; min-width: 0; }
  190. .eyebrow { display: block; color: #1f8cff; font-size: 20rpx; font-weight: 800; }
  191. .page-title {
  192. display: block;
  193. margin-top: 10rpx;
  194. color: #172033;
  195. font-size: 40rpx;
  196. line-height: 1.2;
  197. font-weight: 900;
  198. }
  199. .page-desc { display: block; margin-top: 12rpx; color: #64748b; font-size: 24rpx; line-height: 1.5; }
  200. .create-btn {
  201. width: 112rpx;
  202. height: 68rpx;
  203. border-radius: 999rpx;
  204. color: #ffffff;
  205. background: #1f8cff;
  206. font-size: 26rpx;
  207. line-height: 68rpx;
  208. text-align: center;
  209. font-weight: 800;
  210. box-shadow: 0 12rpx 24rpx rgba(31, 140, 255, 0.22);
  211. flex-shrink: 0;
  212. }
  213. .stats-row {
  214. margin-top: 18rpx;
  215. display: grid;
  216. grid-template-columns: repeat(3, 1fr);
  217. gap: 14rpx;
  218. }
  219. .stat-card {
  220. padding: 20rpx 10rpx;
  221. border-radius: 20rpx;
  222. background: #ffffff;
  223. text-align: center;
  224. box-shadow: 0 8rpx 20rpx rgba(31, 52, 86, 0.04);
  225. }
  226. .stat-num { display: block; color: #172033; font-size: 34rpx; line-height: 1; font-weight: 900; }
  227. .stat-label { display: block; margin-top: 10rpx; color: #7c8a9b; font-size: 22rpx; }
  228. .gallery {
  229. margin-top: 24rpx;
  230. display: grid;
  231. grid-template-columns: repeat(2, minmax(0, 1fr));
  232. gap: 22rpx;
  233. }
  234. .work-card {
  235. border-radius: 24rpx;
  236. background: #ffffff;
  237. overflow: hidden;
  238. box-shadow: 0 10rpx 28rpx rgba(31, 52, 86, 0.06);
  239. }
  240. .poster {
  241. position: relative;
  242. width: 100%;
  243. height: 440rpx;
  244. background: #dfe7f2;
  245. overflow: hidden;
  246. }
  247. .poster-img,
  248. .poster-empty { width: 100%; height: 100%; }
  249. .poster-empty { background: linear-gradient(135deg, #eef4fb, #dbe6f4); }
  250. .status-chip {
  251. position: absolute;
  252. left: 14rpx;
  253. top: 14rpx;
  254. height: 42rpx;
  255. padding: 0 16rpx;
  256. border-radius: 999rpx;
  257. color: #ffffff;
  258. font-size: 22rpx;
  259. line-height: 42rpx;
  260. font-weight: 800;
  261. background: rgba(15, 23, 42, 0.48);
  262. }
  263. .status-chip.done { background: rgba(31, 140, 255, 0.9); }
  264. .status-chip.pending { background: rgba(249, 115, 22, 0.92); }
  265. .status-chip.failed { background: rgba(239, 68, 68, 0.9); }
  266. .progress-mask,
  267. .failed-mask {
  268. position: absolute;
  269. left: 0;
  270. right: 0;
  271. top: 0;
  272. bottom: 0;
  273. padding: 24rpx;
  274. display: flex;
  275. flex-direction: column;
  276. align-items: center;
  277. justify-content: center;
  278. color: #ffffff;
  279. font-size: 26rpx;
  280. text-align: center;
  281. background: rgba(15, 23, 42, 0.52);
  282. }
  283. .failed-mask { background: rgba(127, 29, 29, 0.72); font-size: 24rpx; }
  284. .spinner {
  285. width: 48rpx;
  286. height: 48rpx;
  287. margin-bottom: 18rpx;
  288. border: 6rpx solid rgba(255, 255, 255, 0.45);
  289. border-top-color: #ffffff;
  290. border-radius: 50%;
  291. animation: spin 0.8s linear infinite;
  292. }
  293. .card-info { padding: 18rpx; }
  294. .mode-name {
  295. display: block;
  296. margin-bottom: 10rpx;
  297. color: #172033;
  298. font-size: 25rpx;
  299. font-weight: 800;
  300. }
  301. .time-line { display: flex; align-items: center; gap: 12rpx; color: #64748b; font-size: 22rpx; }
  302. .time-line text { flex: 1; min-width: 0; }
  303. .delete-btn {
  304. width: 74rpx;
  305. height: 44rpx;
  306. border-radius: 999rpx;
  307. color: #ef4444;
  308. background: #fff1f2;
  309. font-size: 22rpx;
  310. line-height: 44rpx;
  311. text-align: center;
  312. flex-shrink: 0;
  313. }
  314. .empty-state {
  315. margin-top: 86rpx;
  316. padding: 56rpx 34rpx;
  317. border-radius: 28rpx;
  318. background: #ffffff;
  319. display: flex;
  320. flex-direction: column;
  321. align-items: center;
  322. text-align: center;
  323. box-shadow: 0 10rpx 28rpx rgba(31, 52, 86, 0.05);
  324. }
  325. .empty-preview {
  326. width: 150rpx;
  327. height: 200rpx;
  328. border-radius: 32rpx;
  329. background: linear-gradient(135deg, #edf6ff, #fff4f0);
  330. display: flex;
  331. align-items: center;
  332. justify-content: center;
  333. }
  334. .empty-photo {
  335. width: 88rpx;
  336. height: 108rpx;
  337. border-radius: 14rpx;
  338. border: 6rpx solid #1f8cff;
  339. background: linear-gradient(135deg, #d7d0c6 50%, #5db2d9 50%);
  340. }
  341. .empty-title { margin-top: 28rpx; color: #172033; font-size: 32rpx; font-weight: 900; }
  342. .empty-desc { margin-top: 12rpx; color: #64748b; font-size: 25rpx; line-height: 1.5; }
  343. .empty-action {
  344. margin-top: 28rpx;
  345. width: 220rpx;
  346. height: 72rpx;
  347. border-radius: 999rpx;
  348. color: #ffffff;
  349. background: #1f8cff;
  350. font-size: 28rpx;
  351. line-height: 72rpx;
  352. font-weight: 800;
  353. }
  354. .loading,
  355. .end-text { padding: 46rpx 0 20rpx; color: #7c8a9b; font-size: 24rpx; text-align: center; }
  356. @keyframes spin {
  357. to { transform: rotate(360deg); }
  358. }
  359. </style>