|
- <template>
- <view class="page">
- <scroll-view class="scroll" scroll-y>
- <view class="wrap">
- <view class="hero-card id-hero">
- <view class="hero-copy">
- <text class="eyebrow">ID PHOTO STUDIO</text>
- <text class="hero-title">快速制作标准证件照</text>
- <text class="hero-desc">选择尺寸、背景和服装,生成适合报名、签证、证件使用的照片。</text>
- </view>
- <view class="hero-preview">
- <view class="id-photo-sample bg-blue">
- <view class="person-head"></view>
- <view class="person-body"></view>
- </view>
- <view class="id-photo-sample bg-white">
- <view class="person-head"></view>
- <view class="person-body"></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="search-row">
- <input class="search-input" placeholder="输入证件照名称" />
- <view class="search-btn" @click="showTodo('尺寸搜索')">搜索</view>
- </view>
- <scroll-view class="tab-scroll" scroll-x>
- <view class="tab-list">
- <view
- v-for="tab in sizeTabs"
- :key="tab.key"
- class="tab-item"
- :class="{ active: activeSizeTab === tab.key }"
- @click="selectSizeTab(tab.key)"
- >
- {{ tab.name }}
- </view>
- </view>
- </scroll-view>
- <scroll-view class="size-scroll" scroll-x>
- <view class="size-list">
- <view
- v-for="item in currentSizes"
- :key="item.name"
- class="size-card"
- :class="{ active: activeSize === item.name }"
- @click="activeSize = item.name"
- >
- <text class="size-name">{{ item.name }}</text>
- <text class="size-meta">{{ item.pixel }}</text>
- <text class="size-desc">{{ item.print }}</text>
- </view>
- </view>
- </scroll-view>
- </view>
-
- <view class="section">
- <view class="section-head">
- <text class="tool-section-title">选背景</text>
- <text class="section-tip">生成后可替换</text>
- </view>
- <view class="bg-row">
- <view
- v-for="item in bgOptions"
- :key="item.name"
- class="bg-item"
- :class="{ active: activeBg === item.name }"
- @click="activeBg = item.name"
- >
- <view class="bg-dot" :class="item.className"></view>
- <text>{{ item.name }}</text>
- </view>
- </view>
- </view>
-
- <view class="section compact">
- <view class="section-head">
- <text class="tool-section-title">性别</text>
- <text class="section-tip">用于服装推荐</text>
- </view>
- <view class="chip-row">
- <view
- v-for="item in genderOptions"
- :key="item"
- class="chip"
- :class="{ active: activeGender === item }"
- @click="activeGender = item"
- >
- {{ item }}
- </view>
- </view>
- </view>
-
- <view class="section compact">
- <view class="section-head">
- <text class="tool-section-title">选服装</text>
- <text class="section-tip">可选</text>
- </view>
- <view class="chip-row">
- <view
- v-for="item in outfitOptions"
- :key="item"
- class="chip"
- :class="{ active: activeOutfit === item }"
- @click="activeOutfit = 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="chip-row keyword-row">
- <view v-for="item in keywordOptions" :key="item" class="keyword-chip" @click="showTodo(`添加 ${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 activeSizeTab = ref('common')
- const activeSize = ref('小二寸')
- const activeBg = ref('蓝色')
- const activeGender = ref('先生')
- const activeOutfit = ref('不换装')
- const balance = ref({ points: 0 })
- const quote = ref({ points: 0 })
- const appKey = 'id-photo'
-
- const sizeTabs = [
- { key: 'common', name: '常用尺寸' },
- { key: 'exam', name: '考试证照' },
- { key: 'passport', name: '护照签证' },
- { key: 'personal', name: '个人证件' }
- ]
-
- const sizeGroups = {
- common: [
- { name: '自定义尺寸', pixel: '满足各类需求', print: '按需设置' },
- { name: '小二寸', pixel: '413x531px', print: '35x45mm' },
- { name: '一寸', pixel: '295x413px', print: '25x35mm' },
- { name: '小一寸', pixel: '260x378px', print: '22x32mm' },
- { name: '大二寸', pixel: '413x626px', print: '35x53mm' },
- { name: '二寸', pixel: '413x579px', print: '35x49mm' }
- ],
- exam: [
- { name: '教师资格证', pixel: '295x413px', print: '25x35mm' },
- { name: '英语四六级', pixel: '144x192px', print: '33x48mm' },
- { name: '计算机考试', pixel: '144x192px', print: '33x48mm' },
- { name: '普通话测试', pixel: '390x567px', print: '33x48mm' }
- ],
- passport: [
- { name: '护照', pixel: '390x567px', print: '33x48mm' },
- { name: '美国签证', pixel: '600x600px', print: '51x51mm' },
- { name: '日本签证', pixel: '413x531px', print: '35x45mm' },
- { name: '韩国签证', pixel: '413x531px', print: '35x45mm' }
- ],
- personal: [
- { name: '标准证件照', pixel: '462x612px', print: '35x48mm' },
- { name: '入学照片', pixel: '295x413px', print: '25x35mm' },
- { name: '离婚证照片', pixel: '413x531px', print: '35x45mm' },
- { name: '微信头像', pixel: '1000x1000px', print: '85x85mm' }
- ]
- }
-
- const currentSizes = computed(() => sizeGroups[activeSizeTab.value] || [])
- 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}点` : '立即制作')
-
- const bgOptions = [
- { name: '蓝色', className: 'blue' },
- { name: '白色', className: 'white' },
- { name: '红色', className: 'red' },
- { name: '深蓝', className: 'navy' },
- { name: '浅蓝渐变', className: 'gradient-blue' },
- { name: '深色渐变', className: 'gradient-dark' }
- ]
- const genderOptions = ['先生', '女士', '小男孩', '小女孩']
- const outfitOptions = ['不换装', '白衬衣', '浅蓝色衬衣', '西装', '休闲装']
- const keywordOptions = ['磨皮', '去皱', '瘦脸']
-
- 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 showTodo(name) {
- toast(`${name}逻辑待接入`)
- }
-
- function selectSizeTab(key) {
- activeSizeTab.value = key
- const list = sizeGroups[key] || []
- if (list.length) activeSize.value = list[0].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, #0b8cff 0%, #36b6ff 58%, #79e0df 100%);
- overflow: hidden;
- box-sizing: border-box;
- }
-
- .hero-card::before {
- content: '';
- position: absolute;
- right: -92rpx;
- top: -110rpx;
- width: 300rpx;
- height: 300rpx;
- border-radius: 50%;
- background: rgba(255, 255, 255, 0.18);
- }
-
- .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: 176rpx;
- height: 126rpx;
- }
-
- .id-photo-sample {
- position: absolute;
- width: 90rpx;
- height: 118rpx;
- border-radius: 14rpx;
- background: #fff;
- box-shadow: 0 16rpx 28rpx rgba(0, 62, 130, 0.2);
- overflow: hidden;
- }
-
- .id-photo-sample.bg-blue {
- left: 0;
- top: 0;
- background: #5ba7f8;
- }
-
- .id-photo-sample.bg-white {
- right: 0;
- bottom: 0;
- background: #f7fbff;
- }
-
- .person-head {
- width: 36rpx;
- height: 36rpx;
- margin: 24rpx auto 0;
- border-radius: 50%;
- background: #ffffff;
- }
-
- .bg-white .person-head {
- background: #7db7f7;
- }
-
- .person-body {
- width: 64rpx;
- height: 36rpx;
- margin: 8rpx auto 0;
- border-radius: 28rpx 28rpx 12rpx 12rpx;
- background: #ffffff;
- }
-
- .bg-white .person-body {
- background: #7db7f7;
- }
-
- .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;
- }
-
- .section.compact {
- padding-bottom: 24rpx;
- }
-
- .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;
- }
-
- .search-row {
- margin-top: 22rpx;
- height: 72rpx;
- border: 2rpx solid rgba(11, 140, 255, 0.22);
- border-radius: 999rpx;
- display: flex;
- overflow: hidden;
- background: #fff;
- }
-
- .search-input {
- flex: 1;
- height: 72rpx;
- padding: 0 24rpx;
- color: #172033;
- font-size: 25rpx;
- }
-
- .search-btn {
- width: 132rpx;
- color: #fff;
- background: #0b8cff;
- font-size: 26rpx;
- font-weight: 800;
- line-height: 72rpx;
- text-align: center;
- }
-
- .tab-scroll,
- .size-scroll {
- margin-left: -28rpx;
- margin-right: -28rpx;
- width: auto;
- white-space: nowrap;
- }
-
- .tab-list {
- padding: 20rpx 28rpx 14rpx;
- display: flex;
- gap: 34rpx;
- }
-
- .tab-item {
- position: relative;
- flex-shrink: 0;
- height: 48rpx;
- color: #7f8896;
- font-size: 25rpx;
- line-height: 48rpx;
- }
-
- .tab-item.active {
- color: #172033;
- font-weight: 800;
- }
-
- .tab-item.active::after {
- content: '';
- position: absolute;
- left: 12rpx;
- right: 12rpx;
- bottom: 0;
- height: 5rpx;
- border-radius: 999rpx;
- background: #0b8cff;
- }
-
- .size-list {
- padding: 4rpx 28rpx 4rpx;
- display: flex;
- gap: 18rpx;
- }
-
- .size-card {
- width: 196rpx;
- min-height: 156rpx;
- padding: 20rpx 14rpx;
- border-radius: 20rpx;
- border: 2rpx solid transparent;
- background: #f7f9fd;
- box-sizing: border-box;
- text-align: center;
- flex-shrink: 0;
- }
-
- .size-card.active {
- color: #fff;
- background: #4358ff;
- box-shadow: 0 12rpx 26rpx rgba(67, 88, 255, 0.2);
- }
-
- .size-name,
- .size-meta,
- .size-desc {
- display: block;
- }
-
- .size-name {
- color: #172033;
- font-size: 28rpx;
- font-weight: 800;
- line-height: 1.2;
- }
-
- .size-meta {
- margin-top: 16rpx;
- color: #8a95a6;
- font-size: 23rpx;
- font-weight: 700;
- }
-
- .size-desc {
- margin-top: 8rpx;
- color: #9aa4b2;
- font-size: 21rpx;
- }
-
- .size-card.active .size-name,
- .size-card.active .size-meta,
- .size-card.active .size-desc {
- color: rgba(255, 255, 255, 0.9);
- }
-
- .bg-row {
- margin-top: 22rpx;
- display: flex;
- gap: 20rpx;
- overflow-x: auto;
- }
-
- .bg-item {
- width: 100rpx;
- color: #596579;
- font-size: 21rpx;
- font-weight: 700;
- text-align: center;
- flex-shrink: 0;
- }
-
- .bg-item.active {
- color: #0b8cff;
- }
-
- .bg-dot {
- width: 76rpx;
- height: 76rpx;
- margin: 0 auto 10rpx;
- border-radius: 50%;
- border: 4rpx solid transparent;
- box-shadow: 0 8rpx 18rpx rgba(15, 23, 42, 0.1);
- }
-
- .bg-item.active .bg-dot {
- border-color: #0b8cff;
- box-shadow: 0 0 0 6rpx rgba(11, 140, 255, 0.1), 0 10rpx 22rpx rgba(15, 23, 42, 0.12);
- }
-
- .blue { background: #4194f4; }
- .white { background: #fff; border-color: #e5e7eb; }
- .red { background: #ef3028; }
- .navy { background: #326da4; }
- .gradient-blue { background: linear-gradient(180deg, #64a9f7 0%, #d8ecff 100%); }
- .gradient-dark { background: linear-gradient(180deg, #263345 0%, #4f6f84 100%); }
-
- .chip-row {
- margin-top: 18rpx;
- display: flex;
- flex-wrap: wrap;
- gap: 14rpx;
- }
-
- .chip,
- .keyword-chip {
- height: 58rpx;
- padding: 0 24rpx;
- border-radius: 30rpx;
- color: #596579;
- font-size: 25rpx;
- font-weight: 700;
- line-height: 58rpx;
- background: #f6f9fe;
- border: 2rpx solid transparent;
- box-sizing: border-box;
- }
-
- .chip.active {
- color: #0b8cff;
- background: #eff8ff;
- border-color: #0b8cff;
- }
-
- .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;
- }
-
- .keyword-chip {
- color: #0b8cff;
- 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-icon {
- width: 24rpx;
- height: 24rpx;
- margin-right: 8rpx;
- }
- </style>
|