|
- <template>
- <view class="wrap">
- <view class="hero-card">
- <view class="hero-copy">
- <text class="eyebrow">OLD PHOTO RESTORE</text>
- <text class="hero-title">老照片修复 / 上色</text>
- <text class="hero-desc">黑白、泛黄、模糊、有划痕的照片都能修。AI 会重绘上色,五官可能有轻微变化。</text>
- <view class="hero-actions">
- <view v-if="hasDemo" class="hero-pill" @click="openDemoCompare">查看对比</view>
- <view class="hero-link" @click="chooseImage">上传照片</view>
- </view>
- </view>
- <!-- demo 前后对比缩略图(配置项 old_photo_demo),点击进大图对比 -->
- <view v-if="hasDemo" class="hero-thumb" @click="openDemoCompare">
- <view class="mini-compare">
- <image class="compare-img" :src="demoPair.result" mode="aspectFill" />
- <view class="compare-before mini-before">
- <image class="compare-img" :src="demoPair.original" mode="aspectFill" />
- </view>
- <view class="compare-line mini-line"></view>
- <!-- 缩略图只有 168rpx 宽,标签用两个字,否则会挤到中线上 -->
- <view class="compare-tag left">原图</view>
- <view class="compare-tag right">修复</view>
- </view>
- </view>
- </view>
-
- <!-- ==================== 结果 ==================== -->
- <view v-if="job.photo_open_id" class="section">
- <view class="section-head">
- <text class="tool-section-title">{{ job.title || '修复结果' }}</text>
- <text class="section-tip">{{ job.status_text }}</text>
- </view>
-
- <!-- 处理中 -->
- <view v-if="isPending" class="progress-box">
- <view class="progress-ring"></view>
- <text class="progress-title">{{ progressTitle }}</text>
- <text class="progress-sub">已等待 {{ waitedSeconds }} 秒,可以留在本页等待,也可以稍后从历史记录查看</text>
- </view>
-
- <!-- 失败 -->
- <view v-else-if="job.status < 0" class="fail-box">
- <text class="fail-title">{{ job.error_msg || '生成失败' }}</text>
- <text class="fail-sub">本次未扣点,可以换一张更清晰的照片重试</text>
- </view>
-
- <!-- 成功:默认直接展示修复后的成品,对比放到弹层里 -->
- <view v-else class="result-box">
- <view class="result-stage" @click="previewResult">
- <image class="result-img" :src="job.result_url" mode="widthFix" />
- </view>
- <view class="result-actions">
- <view class="ghost-btn" @click="openResultCompare">查看对比</view>
- <view class="ghost-btn" @click="saveResult">保存到相册</view>
- <view class="ghost-btn" @click="resetJob">再修一张</view>
- </view>
- </view>
- </view>
-
- <!-- ==================== 上传 ==================== -->
- <view v-if="!job.photo_open_id || job.status < 0" class="upload-card">
- <view class="card-head">
- <view>
- <text class="card-title"><text class="required">*</text>上传照片</text>
- <text class="card-desc">支持黑白、泛黄、模糊、划痕或破损照片,单张不超过 10M</text>
- </view>
- <text class="card-tip">{{ localImage ? '1/1' : '0/1' }}</text>
- </view>
- <view class="upload-strip" @click="chooseImage">
- <view class="upload-thumb">
- <image v-if="localImage" class="upload-img" :src="localImage" mode="aspectFill" />
- <text v-else class="upload-plus">+</text>
- </view>
- <view class="upload-copy">
- <text class="upload-title">{{ localImage ? '已选择照片,可点击更换' : '上传需要修复的老照片' }}</text>
- <text class="upload-sub">建议主体清晰、人物未被严重遮挡</text>
- </view>
- <view class="upload-action">{{ localImage ? '更换' : '上传' }}</view>
- </view>
- </view>
-
- <!-- ==================== 档位 ==================== -->
- <view v-if="!job.photo_open_id || job.status < 0" class="section">
- <view class="section-head">
- <text class="tool-section-title">修复档位</text>
- <text class="section-tip">按张计费</text>
- </view>
- <view class="mode-list">
- <view
- v-for="item in modes"
- :key="item.key"
- class="mode-item"
- :class="{ active: activeMode === item.key }"
- @click="selectMode(item.key)"
- >
- <view class="mode-main">
- <text class="mode-name">{{ item.name }}</text>
- <text class="mode-desc">{{ item.desc }}</text>
- </view>
- <text class="mode-points">{{ item.points }}点</text>
- </view>
- </view>
- </view>
-
- <!-- ==================== 补充描述 ==================== -->
- <view v-if="!job.photo_open_id || job.status < 0" class="section">
- <view class="section-head">
- <text class="tool-section-title">补充说明</text>
- <text class="section-tip">选填</text>
- </view>
- <textarea
- v-model="extraPrompt"
- class="textarea"
- maxlength="100"
- placeholder="可以补充照片里的事实信息,例如:军装是绿色的、背景是老家的院子"
- />
- <view class="tips">
- <text v-for="tip in tips" :key="tip" class="tip-item">· {{ tip }}</text>
- </view>
- </view>
-
- <!-- ==================== 底部操作 ==================== -->
- <view class="bottom-bar">
- <view class="balance">
- <text class="balance-copy">{{ balanceText }}</text>
- <view v-if="isBalanceInsufficient" class="recharge-link" @click="goRecharge">去充值</view>
- </view>
- <view class="buttons">
- <view class="history-btn" @click="goHistory">历史记录</view>
- <view class="submit-btn" :class="{ disabled: submitDisabled }" @click="submit">
- <image class="submit-icon" src="/static/icons/bolt.svg" mode="aspectFit" />
- <text>{{ submitText }}</text>
- </view>
- </view>
- </view>
-
- <!-- ==================== 前后对比(demo 与成品共用,样式同动漫头像) ==================== -->
- <view v-if="compareVisible" class="compare-modal">
- <view class="compare-backdrop" @click="closeCompare"></view>
- <view class="compare-panel">
- <view class="compare-panel-head">
- <text>{{ comparePair.title }}</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="comparePair.after" mode="aspectFit" />
- <view class="compare-before" :style="{ width: comparePercent + '%' }">
- <image class="compare-img" :src="comparePair.before" 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, onShow, onUnload } from '@dcloudio/uni-app'
- import { commonApi, oldPhotoApi, userApi } from '@/api/index.js'
- import { createOldPhoto } from '@/utils/upload.js'
- import { guid } from '@/utils/util.js'
- import { useUserStore } from '@/store/user.js'
- import { useApp } from '@/utils/useApp.js'
-
- const userStore = useUserStore()
- const { toast } = useApp()
-
- // 轮询:混元未买并发,排队 + 生成通常几十秒,最长按后端 20 分钟超时来兜
- const POLL_INTERVAL = 3000
- const POLL_LIMIT = 5 * 60 * 1000
-
- // 首屏示例对比图由配置项 old_photo_demo 下发(同动漫头像的 anime_demo_v2),不在端上硬编码
- const DEMO_CONFIG_KEY = 'old_photo_demo'
-
- const modes = ref([])
- const tips = ref([])
- const activeMode = ref('colorize')
- const extraPrompt = ref('')
- const localImage = ref('')
- const balance = ref({ points: 0 })
- const quote = ref({ points: 0 })
- const job = ref({})
- const submitting = ref(false)
- const comparePercent = ref(50)
- const compareVisible = ref(false)
- // 示例对比图(配置下发)与对比弹层的数据源(示例 或 本次成品)
- const demoPair = ref({ original: '', result: '' })
- const comparePair = ref({ before: '', after: '', title: '效果对比' })
- const waitedSeconds = ref(0)
-
- let pollTimer = null
- let pollStart = 0
-
- const hasDemo = computed(() => Boolean(demoPair.value.original && demoPair.value.result))
- const currentPoints = computed(() => Number(balance.value.points || balance.value.point_balance || 0))
- const currentMode = computed(() => modes.value.find((item) => item.key === activeMode.value) || {})
- const currentCost = computed(() => Number(currentMode.value.points || quote.value.points || 0))
- const isPending = computed(() => [10, 20].includes(Number(job.value.status)))
- const balanceText = computed(() => {
- const cost = currentCost.value
- if (!userStore.isLogin) return cost ? `老照片修复 · ${cost}点/张` : '老照片修复'
- return cost ? `余额${currentPoints.value}点 · 预计消耗${cost}点` : `余额${currentPoints.value}点`
- })
- const isBalanceInsufficient = computed(
- () => userStore.isLogin && currentCost.value > 0 && currentPoints.value < currentCost.value
- )
- const submitDisabled = computed(() => submitting.value || isPending.value)
- const submitText = computed(() => {
- if (submitting.value) return '提交中...'
- if (isPending.value) return '正在生成...'
- const cost = currentCost.value
- if (!userStore.isLogin) return cost ? `登录后修复 · ${cost}点` : '登录后修复'
- return cost ? `立即修复 · ${cost}点` : '立即修复'
- })
- const progressTitle = computed(() => (Number(job.value.status) === 10 ? '排队中,前面还有任务' : 'AI 正在修复上色'))
-
- onLoad((options) => {
- loadConfig()
- loadDemo()
- if (options && options.work) loadJob(options.work, true)
- })
-
- onShow(() => {
- if (userStore.isLogin) loadBalance()
- // 从历史记录页返回时带回来的记录 id
- const workId = uni.getStorageSync('old_photo_work_id')
- if (workId) {
- uni.removeStorageSync('old_photo_work_id')
- loadJob(workId)
- }
- })
-
- onUnload(() => stopPoll())
-
- async function loadConfig() {
- try {
- const res = await oldPhotoApi.getConfig({ mode: activeMode.value })
- const data = res.data || {}
- modes.value = data.modes || []
- tips.value = data.tips || []
- if (data.default_mode && !modes.value.find((item) => item.key === activeMode.value)) {
- activeMode.value = data.default_mode
- }
- const billing = data.billing || {}
- quote.value = billing.quote || {}
- if (billing.balance) balance.value = billing.balance
- } catch (e) {
- toast(e.msg || '配置加载失败')
- }
- }
-
- async function loadBalance() {
- try {
- const res = await userApi.getBalance()
- balance.value = res.data || {}
- } catch (e) {}
- }
-
- /**
- * 示例对比图:配置项 old_photo_demo,格式 [{ original, result }]
- * 兼容 after/anime 字段名与「原图,结果图」这种逗号分隔的老写法;配置缺失就不展示示例入口。
- */
- async function loadDemo() {
- try {
- const res = await commonApi.getConfig({ keys: DEMO_CONFIG_KEY })
- const rows = Array.isArray(res.list) ? res.list : []
- const item = rows.find((it) => it.key === DEMO_CONFIG_KEY)
- demoPair.value = normalizeDemo(item && item.value)
- } catch (e) {}
- }
-
- function normalizeDemo(value) {
- const empty = { original: '', result: '' }
- if (!value) return empty
- try {
- const data = typeof value === 'string' ? JSON.parse(value) : value
- const first = Array.isArray(data) ? data[0] : data
- if (!first) return empty
- return {
- original: first.original || first.before || first.source || '',
- result: first.result || first.after || first.anime || ''
- }
- } catch (e) {
- const pics = String(value).split(',').map((item) => item.trim()).filter(Boolean)
- if (pics.length >= 2) return { original: pics[0], result: pics[1] }
- return empty
- }
- }
-
- function selectMode(key) {
- if (activeMode.value === key) return
- activeMode.value = key
- }
-
- function chooseImage() {
- if (!requireLogin()) return
- uni.chooseImage({
- count: 1,
- sizeType: ['original'], // 老照片细节要留住,不做端上压缩
- sourceType: ['album', 'camera'],
- success: (res) => {
- const file = (res.tempFiles && res.tempFiles[0]) || {}
- if (Number(file.size || 0) > 10 * 1024 * 1024) {
- toast('图片不能超过 10M')
- return
- }
- localImage.value = res.tempFilePaths[0]
- job.value = {}
- }
- })
- }
-
- async function submit() {
- if (!requireLogin()) return
- if (submitDisabled.value) return
- if (!localImage.value) return toast('请先上传老照片')
- if (isBalanceInsufficient.value) return showBalanceModal()
-
- submitting.value = true
- uni.showLoading({ title: '提交中...', mask: true })
- try {
- const res = await createOldPhoto(localImage.value, {
- mode: activeMode.value,
- extra_prompt: extraPrompt.value,
- request_id: guid()
- })
- job.value = res.data || {}
- comparePercent.value = 50
- if (Number(job.value.status) === 30) {
- onJobDone()
- } else {
- startPoll()
- }
- } catch (e) {
- // 1011 = 点数不足,后端统一错误码
- if (e && e.code === 1011) showBalanceModal()
- else toast((e && e.msg) || '提交失败,请重试')
- } finally {
- submitting.value = false
- uni.hideLoading()
- }
- }
-
- async function loadJob(photoOpenId, silent = false) {
- try {
- const res = await oldPhotoApi.getDetail({ photo_open_id: photoOpenId })
- job.value = res.data || {}
- if (job.value.source_url) localImage.value = ''
- if (isPending.value) startPoll()
- else if (Number(job.value.status) === 30) onJobDone()
- } catch (e) {
- if (!silent) toast((e && e.msg) || '记录加载失败')
- }
- }
-
- function startPoll() {
- stopPoll()
- pollStart = Date.now()
- waitedSeconds.value = 0
- pollTimer = setInterval(async () => {
- waitedSeconds.value = Math.round((Date.now() - pollStart) / 1000)
- if (Date.now() - pollStart > POLL_LIMIT) {
- stopPoll()
- toast('生成时间较长,可稍后从历史记录查看')
- return
- }
- try {
- const res = await oldPhotoApi.getDetail({ photo_open_id: job.value.photo_open_id })
- const data = res.data || {}
- job.value = data
- if (Number(data.status) === 30) {
- stopPoll()
- onJobDone()
- } else if (Number(data.status) < 0) {
- stopPoll()
- toast(data.error_msg || '生成失败,本次未扣点')
- }
- } catch (e) {
- // 单次失败不中断轮询
- }
- }, POLL_INTERVAL)
- }
-
- function stopPoll() {
- if (pollTimer) clearInterval(pollTimer)
- pollTimer = null
- }
-
- function onJobDone() {
- comparePercent.value = 50
- compareVisible.value = false
- loadBalance()
- }
-
- function resetJob() {
- stopPoll()
- job.value = {}
- localImage.value = ''
- extraPrompt.value = ''
- compareVisible.value = false
- }
-
- function previewResult() {
- if (!job.value.result_url) return
- uni.previewImage({
- urls: [job.value.result_url],
- current: job.value.result_url
- })
- }
-
- function openDemoCompare() {
- if (!hasDemo.value) return
- comparePair.value = {
- before: demoPair.value.original,
- after: demoPair.value.result,
- title: '效果对比'
- }
- comparePercent.value = 50
- compareVisible.value = true
- }
-
- function openResultCompare() {
- if (!job.value.result_url || !job.value.source_url) return
- comparePair.value = {
- before: job.value.source_url,
- after: job.value.result_url,
- title: job.value.title || '修复结果'
- }
- comparePercent.value = 50
- compareVisible.value = true
- }
-
- function closeCompare() {
- compareVisible.value = false
- }
-
- function saveResult() {
- if (!job.value.result_url) return
- uni.showLoading({ title: '保存中...', mask: true })
- uni.downloadFile({
- url: job.value.result_url,
- success: (res) => {
- uni.saveImageToPhotosAlbum({
- filePath: res.tempFilePath,
- success: () => toast('已保存到相册'),
- fail: () => toast('保存失败,请检查相册权限')
- })
- },
- fail: () => toast('图片下载失败'),
- complete: () => uni.hideLoading()
- })
- }
-
- function goHistory() {
- if (!requireLogin()) return
- uni.navigateTo({ url: '/pages/tool/old-photo-history' })
- }
-
- 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 requireLogin() {
- if (userStore.isLogin) return true
- uni.navigateTo({ url: '/pages/login/login' })
- return false
- }
-
- function showBalanceModal() {
- const cost = currentCost.value
- uni.showModal({
- title: '温馨提示',
- content: cost ? `本次修复需要 ${cost} 点,当前点数不足,请充值后继续。` : '点数不足,请充值后继续。',
- confirmText: '去充值',
- cancelText: '知道了',
- success: (res) => {
- if (res.confirm) uni.navigateTo({ url: '/pages/recharge/recharge' })
- }
- })
- }
-
- function goRecharge() {
- uni.navigateTo({ url: '/pages/recharge/recharge' })
- }
- </script>
-
- <style lang="scss" scoped>
- .wrap {
- min-height: 100vh;
- padding: 24rpx 28rpx calc(300rpx + env(safe-area-inset-bottom));
- background: #f3f7fb;
- box-sizing: border-box;
- }
-
- .hero-card {
- position: relative;
- padding: 30rpx;
- border-radius: 28rpx;
- color: #fff;
- background: linear-gradient(135deg, #172033 0%, #2d5d7d 56%, #5cb7c4 100%);
- overflow: hidden;
- display: flex;
- align-items: center;
- box-sizing: border-box;
- }
- .hero-card::before {
- content: '';
- position: absolute;
- right: -90rpx;
- top: -90rpx;
- width: 280rpx;
- height: 280rpx;
- border-radius: 50%;
- background: rgba(255, 255, 255, 0.12);
- }
- .hero-copy {
- position: relative;
- z-index: 1;
- flex: 1;
- min-width: 0;
- }
- .eyebrow { display: block; color: rgba(255, 255, 255, 0.68); font-size: 21rpx; font-weight: 800; }
- .hero-title { display: block; margin-top: 14rpx; color: #fff; font-size: 38rpx; font-weight: 900; }
- .hero-desc {
- display: block;
- margin-top: 14rpx;
- color: rgba(255, 255, 255, 0.82);
- font-size: 23rpx;
- line-height: 1.5;
- }
- .hero-actions { margin-top: 22rpx; display: flex; gap: 16rpx; }
- .hero-pill,
- .hero-link {
- height: 60rpx;
- padding: 0 24rpx;
- border-radius: 999rpx;
- font-size: 25rpx;
- font-weight: 800;
- line-height: 60rpx;
- text-align: center;
- }
- .hero-pill { color: #17466b; background: #fff; }
- .hero-link {
- color: #fff;
- background: rgba(255, 255, 255, 0.18);
- border: 2rpx solid rgba(255, 255, 255, 0.42);
- line-height: 56rpx;
- }
-
- .hero-thumb {
- position: relative;
- z-index: 1;
- margin-left: 22rpx;
- flex-shrink: 0;
- }
- /* 对比组件:与动漫头像保持一致 */
- .mini-compare,
- .large-compare {
- position: relative;
- overflow: hidden;
- background: #dfe8f4;
- }
- .mini-compare {
- width: 168rpx;
- height: 252rpx;
- border-radius: 28rpx;
- border: 2rpx solid rgba(255, 255, 255, 0.45);
- box-shadow: 0 18rpx 38rpx rgba(20, 37, 66, 0.28);
- }
- .compare-img {
- width: 100%;
- height: 100%;
- }
- .compare-before {
- position: absolute;
- left: 0;
- top: 0;
- bottom: 0;
- overflow: hidden;
- }
- .mini-before { width: 50%; }
- .compare-before .compare-img { width: 164rpx; }
- .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.mini-line { left: 50%; }
- .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; }
-
- .upload-card,
- .section {
- margin-top: 22rpx;
- padding: 28rpx;
- border-radius: 24rpx;
- background: #fff;
- box-shadow: 0 8rpx 28rpx rgba(34, 68, 112, 0.05);
- }
- .card-head,
- .section-head { display: flex; align-items: baseline; }
- .card-title,
- .tool-section-title { display: block; color: #172033; font-size: 31rpx; font-weight: 800; }
- .required { margin-right: 4rpx; color: #ef4444; }
- .card-desc { display: block; margin-top: 8rpx; color: #8a95a6; font-size: 23rpx; line-height: 1.35; }
- .card-tip,
- .section-tip { margin-left: auto; color: #8a95a6; font-size: 23rpx; }
-
- .upload-strip {
- margin-top: 22rpx;
- min-height: 142rpx;
- padding: 18rpx;
- border: 2rpx dashed rgba(11, 140, 255, 0.28);
- border-radius: 20rpx;
- background: #f8fbff;
- display: flex;
- align-items: center;
- box-sizing: border-box;
- }
- .upload-thumb {
- width: 104rpx;
- height: 104rpx;
- border-radius: 18rpx;
- background: #eef6ff;
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
- overflow: hidden;
- }
- .upload-img { width: 100%; height: 100%; }
- .upload-plus { color: #0b8cff; font-size: 48rpx; line-height: 1; }
- .upload-copy { min-width: 0; flex: 1; margin-left: 20rpx; }
- .upload-title { display: block; color: #172033; font-size: 29rpx; font-weight: 800; }
- .upload-sub { display: block; margin-top: 8rpx; color: #7f8896; font-size: 23rpx; line-height: 1.4; }
- .upload-action { margin-left: 18rpx; color: #0b8cff; font-size: 25rpx; font-weight: 800; }
-
- .mode-list { margin-top: 20rpx; }
- .mode-item {
- margin-bottom: 14rpx;
- padding: 22rpx;
- border-radius: 20rpx;
- background: #f7f9fd;
- border: 2rpx solid transparent;
- display: flex;
- align-items: center;
- box-sizing: border-box;
- }
- .mode-item:last-child { margin-bottom: 0; }
- .mode-item.active { background: #eff8ff; border-color: #0b8cff; }
- .mode-main { min-width: 0; flex: 1; }
- .mode-name { display: block; color: #172033; font-size: 28rpx; font-weight: 800; }
- .mode-desc { display: block; margin-top: 6rpx; color: #8a95a6; font-size: 23rpx; line-height: 1.4; }
- .mode-points { margin-left: 18rpx; color: #0b8cff; font-size: 27rpx; font-weight: 800; flex-shrink: 0; }
-
- .textarea {
- margin-top: 20rpx;
- width: 100%;
- min-height: 150rpx;
- padding: 22rpx;
- border-radius: 20rpx;
- color: #172033;
- font-size: 25rpx;
- line-height: 1.5;
- background: #f7f9fd;
- box-sizing: border-box;
- }
- .tips { margin-top: 18rpx; }
- .tip-item { display: block; margin-top: 6rpx; color: #8a95a6; font-size: 22rpx; line-height: 1.5; }
-
- .progress-box { padding: 50rpx 0; display: flex; flex-direction: column; align-items: center; }
- .progress-ring {
- width: 64rpx;
- height: 64rpx;
- border: 6rpx solid #e0f0ff;
- border-top-color: #0b8cff;
- border-radius: 50%;
- animation: spin 0.9s linear infinite;
- }
- @keyframes spin { to { transform: rotate(360deg); } }
- .progress-title { margin-top: 22rpx; color: #172033; font-size: 28rpx; font-weight: 800; }
- .progress-sub {
- margin-top: 10rpx;
- padding: 0 40rpx;
- color: #8a95a6;
- font-size: 23rpx;
- line-height: 1.5;
- text-align: center;
- }
-
- .fail-box { padding: 44rpx 0; display: flex; flex-direction: column; align-items: center; }
- .fail-title { color: #f04438; font-size: 28rpx; font-weight: 800; }
- .fail-sub { margin-top: 10rpx; color: #8a95a6; font-size: 23rpx; }
-
- .result-box { margin-top: 20rpx; }
- .result-stage {
- border-radius: 20rpx;
- background: #f1f5fa;
- overflow: hidden;
- }
- .result-img { display: block; width: 100%; }
-
- .result-actions { margin-top: 20rpx; display: flex; gap: 16rpx; }
- .ghost-btn {
- flex: 1;
- height: 76rpx;
- border-radius: 999rpx;
- color: #1f8cff;
- font-size: 25rpx;
- font-weight: 800;
- line-height: 76rpx;
- text-align: center;
- background: #eef7ff;
- }
-
- .bottom-bar {
- position: fixed;
- left: 0;
- right: 0;
- bottom: 0;
- z-index: 80;
- padding: 18rpx 28rpx calc(18rpx + env(safe-area-inset-bottom));
- background: rgba(255, 255, 255, 0.96);
- box-shadow: 0 -10rpx 30rpx rgba(32, 56, 92, 0.1);
- }
- .balance {
- margin-bottom: 14rpx;
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 20rpx;
- color: #7b8797;
- font-size: 24rpx;
- line-height: 1.35;
- }
- .balance-copy { min-width: 0; flex: 1; }
- .recharge-link { color: #0b8cff; font-size: 24rpx; font-weight: 800; flex-shrink: 0; }
- .buttons { display: flex; gap: 22rpx; }
- .history-btn,
- .submit-btn {
- height: 86rpx;
- border-radius: 999rpx;
- font-size: 30rpx;
- font-weight: 800;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .history-btn { width: 210rpx; color: #1f8cff; background: #eef7ff; }
- .submit-btn {
- flex: 1;
- color: #fff;
- background: linear-gradient(135deg, #1f8cff, #41b9ff);
- box-shadow: 0 12rpx 24rpx rgba(31, 140, 255, 0.28);
- }
- .submit-btn.disabled { opacity: 0.6; }
- .submit-icon { width: 24rpx; height: 24rpx; margin-right: 8rpx; }
-
- /* 对比弹层:与动漫头像保持一致 */
- .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 {
- width: 100%;
- height: 900rpx;
- max-height: 70vh;
- border-radius: 22rpx;
- }
- .large-compare .compare-before .compare-img {
- width: calc(100vw - 120rpx);
- }
- .compare-guide {
- display: block;
- margin-top: 18rpx;
- color: #64748b;
- font-size: 24rpx;
- text-align: center;
- }
- </style>
|