Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.
 
 
 

798 Zeilen
18 KiB

  1. <template>
  2. <view class="page">
  3. <scroll-view class="scroll" scroll-y>
  4. <view class="wrap">
  5. <view class="hero-card id-hero">
  6. <view class="hero-copy">
  7. <text class="eyebrow">ID PHOTO STUDIO</text>
  8. <text class="hero-title">快速制作标准证件照</text>
  9. <text class="hero-desc">选择尺寸、背景和服装,生成适合报名、签证、证件使用的照片。</text>
  10. </view>
  11. <view class="hero-preview">
  12. <view class="id-photo-sample bg-blue">
  13. <view class="person-head"></view>
  14. <view class="person-body"></view>
  15. </view>
  16. <view class="id-photo-sample bg-white">
  17. <view class="person-head"></view>
  18. <view class="person-body"></view>
  19. </view>
  20. </view>
  21. </view>
  22. <view class="upload-card">
  23. <view class="card-head">
  24. <view>
  25. <text class="card-title"><text class="required">*</text>上传图片</text>
  26. <text class="card-desc">上传正面半身照,自动裁切为证件照</text>
  27. </view>
  28. <text class="card-tip">0/1</text>
  29. </view>
  30. <view class="upload-strip" @click="showTodo('请选择证件照图片')">
  31. <view class="upload-thumb">
  32. <text class="upload-plus">+</text>
  33. </view>
  34. <view class="upload-copy">
  35. <text class="upload-title">选择一张清晰照片</text>
  36. <text class="upload-sub">建议五官无遮挡、背景简单、光线均匀</text>
  37. </view>
  38. <view class="upload-action">上传</view>
  39. </view>
  40. </view>
  41. <view class="section">
  42. <view class="section-head">
  43. <text class="tool-section-title">选尺寸</text>
  44. <text class="section-tip">常用规格</text>
  45. </view>
  46. <view class="search-row">
  47. <input class="search-input" placeholder="输入证件照名称" />
  48. <view class="search-btn" @click="showTodo('尺寸搜索')">搜索</view>
  49. </view>
  50. <scroll-view class="tab-scroll" scroll-x>
  51. <view class="tab-list">
  52. <view
  53. v-for="tab in sizeTabs"
  54. :key="tab.key"
  55. class="tab-item"
  56. :class="{ active: activeSizeTab === tab.key }"
  57. @click="selectSizeTab(tab.key)"
  58. >
  59. {{ tab.name }}
  60. </view>
  61. </view>
  62. </scroll-view>
  63. <scroll-view class="size-scroll" scroll-x>
  64. <view class="size-list">
  65. <view
  66. v-for="item in currentSizes"
  67. :key="item.name"
  68. class="size-card"
  69. :class="{ active: activeSize === item.name }"
  70. @click="activeSize = item.name"
  71. >
  72. <text class="size-name">{{ item.name }}</text>
  73. <text class="size-meta">{{ item.pixel }}</text>
  74. <text class="size-desc">{{ item.print }}</text>
  75. </view>
  76. </view>
  77. </scroll-view>
  78. </view>
  79. <view class="section">
  80. <view class="section-head">
  81. <text class="tool-section-title">选背景</text>
  82. <text class="section-tip">生成后可替换</text>
  83. </view>
  84. <view class="bg-row">
  85. <view
  86. v-for="item in bgOptions"
  87. :key="item.name"
  88. class="bg-item"
  89. :class="{ active: activeBg === item.name }"
  90. @click="activeBg = item.name"
  91. >
  92. <view class="bg-dot" :class="item.className"></view>
  93. <text>{{ item.name }}</text>
  94. </view>
  95. </view>
  96. </view>
  97. <view class="section compact">
  98. <view class="section-head">
  99. <text class="tool-section-title">性别</text>
  100. <text class="section-tip">用于服装推荐</text>
  101. </view>
  102. <view class="chip-row">
  103. <view
  104. v-for="item in genderOptions"
  105. :key="item"
  106. class="chip"
  107. :class="{ active: activeGender === item }"
  108. @click="activeGender = item"
  109. >
  110. {{ item }}
  111. </view>
  112. </view>
  113. </view>
  114. <view class="section compact">
  115. <view class="section-head">
  116. <text class="tool-section-title">选服装</text>
  117. <text class="section-tip">可选</text>
  118. </view>
  119. <view class="chip-row">
  120. <view
  121. v-for="item in outfitOptions"
  122. :key="item"
  123. class="chip"
  124. :class="{ active: activeOutfit === item }"
  125. @click="activeOutfit = item"
  126. >
  127. {{ item }}
  128. </view>
  129. </view>
  130. </view>
  131. <view class="section">
  132. <view class="section-head">
  133. <text class="tool-section-title">描述词</text>
  134. <text class="section-tip">可补充要求</text>
  135. </view>
  136. <textarea class="textarea" placeholder="如自然美颜、肤色更均匀、保留本人特征等"></textarea>
  137. <view class="chip-row keyword-row">
  138. <view v-for="item in keywordOptions" :key="item" class="keyword-chip" @click="showTodo(`添加 ${item}`)">
  139. {{ item }}
  140. </view>
  141. </view>
  142. </view>
  143. </view>
  144. </scroll-view>
  145. <view class="bottom-bar">
  146. <view class="balance">
  147. <text class="balance-copy">{{ balanceText }}</text>
  148. <view v-if="isBalanceInsufficient" class="recharge-link" @click="goRecharge">去充值</view>
  149. </view>
  150. <view class="buttons">
  151. <view class="history-btn" @click="showTodo('历史记录')">历史记录</view>
  152. <view class="submit-btn" @click="showTodo('证件照制作')">
  153. <image class="submit-icon" src="/static/icons/bolt.svg" mode="aspectFit" />
  154. <text>{{ submitText }}</text>
  155. </view>
  156. </view>
  157. </view>
  158. </view>
  159. </template>
  160. <script setup>
  161. import { computed, ref } from 'vue'
  162. import { onLoad, onShow } from '@dcloudio/uni-app'
  163. import { billingApi, userApi } from '@/api/index.js'
  164. import { useUserStore } from '@/store/user.js'
  165. import { useApp } from '@/utils/useApp.js'
  166. const userStore = useUserStore()
  167. const { toast } = useApp()
  168. const activeSizeTab = ref('common')
  169. const activeSize = ref('小二寸')
  170. const activeBg = ref('蓝色')
  171. const activeGender = ref('先生')
  172. const activeOutfit = ref('不换装')
  173. const balance = ref({ points: 0 })
  174. const quote = ref({ points: 0 })
  175. const appKey = 'id-photo'
  176. const sizeTabs = [
  177. { key: 'common', name: '常用尺寸' },
  178. { key: 'exam', name: '考试证照' },
  179. { key: 'passport', name: '护照签证' },
  180. { key: 'personal', name: '个人证件' }
  181. ]
  182. const sizeGroups = {
  183. common: [
  184. { name: '自定义尺寸', pixel: '满足各类需求', print: '按需设置' },
  185. { name: '小二寸', pixel: '413x531px', print: '35x45mm' },
  186. { name: '一寸', pixel: '295x413px', print: '25x35mm' },
  187. { name: '小一寸', pixel: '260x378px', print: '22x32mm' },
  188. { name: '大二寸', pixel: '413x626px', print: '35x53mm' },
  189. { name: '二寸', pixel: '413x579px', print: '35x49mm' }
  190. ],
  191. exam: [
  192. { name: '教师资格证', pixel: '295x413px', print: '25x35mm' },
  193. { name: '英语四六级', pixel: '144x192px', print: '33x48mm' },
  194. { name: '计算机考试', pixel: '144x192px', print: '33x48mm' },
  195. { name: '普通话测试', pixel: '390x567px', print: '33x48mm' }
  196. ],
  197. passport: [
  198. { name: '护照', pixel: '390x567px', print: '33x48mm' },
  199. { name: '美国签证', pixel: '600x600px', print: '51x51mm' },
  200. { name: '日本签证', pixel: '413x531px', print: '35x45mm' },
  201. { name: '韩国签证', pixel: '413x531px', print: '35x45mm' }
  202. ],
  203. personal: [
  204. { name: '标准证件照', pixel: '462x612px', print: '35x48mm' },
  205. { name: '入学照片', pixel: '295x413px', print: '25x35mm' },
  206. { name: '离婚证照片', pixel: '413x531px', print: '35x45mm' },
  207. { name: '微信头像', pixel: '1000x1000px', print: '85x85mm' }
  208. ]
  209. }
  210. const currentSizes = computed(() => sizeGroups[activeSizeTab.value] || [])
  211. const currentPoints = computed(() => Number(balance.value.points || balance.value.point_balance || 0))
  212. const currentCost = computed(() => Number(quote.value.points || 0))
  213. const balanceText = computed(() => {
  214. const cost = currentCost.value
  215. if (!userStore.isLogin) return cost ? `证件照 · ${cost}点/次` : '证件照'
  216. return cost ? `余额${currentPoints.value}点 · 预计消耗${cost}点` : `余额${currentPoints.value}点`
  217. })
  218. const isBalanceInsufficient = computed(() => userStore.isLogin && currentCost.value > 0 && currentPoints.value < currentCost.value)
  219. const submitText = computed(() => currentCost.value ? `立即制作 · ${currentCost.value}点` : '立即制作')
  220. const bgOptions = [
  221. { name: '蓝色', className: 'blue' },
  222. { name: '白色', className: 'white' },
  223. { name: '红色', className: 'red' },
  224. { name: '深蓝', className: 'navy' },
  225. { name: '浅蓝渐变', className: 'gradient-blue' },
  226. { name: '深色渐变', className: 'gradient-dark' }
  227. ]
  228. const genderOptions = ['先生', '女士', '小男孩', '小女孩']
  229. const outfitOptions = ['不换装', '白衬衣', '浅蓝色衬衣', '西装', '休闲装']
  230. const keywordOptions = ['磨皮', '去皱', '瘦脸']
  231. onLoad(() => {
  232. loadBillingConfig()
  233. })
  234. onShow(() => {
  235. if (userStore.isLogin) loadBalance()
  236. loadBillingConfig()
  237. })
  238. async function loadBalance() {
  239. try {
  240. const res = await userApi.getBalance()
  241. balance.value = res.data || {}
  242. } catch (e) {}
  243. }
  244. async function loadBillingConfig() {
  245. try {
  246. const res = await billingApi.getConfig({ app_key: appKey, image_count: 1 })
  247. const data = res.data || {}
  248. quote.value = data.quote || {}
  249. if (data.balance) balance.value = data.balance
  250. } catch (e) {}
  251. }
  252. function goRecharge() {
  253. uni.navigateTo({ url: '/pages/recharge/recharge' })
  254. }
  255. function showTodo(name) {
  256. toast(`${name}逻辑待接入`)
  257. }
  258. function selectSizeTab(key) {
  259. activeSizeTab.value = key
  260. const list = sizeGroups[key] || []
  261. if (list.length) activeSize.value = list[0].name
  262. }
  263. </script>
  264. <style lang="scss" scoped>
  265. .page {
  266. height: 100vh;
  267. background: #f3f7fb;
  268. display: flex;
  269. flex-direction: column;
  270. overflow: hidden;
  271. }
  272. .scroll {
  273. flex: 1;
  274. height: 0;
  275. }
  276. .wrap {
  277. padding: 24rpx 28rpx 300rpx;
  278. }
  279. .hero-card {
  280. position: relative;
  281. min-height: 260rpx;
  282. padding: 30rpx;
  283. border-radius: 28rpx;
  284. color: #fff;
  285. background: linear-gradient(135deg, #0b8cff 0%, #36b6ff 58%, #79e0df 100%);
  286. overflow: hidden;
  287. box-sizing: border-box;
  288. }
  289. .hero-card::before {
  290. content: '';
  291. position: absolute;
  292. right: -92rpx;
  293. top: -110rpx;
  294. width: 300rpx;
  295. height: 300rpx;
  296. border-radius: 50%;
  297. background: rgba(255, 255, 255, 0.18);
  298. }
  299. .hero-copy,
  300. .hero-preview {
  301. position: relative;
  302. z-index: 1;
  303. }
  304. .eyebrow {
  305. display: block;
  306. color: rgba(255, 255, 255, 0.72);
  307. font-size: 21rpx;
  308. font-weight: 800;
  309. }
  310. .hero-title {
  311. display: block;
  312. margin-top: 16rpx;
  313. width: 430rpx;
  314. color: #fff;
  315. font-size: 40rpx;
  316. font-weight: 900;
  317. line-height: 1.24;
  318. }
  319. .hero-desc {
  320. display: block;
  321. margin-top: 14rpx;
  322. width: 420rpx;
  323. color: rgba(255, 255, 255, 0.86);
  324. font-size: 24rpx;
  325. line-height: 1.5;
  326. }
  327. .hero-preview {
  328. position: absolute;
  329. right: 30rpx;
  330. bottom: 28rpx;
  331. width: 176rpx;
  332. height: 126rpx;
  333. }
  334. .id-photo-sample {
  335. position: absolute;
  336. width: 90rpx;
  337. height: 118rpx;
  338. border-radius: 14rpx;
  339. background: #fff;
  340. box-shadow: 0 16rpx 28rpx rgba(0, 62, 130, 0.2);
  341. overflow: hidden;
  342. }
  343. .id-photo-sample.bg-blue {
  344. left: 0;
  345. top: 0;
  346. background: #5ba7f8;
  347. }
  348. .id-photo-sample.bg-white {
  349. right: 0;
  350. bottom: 0;
  351. background: #f7fbff;
  352. }
  353. .person-head {
  354. width: 36rpx;
  355. height: 36rpx;
  356. margin: 24rpx auto 0;
  357. border-radius: 50%;
  358. background: #ffffff;
  359. }
  360. .bg-white .person-head {
  361. background: #7db7f7;
  362. }
  363. .person-body {
  364. width: 64rpx;
  365. height: 36rpx;
  366. margin: 8rpx auto 0;
  367. border-radius: 28rpx 28rpx 12rpx 12rpx;
  368. background: #ffffff;
  369. }
  370. .bg-white .person-body {
  371. background: #7db7f7;
  372. }
  373. .upload-card,
  374. .section {
  375. margin-top: 22rpx;
  376. padding: 28rpx;
  377. border-radius: 24rpx;
  378. background: #fff;
  379. box-shadow: 0 8rpx 28rpx rgba(34, 68, 112, 0.05);
  380. }
  381. .upload-card:first-child {
  382. margin-top: 0;
  383. }
  384. .section.compact {
  385. padding-bottom: 24rpx;
  386. }
  387. .card-head,
  388. .section-head {
  389. display: flex;
  390. align-items: baseline;
  391. }
  392. .card-title,
  393. .tool-section-title {
  394. display: block;
  395. color: #172033;
  396. font-size: 31rpx;
  397. font-weight: 800;
  398. }
  399. .required {
  400. margin-right: 4rpx;
  401. color: #ef4444;
  402. }
  403. .card-desc {
  404. display: block;
  405. margin-top: 8rpx;
  406. color: #8a95a6;
  407. font-size: 23rpx;
  408. line-height: 1.35;
  409. }
  410. .card-tip,
  411. .section-tip {
  412. margin-left: auto;
  413. color: #8a95a6;
  414. font-size: 23rpx;
  415. }
  416. .upload-strip {
  417. margin-top: 22rpx;
  418. min-height: 142rpx;
  419. padding: 18rpx;
  420. border: 2rpx dashed rgba(11, 140, 255, 0.28);
  421. border-radius: 20rpx;
  422. background: #f8fbff;
  423. display: flex;
  424. align-items: center;
  425. box-sizing: border-box;
  426. }
  427. .upload-thumb {
  428. width: 104rpx;
  429. height: 104rpx;
  430. border-radius: 18rpx;
  431. background: #eef6ff;
  432. display: flex;
  433. align-items: center;
  434. justify-content: center;
  435. flex-shrink: 0;
  436. }
  437. .upload-plus {
  438. color: #0b8cff;
  439. font-size: 48rpx;
  440. line-height: 1;
  441. }
  442. .upload-copy {
  443. min-width: 0;
  444. flex: 1;
  445. margin-left: 20rpx;
  446. }
  447. .upload-title {
  448. display: block;
  449. color: #172033;
  450. font-size: 29rpx;
  451. font-weight: 800;
  452. }
  453. .upload-sub {
  454. display: block;
  455. margin-top: 8rpx;
  456. color: #7f8896;
  457. font-size: 23rpx;
  458. line-height: 1.4;
  459. }
  460. .upload-action {
  461. margin-left: 18rpx;
  462. color: #0b8cff;
  463. font-size: 25rpx;
  464. font-weight: 800;
  465. }
  466. .search-row {
  467. margin-top: 22rpx;
  468. height: 72rpx;
  469. border: 2rpx solid rgba(11, 140, 255, 0.22);
  470. border-radius: 999rpx;
  471. display: flex;
  472. overflow: hidden;
  473. background: #fff;
  474. }
  475. .search-input {
  476. flex: 1;
  477. height: 72rpx;
  478. padding: 0 24rpx;
  479. color: #172033;
  480. font-size: 25rpx;
  481. }
  482. .search-btn {
  483. width: 132rpx;
  484. color: #fff;
  485. background: #0b8cff;
  486. font-size: 26rpx;
  487. font-weight: 800;
  488. line-height: 72rpx;
  489. text-align: center;
  490. }
  491. .tab-scroll,
  492. .size-scroll {
  493. margin-left: -28rpx;
  494. margin-right: -28rpx;
  495. width: auto;
  496. white-space: nowrap;
  497. }
  498. .tab-list {
  499. padding: 20rpx 28rpx 14rpx;
  500. display: flex;
  501. gap: 34rpx;
  502. }
  503. .tab-item {
  504. position: relative;
  505. flex-shrink: 0;
  506. height: 48rpx;
  507. color: #7f8896;
  508. font-size: 25rpx;
  509. line-height: 48rpx;
  510. }
  511. .tab-item.active {
  512. color: #172033;
  513. font-weight: 800;
  514. }
  515. .tab-item.active::after {
  516. content: '';
  517. position: absolute;
  518. left: 12rpx;
  519. right: 12rpx;
  520. bottom: 0;
  521. height: 5rpx;
  522. border-radius: 999rpx;
  523. background: #0b8cff;
  524. }
  525. .size-list {
  526. padding: 4rpx 28rpx 4rpx;
  527. display: flex;
  528. gap: 18rpx;
  529. }
  530. .size-card {
  531. width: 196rpx;
  532. min-height: 156rpx;
  533. padding: 20rpx 14rpx;
  534. border-radius: 20rpx;
  535. border: 2rpx solid transparent;
  536. background: #f7f9fd;
  537. box-sizing: border-box;
  538. text-align: center;
  539. flex-shrink: 0;
  540. }
  541. .size-card.active {
  542. color: #fff;
  543. background: #4358ff;
  544. box-shadow: 0 12rpx 26rpx rgba(67, 88, 255, 0.2);
  545. }
  546. .size-name,
  547. .size-meta,
  548. .size-desc {
  549. display: block;
  550. }
  551. .size-name {
  552. color: #172033;
  553. font-size: 28rpx;
  554. font-weight: 800;
  555. line-height: 1.2;
  556. }
  557. .size-meta {
  558. margin-top: 16rpx;
  559. color: #8a95a6;
  560. font-size: 23rpx;
  561. font-weight: 700;
  562. }
  563. .size-desc {
  564. margin-top: 8rpx;
  565. color: #9aa4b2;
  566. font-size: 21rpx;
  567. }
  568. .size-card.active .size-name,
  569. .size-card.active .size-meta,
  570. .size-card.active .size-desc {
  571. color: rgba(255, 255, 255, 0.9);
  572. }
  573. .bg-row {
  574. margin-top: 22rpx;
  575. display: flex;
  576. gap: 20rpx;
  577. overflow-x: auto;
  578. }
  579. .bg-item {
  580. width: 100rpx;
  581. color: #596579;
  582. font-size: 21rpx;
  583. font-weight: 700;
  584. text-align: center;
  585. flex-shrink: 0;
  586. }
  587. .bg-item.active {
  588. color: #0b8cff;
  589. }
  590. .bg-dot {
  591. width: 76rpx;
  592. height: 76rpx;
  593. margin: 0 auto 10rpx;
  594. border-radius: 50%;
  595. border: 4rpx solid transparent;
  596. box-shadow: 0 8rpx 18rpx rgba(15, 23, 42, 0.1);
  597. }
  598. .bg-item.active .bg-dot {
  599. border-color: #0b8cff;
  600. box-shadow: 0 0 0 6rpx rgba(11, 140, 255, 0.1), 0 10rpx 22rpx rgba(15, 23, 42, 0.12);
  601. }
  602. .blue { background: #4194f4; }
  603. .white { background: #fff; border-color: #e5e7eb; }
  604. .red { background: #ef3028; }
  605. .navy { background: #326da4; }
  606. .gradient-blue { background: linear-gradient(180deg, #64a9f7 0%, #d8ecff 100%); }
  607. .gradient-dark { background: linear-gradient(180deg, #263345 0%, #4f6f84 100%); }
  608. .chip-row {
  609. margin-top: 18rpx;
  610. display: flex;
  611. flex-wrap: wrap;
  612. gap: 14rpx;
  613. }
  614. .chip,
  615. .keyword-chip {
  616. height: 58rpx;
  617. padding: 0 24rpx;
  618. border-radius: 30rpx;
  619. color: #596579;
  620. font-size: 25rpx;
  621. font-weight: 700;
  622. line-height: 58rpx;
  623. background: #f6f9fe;
  624. border: 2rpx solid transparent;
  625. box-sizing: border-box;
  626. }
  627. .chip.active {
  628. color: #0b8cff;
  629. background: #eff8ff;
  630. border-color: #0b8cff;
  631. }
  632. .textarea {
  633. margin-top: 20rpx;
  634. width: 100%;
  635. min-height: 144rpx;
  636. padding: 22rpx;
  637. border-radius: 20rpx;
  638. color: #172033;
  639. font-size: 25rpx;
  640. line-height: 1.5;
  641. background: #f7f9fd;
  642. box-sizing: border-box;
  643. }
  644. .keyword-chip {
  645. color: #0b8cff;
  646. background: #eef7ff;
  647. }
  648. .bottom-bar {
  649. position: fixed;
  650. left: 0;
  651. right: 0;
  652. bottom: 0;
  653. z-index: 80;
  654. padding: 18rpx 28rpx calc(18rpx + env(safe-area-inset-bottom));
  655. background: rgba(255, 255, 255, 0.96);
  656. box-shadow: 0 -10rpx 30rpx rgba(32, 56, 92, 0.1);
  657. }
  658. .balance {
  659. margin-bottom: 14rpx;
  660. display: flex;
  661. align-items: center;
  662. justify-content: space-between;
  663. gap: 20rpx;
  664. color: #7b8797;
  665. font-size: 24rpx;
  666. line-height: 1.35;
  667. }
  668. .balance-copy {
  669. min-width: 0;
  670. flex: 1;
  671. }
  672. .recharge-link {
  673. color: #0b8cff;
  674. font-size: 24rpx;
  675. font-weight: 800;
  676. flex-shrink: 0;
  677. }
  678. .buttons {
  679. display: flex;
  680. gap: 22rpx;
  681. }
  682. .history-btn,
  683. .submit-btn {
  684. height: 86rpx;
  685. border-radius: 999rpx;
  686. font-size: 30rpx;
  687. font-weight: 800;
  688. display: flex;
  689. align-items: center;
  690. justify-content: center;
  691. }
  692. .history-btn {
  693. width: 210rpx;
  694. color: #1f8cff;
  695. background: #eef7ff;
  696. }
  697. .submit-btn {
  698. flex: 1;
  699. color: #fff;
  700. background: linear-gradient(135deg, #1f8cff, #41b9ff);
  701. box-shadow: 0 12rpx 24rpx rgba(31, 140, 255, 0.28);
  702. }
  703. .submit-icon {
  704. width: 24rpx;
  705. height: 24rpx;
  706. margin-right: 8rpx;
  707. }
  708. </style>