|
- <template>
- <view class="page">
- <scroll-view class="scroll" scroll-y>
- <view class="wrap">
- <view class="hero-card portrait-hero">
- <view class="hero-copy">
- <text class="eyebrow">PORTRAIT PHOTO STUDIO</text>
- <text class="hero-title">生成你的职业形象照</text>
- <text class="hero-desc">从商务形象、普通职业到艺术写真,快速得到适合头像和简历的照片。</text>
- </view>
- <view class="hero-preview">
- <view class="portrait-card-mini main">
- <view class="portrait-face"></view>
- <view class="portrait-suit"></view>
- </view>
- <view class="portrait-card-mini sub">
- <view class="portrait-face"></view>
- <view class="portrait-suit"></view>
- </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="segmented">
- <view
- v-for="item in styleTabs"
- :key="item.key"
- class="segment"
- :class="{ active: activeStyle === item.key }"
- @click="activeStyle = item.key"
- >
- {{ item.name }}
- </view>
- </view>
-
- <view v-if="activeStyle === 'business'" class="config-panel">
- <view class="sub-block">
- <text class="sub-title">纯色背景</text>
- <view class="option-grid">
- <view v-for="item in businessBg" :key="item.name" class="option" :class="{ 'active-first': item.name === '白色' }">
- <view class="color-dot" :class="item.className"></view>{{ item.name }}
- </view>
- </view>
- </view>
- <view class="sub-block">
- <text class="sub-title">服装</text>
- <view class="option-grid two">
- <view class="option active-first">正装</view>
- <view class="option">衬衫</view>
- </view>
- </view>
- <view class="sub-block">
- <text class="sub-title">取景范围</text>
- <view class="option-grid three">
- <view class="option active-first">半身</view>
- <view class="option">全身</view>
- <view class="option">胸像</view>
- </view>
- </view>
- </view>
-
- <view v-if="activeStyle === 'career'" class="config-panel">
- <view class="sub-block">
- <text class="sub-title">背景</text>
- <view class="option-grid three">
- <view v-for="item in careerBg" :key="item" class="option" :class="{ 'active-first': item === '浅灰' }">{{ item }}</view>
- </view>
- </view>
- <view class="sub-block">
- <text class="sub-title">服装</text>
- <view class="option-grid">
- <view v-for="item in careerOutfit" :key="item" class="option" :class="{ 'active-first': item === '针织衫' }">{{ item }}</view>
- </view>
- </view>
- </view>
-
- <view v-if="activeStyle === 'art'" class="config-panel">
- <view class="option-grid three">
- <view v-for="item in artStyles" :key="item" class="option" :class="{ 'active-first': item === '古风' }">{{ item }}</view>
- </view>
- </view>
- </view>
-
- <view class="section">
- <view class="section-head">
- <text class="tool-section-title">图片比例</text>
- <text class="section-tip">构图尺寸</text>
- </view>
- <view class="ratio-grid">
- <view
- v-for="item in ratios"
- :key="item"
- class="ratio"
- :class="{ active: activeRatio === item }"
- @click="activeRatio = item"
- >
- {{ item }}
- </view>
- </view>
- </view>
-
- <view class="section">
- <view class="section-head">
- <text class="tool-section-title">生成数量</text>
- <text class="price">20点/张,共{{ quantity * 20 }}点</text>
- </view>
- <view class="quantity">
- <view class="quantity-btn" @click="decreaseQuantity">-</view>
- <text class="quantity-value">{{ quantity }}</text>
- <view class="quantity-btn" @click="increaseQuantity">+</view>
- </view>
- <text class="note">按单张收费,每次最多生成10张</text>
- </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>
- </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, watch } 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('business')
- const activeRatio = ref('1:1')
- const quantity = ref(1)
- const balance = ref({ points: 0 })
- const quote = ref({ points: 0 })
- const appKey = 'portrait-photo'
-
- const styleTabs = [
- { key: 'business', name: '商务形象照' },
- { key: 'career', name: '普通职业照' },
- { key: 'art', name: '艺术写真' }
- ]
- const businessBg = [
- { name: '白色', className: 'white' },
- { name: '浅灰', className: 'gray' },
- { name: '商务蓝', className: 'blue' },
- { name: '深蓝', className: 'deep-blue' }
- ]
- const careerBg = ['浅灰', '米色', '办公室', '书架', '咖啡角', '落地窗']
- const careerOutfit = ['针织衫', 'T恤配牛仔裤', '西装外套配T恤']
- const artStyles = ['古风', '藏族', '苗族', '都市青年', '潮人', '敦煌飞仙', '港风复古', '新中式', '法式油画']
- const ratios = ['1:1', '3:4', '4:3', '9:16', '16:9']
- const portraitModel = computed(() => activeStyle.value === 'art' ? 'premium' : 'standard')
- 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()
- })
-
- watch([activeStyle, quantity], () => {
- 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,
- model: portraitModel.value,
- count: quantity.value
- })
- 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 showTodo(name) {
- toast(`${name}逻辑待接入`)
- }
-
- function decreaseQuantity() {
- quantity.value = Math.max(1, quantity.value - 1)
- }
-
- function increaseQuantity() {
- quantity.value = Math.min(10, quantity.value + 1)
- }
- </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, #2447d8 0%, #0b8cff 56%, #55d6c6 100%);
- overflow: hidden;
- box-sizing: border-box;
- }
- .hero-card::before {
- content: '';
- position: absolute;
- right: -90rpx;
- top: -110rpx;
- width: 300rpx;
- height: 300rpx;
- border-radius: 50%;
- background: rgba(255, 255, 255, 0.16);
- }
- .hero-copy,
- .hero-preview {
- position: relative;
- z-index: 1;
- }
- .eyebrow {
- display: block;
- color: rgba(255, 255, 255, 0.72);
- font-size: 21rpx;
- font-weight: 800;
- }
- .hero-title {
- display: block;
- margin-top: 16rpx;
- width: 430rpx;
- color: #fff;
- font-size: 40rpx;
- font-weight: 900;
- line-height: 1.24;
- }
- .hero-desc {
- display: block;
- margin-top: 14rpx;
- width: 420rpx;
- color: rgba(255, 255, 255, 0.86);
- font-size: 24rpx;
- line-height: 1.5;
- }
- .hero-preview {
- position: absolute;
- right: 30rpx;
- bottom: 28rpx;
- width: 178rpx;
- height: 126rpx;
- }
- .portrait-card-mini {
- position: absolute;
- width: 92rpx;
- height: 120rpx;
- border-radius: 18rpx;
- overflow: hidden;
- box-shadow: 0 16rpx 30rpx rgba(0, 54, 125, 0.22);
- }
- .portrait-card-mini.main {
- left: 0;
- top: 0;
- background: linear-gradient(180deg, #e8f4ff 0%, #9bd0ff 100%);
- }
- .portrait-card-mini.sub {
- right: 0;
- bottom: 0;
- background: linear-gradient(180deg, #fff4e7 0%, #ebb875 100%);
- }
- .portrait-face {
- width: 38rpx;
- height: 42rpx;
- margin: 22rpx auto 0;
- border-radius: 50% 50% 45% 45%;
- background: #ffe0c4;
- }
- .portrait-suit {
- width: 70rpx;
- height: 48rpx;
- margin: 4rpx auto 0;
- border-radius: 30rpx 30rpx 12rpx 12rpx;
- background: #1f3a5f;
- }
-
- .upload-card,
- .section {
- margin-top: 22rpx;
- padding: 28rpx;
- border-radius: 24rpx;
- background: #fff;
- box-shadow: 0 8rpx 28rpx rgba(34, 68, 112, 0.05);
- }
- .upload-card:first-child { margin-top: 0; }
-
- .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; }
-
- .segmented {
- margin-top: 22rpx;
- padding: 6rpx;
- border-radius: 18rpx;
- background: #f7f9fd;
- display: grid;
- grid-template-columns: repeat(3, minmax(0, 1fr));
- gap: 6rpx;
- }
- .segment {
- height: 66rpx;
- border-radius: 16rpx;
- color: #596579;
- font-size: 24rpx;
- font-weight: 800;
- line-height: 66rpx;
- text-align: center;
- }
- .segment.active {
- color: #0b8cff;
- background: #fff;
- box-shadow: 0 8rpx 18rpx rgba(11, 140, 255, 0.12);
- }
- .config-panel {
- margin-top: 24rpx;
- padding-top: 22rpx;
- border-top: 1rpx solid #edf2f7;
- }
- .sub-block + .sub-block { margin-top: 24rpx; }
- .sub-title {
- display: block;
- margin-bottom: 14rpx;
- color: #7f8896;
- font-size: 24rpx;
- font-weight: 800;
- }
- .option-grid {
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: 14rpx;
- }
- .option-grid.three { grid-template-columns: repeat(3, minmax(0, 1fr)); }
- .option-grid.two { grid-template-columns: repeat(2, minmax(0, 1fr)); }
- .option {
- min-height: 66rpx;
- padding: 0 12rpx;
- border-radius: 16rpx;
- color: #596579;
- font-size: 24rpx;
- font-weight: 800;
- background: #f7f9fd;
- border: 2rpx solid transparent;
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 10rpx;
- text-align: center;
- box-sizing: border-box;
- }
- .option.active-first {
- color: #0b8cff;
- background: #eff8ff;
- border-color: #0b8cff;
- }
- .color-dot {
- width: 28rpx;
- height: 28rpx;
- border-radius: 50%;
- border: 1rpx solid rgba(15, 23, 42, 0.16);
- flex-shrink: 0;
- }
- .white { background: #fff; }
- .gray { background: #d9dde3; }
- .blue { background: #6aa7e8; }
- .deep-blue { background: #245a9c; }
-
- .ratio-grid {
- margin-top: 20rpx;
- display: grid;
- grid-template-columns: repeat(5, minmax(0, 1fr));
- gap: 10rpx;
- }
- .ratio {
- height: 64rpx;
- border-radius: 16rpx;
- color: #596579;
- font-size: 24rpx;
- font-weight: 800;
- line-height: 64rpx;
- text-align: center;
- background: #f7f9fd;
- border: 2rpx solid transparent;
- }
- .ratio.active {
- color: #0b8cff;
- background: #eff8ff;
- border-color: #0b8cff;
- }
- .price {
- margin-left: auto;
- color: #ff681f;
- font-size: 24rpx;
- font-weight: 800;
- }
- .quantity {
- margin-top: 22rpx;
- display: grid;
- grid-template-columns: 80rpx minmax(0, 1fr) 80rpx;
- gap: 18rpx;
- align-items: center;
- }
- .quantity-btn {
- height: 72rpx;
- border-radius: 16rpx;
- color: #fff;
- background: #0b8cff;
- font-size: 40rpx;
- font-weight: 800;
- line-height: 72rpx;
- text-align: center;
- }
- .quantity-value {
- height: 72rpx;
- border-radius: 16rpx;
- color: #172033;
- background: #f7f9fd;
- font-size: 32rpx;
- font-weight: 800;
- line-height: 72rpx;
- text-align: center;
- }
- .note {
- display: block;
- margin-top: 14rpx;
- color: #8a95a6;
- font-size: 23rpx;
- }
- .textarea {
- margin-top: 20rpx;
- width: 100%;
- min-height: 144rpx;
- padding: 22rpx;
- border-radius: 20rpx;
- color: #172033;
- font-size: 25rpx;
- line-height: 1.5;
- background: #f7f9fd;
- box-sizing: border-box;
- }
- .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>
|