Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 

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