index.vue 8.6 KB

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