index.vue 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. <template>
  2. <view class="page-index">
  3. <!-- Hero header -->
  4. <view class="hero">
  5. <text class="hero-title">数字能量</text>
  6. <text class="hero-sub">输入出生信息,解锁专属命盘</text>
  7. </view>
  8. <!-- Birth input section -->
  9. <view class="input-section">
  10. <view class="input-card">
  11. <text class="label">姓名(选填)</text>
  12. <input
  13. class="input-field"
  14. v-model="name"
  15. placeholder="请输入姓名"
  16. />
  17. <view class="row-inputs">
  18. <view class="third-input">
  19. <text class="label">年份</text>
  20. <input
  21. class="input-field"
  22. type="digit"
  23. maxlength="4"
  24. v-model="birthYear"
  25. placeholder="1990"
  26. />
  27. </view>
  28. <view class="third-input">
  29. <text class="label">月份</text>
  30. <picker class="picker-wrap" mode="selector" :range="monthRange" @change="onMonthChange">
  31. <view class="input-field picker-field">
  32. <text :class="birthMonth ? 'picker-val' : 'picker-ph'">{{ birthMonth || '选择' }}</text>
  33. <text class="picker-arrow">▼</text>
  34. </view>
  35. </picker>
  36. </view>
  37. <view class="third-input">
  38. <text class="label">日期</text>
  39. <picker class="picker-wrap" mode="selector" :range="dayRange" @change="onDayChange">
  40. <view class="input-field picker-field">
  41. <text :class="birthDay ? 'picker-val' : 'picker-ph'">{{ birthDay || '选择' }}</text>
  42. <text class="picker-arrow">▼</text>
  43. </view>
  44. </picker>
  45. </view>
  46. </view>
  47. <text class="label mt-16">想了解的问题(选填)</text>
  48. <view class="question-wrap">
  49. <textarea
  50. class="question-input"
  51. v-model="questions"
  52. maxlength="100"
  53. placeholder="例如:我的财运如何?最近适合换工作吗?感情方面有什么建议?"
  54. />
  55. <text class="char-counter">{{ questions.length }}/100</text>
  56. </view>
  57. <view class="quick-chips">
  58. <view
  59. v-for="q in quickQuestions"
  60. :key="q"
  61. class="chip"
  62. :class="{ active: questions === q }"
  63. @click="selectQuick(q)"
  64. >
  65. <text>{{ q }}</text>
  66. </view>
  67. </view>
  68. <button class="analyze-btn" @click="onAnalyze">开始咨询</button>
  69. </view>
  70. </view>
  71. </view>
  72. </template>
  73. <script setup>
  74. import { ref, computed } from 'vue'
  75. import { useChartStore } from '@/stores/chart'
  76. import { consultationApi } from '@/utils/api'
  77. const store = useChartStore()
  78. const name = ref('')
  79. const birthYear = ref('')
  80. const birthMonth = ref('')
  81. const birthDay = ref('')
  82. const questions = ref('')
  83. const confirming = ref(false) // prevent double-click
  84. const quickQuestions = ['职业发展', '财务分析', '感情运势', '健康建议']
  85. function selectQuick(q) {
  86. questions.value = questions.value === q ? '' : q
  87. }
  88. const monthRange = Array.from({ length: 12 }, (_, i) => String(i + 1))
  89. const dayRange = Array.from({ length: 31 }, (_, i) => String(i + 1))
  90. function onMonthChange(e) {
  91. birthMonth.value = monthRange[e.detail.value]
  92. }
  93. function onDayChange(e) {
  94. birthDay.value = dayRange[e.detail.value]
  95. }
  96. function formatBirthday(year, month, day) {
  97. return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`
  98. }
  99. async function onAnalyze() {
  100. if (confirming.value) return
  101. const year = parseInt(birthYear.value, 10)
  102. const month = parseInt(birthMonth.value, 10)
  103. const day = parseInt(birthDay.value, 10)
  104. if (!year || !month || !day) {
  105. uni.showToast({ title: '请输入完整的出生年月日', icon: 'none' })
  106. return
  107. }
  108. if (year < 1900 || year > new Date().getFullYear()) {
  109. uni.showToast({ title: '请输入正确的出生年份', icon: 'none' })
  110. return
  111. }
  112. if (month < 1 || month > 12) {
  113. uni.showToast({ title: '请输入正确的月份(1-12)', icon: 'none' })
  114. return
  115. }
  116. if (day < 1 || day > 31) {
  117. uni.showToast({ title: '请输入正确的日期(1-31)', icon: 'none' })
  118. return
  119. }
  120. const birthday = formatBirthday(year, month, day)
  121. confirming.value = true
  122. try {
  123. // Check if this birthday has existing records — US-1.1 §7-9
  124. const isNew = await checkNewBirthday(birthday)
  125. if (!isNew) {
  126. // Existing birthday — proceed directly (US-1.1 §7-8: returns existing record)
  127. confirming.value = false
  128. await proceedConsultation(year, month, day, birthday)
  129. return
  130. }
  131. } catch (_e) {
  132. // If check fails (e.g. network), allow proceed without confirm
  133. }
  134. // New birthday — show confirm dialog (US-1.1 §9-11)
  135. uni.showModal({
  136. title: '确认咨询',
  137. content: '此生日将开启全新的命盘咨询,确认吗?',
  138. confirmText: '确认开启',
  139. cancelText: '再想想',
  140. success: async (res) => {
  141. confirming.value = false
  142. if (res.confirm) {
  143. await proceedConsultation(year, month, day, birthday)
  144. }
  145. // res.cancel → stay on page, do nothing (US-1.1 §11)
  146. },
  147. fail: () => { confirming.value = false }
  148. })
  149. }
  150. async function checkNewBirthday(birthday) {
  151. try {
  152. const result = await consultationApi.check({ birthday })
  153. return !result.exists
  154. } catch {
  155. return false
  156. }
  157. }
  158. async function proceedConsultation(year, month, day, birthday) {
  159. const result = await store.startConsultation(name.value, birthday, questions.value)
  160. // Parse backend chart data into birth props for TriangleChart
  161. if (store.currentChart) {
  162. store.currentBirthYear = year
  163. store.currentBirthMonth = month
  164. store.currentBirthDay = day
  165. }
  166. // Navigate with recordId for URL shareability (US-1.1 §14)
  167. const recordId = store.currentRecordId || (result.record && result.record.id)
  168. const url = recordId
  169. ? `/pages/chart/index?recordId=${recordId}`
  170. : '/pages/chart/index'
  171. uni.navigateTo({ url })
  172. }
  173. </script>
  174. <style scoped lang="scss">
  175. .page-index {
  176. min-height: 100vh;
  177. padding-bottom: 40px;
  178. background: linear-gradient(180deg, #0c0a1a 0%, #120d28 60%, #0f0c1e 100%);
  179. }
  180. /* Hero */
  181. .hero {
  182. padding: 40px 20px 20px;
  183. text-align: center;
  184. }
  185. .hero-title {
  186. font-size: 32px;
  187. font-weight: 700;
  188. color: #fbbf24;
  189. text-shadow: 0 0 30px rgba(251, 191, 36, 0.2);
  190. letter-spacing: 4px;
  191. }
  192. .hero-sub {
  193. display: block;
  194. margin-top: 10px;
  195. font-size: 14px;
  196. color: rgba(255, 255, 255, 0.4);
  197. letter-spacing: 2px;
  198. }
  199. /* Input card */
  200. .input-section {
  201. padding: 0 16px;
  202. }
  203. .input-card {
  204. background: rgba(255, 255, 255, 0.03);
  205. border: 1px solid rgba(255, 255, 255, 0.06);
  206. border-radius: 16px;
  207. padding: 24px 20px;
  208. backdrop-filter: blur(10px);
  209. }
  210. .label {
  211. font-size: 13px;
  212. color: rgba(255, 255, 255, 0.5);
  213. margin-bottom: 8px;
  214. display: block;
  215. letter-spacing: 1px;
  216. }
  217. .input-field {
  218. height: 44px;
  219. border: 1px solid rgba(255, 255, 255, 0.08);
  220. border-radius: 10px;
  221. padding: 0 14px;
  222. font-size: 16px;
  223. width: 100%;
  224. color: rgba(255, 255, 255, 0.85);
  225. background: rgba(255, 255, 255, 0.04);
  226. box-sizing: border-box;
  227. }
  228. .mt-16 {
  229. margin-top: 16px;
  230. display: block;
  231. }
  232. .row-inputs {
  233. display: flex;
  234. gap: 10rpx;
  235. }
  236. .third-input {
  237. flex: 1;
  238. min-width: 0;
  239. }
  240. /* Picker */
  241. .picker-wrap {
  242. width: 100%;
  243. }
  244. .picker-field {
  245. display: flex;
  246. align-items: center;
  247. justify-content: space-between;
  248. }
  249. .picker-val {
  250. color: rgba(255, 255, 255, 0.85);
  251. font-size: 16px;
  252. }
  253. .picker-ph {
  254. color: rgba(255, 255, 255, 0.25);
  255. font-size: 16px;
  256. }
  257. .picker-arrow {
  258. font-size: 10px;
  259. color: rgba(255, 255, 255, 0.3);
  260. margin-left: 4px;
  261. }
  262. .question-wrap {
  263. position: relative;
  264. }
  265. .question-input {
  266. width: 100%;
  267. min-height: 80px;
  268. border: 1px solid rgba(255, 255, 255, 0.08);
  269. border-radius: 10px;
  270. padding: 12px 14px 28px;
  271. font-size: 14px;
  272. line-height: 1.5;
  273. box-sizing: border-box;
  274. color: rgba(255, 255, 255, 0.85);
  275. background: rgba(255, 255, 255, 0.04);
  276. }
  277. .char-counter {
  278. position: absolute;
  279. right: 10px;
  280. bottom: 8px;
  281. font-size: 11px;
  282. color: rgba(255, 255, 255, 0.3);
  283. }
  284. .quick-chips {
  285. display: flex;
  286. flex-wrap: wrap;
  287. gap: 8px;
  288. margin: 12px 0 0;
  289. }
  290. .chip {
  291. background: rgba(255, 255, 255, 0.04);
  292. border: 1px solid rgba(255, 255, 255, 0.08);
  293. border-radius: 18px;
  294. padding: 6px 18px;
  295. font-size: 13px;
  296. color: rgba(255, 255, 255, 0.6);
  297. }
  298. .chip.active {
  299. background: rgba(251, 191, 36, 0.12);
  300. border-color: rgba(251, 191, 36, 0.25);
  301. color: #fbbf24;
  302. }
  303. .analyze-btn {
  304. width: 100%;
  305. height: 46px;
  306. background: linear-gradient(135deg, #f59e0b, #d97706);
  307. color: #fff;
  308. border: none;
  309. border-radius: 12px;
  310. font-size: 16px;
  311. font-weight: 600;
  312. line-height: 46px;
  313. letter-spacing: 2px;
  314. margin-top: 4px;
  315. }
  316. </style>