index.vue 8.4 KB

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