You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

1115 lines
26 KiB

  1. <template>
  2. <view class="page">
  3. <scroll-view class="scroll" scroll-y>
  4. <view class="wrap">
  5. <view class="hero">
  6. <view class="hero-copy">
  7. <text class="eyebrow">ANIME AVATAR STUDIO</text>
  8. <text class="hero-title">上传自拍,生成你的动漫头像</text>
  9. <text class="hero-desc">用真实对比看效果。支持竖图、半身照和普通自拍,脸部清晰会更稳定。</text>
  10. <view class="hero-actions">
  11. <view class="hero-pill" @click="openCompare">查看对比</view>
  12. <view class="hero-link" @click="chooseImage">上传照片</view>
  13. </view>
  14. </view>
  15. <view class="hero-compare-card" @click="openCompare">
  16. <view class="mini-compare">
  17. <image v-if="demoPair.anime" class="compare-img" :src="demoPair.anime" mode="aspectFill" />
  18. <view class="compare-before mini-before">
  19. <image v-if="demoPair.original" class="compare-img" :src="demoPair.original" mode="aspectFill" />
  20. </view>
  21. <view class="compare-line mini-line"></view>
  22. <view class="compare-tag left">原图</view>
  23. <view class="compare-tag right">动漫</view>
  24. </view>
  25. </view>
  26. </view>
  27. <view class="upload-card">
  28. <view class="card-head">
  29. <view>
  30. <text class="card-title">参考照片</text>
  31. <text class="card-desc">建议使用正面、上半身、脸部无遮挡的照片</text>
  32. </view>
  33. <text class="required">必选</text>
  34. </view>
  35. <view class="upload-strip" @click="chooseImage">
  36. <view class="upload-thumb">
  37. <image v-if="previewImage" class="upload-img" :src="previewImage" mode="aspectFill" />
  38. <view v-else class="upload-empty">
  39. <text class="plus">+</text>
  40. </view>
  41. <view v-if="uploading" class="upload-mask">上传中</view>
  42. </view>
  43. <view class="upload-copy">
  44. <text class="upload-title">{{ previewImage ? '已选择照片' : '选择一张清晰自拍' }}</text>
  45. <text class="upload-sub">不用裁成正方形,竖图和半身照也可以</text>
  46. </view>
  47. <view class="upload-action">{{ previewImage ? '更换' : '上传' }}</view>
  48. </view>
  49. <view class="upload-tips">
  50. <view class="upload-tip">正面</view>
  51. <view class="upload-tip">脸部清晰</view>
  52. <view class="upload-tip">避免遮挡</view>
  53. </view>
  54. </view>
  55. <view v-if="compareVisible" class="compare-modal">
  56. <view class="compare-backdrop" @click="closeCompare"></view>
  57. <view class="compare-panel">
  58. <view class="compare-panel-head">
  59. <text>效果对比</text>
  60. <view class="close-btn" @click="closeCompare">×</view>
  61. </view>
  62. <view
  63. class="large-compare"
  64. @touchstart.stop="onCompareTouch"
  65. @touchmove.stop.prevent="onCompareTouch"
  66. @click.stop="onCompareTap"
  67. >
  68. <image v-if="demoPair.anime" class="compare-img" :src="demoPair.anime" mode="aspectFill" />
  69. <view class="compare-before" :style="{ width: comparePercent + '%' }">
  70. <image v-if="demoPair.original" class="compare-img" :src="demoPair.original" mode="aspectFill" />
  71. </view>
  72. <view class="compare-line large" :style="{ left: comparePercent + '%' }">
  73. <view class="compare-handle large">
  74. <text>‹</text>
  75. <text>›</text>
  76. </view>
  77. </view>
  78. <view class="compare-tag left">原图</view>
  79. <view class="compare-tag right">动漫图</view>
  80. </view>
  81. <text class="compare-guide">左右滑动中间按钮查看变化</text>
  82. </view>
  83. </view>
  84. <view class="section">
  85. <view class="section-head">
  86. <text class="anime-section-title">头像风格</text>
  87. <text class="section-tip">横向滑动选择</text>
  88. </view>
  89. <scroll-view class="style-scroll" scroll-x>
  90. <view class="style-list">
  91. <view
  92. v-for="item in styleList"
  93. :key="item.value"
  94. class="style-card"
  95. :class="{ active: form.img2img_style === String(item.value) }"
  96. @click="selectStyle(item)"
  97. >
  98. <image v-if="item.pic" class="style-img" :src="item.pic" mode="aspectFill" />
  99. <view v-else class="style-img placeholder">
  100. <text>{{ item.name }}</text>
  101. </view>
  102. <text class="style-name">{{ item.name }}</text>
  103. </view>
  104. </view>
  105. </scroll-view>
  106. </view>
  107. <view class="section compact">
  108. <view class="section-head">
  109. <view>
  110. <text class="anime-section-title">自由度</text>
  111. <text class="section-desc">{{ strengthCopy }}</text>
  112. </view>
  113. <text class="strength-value">{{ form.img2img_strength.toFixed(2) }}</text>
  114. </view>
  115. <slider
  116. :value="Number(form.img2img_strength)"
  117. :min="0.1"
  118. :max="1"
  119. :step="0.01"
  120. activeColor="#1f8cff"
  121. backgroundColor="#e7edf7"
  122. block-color="#ffffff"
  123. @changing="onStrengthChange"
  124. @change="onStrengthChange"
  125. />
  126. <view class="strength-scale">
  127. <text>更像原图</text>
  128. <text>更有创意</text>
  129. </view>
  130. </view>
  131. <view class="section">
  132. <view class="section-head">
  133. <text class="anime-section-title">画面偏好</text>
  134. <text class="section-tip">可选</text>
  135. </view>
  136. <view class="input-block">
  137. <text class="input-label">描述词</text>
  138. <input v-model="form.content" class="input" maxlength="100" placeholder="例如:浅色背景、清爽、微笑" />
  139. </view>
  140. <view class="input-block">
  141. <text class="input-label">反面提示词</text>
  142. <input v-model="form.no" class="input" maxlength="100" placeholder="例如:模糊、变形、低清晰度" />
  143. </view>
  144. </view>
  145. <view class="tips-card">
  146. <view class="tip-line">
  147. <text class="tip-mark">01</text>
  148. <text>上传照片后可直接生成,处理完成会进入作品详情页。</text>
  149. </view>
  150. <view class="tip-line">
  151. <text class="tip-mark">02</text>
  152. <text>历史记录里可以查看生成进度,也可以继续打开已完成作品。</text>
  153. </view>
  154. </view>
  155. </view>
  156. </scroll-view>
  157. <view class="bottom-bar">
  158. <view class="balance">
  159. <text class="balance-copy">{{ balanceText || '动漫头像' }}</text>
  160. <view v-if="isBalanceInsufficient" class="recharge-link" @click="goRecharge">去充值</view>
  161. </view>
  162. <view class="buttons">
  163. <view class="history-btn" @click="goHistory">历史记录</view>
  164. <view class="submit-btn" :class="{ disabled: submitting }" @click="submit">
  165. <image v-if="!submitting" class="submit-icon" src="https://cdn.aionline.cc/ai/icons/bolt.svg" mode="aspectFit" />
  166. <text>{{ submitting ? '生成中...' : submitText }}</text>
  167. </view>
  168. </view>
  169. </view>
  170. </view>
  171. </template>
  172. <script setup>
  173. import { computed, reactive, ref } from 'vue'
  174. import { onLoad, onShow } from '@dcloudio/uni-app'
  175. import { billingApi, commonApi, toolApi, userApi } from '@/api/index.js'
  176. import { useUserStore } from '@/store/user.js'
  177. import { useApp } from '@/utils/useApp.js'
  178. import { uploadImage } from '@/utils/upload.js'
  179. const userStore = useUserStore()
  180. const { toast } = useApp()
  181. const ANIME_BILLING_MODEL = 'tencent_anime'
  182. const appDetail = ref({})
  183. const demoList = ref([])
  184. const styleList = ref([{ value: '201', name: '默认', model: 2, pic: 'https://cdn.aionline.cc/ai/toolbox/home_card_avatar.png' }])
  185. const previewImage = ref('')
  186. const uploading = ref(false)
  187. const submitting = ref(false)
  188. const compareVisible = ref(false)
  189. const comparePercent = ref(50)
  190. const balance = ref({ points: 0 })
  191. const billingConfig = ref({})
  192. const quote = ref({ points: 0 })
  193. const copyId = ref('')
  194. const form = reactive({
  195. imgs: [],
  196. content: '',
  197. ai_type: 3,
  198. style_type: 1,
  199. ar: '1:1',
  200. no: '',
  201. model: ANIME_BILLING_MODEL,
  202. model_type: 6,
  203. light: '',
  204. text: '',
  205. speed: '',
  206. img2img_style: '201',
  207. age: 80,
  208. change_sex_type: '0',
  209. img2img_strength: 0.6,
  210. num: 1
  211. })
  212. const balanceText = computed(() => {
  213. const cost = Number(quote.value.points || 0)
  214. if (!userStore.isLogin) return cost ? `动漫头像 · ${cost}点/张` : '动漫头像'
  215. return cost ? `余额${currentPoints.value}点 · 预计消耗${cost}点` : `余额${currentPoints.value}点`
  216. })
  217. const currentPoints = computed(() => Number(balance.value.points || balance.value.point_balance || 0))
  218. const isBalanceInsufficient = computed(() => {
  219. const cost = Number(quote.value.points || 0)
  220. return userStore.isLogin && cost > 0 && currentPoints.value >= 0 && currentPoints.value < cost
  221. })
  222. const submitText = computed(() => {
  223. const cost = Number(quote.value.points || 0)
  224. if (!userStore.isLogin) return cost ? `登录后生成 · ${cost}点` : '登录后生成'
  225. return cost ? `立即生成 · ${cost}点` : '立即生成'
  226. })
  227. const strengthCopy = computed(() => {
  228. return '推荐 0.6 - 0.8,保留人脸特征'
  229. })
  230. const demoPair = computed(() => demoList.value[0] || { original: '', anime: '' })
  231. onLoad((options) => {
  232. copyId.value = options.copy_id || ''
  233. loadAppDetail()
  234. loadBillingConfig()
  235. loadDemo()
  236. loadStyles()
  237. if (copyId.value) loadCopyDetail()
  238. })
  239. onShow(() => {
  240. if (userStore.isLogin) loadBalance()
  241. loadBillingConfig()
  242. const storageCopyId = uni.getStorageSync('anime_copy_id')
  243. if (storageCopyId) {
  244. uni.removeStorageSync('anime_copy_id')
  245. copyId.value = storageCopyId
  246. loadCopyDetail()
  247. }
  248. })
  249. async function loadAppDetail() {
  250. try {
  251. const res = await toolApi.getAppDetail({ key: 'anime_avatar' })
  252. appDetail.value = res.data || {}
  253. } catch (e) {}
  254. }
  255. async function loadDemo() {
  256. try {
  257. const res = await commonApi.getConfig({ keys: 'anime_demo_v2,anime_demo' })
  258. const rows = Array.isArray(res.list) ? res.list : []
  259. const v2 = rows.find((it) => it.key === 'anime_demo_v2')
  260. const old = rows.find((it) => it.key === 'anime_demo')
  261. demoList.value = normalizeDemoList(v2 && v2.value)
  262. if (!demoList.value.length) {
  263. const legacyPics = normalizeImageList(old && old.value)
  264. if (legacyPics.length) {
  265. demoList.value = [{ original: legacyPics[1] || legacyPics[0], anime: legacyPics[0] }]
  266. }
  267. }
  268. if (!demoList.value.length) {
  269. demoList.value = [{ original: 'https://cdn.aionline.cc/ai/toolbox/home_card_avatar.png', anime: 'https://cdn.aionline.cc/ai/toolbox/home_card_avatar.png' }]
  270. }
  271. } catch (e) {}
  272. }
  273. async function loadStyles() {
  274. try {
  275. const res = await toolApi.getAnimeStyleList({ model: '2' })
  276. const rows = Array.isArray(res.list) ? res.list : (Array.isArray(res.data) ? res.data : (res.data && res.data.list) || [])
  277. const list = rows.map((item) => ({
  278. ...item,
  279. value: String(item.value),
  280. pic: getStyleImage(item)
  281. }))
  282. if (list.length) {
  283. styleList.value = list
  284. selectStyle(list[0], false)
  285. }
  286. } catch (e) {}
  287. }
  288. async function loadCopyDetail() {
  289. try {
  290. const res = await toolApi.getAnimeDetail({ chat_open_id: copyId.value })
  291. if (!res.data) return
  292. form.img2img_style = String(res.data.img2img_style || form.img2img_style)
  293. form.img2img_strength = Number(res.data.img2img_strength || form.img2img_strength)
  294. form.no = res.data.no || ''
  295. form.content = res.data.prompts || ''
  296. } catch (e) {}
  297. }
  298. async function loadBalance() {
  299. try {
  300. const res = await userApi.getBalance()
  301. balance.value = res.data || {}
  302. } catch (e) {}
  303. }
  304. async function loadBillingConfig() {
  305. try {
  306. const res = await billingApi.getConfig({ app_key: 'anime-avatar', model: ANIME_BILLING_MODEL })
  307. billingConfig.value = res.data || {}
  308. quote.value = billingConfig.value.quote || {}
  309. if (billingConfig.value.balance) balance.value = billingConfig.value.balance
  310. } catch (e) {}
  311. }
  312. function selectStyle(item, resetStrength = true) {
  313. form.img2img_style = String(item.value)
  314. form.ai_type = 3
  315. form.img2img_strength = resetStrength ? 0.6 : form.img2img_strength || 0.6
  316. }
  317. function normalizeImageList(value) {
  318. if (Array.isArray(value)) return value.filter(Boolean)
  319. if (!value) return []
  320. return String(value).split(',').map((item) => item.trim()).filter(Boolean)
  321. }
  322. function normalizeDemoList(value) {
  323. if (!value) return []
  324. try {
  325. const data = typeof value === 'string' ? JSON.parse(value) : value
  326. const list = Array.isArray(data) ? data : [data]
  327. return list
  328. .map((item) => ({
  329. original: item.original || item.before || item.source || '',
  330. anime: item.anime || item.after || item.result || ''
  331. }))
  332. .filter((item) => item.original && item.anime)
  333. .slice(0, 1)
  334. } catch (e) {
  335. const pics = normalizeImageList(value)
  336. if (pics.length >= 2) return [{ original: pics[0], anime: pics[1] }]
  337. return []
  338. }
  339. }
  340. function getStyleImage(item) {
  341. return item.pic || item.img || item.image || item.cover || item.icon || item.url || ''
  342. }
  343. function onStrengthChange(e) {
  344. form.img2img_strength = Number(e.detail.value)
  345. }
  346. function openCompare() {
  347. if (!demoPair.value.original || !demoPair.value.anime) return
  348. comparePercent.value = 50
  349. compareVisible.value = true
  350. }
  351. function closeCompare() {
  352. compareVisible.value = false
  353. }
  354. function onCompareTap(e) {
  355. updateComparePercent(e)
  356. }
  357. function onCompareTouch(e) {
  358. updateComparePercent(e)
  359. }
  360. function updateComparePercent(e) {
  361. const touch = e.touches && e.touches[0] ? e.touches[0] : e
  362. const x = touch.clientX || 0
  363. uni.createSelectorQuery()
  364. .select('.large-compare')
  365. .boundingClientRect((rect) => {
  366. if (!rect || !rect.width) return
  367. const percent = ((x - rect.left) / rect.width) * 100
  368. comparePercent.value = Math.max(8, Math.min(92, Number(percent.toFixed(1))))
  369. })
  370. .exec()
  371. }
  372. function requireLogin() {
  373. if (userStore.isLogin) return true
  374. uni.navigateTo({ url: '/pages/login/login' })
  375. return false
  376. }
  377. function showBalanceModal() {
  378. const cost = Number(quote.value.points || 0)
  379. uni.showModal({
  380. title: '温馨提示',
  381. content: cost ? `本次生成需要 ${cost} 点,当前点数不足,请充值后继续。` : '点数不足,请充值后继续。',
  382. confirmText: '去充值',
  383. cancelText: '知道了',
  384. success: (res) => {
  385. if (res.confirm) uni.navigateTo({ url: '/pages/recharge/recharge' })
  386. }
  387. })
  388. }
  389. function chooseImage() {
  390. if (!requireLogin()) return
  391. uni.chooseImage({
  392. count: 1,
  393. sizeType: ['compressed'],
  394. sourceType: ['album', 'camera'],
  395. success: async (res) => {
  396. const filePath = res.tempFilePaths[0]
  397. previewImage.value = filePath
  398. uploading.value = true
  399. try {
  400. const uploadRes = await uploadImage(filePath, res.tempFiles && res.tempFiles[0])
  401. form.imgs = [uploadRes.data.url]
  402. if (uploadRes.data.width) form.width = uploadRes.data.width
  403. if (uploadRes.data.height) form.height = uploadRes.data.height
  404. } catch (e) {
  405. previewImage.value = ''
  406. form.imgs = []
  407. toast(e.msg || '上传失败')
  408. } finally {
  409. uploading.value = false
  410. }
  411. }
  412. })
  413. }
  414. function goHistory() {
  415. if (!requireLogin()) return
  416. uni.navigateTo({ url: '/pages/tool/anime-history' })
  417. }
  418. function goRecharge() {
  419. uni.navigateTo({ url: '/pages/recharge/recharge' })
  420. }
  421. async function submit() {
  422. if (!requireLogin()) return
  423. if (uploading.value) return toast('图片正在上传')
  424. if (!form.imgs.length) return toast('请上传参考图')
  425. if (submitting.value) return
  426. const params = {
  427. ...form,
  428. imgs: form.imgs.slice(0, 1).join(',')
  429. }
  430. submitting.value = true
  431. try {
  432. const res = await toolApi.submitAnime(params)
  433. if (res.data && res.data.quote) quote.value = res.data.quote
  434. const id = res.data && res.data.answer_chat_open_id
  435. if (id) {
  436. uni.navigateTo({ url: `/pages/tool/work-detail?id=${id}_0&from=anime` })
  437. } else {
  438. toast('提交成功')
  439. }
  440. } catch (e) {
  441. if (e.code === 1011) {
  442. showBalanceModal()
  443. } else if (e.code === 1012) {
  444. form.content = ''
  445. uni.showModal({ title: '温馨提示', content: e.msg || '', showCancel: false, confirmText: '知道了' })
  446. } else {
  447. toast(e.msg || '生成失败')
  448. }
  449. } finally {
  450. submitting.value = false
  451. }
  452. }
  453. </script>
  454. <style lang="scss" scoped>
  455. .page {
  456. height: 100vh;
  457. background: #f5f7fb;
  458. display: flex;
  459. flex-direction: column;
  460. overflow: hidden;
  461. }
  462. .scroll {
  463. flex: 1;
  464. height: 0;
  465. }
  466. .wrap {
  467. padding: 24rpx 28rpx 300rpx;
  468. }
  469. .hero {
  470. min-height: 348rpx;
  471. padding: 32rpx;
  472. border-radius: 28rpx;
  473. background: linear-gradient(135deg, #ffffff 0%, #edf6ff 58%, #fff7f0 100%);
  474. display: flex;
  475. justify-content: space-between;
  476. align-items: center;
  477. gap: 24rpx;
  478. overflow: hidden;
  479. position: relative;
  480. }
  481. .hero-copy {
  482. flex: 1;
  483. min-width: 0;
  484. display: flex;
  485. flex-direction: column;
  486. position: relative;
  487. z-index: 1;
  488. }
  489. .eyebrow {
  490. color: #1f8cff;
  491. font-size: 20rpx;
  492. font-weight: 800;
  493. letter-spacing: 0;
  494. }
  495. .hero-title {
  496. margin-top: 12rpx;
  497. color: #172033;
  498. font-size: 42rpx;
  499. line-height: 1.18;
  500. font-weight: 800;
  501. }
  502. .hero-desc {
  503. margin-top: 18rpx;
  504. color: #66758a;
  505. font-size: 24rpx;
  506. line-height: 1.55;
  507. }
  508. .hero-actions {
  509. margin-top: 24rpx;
  510. display: flex;
  511. align-items: center;
  512. gap: 16rpx;
  513. }
  514. .hero-pill,
  515. .hero-link {
  516. height: 56rpx;
  517. padding: 0 22rpx;
  518. border-radius: 999rpx;
  519. font-size: 24rpx;
  520. line-height: 56rpx;
  521. font-weight: 800;
  522. }
  523. .hero-pill {
  524. color: #ffffff;
  525. background: #1f8cff;
  526. box-shadow: 0 10rpx 20rpx rgba(31, 140, 255, 0.22);
  527. }
  528. .hero-link {
  529. color: #1f8cff;
  530. background: #ffffff;
  531. }
  532. .hero-compare-card {
  533. width: 188rpx;
  534. flex-shrink: 0;
  535. display: flex;
  536. flex-direction: column;
  537. align-items: center;
  538. }
  539. .mini-compare,
  540. .large-compare {
  541. position: relative;
  542. overflow: hidden;
  543. background: #dfe8f4;
  544. }
  545. .mini-compare {
  546. width: 168rpx;
  547. height: 252rpx;
  548. border-radius: 28rpx;
  549. box-shadow: 0 18rpx 38rpx rgba(31, 62, 112, 0.18);
  550. }
  551. .compare-img {
  552. width: 100%;
  553. height: 100%;
  554. }
  555. .compare-before {
  556. position: absolute;
  557. left: 0;
  558. top: 0;
  559. bottom: 0;
  560. overflow: hidden;
  561. }
  562. .mini-before {
  563. width: 50%;
  564. }
  565. .compare-before .compare-img {
  566. width: 168rpx;
  567. }
  568. .compare-line {
  569. position: absolute;
  570. top: 0;
  571. bottom: 0;
  572. width: 4rpx;
  573. margin-left: -2rpx;
  574. background: rgba(255, 255, 255, 0.94);
  575. box-shadow: 0 0 12rpx rgba(20, 37, 66, 0.22);
  576. }
  577. .compare-line.mini-line {
  578. left: 50%;
  579. }
  580. .compare-line.large {
  581. width: 6rpx;
  582. margin-left: -3rpx;
  583. }
  584. .compare-handle {
  585. position: absolute;
  586. left: 50%;
  587. top: 50%;
  588. width: 38rpx;
  589. height: 58rpx;
  590. margin-left: -19rpx;
  591. margin-top: -29rpx;
  592. border-radius: 999rpx;
  593. background: #ffffff;
  594. box-shadow: 0 8rpx 18rpx rgba(20, 37, 66, 0.22);
  595. display: flex;
  596. align-items: center;
  597. justify-content: center;
  598. gap: 1rpx;
  599. color: #1f8cff;
  600. font-size: 24rpx;
  601. font-weight: 900;
  602. }
  603. .compare-handle.large {
  604. width: 54rpx;
  605. height: 78rpx;
  606. margin-left: -27rpx;
  607. margin-top: -39rpx;
  608. font-size: 32rpx;
  609. }
  610. .compare-tag {
  611. position: absolute;
  612. top: 12rpx;
  613. padding: 6rpx 12rpx;
  614. border-radius: 999rpx;
  615. color: #ffffff;
  616. background: rgba(15, 23, 42, 0.46);
  617. font-size: 20rpx;
  618. font-weight: 700;
  619. }
  620. .compare-tag.left {
  621. left: 12rpx;
  622. }
  623. .compare-tag.right {
  624. right: 12rpx;
  625. }
  626. .upload-card,
  627. .section,
  628. .tips-card {
  629. margin-top: 22rpx;
  630. padding: 28rpx;
  631. border-radius: 24rpx;
  632. background: #ffffff;
  633. box-shadow: 0 10rpx 28rpx rgba(32, 56, 92, 0.05);
  634. }
  635. .card-head,
  636. .section-head {
  637. display: flex;
  638. align-items: flex-start;
  639. justify-content: space-between;
  640. gap: 20rpx;
  641. }
  642. .card-title,
  643. .anime-section-title {
  644. display: block;
  645. color: #172033;
  646. font-size: 32rpx;
  647. font-weight: 800;
  648. }
  649. .card-desc,
  650. .section-desc,
  651. .section-tip {
  652. display: block;
  653. margin-top: 8rpx;
  654. color: #8a97a8;
  655. font-size: 23rpx;
  656. line-height: 1.45;
  657. }
  658. .required {
  659. padding: 8rpx 16rpx;
  660. border-radius: 999rpx;
  661. color: #ff6b6b;
  662. background: #fff1f1;
  663. font-size: 22rpx;
  664. font-weight: 700;
  665. }
  666. .upload-strip {
  667. margin-top: 24rpx;
  668. min-height: 162rpx;
  669. padding: 18rpx;
  670. border: 2rpx solid #e5edf7;
  671. border-radius: 22rpx;
  672. background: #f7faff;
  673. display: flex;
  674. align-items: center;
  675. gap: 20rpx;
  676. }
  677. .upload-thumb {
  678. width: 96rpx;
  679. height: 144rpx;
  680. border-radius: 18rpx;
  681. background: #edf3fb;
  682. overflow: hidden;
  683. position: relative;
  684. flex-shrink: 0;
  685. }
  686. .upload-img {
  687. width: 100%;
  688. height: 100%;
  689. }
  690. .upload-empty,
  691. .upload-mask {
  692. width: 100%;
  693. height: 100%;
  694. display: flex;
  695. flex-direction: column;
  696. align-items: center;
  697. justify-content: center;
  698. }
  699. .plus {
  700. width: 54rpx;
  701. height: 54rpx;
  702. border-radius: 50%;
  703. color: #ffffff;
  704. background: #1f8cff;
  705. font-size: 42rpx;
  706. font-weight: 300;
  707. display: flex;
  708. align-items: center;
  709. justify-content: center;
  710. line-height: 1;
  711. }
  712. .upload-copy {
  713. flex: 1;
  714. min-width: 0;
  715. }
  716. .upload-title {
  717. display: block;
  718. color: #172033;
  719. font-size: 30rpx;
  720. font-weight: 800;
  721. }
  722. .upload-sub {
  723. display: block;
  724. margin-top: 10rpx;
  725. color: #8a97a8;
  726. font-size: 24rpx;
  727. line-height: 1.4;
  728. }
  729. .upload-action {
  730. width: 96rpx;
  731. height: 58rpx;
  732. border-radius: 999rpx;
  733. color: #ffffff;
  734. background: #1f8cff;
  735. font-size: 24rpx;
  736. line-height: 58rpx;
  737. text-align: center;
  738. font-weight: 800;
  739. flex-shrink: 0;
  740. }
  741. .upload-mask {
  742. position: absolute;
  743. left: 0;
  744. right: 0;
  745. top: 0;
  746. bottom: 0;
  747. color: #ffffff;
  748. background: rgba(17, 30, 52, 0.62);
  749. font-size: 22rpx;
  750. }
  751. .upload-tips {
  752. margin-top: 18rpx;
  753. display: flex;
  754. gap: 14rpx;
  755. }
  756. .upload-tip {
  757. padding: 8rpx 18rpx;
  758. border-radius: 999rpx;
  759. color: #526174;
  760. background: #f1f5fa;
  761. font-size: 22rpx;
  762. font-weight: 700;
  763. }
  764. .compare-modal {
  765. position: fixed;
  766. left: 0;
  767. right: 0;
  768. top: 0;
  769. bottom: 0;
  770. z-index: 120;
  771. }
  772. .compare-backdrop {
  773. position: absolute;
  774. left: 0;
  775. right: 0;
  776. top: 0;
  777. bottom: 0;
  778. background: rgba(15, 23, 42, 0.58);
  779. }
  780. .compare-panel {
  781. position: absolute;
  782. left: 36rpx;
  783. right: 36rpx;
  784. top: 50%;
  785. transform: translateY(-50%);
  786. padding: 24rpx;
  787. border-radius: 28rpx;
  788. background: #ffffff;
  789. }
  790. .compare-panel-head {
  791. display: flex;
  792. align-items: center;
  793. justify-content: space-between;
  794. color: #172033;
  795. font-size: 32rpx;
  796. font-weight: 800;
  797. margin-bottom: 20rpx;
  798. }
  799. .close-btn {
  800. width: 56rpx;
  801. height: 56rpx;
  802. border-radius: 50%;
  803. color: #64748b;
  804. background: #f1f5f9;
  805. font-size: 42rpx;
  806. line-height: 50rpx;
  807. text-align: center;
  808. font-weight: 300;
  809. }
  810. .large-compare {
  811. width: 100%;
  812. height: 900rpx;
  813. max-height: 70vh;
  814. border-radius: 22rpx;
  815. }
  816. .large-compare .compare-before .compare-img {
  817. width: calc(100vw - 120rpx);
  818. }
  819. .compare-guide {
  820. display: block;
  821. margin-top: 18rpx;
  822. color: #64748b;
  823. font-size: 24rpx;
  824. text-align: center;
  825. }
  826. .style-scroll {
  827. margin-top: 24rpx;
  828. white-space: nowrap;
  829. }
  830. .style-list {
  831. display: inline-flex;
  832. gap: 18rpx;
  833. }
  834. .style-card {
  835. width: 172rpx;
  836. padding: 10rpx;
  837. border: 2rpx solid transparent;
  838. border-radius: 22rpx;
  839. background: #f6f9fd;
  840. text-align: center;
  841. box-sizing: border-box;
  842. }
  843. .style-card.active {
  844. border-color: #1f8cff;
  845. background: #eef7ff;
  846. }
  847. .style-img {
  848. width: 148rpx;
  849. height: 148rpx;
  850. border-radius: 18rpx;
  851. display: flex;
  852. align-items: center;
  853. justify-content: center;
  854. overflow: hidden;
  855. color: #77869a;
  856. background: #eaf0f8;
  857. font-size: 24rpx;
  858. }
  859. .style-name {
  860. display: block;
  861. margin-top: 10rpx;
  862. color: #334155;
  863. font-size: 24rpx;
  864. font-weight: 700;
  865. overflow: hidden;
  866. text-overflow: ellipsis;
  867. white-space: nowrap;
  868. }
  869. .compact {
  870. padding-bottom: 22rpx;
  871. }
  872. .strength-value {
  873. color: #1f8cff;
  874. font-size: 32rpx;
  875. font-weight: 800;
  876. }
  877. .strength-scale {
  878. margin-top: -4rpx;
  879. display: flex;
  880. justify-content: space-between;
  881. color: #8a97a8;
  882. font-size: 22rpx;
  883. }
  884. .input-block {
  885. margin-top: 24rpx;
  886. }
  887. .input-label {
  888. display: block;
  889. margin-bottom: 12rpx;
  890. color: #526174;
  891. font-size: 24rpx;
  892. font-weight: 700;
  893. }
  894. .input {
  895. height: 86rpx;
  896. padding: 0 24rpx;
  897. border-radius: 18rpx;
  898. background: #f6f9fd;
  899. color: #172033;
  900. font-size: 28rpx;
  901. }
  902. .tips-card {
  903. color: #5f6f83;
  904. font-size: 24rpx;
  905. line-height: 1.55;
  906. }
  907. .tip-line {
  908. display: flex;
  909. gap: 16rpx;
  910. }
  911. .tip-line + .tip-line {
  912. margin-top: 16rpx;
  913. }
  914. .tip-mark {
  915. width: 44rpx;
  916. height: 32rpx;
  917. border-radius: 999rpx;
  918. color: #1f8cff;
  919. background: #eef7ff;
  920. font-size: 20rpx;
  921. line-height: 32rpx;
  922. text-align: center;
  923. font-weight: 800;
  924. flex-shrink: 0;
  925. }
  926. .bottom-bar {
  927. position: fixed;
  928. left: 0;
  929. right: 0;
  930. bottom: 0;
  931. z-index: 80;
  932. padding: 18rpx 28rpx calc(18rpx + env(safe-area-inset-bottom));
  933. background: rgba(255, 255, 255, 0.96);
  934. box-shadow: 0 -10rpx 30rpx rgba(32, 56, 92, 0.1);
  935. }
  936. .balance {
  937. color: #7b8797;
  938. font-size: 24rpx;
  939. margin-bottom: 14rpx;
  940. display: flex;
  941. align-items: center;
  942. justify-content: space-between;
  943. gap: 20rpx;
  944. line-height: 1.35;
  945. }
  946. .balance-copy {
  947. min-width: 0;
  948. flex: 1;
  949. }
  950. .recharge-link {
  951. flex-shrink: 0;
  952. color: #0b8cff;
  953. font-size: 24rpx;
  954. font-weight: 800;
  955. }
  956. .buttons {
  957. display: flex;
  958. gap: 22rpx;
  959. }
  960. .history-btn,
  961. .submit-btn {
  962. height: 86rpx;
  963. border-radius: 999rpx;
  964. font-size: 30rpx;
  965. font-weight: 800;
  966. margin: 0;
  967. display: flex;
  968. align-items: center;
  969. justify-content: center;
  970. }
  971. .history-btn {
  972. width: 210rpx;
  973. color: #1f8cff;
  974. background: #eef7ff;
  975. }
  976. .submit-btn {
  977. flex: 1;
  978. color: #ffffff;
  979. background: linear-gradient(135deg, #1f8cff, #41b9ff);
  980. box-shadow: 0 12rpx 24rpx rgba(31, 140, 255, 0.28);
  981. }
  982. .submit-icon {
  983. width: 24rpx;
  984. height: 24rpx;
  985. margin-right: 8rpx;
  986. flex-shrink: 0;
  987. }
  988. .submit-btn.disabled {
  989. opacity: 0.72;
  990. }
  991. </style>