25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.
 
 
 

1296 satır
31 KiB

  1. <template>
  2. <view class="page">
  3. <view class="wrap">
  4. <view class="hero">
  5. <view class="hero-copy">
  6. <text class="eyebrow">AI NAME STUDIO</text>
  7. <text class="hero-title">给宝宝取一个有出处的好名字</text>
  8. <text class="hero-desc">
  9. 结合姓氏、性别、诗词典故和音律寓意,生成可直接挑选的名字方案。
  10. </text>
  11. </view>
  12. <view class="hero-preview">
  13. <view class="preview-card main">
  14. <text class="preview-name">{{ previewName }}</text>
  15. <text class="preview-meta">李白《独坐敬亭山》</text>
  16. </view>
  17. <view class="preview-card sub">
  18. <text>音律</text>
  19. <text>寓意</text>
  20. <text>出处</text>
  21. </view>
  22. </view>
  23. </view>
  24. <view class="primary-card">
  25. <view class="card-head">
  26. <text class="card-title">基础信息</text>
  27. <text class="card-tip">必填</text>
  28. </view>
  29. <view class="surname-box">
  30. <view class="surname-field">
  31. <text class="field-label">姓氏</text>
  32. <input v-model="form.surname" class="surname-input" maxlength="5" placeholder="输入姓氏" />
  33. </view>
  34. <view class="gender-toggle">
  35. <view
  36. v-for="item in sexOptions"
  37. :key="item.id"
  38. class="gender-item"
  39. :class="{ active: form.sex === item.id }"
  40. @click="form.sex = item.id"
  41. >
  42. {{ item.label }}
  43. </view>
  44. </view>
  45. </view>
  46. <view class="birth-panel">
  47. <view class="birth-switch" :class="{ off: birthInfoIgnored }" @tap="toggleBirthInfoReference">
  48. <view class="birth-switch-copy">
  49. <text class="birth-switch-title">参考出生信息</text>
  50. <text class="birth-switch-desc">
  51. {{ birthInfoIgnored ? '已关闭,生成时不参考农历和五行' : '开启后会结合农历和五行倾向' }}
  52. </text>
  53. </view>
  54. <view class="birth-switch-track" :class="{ on: !birthInfoIgnored }">
  55. <view class="birth-switch-thumb"></view>
  56. </view>
  57. </view>
  58. <view v-if="!birthInfoIgnored" class="birth-grid">
  59. <view class="date-row" @tap="openBirthCalendar">
  60. <view>
  61. <text class="date-label">出生日期</text>
  62. <text class="date-value">{{ birthDateText }}</text>
  63. </view>
  64. <text class="date-arrow">›</text>
  65. </view>
  66. <view class="date-row" :class="{ muted: birthTimeUnknown }" @tap="openBirthTimePicker">
  67. <view>
  68. <text class="date-label">出生时分</text>
  69. <text class="date-value">{{ birthClockText }}</text>
  70. </view>
  71. <text class="date-arrow">›</text>
  72. </view>
  73. </view>
  74. <view v-if="!birthInfoIgnored && birthLunarText" class="lunar-card">
  75. <text class="lunar-label">农历</text>
  76. <text class="lunar-value">{{ birthLunarText }}</text>
  77. </view>
  78. <view v-if="!birthInfoIgnored" class="unknown-time" :class="{ active: birthTimeUnknown }" @tap="toggleBirthTimeUnknown">
  79. <text class="unknown-dot"></text>
  80. <text>不知道具体时间</text>
  81. </view>
  82. </view>
  83. </view>
  84. <view class="section">
  85. <view class="name-section-head">
  86. <text class="name-section-title">名字出处</text>
  87. <text class="name-section-desc">选择偏好的文化来源</text>
  88. </view>
  89. <view class="source-grid">
  90. <view
  91. v-for="item in visibleSourceOptions"
  92. :key="item.id"
  93. class="source-item"
  94. :class="{ active: form.name_source === item.id }"
  95. @click="form.name_source = item.id"
  96. >
  97. <text class="source-title">{{ item.label }}</text>
  98. <text class="source-desc">{{ item.desc }}</text>
  99. </view>
  100. <view class="source-item more" @click="sourceExpanded = !sourceExpanded">
  101. <text class="source-title">{{ sourceExpanded ? '收起' : '更多' }}</text>
  102. <text class="source-desc">{{ sourceExpanded ? '精简显示' : '展开全部' }}</text>
  103. </view>
  104. </view>
  105. </view>
  106. <view class="section">
  107. <view class="name-section-head">
  108. <text class="name-section-title">名字偏好</text>
  109. <text class="name-section-desc">用于控制生成结果的方向</text>
  110. </view>
  111. <view class="preference-block">
  112. <text class="preference-label">名字字数 <text class="muted">包含姓氏</text></text>
  113. <view class="chip-row">
  114. <view
  115. v-for="item in wordOptions"
  116. :key="item.id"
  117. class="chip"
  118. :class="{ active: form.words_num === item.id }"
  119. @click="form.words_num = item.id"
  120. >
  121. {{ item.label }}
  122. </view>
  123. </view>
  124. </view>
  125. <view class="preference-block">
  126. <text class="preference-label">是否叠词</text>
  127. <view class="chip-row">
  128. <view
  129. v-for="item in redupOptions"
  130. :key="item.id"
  131. class="chip"
  132. :class="{ active: form.is_redup === item.id }"
  133. @click="form.is_redup = item.id"
  134. >
  135. {{ item.label }}
  136. </view>
  137. </view>
  138. </view>
  139. <view class="special-row">
  140. <view>
  141. <text class="preference-label">指定包含字</text>
  142. <text class="special-tip">可选,限制 1 个字</text>
  143. </view>
  144. <input v-model="form.special_word" class="special-input" maxlength="1" placeholder="如:辰" />
  145. </view>
  146. <view class="extra-row">
  147. <view class="extra-head">
  148. <view>
  149. <text class="preference-label">补充描述</text>
  150. <text class="special-tip">额外要求、寓意和期望</text>
  151. </view>
  152. <text class="extra-count">{{ (form.content || '').length }}/200</text>
  153. </view>
  154. <textarea
  155. v-model="form.content"
  156. class="extra-textarea"
  157. maxlength="200"
  158. auto-height
  159. cursor-spacing="80"
  160. placeholder="您可以补充您的额外要求,也可以添加您的寓意和期望。200字以内..."
  161. />
  162. </view>
  163. </view>
  164. <view class="promise">
  165. <view class="promise-item">
  166. <text class="promise-mark">01</text>
  167. <text>可关闭出生信息参考,关闭后不结合农历和五行</text>
  168. </view>
  169. <view class="promise-item">
  170. <text class="promise-mark">02</text>
  171. <text>开启后可选择日期和时分,不确定时辰可选“不知道具体时间”</text>
  172. </view>
  173. </view>
  174. </view>
  175. <view class="bottom-bar">
  176. <view class="balance">
  177. <text class="balance-copy">{{ balanceText || 'AI取名' }}</text>
  178. <view v-if="isBalanceInsufficient" class="recharge-link" @click="goRecharge">去充值</view>
  179. </view>
  180. <view class="buttons">
  181. <view class="history-btn" @click="goHistory">历史记录</view>
  182. <view class="submit-btn" :class="{ disabled: submitting }" @click="submit">
  183. <image v-if="!submitting" class="submit-icon" src="https://cdn.aionline.cc/ai/icons/bolt.svg" mode="aspectFit" />
  184. <text>{{ submitting ? '生成中...' : submitText }}</text>
  185. </view>
  186. </view>
  187. </view>
  188. <up-calendar
  189. :show="birthCalendarVisible"
  190. title="选择出生日期"
  191. mode="single"
  192. color="#0b8cff"
  193. :default-date="calendarDefaultDate"
  194. :min-date="calendarMinDate"
  195. :max-date="calendarMaxDate"
  196. :month-num="calendarMonthNum"
  197. show-lunar
  198. :show-today="false"
  199. :close-on-click-overlay="true"
  200. confirm-text="确定"
  201. z-index="300"
  202. @confirm="onBirthDateConfirm"
  203. @close="birthCalendarVisible = false"
  204. />
  205. <up-datetime-picker
  206. v-model="birthClockPickerValue"
  207. :show="birthTimePickerVisible"
  208. mode="time"
  209. title="选择出生时间"
  210. :formatter="formatBirthTimeColumn"
  211. confirm-color="#0b8cff"
  212. :close-on-click-overlay="true"
  213. z-index="310"
  214. @confirm="onBirthTimeConfirm"
  215. @cancel="birthTimePickerVisible = false"
  216. @close="birthTimePickerVisible = false"
  217. />
  218. </view>
  219. </template>
  220. <script setup>
  221. import { computed, reactive, ref } from 'vue'
  222. import { onLoad, onShow } from '@dcloudio/uni-app'
  223. import dayjs from 'dayjs'
  224. import { billingApi, toolApi, userApi } from '@/api/index.js'
  225. import { useUserStore } from '@/store/user.js'
  226. import { useApp } from '@/utils/useApp.js'
  227. import LunarCalendar from '@/uni_modules/uview-plus/libs/util/calendar.js'
  228. const userStore = useUserStore()
  229. const { toast } = useApp()
  230. const appDetail = ref({})
  231. const robotInfo = ref({})
  232. const todayDate = dayjs().format('YYYY-MM-DD')
  233. const birthCacheKey = 'ai_name_birth_cache'
  234. const birthDate = ref(todayDate)
  235. const birthClock = ref('')
  236. const birthClockPickerValue = ref('08:00')
  237. const birthTimeUnknown = ref(true)
  238. const birthInfoIgnored = ref(false)
  239. const birthCalendarVisible = ref(false)
  240. const birthTimePickerVisible = ref(false)
  241. const submitting = ref(false)
  242. const sourceExpanded = ref(false)
  243. const balance = ref({ points: 0 })
  244. const billingConfig = ref({})
  245. const quote = ref({ points: 0 })
  246. const form = reactive({
  247. model: 4,
  248. language_type: 1,
  249. write_type: 1,
  250. tone_type: 1,
  251. length_type: 1,
  252. work_type: 1,
  253. reporting: '',
  254. department: '',
  255. content: '',
  256. surname: '',
  257. sex: '1',
  258. name_source: '0',
  259. birth_time: dayjs(todayDate).valueOf(),
  260. birth_date: todayDate,
  261. birth_clock: '',
  262. birth_time_unknown: 1,
  263. words_num: '0',
  264. is_redup: '0',
  265. special_word: ''
  266. })
  267. const sexOptions = [
  268. { id: '1', label: '男孩' },
  269. { id: '2', label: '女孩' }
  270. ]
  271. const sourceOptions = [
  272. { id: '0', label: '不限', desc: '综合推荐' },
  273. { id: '1', label: '诗经', desc: '温润雅致' },
  274. { id: '2', label: '楚辞', desc: '浪漫开阔' },
  275. { id: '3', label: '离骚', desc: '清朗有骨' },
  276. { id: '4', label: '周易', desc: '稳重含蓄' },
  277. { id: '5', label: '史记', desc: '大气有典' },
  278. { id: '6', label: '诗词歌赋', desc: '文采舒展' },
  279. { id: '7', label: '成语典故', desc: '寓意明确' },
  280. { id: '8', label: '佛教', desc: '清净平和' },
  281. { id: '9', label: '道教', desc: '自然洒脱' },
  282. { id: '10', label: '自然景观', desc: '明亮灵动' }
  283. ]
  284. const wordOptions = [
  285. { id: '0', label: '不限' },
  286. { id: '2', label: '2字' },
  287. { id: '3', label: '3字' },
  288. { id: '4', label: '4字' },
  289. { id: '5', label: '5字' }
  290. ]
  291. const redupOptions = [
  292. { id: '0', label: '不限' },
  293. { id: '1', label: '是' },
  294. { id: '2', label: '否' }
  295. ]
  296. const visibleSourceOptions = computed(() => {
  297. if (sourceExpanded.value) return sourceOptions
  298. const selected = sourceOptions.find((item) => item.id === form.name_source)
  299. const rows = [sourceOptions[0]]
  300. if (selected && selected.id !== '0') rows.push(selected)
  301. sourceOptions.slice(1).forEach((item) => {
  302. if (rows.length >= 5) return
  303. if (!rows.some((row) => row.id === item.id)) rows.push(item)
  304. })
  305. return rows
  306. })
  307. const previewName = computed(() => {
  308. return '白敬亭'
  309. })
  310. const calendarMinDate = dayjs().subtract(2, 'year').format('YYYY-MM-DD')
  311. const calendarMaxDate = dayjs().add(2, 'year').format('YYYY-MM-DD')
  312. const calendarMonthNum = dayjs(calendarMaxDate).diff(dayjs(calendarMinDate), 'month') + 1
  313. const calendarDefaultDate = computed(() => birthDate.value || todayDate)
  314. const birthDateText = computed(() => {
  315. return birthDate.value || '请选择日期'
  316. })
  317. const birthLunarText = computed(() => {
  318. return getLunarText(birthDate.value)
  319. })
  320. const birthClockText = computed(() => {
  321. if (birthTimeUnknown.value) return '不知道具体时间'
  322. return birthClock.value || '请选择时分'
  323. })
  324. const balanceText = computed(() => {
  325. const cost = Number(quote.value.points || 0)
  326. if (!userStore.isLogin) return cost ? `AI取名 · ${cost}点/次` : 'AI取名'
  327. return cost ? `余额${currentPoints.value}点 · 预计消耗${cost}点` : `余额${currentPoints.value}点`
  328. })
  329. const currentPoints = computed(() => Number(balance.value.points || balance.value.point_balance || 0))
  330. const isBalanceInsufficient = computed(() => {
  331. const cost = Number(quote.value.points || 0)
  332. return userStore.isLogin && cost > 0 && currentPoints.value >= 0 && currentPoints.value < cost
  333. })
  334. const submitText = computed(() => {
  335. const cost = Number(quote.value.points || 0)
  336. if (!userStore.isLogin) return cost ? `登录后取名 · ${cost}点` : '登录后取名'
  337. return cost ? `立即取名 · ${cost}点` : '立即取名'
  338. })
  339. onLoad(() => {
  340. loadAppDetail()
  341. loadBillingConfig()
  342. })
  343. onShow(() => {
  344. if (userStore.isLogin) {
  345. loadBalance()
  346. loadRobot()
  347. }
  348. loadBillingConfig()
  349. restoreCache()
  350. })
  351. async function loadAppDetail() {
  352. try {
  353. const res = await toolApi.getAppDetail({ key: 'naming' })
  354. appDetail.value = res.data || {}
  355. if (res.data && res.data.write_type) form.write_type = res.data.write_type
  356. } catch (e) {}
  357. }
  358. async function loadRobot() {
  359. try {
  360. const res = await toolApi.getNameRobot()
  361. robotInfo.value = res.data || {}
  362. } catch (e) {}
  363. }
  364. async function loadBalance() {
  365. try {
  366. const res = await userApi.getBalance()
  367. balance.value = res.data || {}
  368. } catch (e) {}
  369. }
  370. async function loadBillingConfig() {
  371. try {
  372. const res = await billingApi.getConfig({ app_key: 'ai-name' })
  373. billingConfig.value = res.data || {}
  374. quote.value = billingConfig.value.quote || {}
  375. if (billingConfig.value.balance) balance.value = billingConfig.value.balance
  376. } catch (e) {}
  377. }
  378. function restoreCache() {
  379. const cache = uni.getStorageSync('writing_form')
  380. if (cache && cache.surname && !form.surname) {
  381. const cachedForm = { ...cache }
  382. delete cachedForm.birth_time
  383. delete cachedForm.birth_date
  384. delete cachedForm.birth_clock
  385. delete cachedForm.birth_time_unknown
  386. Object.assign(form, cachedForm)
  387. form.content = String(form.content || '').slice(0, 200)
  388. }
  389. restoreBirthCache()
  390. syncBirthFields()
  391. }
  392. function restoreBirthCache() {
  393. const cache = uni.getStorageSync(birthCacheKey)
  394. if (!cache || typeof cache !== 'object') {
  395. birthDate.value = todayDate
  396. birthClock.value = ''
  397. birthClockPickerValue.value = '08:00'
  398. birthTimeUnknown.value = true
  399. return
  400. }
  401. const cachedDate = normalizeBirthDate(cache.birth_date)
  402. const cachedClock = normalizeBirthClock(cache.birth_clock)
  403. birthInfoIgnored.value = Number(cache.birth_info_ignored) === 1
  404. birthDate.value = cachedDate || todayDate
  405. birthTimeUnknown.value = Number(cache.birth_time_unknown) === 1 || !cachedClock
  406. birthClock.value = birthTimeUnknown.value ? '' : cachedClock
  407. birthClockPickerValue.value = cachedClock || '08:00'
  408. }
  409. function saveBirthCache() {
  410. uni.setStorageSync(birthCacheKey, {
  411. birth_date: normalizeBirthDate(birthDate.value) || todayDate,
  412. birth_clock: birthTimeUnknown.value ? '' : normalizeBirthClock(birthClock.value),
  413. birth_time_unknown: birthTimeUnknown.value ? 1 : 0,
  414. birth_info_ignored: birthInfoIgnored.value ? 1 : 0
  415. })
  416. }
  417. function openBirthCalendar() {
  418. if (birthInfoIgnored.value) return
  419. birthCalendarVisible.value = true
  420. }
  421. function openBirthTimePicker() {
  422. if (birthInfoIgnored.value) return
  423. birthClockPickerValue.value = birthClock.value || '08:00'
  424. birthTimePickerVisible.value = true
  425. }
  426. function onBirthDateConfirm(value) {
  427. const selected = resolveCalendarDate(value)
  428. if (selected) {
  429. birthDate.value = selected
  430. syncBirthFields()
  431. saveBirthCache()
  432. }
  433. birthCalendarVisible.value = false
  434. }
  435. function onBirthTimeConfirm(e) {
  436. const value = e && e.value ? String(e.value) : birthClockPickerValue.value
  437. birthClock.value = normalizeBirthClock(value) || '08:00'
  438. birthClockPickerValue.value = birthClock.value
  439. birthTimeUnknown.value = false
  440. syncBirthFields()
  441. saveBirthCache()
  442. birthTimePickerVisible.value = false
  443. }
  444. function formatBirthTimeColumn(type, value) {
  445. if (type === 'hour') return `${value}时`
  446. if (type === 'minute') return `${value}分`
  447. return value
  448. }
  449. function toggleBirthTimeUnknown() {
  450. if (birthInfoIgnored.value) return
  451. birthTimeUnknown.value = !birthTimeUnknown.value
  452. if (birthTimeUnknown.value) {
  453. birthClock.value = ''
  454. } else {
  455. birthClock.value = birthClockPickerValue.value || '08:00'
  456. birthClockPickerValue.value = birthClock.value
  457. }
  458. syncBirthFields()
  459. saveBirthCache()
  460. }
  461. function toggleBirthInfoReference() {
  462. birthInfoIgnored.value = !birthInfoIgnored.value
  463. if (!birthInfoIgnored.value && !birthDate.value) {
  464. birthDate.value = todayDate
  465. }
  466. syncBirthFields()
  467. saveBirthCache()
  468. }
  469. function syncBirthFields() {
  470. if (birthInfoIgnored.value) {
  471. form.birth_date = ''
  472. form.birth_clock = ''
  473. form.birth_time_unknown = 1
  474. form.birth_time = ''
  475. return
  476. }
  477. if (!birthDate.value) {
  478. birthDate.value = todayDate
  479. }
  480. form.birth_date = birthDate.value || ''
  481. form.birth_clock = birthTimeUnknown.value ? '' : (birthClock.value || '')
  482. form.birth_time_unknown = birthTimeUnknown.value ? 1 : 0
  483. if (!birthDate.value) {
  484. form.birth_time = ''
  485. return
  486. }
  487. const clock = birthTimeUnknown.value || !birthClock.value ? '00:00' : birthClock.value
  488. form.birth_time = dayjs(`${birthDate.value} ${clock}`).valueOf()
  489. }
  490. function resolveCalendarDate(payload) {
  491. if (Array.isArray(payload)) return normalizeBirthDate(payload[0])
  492. if (payload && typeof payload === 'object') {
  493. if (Array.isArray(payload.value)) return normalizeBirthDate(payload.value[0])
  494. return normalizeBirthDate(payload.value || payload.date || payload.result)
  495. }
  496. return normalizeBirthDate(payload)
  497. }
  498. function normalizeBirthDate(value) {
  499. if (!value) return ''
  500. if (value instanceof Date) {
  501. const parsedDate = dayjs(value)
  502. return parsedDate.isValid() ? parsedDate.format('YYYY-MM-DD') : ''
  503. }
  504. const rawValue = String(value).trim()
  505. if (!rawValue) return ''
  506. if (/^\d{8}$/.test(rawValue)) {
  507. const compactDate = `${rawValue.slice(0, 4)}-${rawValue.slice(4, 6)}-${rawValue.slice(6, 8)}`
  508. const parsedCompactDate = dayjs(compactDate)
  509. return parsedCompactDate.isValid() ? parsedCompactDate.format('YYYY-MM-DD') : ''
  510. }
  511. if (/^\d{10,13}$/.test(rawValue)) {
  512. const timestamp = Number(rawValue.length === 10 ? `${rawValue}000` : rawValue)
  513. const parsedTimestamp = dayjs(timestamp)
  514. return parsedTimestamp.isValid() ? parsedTimestamp.format('YYYY-MM-DD') : ''
  515. }
  516. const parsed = dayjs(rawValue.replace(/\//g, '-'))
  517. return parsed.isValid() ? parsed.format('YYYY-MM-DD') : ''
  518. }
  519. function normalizeBirthClock(value) {
  520. if (!value) return ''
  521. const match = String(value).trim().match(/^(\d{1,2}):(\d{1,2})/)
  522. if (!match) return ''
  523. const hour = Number(match[1])
  524. const minute = Number(match[2])
  525. if (hour < 0 || hour > 23 || minute < 0 || minute > 59) return ''
  526. return `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`
  527. }
  528. function getLunarText(date) {
  529. if (!date) return ''
  530. const parsed = dayjs(date)
  531. if (!parsed.isValid()) return ''
  532. try {
  533. const lunar = LunarCalendar.solar2lunar(parsed.year(), parsed.month() + 1, parsed.date())
  534. if (!lunar || lunar === -1) return ''
  535. const leap = lunar.isLeap ? '闰' : ''
  536. return `农历${lunar.gzYear || ''}年 ${leap}${lunar.IMonthCn || ''}${lunar.IDayCn || ''}`
  537. } catch (e) {
  538. return ''
  539. }
  540. }
  541. function requireLogin() {
  542. if (userStore.isLogin) return true
  543. uni.navigateTo({ url: '/pages/login/login' })
  544. return false
  545. }
  546. async function ensureRobot() {
  547. if (robotInfo.value.robot_open_id) return true
  548. await loadRobot()
  549. return !!robotInfo.value.robot_open_id
  550. }
  551. async function goHistory() {
  552. if (!requireLogin()) return
  553. await ensureRobot()
  554. uni.navigateTo({ url: `/pages/tool/name-history?robot_id=${robotInfo.value.robot_open_id || ''}` })
  555. }
  556. function goRecharge() {
  557. uni.navigateTo({ url: '/pages/recharge/recharge' })
  558. }
  559. async function submit() {
  560. if (!requireLogin()) return
  561. if (!form.surname) return toast('请输入姓氏')
  562. if (!form.sex) return toast('请选择性别')
  563. if (submitting.value) return
  564. await ensureRobot()
  565. form.content = String(form.content || '').trim().slice(0, 200)
  566. syncBirthFields()
  567. saveBirthCache()
  568. uni.setStorageSync('writing_form', { ...form })
  569. const params = {
  570. ...robotInfo.value,
  571. ...form,
  572. model: 4
  573. }
  574. if (params.birth_time) params.birth_time = dayjs(params.birth_time).unix()
  575. submitting.value = true
  576. try {
  577. const checkRes = await toolApi.checkName(params)
  578. if (checkRes.data && checkRes.data.quote) quote.value = checkRes.data.quote
  579. if (checkRes.data && checkRes.data.balance) balance.value = checkRes.data.balance
  580. uni.setStorageSync('name_stream_params', params)
  581. uni.navigateTo({
  582. url: `/pages/tool/name-result?create=1&robot_id=${robotInfo.value.robot_open_id || ''}&key=naming`
  583. })
  584. } catch (e) {
  585. if (e.code === 1011) {
  586. uni.showModal({
  587. title: '温馨提示',
  588. content: e.msg || '点数不足,请充值后继续使用',
  589. confirmText: '知道了',
  590. showCancel: false
  591. })
  592. } else {
  593. toast(e.msg || '发送失败')
  594. }
  595. } finally {
  596. submitting.value = false
  597. }
  598. }
  599. </script>
  600. <style lang="scss" scoped>
  601. .page {
  602. min-height: 100vh;
  603. background: #f3f7fb;
  604. }
  605. .wrap {
  606. padding: 22rpx 28rpx 300rpx;
  607. }
  608. .hero {
  609. position: relative;
  610. min-height: 310rpx;
  611. padding: 34rpx 30rpx;
  612. border-radius: 28rpx;
  613. color: #fff;
  614. background: linear-gradient(135deg, #138cff 0%, #34c3ff 54%, #79e0df 100%);
  615. overflow: hidden;
  616. box-sizing: border-box;
  617. }
  618. .hero::before {
  619. content: '';
  620. position: absolute;
  621. width: 360rpx;
  622. height: 360rpx;
  623. right: -110rpx;
  624. top: -140rpx;
  625. border-radius: 50%;
  626. background: rgba(255, 255, 255, 0.2);
  627. }
  628. .hero::after {
  629. content: '';
  630. position: absolute;
  631. width: 240rpx;
  632. height: 240rpx;
  633. right: 86rpx;
  634. bottom: -130rpx;
  635. border-radius: 50%;
  636. background: rgba(255, 255, 255, 0.16);
  637. }
  638. .hero-copy,
  639. .hero-preview {
  640. position: relative;
  641. z-index: 1;
  642. }
  643. .eyebrow {
  644. display: block;
  645. color: rgba(255, 255, 255, 0.76);
  646. font-size: 21rpx;
  647. font-weight: 700;
  648. letter-spacing: 0;
  649. }
  650. .hero-title {
  651. display: block;
  652. margin-top: 18rpx;
  653. width: 420rpx;
  654. color: #fff;
  655. font-size: 42rpx;
  656. font-weight: 800;
  657. line-height: 1.24;
  658. }
  659. .hero-desc {
  660. display: block;
  661. margin-top: 18rpx;
  662. width: 420rpx;
  663. color: rgba(255, 255, 255, 0.88);
  664. font-size: 25rpx;
  665. line-height: 1.55;
  666. }
  667. .hero-preview {
  668. position: absolute;
  669. right: 16rpx;
  670. bottom: 28rpx;
  671. width: 220rpx;
  672. }
  673. .preview-card {
  674. border: 1rpx solid rgba(255, 255, 255, 0.42);
  675. background: rgba(255, 255, 255, 0.2);
  676. box-shadow: 0 18rpx 34rpx rgba(0, 92, 190, 0.16);
  677. }
  678. .preview-card.main {
  679. padding: 22rpx;
  680. border-radius: 24rpx;
  681. }
  682. .preview-name {
  683. display: block;
  684. color: #fff;
  685. font-size: 34rpx;
  686. font-weight: 800;
  687. }
  688. .preview-meta {
  689. display: block;
  690. margin-top: 6rpx;
  691. color: rgba(255, 255, 255, 0.82);
  692. font-size: 21rpx;
  693. }
  694. .preview-card.sub {
  695. margin-top: 12rpx;
  696. height: 48rpx;
  697. padding: 0 14rpx;
  698. border-radius: 18rpx;
  699. display: flex;
  700. align-items: center;
  701. justify-content: space-between;
  702. color: rgba(255, 255, 255, 0.86);
  703. font-size: 20rpx;
  704. }
  705. .primary-card,
  706. .section,
  707. .promise {
  708. margin-top: 22rpx;
  709. border-radius: 24rpx;
  710. background: #fff;
  711. box-shadow: 0 8rpx 28rpx rgba(34, 68, 112, 0.05);
  712. }
  713. .primary-card,
  714. .section {
  715. padding: 28rpx;
  716. }
  717. .card-head,
  718. .name-section-head {
  719. display: flex;
  720. align-items: baseline;
  721. }
  722. .card-title,
  723. .name-section-title {
  724. color: #172033;
  725. font-size: 31rpx;
  726. font-weight: 800;
  727. }
  728. .card-tip,
  729. .name-section-desc {
  730. margin-left: auto;
  731. color: #8a95a6;
  732. font-size: 23rpx;
  733. }
  734. .surname-box {
  735. margin-top: 22rpx;
  736. padding: 20rpx;
  737. border-radius: 22rpx;
  738. background: #f6f9fe;
  739. display: flex;
  740. align-items: center;
  741. gap: 18rpx;
  742. }
  743. .surname-field {
  744. flex: 1;
  745. min-width: 0;
  746. }
  747. .field-label,
  748. .date-label,
  749. .preference-label {
  750. display: block;
  751. color: #172033;
  752. font-size: 25rpx;
  753. font-weight: 700;
  754. }
  755. .surname-input {
  756. margin-top: 8rpx;
  757. height: 58rpx;
  758. color: #172033;
  759. font-size: 40rpx;
  760. font-weight: 800;
  761. }
  762. .gender-toggle {
  763. width: 196rpx;
  764. padding: 6rpx;
  765. border-radius: 34rpx;
  766. background: #fff;
  767. display: flex;
  768. box-shadow: inset 0 0 0 1rpx #edf2f7;
  769. }
  770. .gender-item {
  771. flex: 1;
  772. height: 54rpx;
  773. border-radius: 28rpx;
  774. color: #7f8896;
  775. font-size: 24rpx;
  776. font-weight: 700;
  777. line-height: 54rpx;
  778. text-align: center;
  779. }
  780. .gender-item.active {
  781. color: #fff;
  782. background: #0b8cff;
  783. }
  784. .birth-panel {
  785. margin-top: 18rpx;
  786. display: flex;
  787. flex-direction: column;
  788. align-items: flex-start;
  789. }
  790. .birth-switch {
  791. width: 100%;
  792. padding: 18rpx 20rpx;
  793. border-radius: 20rpx;
  794. background: #eff8ff;
  795. border: 2rpx solid rgba(11, 140, 255, 0.16);
  796. display: flex;
  797. align-items: center;
  798. box-sizing: border-box;
  799. }
  800. .birth-switch.off {
  801. background: #f7f9fd;
  802. border-color: #edf2f7;
  803. }
  804. .birth-switch-copy {
  805. min-width: 0;
  806. flex: 1;
  807. }
  808. .birth-switch-title {
  809. display: block;
  810. color: #172033;
  811. font-size: 25rpx;
  812. font-weight: 800;
  813. line-height: 1.3;
  814. }
  815. .birth-switch-desc {
  816. display: block;
  817. margin-top: 6rpx;
  818. color: #7f8896;
  819. font-size: 22rpx;
  820. line-height: 1.35;
  821. }
  822. .birth-switch-track {
  823. width: 76rpx;
  824. height: 42rpx;
  825. margin-left: 18rpx;
  826. padding: 4rpx;
  827. border-radius: 999rpx;
  828. background: #d7deea;
  829. box-sizing: border-box;
  830. flex-shrink: 0;
  831. }
  832. .birth-switch-track.on {
  833. background: #0b8cff;
  834. }
  835. .birth-switch-thumb {
  836. width: 34rpx;
  837. height: 34rpx;
  838. border-radius: 50%;
  839. background: #fff;
  840. box-shadow: 0 4rpx 12rpx rgba(23, 32, 51, 0.18);
  841. }
  842. .birth-switch-track.on .birth-switch-thumb {
  843. transform: translateX(34rpx);
  844. }
  845. .birth-grid {
  846. width: 100%;
  847. margin-top: 14rpx;
  848. display: grid;
  849. grid-template-columns: repeat(2, minmax(0, 1fr));
  850. gap: 14rpx;
  851. }
  852. .date-row {
  853. width: 100%;
  854. min-height: 118rpx;
  855. padding: 20rpx 22rpx;
  856. border-radius: 20rpx;
  857. background: #f6f9fe;
  858. display: flex;
  859. align-items: flex-start;
  860. justify-content: space-between;
  861. box-sizing: border-box;
  862. }
  863. .date-row.muted {
  864. opacity: 0.82;
  865. }
  866. .date-value {
  867. display: block;
  868. margin-top: 12rpx;
  869. color: #7f8896;
  870. font-size: 25rpx;
  871. line-height: 1.32;
  872. }
  873. .date-arrow {
  874. margin-left: 12rpx;
  875. color: #a0aabb;
  876. font-size: 36rpx;
  877. line-height: 1;
  878. flex-shrink: 0;
  879. }
  880. .lunar-card {
  881. width: 100%;
  882. margin-top: 14rpx;
  883. padding: 16rpx 20rpx;
  884. border-radius: 18rpx;
  885. display: flex;
  886. align-items: center;
  887. background: #eff8ff;
  888. box-sizing: border-box;
  889. }
  890. .lunar-label {
  891. margin-right: 14rpx;
  892. padding: 4rpx 12rpx;
  893. border-radius: 999rpx;
  894. color: #0b8cff;
  895. font-size: 20rpx;
  896. font-weight: 800;
  897. background: #fff;
  898. flex-shrink: 0;
  899. }
  900. .lunar-value {
  901. min-width: 0;
  902. color: #3a5878;
  903. font-size: 24rpx;
  904. font-weight: 700;
  905. line-height: 1.35;
  906. }
  907. .unknown-time {
  908. margin-top: 14rpx;
  909. padding: 12rpx 18rpx;
  910. border-radius: 999rpx;
  911. display: flex;
  912. align-items: center;
  913. color: #7f8896;
  914. font-size: 23rpx;
  915. font-weight: 700;
  916. background: #f6f9fe;
  917. border: 2rpx solid transparent;
  918. }
  919. .unknown-time.active {
  920. color: #0b8cff;
  921. background: #eff8ff;
  922. border-color: #0b8cff;
  923. }
  924. .unknown-dot {
  925. width: 14rpx;
  926. height: 14rpx;
  927. margin-right: 10rpx;
  928. border-radius: 50%;
  929. background: currentColor;
  930. }
  931. .source-grid {
  932. margin-top: 22rpx;
  933. display: grid;
  934. grid-template-columns: repeat(3, minmax(0, 1fr));
  935. gap: 14rpx;
  936. }
  937. .source-item {
  938. min-height: 86rpx;
  939. padding: 14rpx 10rpx;
  940. border-radius: 18rpx;
  941. background: #f7f9fd;
  942. border: 2rpx solid transparent;
  943. box-sizing: border-box;
  944. }
  945. .source-item.active {
  946. background: #eff8ff;
  947. border-color: #0b8cff;
  948. box-shadow: 0 8rpx 20rpx rgba(11, 140, 255, 0.1);
  949. }
  950. .source-item.more {
  951. background: #fff;
  952. border-color: #e7edf5;
  953. }
  954. .source-title {
  955. display: block;
  956. color: #172033;
  957. font-size: 25rpx;
  958. font-weight: 800;
  959. line-height: 1.25;
  960. text-align: center;
  961. }
  962. .source-desc {
  963. display: block;
  964. margin-top: 6rpx;
  965. color: #8a95a6;
  966. font-size: 20rpx;
  967. line-height: 1.25;
  968. text-align: center;
  969. }
  970. .preference-block {
  971. margin-top: 24rpx;
  972. }
  973. .muted,
  974. .special-tip {
  975. color: #8a95a6;
  976. font-size: 23rpx;
  977. font-weight: 400;
  978. }
  979. .chip-row {
  980. margin-top: 16rpx;
  981. display: flex;
  982. flex-wrap: wrap;
  983. gap: 14rpx;
  984. }
  985. .chip {
  986. height: 58rpx;
  987. min-width: 104rpx;
  988. padding: 0 24rpx;
  989. border-radius: 30rpx;
  990. color: #596579;
  991. font-size: 25rpx;
  992. font-weight: 700;
  993. line-height: 58rpx;
  994. text-align: center;
  995. background: #f6f9fe;
  996. border: 2rpx solid transparent;
  997. box-sizing: border-box;
  998. }
  999. .chip.active {
  1000. color: #0b8cff;
  1001. background: #eff8ff;
  1002. border-color: #0b8cff;
  1003. }
  1004. .special-row {
  1005. margin-top: 26rpx;
  1006. padding: 20rpx 22rpx;
  1007. border-radius: 20rpx;
  1008. background: #f7f9fd;
  1009. display: flex;
  1010. align-items: center;
  1011. }
  1012. .extra-row {
  1013. margin-top: 20rpx;
  1014. padding: 20rpx 22rpx 18rpx;
  1015. border-radius: 20rpx;
  1016. background: #f7f9fd;
  1017. }
  1018. .extra-head {
  1019. display: flex;
  1020. align-items: flex-start;
  1021. justify-content: space-between;
  1022. gap: 20rpx;
  1023. }
  1024. .extra-count {
  1025. color: #9aa5b5;
  1026. font-size: 22rpx;
  1027. line-height: 1.35;
  1028. flex-shrink: 0;
  1029. }
  1030. .special-tip {
  1031. display: block;
  1032. margin-top: 8rpx;
  1033. }
  1034. .extra-textarea {
  1035. width: 100%;
  1036. min-height: 116rpx;
  1037. margin-top: 16rpx;
  1038. color: #172033;
  1039. font-size: 26rpx;
  1040. line-height: 1.55;
  1041. box-sizing: border-box;
  1042. }
  1043. .special-input {
  1044. margin-left: auto;
  1045. width: 128rpx;
  1046. height: 66rpx;
  1047. border-radius: 18rpx;
  1048. color: #172033;
  1049. font-size: 32rpx;
  1050. font-weight: 800;
  1051. text-align: center;
  1052. background: #fff;
  1053. }
  1054. .promise {
  1055. padding: 24rpx 26rpx;
  1056. }
  1057. .promise-item {
  1058. display: flex;
  1059. align-items: center;
  1060. color: #596579;
  1061. font-size: 24rpx;
  1062. line-height: 1.5;
  1063. & + & {
  1064. margin-top: 16rpx;
  1065. }
  1066. }
  1067. .promise-mark {
  1068. width: 48rpx;
  1069. height: 34rpx;
  1070. margin-right: 14rpx;
  1071. border-radius: 18rpx;
  1072. color: #0b8cff;
  1073. font-size: 19rpx;
  1074. font-weight: 800;
  1075. line-height: 34rpx;
  1076. text-align: center;
  1077. background: #eef6ff;
  1078. flex-shrink: 0;
  1079. }
  1080. .bottom-bar {
  1081. position: fixed;
  1082. left: 0;
  1083. right: 0;
  1084. bottom: 0;
  1085. z-index: 50;
  1086. padding: 18rpx 28rpx calc(18rpx + env(safe-area-inset-bottom));
  1087. background: rgba(255, 255, 255, 0.96);
  1088. box-shadow: 0 -8rpx 24rpx rgba(35, 55, 90, 0.08);
  1089. }
  1090. .balance {
  1091. color: #7b8797;
  1092. font-size: 24rpx;
  1093. margin-bottom: 14rpx;
  1094. display: flex;
  1095. align-items: center;
  1096. justify-content: space-between;
  1097. gap: 20rpx;
  1098. line-height: 1.35;
  1099. }
  1100. .balance-copy {
  1101. min-width: 0;
  1102. flex: 1;
  1103. }
  1104. .recharge-link {
  1105. flex-shrink: 0;
  1106. color: #0b8cff;
  1107. font-size: 24rpx;
  1108. font-weight: 800;
  1109. }
  1110. .buttons {
  1111. display: flex;
  1112. gap: 22rpx;
  1113. }
  1114. .history-btn,
  1115. .submit-btn {
  1116. height: 84rpx;
  1117. border-radius: 44rpx;
  1118. font-size: 30rpx;
  1119. margin: 0;
  1120. display: flex;
  1121. align-items: center;
  1122. justify-content: center;
  1123. }
  1124. .history-btn {
  1125. width: 210rpx;
  1126. color: #0b8cff;
  1127. background: #eef6ff;
  1128. }
  1129. .submit-btn {
  1130. flex: 1;
  1131. color: #fff;
  1132. font-weight: 700;
  1133. background: #0b8cff;
  1134. box-shadow: 0 12rpx 24rpx rgba(11, 140, 255, 0.2);
  1135. }
  1136. .submit-icon {
  1137. width: 24rpx;
  1138. height: 24rpx;
  1139. margin-right: 8rpx;
  1140. flex-shrink: 0;
  1141. }
  1142. .submit-btn.disabled {
  1143. opacity: 0.72;
  1144. }
  1145. </style>