|
- <template>
- <view class="page">
- <scroll-view class="scroll" scroll-y>
- <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">适合黑白、泛黄、模糊、划痕或破损照片,先搭建 UI,生成逻辑后续接入。</text>
- </view>
- <view class="hero-preview">
- <view class="photo before"></view>
- <view class="photo after"></view>
- </view>
- </view>
-
- <view class="upload-card">
- <view class="card-head">
- <view>
- <text class="card-title"><text class="required">*</text>上传照片</text>
- <text class="card-desc">支持黑白、泛黄、模糊、划痕或破损照片</text>
- </view>
- <text class="card-tip">0/1</text>
- </view>
- <view class="upload-strip" @click="showTodo('选择老照片')">
- <view class="upload-thumb"><text class="upload-plus">+</text></view>
- <view class="upload-copy">
- <text class="upload-title">上传需要修复的老照片</text>
- <text class="upload-sub">建议选择主体清晰、人物不被严重遮挡的照片</text>
- </view>
- <view class="upload-action">上传</view>
- </view>
- </view>
-
- <view class="section">
- <view class="section-head">
- <text class="tool-section-title">照片风格</text>
- <text class="section-tip">输出方式</text>
- </view>
- <view class="style-grid">
- <view
- v-for="item in styleOptions"
- :key="item"
- class="style-item"
- :class="{ active: activeStyle === item }"
- @click="activeStyle = item"
- >
- {{ item }}
- </view>
- </view>
- </view>
-
- <view class="section">
- <view class="section-head">
- <text class="tool-section-title">描述关键词</text>
- <text class="section-tip">点击添加</text>
- </view>
- <textarea class="textarea" placeholder="补充照片修复要求,例如保留原始五官、增强清晰度、自然上色"></textarea>
- <view class="keyword-row">
- <view
- v-for="item in keywords"
- :key="item"
- class="keyword-chip"
- :class="{ active: selectedKeywords.includes(item) }"
- @click="toggleKeyword(item)"
- >
- {{ item }}
- </view>
- </view>
- </view>
- </view>
- </scroll-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="showTodo('历史记录')">历史记录</view>
- <view class="submit-btn" @click="showTodo('老照片修复')">
- <image class="submit-icon" src="/static/icons/bolt.svg" mode="aspectFit" />
- <text>{{ submitText }}</text>
- </view>
- </view>
- </view>
- </view>
- </template>
-
- <script setup>
- import { computed, ref } from 'vue'
- import { onLoad, onShow } from '@dcloudio/uni-app'
- import { billingApi, userApi } from '@/api/index.js'
- import { useUserStore } from '@/store/user.js'
- import { useApp } from '@/utils/useApp.js'
-
- const userStore = useUserStore()
- const { toast } = useApp()
- const activeStyle = ref('彩色')
- const selectedKeywords = ref(['4K', '自然上色'])
- const balance = ref({ points: 0 })
- const quote = ref({ points: 0 })
- const appKey = 'old-photo'
-
- const styleOptions = ['彩色', '黑白']
- const keywords = [
- '4K',
- '自然上色',
- '去掉划痕',
- '去褶皱',
- '去噪点',
- '去除斑驳破损',
- '衣服修复',
- '面部还原真实人像',
- '还原人脸五官细节',
- '眼神神态自然',
- '修补缺失破损区域',
- '画质超清增强',
- '锐化细节',
- '保留年代质感',
- '人物容貌不变',
- '不扭曲五官'
- ]
- const currentPoints = computed(() => Number(balance.value.points || balance.value.point_balance || 0))
- const currentCost = computed(() => Number(quote.value.points || 0))
- 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 submitText = computed(() => currentCost.value ? `立即修复 · ${currentCost.value}点` : '立即修复')
-
- onLoad(() => {
- loadBillingConfig()
- })
-
- onShow(() => {
- if (userStore.isLogin) loadBalance()
- loadBillingConfig()
- })
-
- async function loadBalance() {
- try {
- const res = await userApi.getBalance()
- balance.value = res.data || {}
- } catch (e) {}
- }
-
- async function loadBillingConfig() {
- try {
- const res = await billingApi.getConfig({ app_key: appKey, image_count: 1 })
- const data = res.data || {}
- quote.value = data.quote || {}
- if (data.balance) balance.value = data.balance
- } catch (e) {}
- }
-
- function goRecharge() {
- uni.navigateTo({ url: '/pages/recharge/recharge' })
- }
-
- function toggleKeyword(item) {
- const index = selectedKeywords.value.indexOf(item)
- if (index >= 0) {
- selectedKeywords.value.splice(index, 1)
- } else {
- selectedKeywords.value.push(item)
- }
- }
-
- function showTodo(name) {
- toast(`${name}逻辑待接入`)
- }
- </script>
-
- <style lang="scss" scoped>
- .page {
- height: 100vh;
- background: #f3f7fb;
- display: flex;
- flex-direction: column;
- overflow: hidden;
- }
- .scroll { flex: 1; height: 0; }
- .wrap { padding: 24rpx 28rpx 300rpx; }
-
- .hero-card {
- position: relative;
- min-height: 260rpx;
- padding: 30rpx;
- border-radius: 28rpx;
- color: #fff;
- background: linear-gradient(135deg, #172033 0%, #2d5d7d 56%, #5cb7c4 100%);
- overflow: hidden;
- 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,
- .hero-preview {
- position: relative;
- z-index: 1;
- }
- .eyebrow {
- display: block;
- color: rgba(255, 255, 255, 0.68);
- font-size: 21rpx;
- font-weight: 800;
- }
- .hero-title {
- display: block;
- margin-top: 16rpx;
- color: #fff;
- font-size: 40rpx;
- font-weight: 900;
- }
- .hero-desc {
- display: block;
- margin-top: 14rpx;
- width: 430rpx;
- color: rgba(255, 255, 255, 0.82);
- font-size: 24rpx;
- line-height: 1.5;
- }
- .hero-preview {
- position: absolute;
- right: 30rpx;
- bottom: 28rpx;
- width: 170rpx;
- height: 116rpx;
- }
- .photo {
- position: absolute;
- width: 96rpx;
- height: 116rpx;
- border-radius: 16rpx;
- box-shadow: 0 16rpx 28rpx rgba(0, 0, 0, 0.18);
- }
- .photo.before {
- left: 0;
- top: 0;
- background: linear-gradient(135deg, #d7d0c6, #6c6259);
- }
- .photo.after {
- right: 0;
- bottom: 0;
- background: linear-gradient(135deg, #f4d4a0, #5db2d9 50%, #254f6d);
- }
-
- .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;
- }
- .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; }
-
- .style-grid {
- margin-top: 20rpx;
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: 14rpx;
- }
- .style-item {
- height: 76rpx;
- border-radius: 18rpx;
- color: #596579;
- font-size: 26rpx;
- font-weight: 800;
- line-height: 76rpx;
- text-align: center;
- background: #f7f9fd;
- border: 2rpx solid transparent;
- }
- .style-item.active {
- color: #0b8cff;
- background: #eff8ff;
- border-color: #0b8cff;
- }
- .textarea {
- margin-top: 20rpx;
- width: 100%;
- min-height: 180rpx;
- padding: 22rpx;
- border-radius: 20rpx;
- color: #172033;
- font-size: 25rpx;
- line-height: 1.5;
- background: #f7f9fd;
- box-sizing: border-box;
- }
- .keyword-row {
- margin-top: 18rpx;
- display: flex;
- flex-wrap: wrap;
- gap: 14rpx;
- }
- .keyword-chip {
- min-height: 58rpx;
- padding: 0 22rpx;
- border-radius: 30rpx;
- color: #0b8cff;
- font-size: 24rpx;
- font-weight: 700;
- line-height: 58rpx;
- background: #eef7ff;
- border: 2rpx solid transparent;
- box-sizing: border-box;
- }
- .keyword-chip.active {
- color: #fff;
- background: #0b8cff;
- }
- .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-icon {
- width: 24rpx;
- height: 24rpx;
- margin-right: 8rpx;
- }
- </style>
|