Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 
 

435 řádky
12 KiB

  1. <template>
  2. <view class="page">
  3. <scroll-view class="scroll" scroll-y>
  4. <view class="wrap">
  5. <!-- 处理中 / 失败 -->
  6. <view v-if="isLoadingState" class="state-card">
  7. <view v-if="isPending" class="spinner"></view>
  8. <text class="state-title">{{ isPending ? (Number(detail.status) === 10 ? '排队中,请耐心等待' : 'AI 正在修复上色') : '修复失败' }}</text>
  9. <text v-if="isPending && countDown > 0" class="count">{{ countDown }} S</text>
  10. <text v-if="!isPending" class="error">{{ detail.error_msg || '本次未扣点,可以换一张更清晰的照片重试' }}</text>
  11. </view>
  12. <!-- 成品:默认直接看修复后的图 -->
  13. <view v-else class="image-card" @click="preview">
  14. <image class="work-img" :src="detail.result_url" mode="widthFix" />
  15. <view class="compare-entry" @click.stop="openCompare">查看对比</view>
  16. </view>
  17. <view class="info-card">
  18. <view class="title">修复信息</view>
  19. <view class="cell">
  20. <text class="cell-label">修复档位:</text>
  21. <text class="cell-content">{{ detail.mode_name || '-' }}</text>
  22. </view>
  23. <view class="cell">
  24. <text class="cell-label">当前状态:</text>
  25. <text class="cell-content">{{ detail.status_text || '-' }}</text>
  26. </view>
  27. <view v-if="detail.source_width && detail.source_height" class="cell">
  28. <text class="cell-label">原图尺寸:</text>
  29. <text class="cell-content">{{ detail.source_width }} × {{ detail.source_height }}px</text>
  30. </view>
  31. <view v-if="Number(detail.refine) === 1" class="cell">
  32. <text class="cell-label">清晰度:</text>
  33. <text class="cell-content">已做高清增强</text>
  34. </view>
  35. <view class="cell">
  36. <text class="cell-label">创建时间:</text>
  37. <text class="cell-content">{{ formatTime(detail.add_time) }}</text>
  38. </view>
  39. <view class="cell">
  40. <text class="cell-label">功能来源:</text>
  41. <text class="link" @click="createAgain">老照片修复 / 上色</text>
  42. </view>
  43. <text class="note">AI 会重绘上色,五官可能出现轻微变化,建议对照原图确认。</text>
  44. </view>
  45. </view>
  46. </scroll-view>
  47. <view v-if="!isLoadingState" class="bottom-bar">
  48. <view class="bar-btn" @click="download">保存</view>
  49. <view class="bar-btn" @click="openCompare">对比</view>
  50. <view class="main-btn" @click="createAgain">再修一张</view>
  51. </view>
  52. <!-- 前后对比:样式与动漫头像一致 -->
  53. <view v-if="compareVisible" class="compare-modal">
  54. <view class="compare-backdrop" @click="closeCompare"></view>
  55. <view class="compare-panel">
  56. <view class="compare-panel-head">
  57. <text>效果对比</text>
  58. <view class="close-btn" @click="closeCompare">×</view>
  59. </view>
  60. <view
  61. class="large-compare"
  62. @touchstart.stop="onCompareTouch"
  63. @touchmove.stop.prevent="onCompareTouch"
  64. @click.stop="onCompareTouch"
  65. >
  66. <image class="compare-img" :src="detail.result_url" mode="aspectFit" />
  67. <view class="compare-before" :style="{ width: comparePercent + '%' }">
  68. <image class="compare-img" :src="detail.source_url" mode="aspectFit" />
  69. </view>
  70. <view class="compare-line large" :style="{ left: comparePercent + '%' }">
  71. <view class="compare-handle large">
  72. <text>‹</text>
  73. <text>›</text>
  74. </view>
  75. </view>
  76. <view class="compare-tag left">原图</view>
  77. <view class="compare-tag right">修复后</view>
  78. </view>
  79. <text class="compare-guide">左右滑动中间按钮查看变化</text>
  80. </view>
  81. </view>
  82. </view>
  83. </template>
  84. <script setup>
  85. import { computed, ref } from 'vue'
  86. import { onLoad, onUnload } from '@dcloudio/uni-app'
  87. import dayjs from 'dayjs'
  88. import { oldPhotoApi } from '@/api/index.js'
  89. import { useApp } from '@/utils/useApp.js'
  90. const { toast } = useApp()
  91. const id = ref('')
  92. const detail = ref({ status: 10 })
  93. const countDown = ref(0)
  94. const compareVisible = ref(false)
  95. const comparePercent = ref(50)
  96. let timer = 0
  97. let countTimer = 0
  98. const isPending = computed(() => [10, 20].includes(Number(detail.value.status)))
  99. const isLoadingState = computed(() => Number(detail.value.status) !== 30)
  100. onLoad((options) => {
  101. id.value = (options && options.id) || ''
  102. loadDetail()
  103. })
  104. onUnload(() => {
  105. clearTimeout(timer)
  106. clearInterval(countTimer)
  107. })
  108. async function loadDetail() {
  109. if (!id.value) return
  110. try {
  111. const res = await oldPhotoApi.getDetail({ photo_open_id: id.value })
  112. detail.value = res.data || {}
  113. if (isPending.value) waitNext()
  114. } catch (e) {
  115. toast((e && e.msg) || '记录加载失败')
  116. }
  117. }
  118. // 处理中就 3 秒拉一次,后端会在这一步推进任务并转存结果
  119. function waitNext() {
  120. clearTimeout(timer)
  121. clearInterval(countTimer)
  122. countDown.value = 3
  123. countTimer = setInterval(() => {
  124. countDown.value -= 1
  125. if (countDown.value <= 0) clearInterval(countTimer)
  126. }, 1000)
  127. timer = setTimeout(loadDetail, 3000)
  128. }
  129. function formatTime(value) {
  130. if (!value) return '-'
  131. return dayjs(Number(value) * 1000).format('YYYY-MM-DD HH:mm')
  132. }
  133. function preview() {
  134. if (!detail.value.result_url) return
  135. uni.previewImage({ urls: [detail.value.result_url], current: detail.value.result_url })
  136. }
  137. function download() {
  138. if (!detail.value.result_url) return
  139. uni.showLoading({ title: '保存中...', mask: true })
  140. uni.downloadFile({
  141. url: detail.value.result_url,
  142. success: (res) => {
  143. uni.saveImageToPhotosAlbum({
  144. filePath: res.tempFilePath,
  145. success: () => toast('已保存到相册'),
  146. fail: () => toast('保存失败,请检查相册权限')
  147. })
  148. },
  149. fail: () => toast('图片下载失败'),
  150. complete: () => uni.hideLoading()
  151. })
  152. }
  153. function openCompare() {
  154. if (!detail.value.result_url || !detail.value.source_url) return
  155. comparePercent.value = 50
  156. compareVisible.value = true
  157. }
  158. function closeCompare() {
  159. compareVisible.value = false
  160. }
  161. function onCompareTouch(e) {
  162. const touch = (e.touches && e.touches[0]) || (e.changedTouches && e.changedTouches[0]) || e
  163. const x = touch.clientX || 0
  164. uni.createSelectorQuery()
  165. .select('.large-compare')
  166. .boundingClientRect((rect) => {
  167. if (!rect || !rect.width) return
  168. const percent = ((x - rect.left) / rect.width) * 100
  169. comparePercent.value = Math.max(6, Math.min(94, Number(percent.toFixed(1))))
  170. })
  171. .exec()
  172. }
  173. function createAgain() {
  174. const delta = getPageStackDelta('pages/tool/old-photo')
  175. if (delta > 0) {
  176. uni.navigateBack({ delta })
  177. return
  178. }
  179. uni.navigateTo({ url: '/pages/tool/old-photo' })
  180. }
  181. function getPageStackDelta(routeName) {
  182. const pages = getCurrentPages()
  183. const index = pages.findIndex((item) => item.route === routeName)
  184. return index >= 0 ? pages.length - 1 - index : -1
  185. }
  186. </script>
  187. <style lang="scss" scoped>
  188. .page {
  189. height: 100vh;
  190. background: #f4f7fb;
  191. display: flex;
  192. flex-direction: column;
  193. overflow: hidden;
  194. }
  195. .scroll { flex: 1; height: 0; }
  196. /* 底栏是 fixed 的,留够 300rpx */
  197. .wrap { padding: 28rpx 28rpx 300rpx; }
  198. .state-card,
  199. .image-card,
  200. .info-card {
  201. background: #fff;
  202. border-radius: 18rpx;
  203. overflow: hidden;
  204. }
  205. .state-card {
  206. min-height: 540rpx;
  207. display: flex;
  208. flex-direction: column;
  209. align-items: center;
  210. justify-content: center;
  211. color: #172033;
  212. font-size: 30rpx;
  213. }
  214. .spinner {
  215. width: 72rpx;
  216. height: 72rpx;
  217. margin-bottom: 28rpx;
  218. border: 8rpx solid #e9edf5;
  219. border-top-color: #0b8cff;
  220. border-radius: 50%;
  221. animation: spin 0.8s linear infinite;
  222. }
  223. .count,
  224. .error { margin-top: 18rpx; color: #7f8896; font-size: 26rpx; }
  225. .error { color: #f04438; padding: 0 40rpx; text-align: center; }
  226. .image-card { position: relative; }
  227. .work-img { width: 100%; display: block; }
  228. .compare-entry {
  229. position: absolute;
  230. right: 18rpx;
  231. bottom: 18rpx;
  232. height: 56rpx;
  233. padding: 0 22rpx;
  234. border-radius: 999rpx;
  235. color: #fff;
  236. font-size: 24rpx;
  237. font-weight: 800;
  238. line-height: 56rpx;
  239. background: rgba(15, 23, 42, 0.55);
  240. }
  241. .info-card { margin-top: 24rpx; padding: 28rpx; }
  242. .title { margin-bottom: 16rpx; color: #172033; font-size: 32rpx; font-weight: 700; }
  243. .cell {
  244. padding: 10rpx 0;
  245. display: flex;
  246. color: #7f8896;
  247. font-size: 27rpx;
  248. line-height: 1.6;
  249. }
  250. .cell-label { flex-shrink: 0; }
  251. .cell-content { flex: 1; min-width: 0; color: #172033; }
  252. .link { color: #0b8cff; }
  253. .note {
  254. display: block;
  255. margin-top: 16rpx;
  256. padding-top: 16rpx;
  257. border-top: 2rpx solid #f1f5fa;
  258. color: #8a95a6;
  259. font-size: 23rpx;
  260. line-height: 1.5;
  261. }
  262. .bottom-bar {
  263. position: fixed;
  264. left: 0;
  265. right: 0;
  266. bottom: 0;
  267. padding: 18rpx 28rpx calc(18rpx + env(safe-area-inset-bottom));
  268. background: #fff;
  269. display: flex;
  270. gap: 18rpx;
  271. box-shadow: 0 -8rpx 24rpx rgba(35, 55, 90, 0.08);
  272. }
  273. .bar-btn,
  274. .main-btn {
  275. height: 82rpx;
  276. border-radius: 42rpx;
  277. font-size: 29rpx;
  278. display: flex;
  279. align-items: center;
  280. justify-content: center;
  281. }
  282. .bar-btn { width: 150rpx; color: #0b8cff; background: #eef6ff; }
  283. .main-btn { flex: 1; color: #fff; background: #0b8cff; }
  284. /* 对比弹层:与动漫头像保持一致 */
  285. .compare-modal {
  286. position: fixed;
  287. left: 0;
  288. right: 0;
  289. top: 0;
  290. bottom: 0;
  291. z-index: 120;
  292. }
  293. .compare-backdrop {
  294. position: absolute;
  295. left: 0;
  296. right: 0;
  297. top: 0;
  298. bottom: 0;
  299. background: rgba(15, 23, 42, 0.58);
  300. }
  301. .compare-panel {
  302. position: absolute;
  303. left: 36rpx;
  304. right: 36rpx;
  305. top: 50%;
  306. transform: translateY(-50%);
  307. padding: 24rpx;
  308. border-radius: 28rpx;
  309. background: #ffffff;
  310. }
  311. .compare-panel-head {
  312. margin-bottom: 20rpx;
  313. display: flex;
  314. align-items: center;
  315. justify-content: space-between;
  316. color: #172033;
  317. font-size: 32rpx;
  318. font-weight: 800;
  319. }
  320. .close-btn {
  321. width: 56rpx;
  322. height: 56rpx;
  323. border-radius: 50%;
  324. color: #64748b;
  325. background: #f1f5f9;
  326. font-size: 42rpx;
  327. line-height: 50rpx;
  328. text-align: center;
  329. font-weight: 300;
  330. }
  331. .large-compare {
  332. position: relative;
  333. width: 100%;
  334. height: 900rpx;
  335. max-height: 70vh;
  336. border-radius: 22rpx;
  337. background: #dfe8f4;
  338. overflow: hidden;
  339. }
  340. .compare-img { width: 100%; height: 100%; }
  341. .compare-before {
  342. position: absolute;
  343. left: 0;
  344. top: 0;
  345. bottom: 0;
  346. overflow: hidden;
  347. }
  348. .large-compare .compare-before .compare-img { width: calc(100vw - 120rpx); }
  349. .compare-line {
  350. position: absolute;
  351. top: 0;
  352. bottom: 0;
  353. width: 4rpx;
  354. margin-left: -2rpx;
  355. background: rgba(255, 255, 255, 0.94);
  356. box-shadow: 0 0 12rpx rgba(20, 37, 66, 0.22);
  357. }
  358. .compare-line.large { width: 6rpx; margin-left: -3rpx; }
  359. .compare-handle {
  360. position: absolute;
  361. left: 50%;
  362. top: 50%;
  363. width: 38rpx;
  364. height: 58rpx;
  365. margin-left: -19rpx;
  366. margin-top: -29rpx;
  367. border-radius: 999rpx;
  368. background: #ffffff;
  369. box-shadow: 0 8rpx 18rpx rgba(20, 37, 66, 0.22);
  370. display: flex;
  371. align-items: center;
  372. justify-content: center;
  373. gap: 1rpx;
  374. color: #1f8cff;
  375. font-size: 24rpx;
  376. font-weight: 900;
  377. }
  378. .compare-handle.large {
  379. width: 54rpx;
  380. height: 78rpx;
  381. margin-left: -27rpx;
  382. margin-top: -39rpx;
  383. font-size: 32rpx;
  384. }
  385. .compare-tag {
  386. position: absolute;
  387. top: 12rpx;
  388. padding: 6rpx 12rpx;
  389. border-radius: 999rpx;
  390. color: #ffffff;
  391. background: rgba(15, 23, 42, 0.46);
  392. font-size: 20rpx;
  393. font-weight: 700;
  394. }
  395. .compare-tag.left { left: 12rpx; }
  396. .compare-tag.right { right: 12rpx; }
  397. .compare-guide {
  398. display: block;
  399. margin-top: 18rpx;
  400. color: #64748b;
  401. font-size: 24rpx;
  402. text-align: center;
  403. }
  404. @keyframes spin {
  405. to { transform: rotate(360deg); }
  406. }
  407. </style>