|
- <template>
- <view class="page">
- <scroll-view class="scroll" scroll-y>
- <view class="wrap">
- <!-- 处理中 / 失败 -->
- <view v-if="isLoadingState" class="state-card">
- <view v-if="isPending" class="spinner"></view>
- <text class="state-title">{{ isPending ? (Number(detail.status) === 10 ? '排队中,请耐心等待' : 'AI 正在修复上色') : '修复失败' }}</text>
- <text v-if="isPending && countDown > 0" class="count">{{ countDown }} S</text>
- <text v-if="!isPending" class="error">{{ detail.error_msg || '本次未扣点,可以换一张更清晰的照片重试' }}</text>
- </view>
-
- <!-- 成品:默认直接看修复后的图 -->
- <view v-else class="image-card" @click="preview">
- <image class="work-img" :src="detail.result_url" mode="widthFix" />
- <view class="compare-entry" @click.stop="openCompare">查看对比</view>
- </view>
-
- <view class="info-card">
- <view class="title">修复信息</view>
- <view class="cell">
- <text class="cell-label">修复档位:</text>
- <text class="cell-content">{{ detail.mode_name || '-' }}</text>
- </view>
- <view class="cell">
- <text class="cell-label">当前状态:</text>
- <text class="cell-content">{{ detail.status_text || '-' }}</text>
- </view>
- <view v-if="detail.source_width && detail.source_height" class="cell">
- <text class="cell-label">原图尺寸:</text>
- <text class="cell-content">{{ detail.source_width }} × {{ detail.source_height }}px</text>
- </view>
- <view v-if="Number(detail.refine) === 1" class="cell">
- <text class="cell-label">清晰度:</text>
- <text class="cell-content">已做高清增强</text>
- </view>
- <view class="cell">
- <text class="cell-label">创建时间:</text>
- <text class="cell-content">{{ formatTime(detail.add_time) }}</text>
- </view>
- <view class="cell">
- <text class="cell-label">功能来源:</text>
- <text class="link" @click="createAgain">老照片修复 / 上色</text>
- </view>
- <text class="note">AI 会重绘上色,五官可能出现轻微变化,建议对照原图确认。</text>
- </view>
- </view>
- </scroll-view>
-
- <view v-if="!isLoadingState" class="bottom-bar">
- <view class="delete-btn" @click="deleteRecord">删除</view>
- <view class="bar-btn" @click="download">保存</view>
- <view class="bar-btn" @click="openCompare">对比</view>
- <view class="main-btn" @click="createAgain">再修一张</view>
- </view>
-
- <!-- 前后对比:样式与动漫头像一致 -->
- <view v-if="compareVisible" class="compare-modal">
- <view class="compare-backdrop" @click="closeCompare"></view>
- <view class="compare-panel">
- <view class="compare-panel-head">
- <text>效果对比</text>
- <view class="close-btn" @click="closeCompare">×</view>
- </view>
- <view
- class="large-compare"
- @touchstart.stop="onCompareTouch"
- @touchmove.stop.prevent="onCompareTouch"
- @click.stop="onCompareTouch"
- >
- <image class="compare-img" :src="detail.result_url" mode="aspectFit" />
- <view class="compare-before" :style="{ width: comparePercent + '%' }">
- <image class="compare-img" :src="detail.source_url" mode="aspectFit" />
- </view>
- <view class="compare-line large" :style="{ left: comparePercent + '%' }">
- <view class="compare-handle large">
- <text>‹</text>
- <text>›</text>
- </view>
- </view>
- <view class="compare-tag left">原图</view>
- <view class="compare-tag right">修复后</view>
- </view>
- <text class="compare-guide">左右滑动中间按钮查看变化</text>
- </view>
- </view>
- </view>
- </template>
-
- <script setup>
- import { computed, ref } from 'vue'
- import { onLoad, onUnload } from '@dcloudio/uni-app'
- import dayjs from 'dayjs'
- import { oldPhotoApi } from '@/api/index.js'
- import { useApp } from '@/utils/useApp.js'
-
- const { toast } = useApp()
- const id = ref('')
- const detail = ref({ status: 10 })
- const countDown = ref(0)
- const compareVisible = ref(false)
- const comparePercent = ref(50)
- let timer = 0
- let countTimer = 0
-
- const isPending = computed(() => [10, 20].includes(Number(detail.value.status)))
- const isLoadingState = computed(() => Number(detail.value.status) !== 30)
-
- onLoad((options) => {
- id.value = (options && options.id) || ''
- loadDetail()
- })
-
- onUnload(() => {
- clearTimeout(timer)
- clearInterval(countTimer)
- })
-
- async function loadDetail() {
- if (!id.value) return
- try {
- const res = await oldPhotoApi.getDetail({ photo_open_id: id.value })
- detail.value = res.data || {}
- if (isPending.value) waitNext()
- } catch (e) {
- toast((e && e.msg) || '记录加载失败')
- }
- }
-
- // 处理中就 3 秒拉一次,后端会在这一步推进任务并转存结果
- function waitNext() {
- clearTimeout(timer)
- clearInterval(countTimer)
- countDown.value = 3
- countTimer = setInterval(() => {
- countDown.value -= 1
- if (countDown.value <= 0) clearInterval(countTimer)
- }, 1000)
- timer = setTimeout(loadDetail, 3000)
- }
-
- function formatTime(value) {
- if (!value) return '-'
- return dayjs(Number(value) * 1000).format('YYYY-MM-DD HH:mm')
- }
-
- function preview() {
- if (!detail.value.result_url) return
- uni.previewImage({ urls: [detail.value.result_url], current: detail.value.result_url })
- }
-
- function download() {
- if (!detail.value.result_url) return
- uni.showLoading({ title: '保存中...', mask: true })
- uni.downloadFile({
- url: detail.value.result_url,
- success: (res) => {
- uni.saveImageToPhotosAlbum({
- filePath: res.tempFilePath,
- success: () => toast('已保存到相册'),
- fail: () => toast('保存失败,请检查相册权限')
- })
- },
- fail: () => toast('图片下载失败'),
- complete: () => uni.hideLoading()
- })
- }
-
- function deleteRecord() {
- if (!id.value) return
- uni.showModal({
- title: '删除记录',
- content: '删除后将不再展示该老照片记录,是否继续?',
- confirmText: '删除',
- confirmColor: '#f04438',
- success: async (res) => {
- if (!res.confirm) return
- try {
- await oldPhotoApi.delete({ photo_open_id: id.value })
- toast('删除成功')
- const pages = getCurrentPages()
- if (pages.length > 1) {
- uni.navigateBack()
- } else {
- uni.redirectTo({ url: '/pages/works/works' })
- }
- } catch (e) {
- toast((e && e.msg) || '删除失败')
- }
- }
- })
- }
-
- function openCompare() {
- if (!detail.value.result_url || !detail.value.source_url) return
- comparePercent.value = 50
- compareVisible.value = true
- }
-
- function closeCompare() {
- compareVisible.value = false
- }
-
- function onCompareTouch(e) {
- const touch = (e.touches && e.touches[0]) || (e.changedTouches && e.changedTouches[0]) || e
- const x = touch.clientX || 0
- uni.createSelectorQuery()
- .select('.large-compare')
- .boundingClientRect((rect) => {
- if (!rect || !rect.width) return
- const percent = ((x - rect.left) / rect.width) * 100
- comparePercent.value = Math.max(6, Math.min(94, Number(percent.toFixed(1))))
- })
- .exec()
- }
-
- function createAgain() {
- const delta = getPageStackDelta('pages/tool/old-photo')
- if (delta > 0) {
- uni.navigateBack({ delta })
- return
- }
- uni.navigateTo({ url: '/pages/tool/old-photo' })
- }
-
- function getPageStackDelta(routeName) {
- const pages = getCurrentPages()
- const index = pages.findIndex((item) => item.route === routeName)
- return index >= 0 ? pages.length - 1 - index : -1
- }
- </script>
-
- <style lang="scss" scoped>
- .page {
- height: 100vh;
- background: #f4f7fb;
- display: flex;
- flex-direction: column;
- overflow: hidden;
- }
- .scroll { flex: 1; height: 0; }
- /* 底栏是 fixed 的,留够 300rpx */
- .wrap { padding: 28rpx 28rpx 300rpx; }
-
- .state-card,
- .image-card,
- .info-card {
- background: #fff;
- border-radius: 18rpx;
- overflow: hidden;
- }
-
- .state-card {
- min-height: 540rpx;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- color: #172033;
- font-size: 30rpx;
- }
- .spinner {
- width: 72rpx;
- height: 72rpx;
- margin-bottom: 28rpx;
- border: 8rpx solid #e9edf5;
- border-top-color: #0b8cff;
- border-radius: 50%;
- animation: spin 0.8s linear infinite;
- }
- .count,
- .error { margin-top: 18rpx; color: #7f8896; font-size: 26rpx; }
- .error { color: #f04438; padding: 0 40rpx; text-align: center; }
-
- .image-card { position: relative; }
- .work-img { width: 100%; display: block; }
- .compare-entry {
- position: absolute;
- right: 18rpx;
- bottom: 18rpx;
- height: 56rpx;
- padding: 0 22rpx;
- border-radius: 999rpx;
- color: #fff;
- font-size: 24rpx;
- font-weight: 800;
- line-height: 56rpx;
- background: rgba(15, 23, 42, 0.55);
- }
-
- .info-card { margin-top: 24rpx; padding: 28rpx; }
- .title { margin-bottom: 16rpx; color: #172033; font-size: 32rpx; font-weight: 700; }
- .cell {
- padding: 10rpx 0;
- display: flex;
- color: #7f8896;
- font-size: 27rpx;
- line-height: 1.6;
- }
- .cell-label { flex-shrink: 0; }
- .cell-content { flex: 1; min-width: 0; color: #172033; }
- .link { color: #0b8cff; }
- .note {
- display: block;
- margin-top: 16rpx;
- padding-top: 16rpx;
- border-top: 2rpx solid #f1f5fa;
- color: #8a95a6;
- font-size: 23rpx;
- line-height: 1.5;
- }
-
- .bottom-bar {
- position: fixed;
- left: 0;
- right: 0;
- bottom: 0;
- padding: 18rpx 28rpx calc(18rpx + env(safe-area-inset-bottom));
- background: #fff;
- display: flex;
- gap: 18rpx;
- box-shadow: 0 -8rpx 24rpx rgba(35, 55, 90, 0.08);
- }
- .bar-btn,
- .delete-btn,
- .main-btn {
- height: 82rpx;
- border-radius: 42rpx;
- font-size: 29rpx;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .bar-btn { width: 136rpx; color: #0b8cff; background: #eef6ff; }
- .delete-btn { width: 136rpx; color: #f04438; background: #fff1f0; }
- .main-btn { flex: 1; color: #fff; background: #0b8cff; }
-
- /* 对比弹层:与动漫头像保持一致 */
- .compare-modal {
- position: fixed;
- left: 0;
- right: 0;
- top: 0;
- bottom: 0;
- z-index: 120;
- }
- .compare-backdrop {
- position: absolute;
- left: 0;
- right: 0;
- top: 0;
- bottom: 0;
- background: rgba(15, 23, 42, 0.58);
- }
- .compare-panel {
- position: absolute;
- left: 36rpx;
- right: 36rpx;
- top: 50%;
- transform: translateY(-50%);
- padding: 24rpx;
- border-radius: 28rpx;
- background: #ffffff;
- }
- .compare-panel-head {
- margin-bottom: 20rpx;
- display: flex;
- align-items: center;
- justify-content: space-between;
- color: #172033;
- font-size: 32rpx;
- font-weight: 800;
- }
- .close-btn {
- width: 56rpx;
- height: 56rpx;
- border-radius: 50%;
- color: #64748b;
- background: #f1f5f9;
- font-size: 42rpx;
- line-height: 50rpx;
- text-align: center;
- font-weight: 300;
- }
- .large-compare {
- position: relative;
- width: 100%;
- height: 900rpx;
- max-height: 70vh;
- border-radius: 22rpx;
- background: #dfe8f4;
- overflow: hidden;
- }
- .compare-img { width: 100%; height: 100%; }
- .compare-before {
- position: absolute;
- left: 0;
- top: 0;
- bottom: 0;
- overflow: hidden;
- }
- .large-compare .compare-before .compare-img { width: calc(100vw - 120rpx); }
- .compare-line {
- position: absolute;
- top: 0;
- bottom: 0;
- width: 4rpx;
- margin-left: -2rpx;
- background: rgba(255, 255, 255, 0.94);
- box-shadow: 0 0 12rpx rgba(20, 37, 66, 0.22);
- }
- .compare-line.large { width: 6rpx; margin-left: -3rpx; }
- .compare-handle {
- position: absolute;
- left: 50%;
- top: 50%;
- width: 38rpx;
- height: 58rpx;
- margin-left: -19rpx;
- margin-top: -29rpx;
- border-radius: 999rpx;
- background: #ffffff;
- box-shadow: 0 8rpx 18rpx rgba(20, 37, 66, 0.22);
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 1rpx;
- color: #1f8cff;
- font-size: 24rpx;
- font-weight: 900;
- }
- .compare-handle.large {
- width: 54rpx;
- height: 78rpx;
- margin-left: -27rpx;
- margin-top: -39rpx;
- font-size: 32rpx;
- }
- .compare-tag {
- position: absolute;
- top: 12rpx;
- padding: 6rpx 12rpx;
- border-radius: 999rpx;
- color: #ffffff;
- background: rgba(15, 23, 42, 0.46);
- font-size: 20rpx;
- font-weight: 700;
- }
- .compare-tag.left { left: 12rpx; }
- .compare-tag.right { right: 12rpx; }
- .compare-guide {
- display: block;
- margin-top: 18rpx;
- color: #64748b;
- font-size: 24rpx;
- text-align: center;
- }
-
- @keyframes spin {
- to { transform: rotate(360deg); }
- }
- </style>
|