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.
 
 
 

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