Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 

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