index.vue 8.6 KB

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