index.vue 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  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 { onLoad } from '@dcloudio/uni-app'
  57. import { useChartStore } from '@/stores/chart'
  58. import { useUserStore } from '@/stores/user'
  59. import { consultationApi } from '@/utils/api'
  60. const store = useChartStore()
  61. const userStore = useUserStore()
  62. const name = ref('')
  63. const birthday = ref('1990-01-01')
  64. const questions = ref('')
  65. const confirming = ref(false) // prevent double-click
  66. const quickQuestions = ['职业发展', '财务分析', '感情运势', '健康建议']
  67. // Capture referral code from shared link
  68. onLoad((options) => {
  69. if (options.ref) {
  70. uni.setStorageSync('pending_referrer', options.ref.toUpperCase())
  71. }
  72. })
  73. // Date picker range: 1900-01-01 ~ today (US-1.2 §6)
  74. const minDate = '1900-01-01'
  75. const maxDate = computed(() => {
  76. const d = new Date()
  77. const y = d.getFullYear()
  78. const m = String(d.getMonth() + 1).padStart(2, '0')
  79. const day = String(d.getDate()).padStart(2, '0')
  80. return `${y}-${m}-${day}`
  81. })
  82. function selectQuick(q) {
  83. questions.value = questions.value === q ? '' : q
  84. }
  85. function onBirthdayChange(e) {
  86. birthday.value = e.detail.value
  87. }
  88. function parseBirthday(str) {
  89. const parts = str.split('-')
  90. return {
  91. year: parseInt(parts[0], 10),
  92. month: parseInt(parts[1], 10),
  93. day: parseInt(parts[2], 10)
  94. }
  95. }
  96. async function onAnalyze() {
  97. if (confirming.value) return
  98. const { year, month, day } = parseBirthday(birthday.value)
  99. if (!year || !month || !day) {
  100. uni.showToast({ title: '请选择出生日期', icon: 'none' })
  101. return
  102. }
  103. if (year < 1900 || year > new Date().getFullYear()) {
  104. uni.showToast({ title: '请输入正确的出生年份', icon: 'none' })
  105. return
  106. }
  107. if (month < 1 || month > 12) {
  108. uni.showToast({ title: '请输入正确的月份(1-12)', icon: 'none' })
  109. return
  110. }
  111. if (day < 1 || day > 31) {
  112. uni.showToast({ title: '请输入正确的日期(1-31)', icon: 'none' })
  113. return
  114. }
  115. const birthdayStr = birthday.value
  116. confirming.value = true
  117. try {
  118. // Check if this birthday has existing records — US-1.1 §7-9
  119. const isNew = await checkNewBirthday(birthdayStr)
  120. if (!isNew) {
  121. // Existing birthday — proceed directly (US-1.1 §7-8: returns existing record)
  122. confirming.value = false
  123. await proceedConsultation(year, month, day, birthdayStr)
  124. return
  125. }
  126. } catch (_e) {
  127. // If check fails (e.g. network), allow proceed without confirm
  128. }
  129. // New birthday — show confirm dialog (US-1.1 §9-11)
  130. uni.showModal({
  131. title: '确认咨询',
  132. content: '此生日将开启全新的命盘咨询,确认吗?',
  133. confirmText: '确认开启',
  134. cancelText: '再想想',
  135. success: async (res) => {
  136. confirming.value = false
  137. if (res.confirm) {
  138. await proceedConsultation(year, month, day, birthdayStr)
  139. }
  140. // res.cancel → stay on page, do nothing (US-1.1 §11)
  141. },
  142. fail: () => { confirming.value = false }
  143. })
  144. }
  145. async function checkNewBirthday(birthday) {
  146. try {
  147. const result = await consultationApi.check({ birthday })
  148. return !result.exists
  149. } catch {
  150. return false
  151. }
  152. }
  153. async function proceedConsultation(year, month, day, birthday) {
  154. const result = await store.startConsultation(name.value, birthday, questions.value)
  155. // Parse backend chart data into birth props for TriangleChart
  156. if (store.currentChart) {
  157. store.currentBirthYear = year
  158. store.currentBirthMonth = month
  159. store.currentBirthDay = day
  160. }
  161. // Navigate with recordId for URL shareability (US-1.1 §14)
  162. const recordId = store.currentRecordId || (result.record && result.record.id)
  163. const url = recordId
  164. ? `/pages/chart/index?recordId=${recordId}`
  165. : '/pages/chart/index'
  166. uni.navigateTo({ url })
  167. }
  168. function goSubscribe() {
  169. uni.navigateTo({ url: '/pages/subscribe/index' })
  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. /* Date picker */
  231. .date-picker {
  232. width: 100%;
  233. display: block;
  234. }
  235. .date-field {
  236. display: flex;
  237. align-items: center;
  238. justify-content: space-between;
  239. }
  240. .date-val {
  241. font-size: 18px;
  242. color: rgba(255, 255, 255, 0.85);
  243. letter-spacing: 2px;
  244. }
  245. .picker-arrow {
  246. font-size: 10px;
  247. color: rgba(255, 255, 255, 0.3);
  248. }
  249. .question-wrap {
  250. position: relative;
  251. }
  252. .question-input {
  253. width: 100%;
  254. min-height: 80px;
  255. border: 1px solid rgba(255, 255, 255, 0.08);
  256. border-radius: 10px;
  257. padding: 12px 14px 28px;
  258. font-size: 14px;
  259. line-height: 1.5;
  260. box-sizing: border-box;
  261. color: rgba(255, 255, 255, 0.85);
  262. background: rgba(255, 255, 255, 0.04);
  263. }
  264. .char-counter {
  265. position: absolute;
  266. right: 10px;
  267. bottom: 8px;
  268. font-size: 11px;
  269. color: rgba(255, 255, 255, 0.3);
  270. }
  271. .quick-chips {
  272. display: flex;
  273. flex-wrap: wrap;
  274. gap: 8px;
  275. margin: 12px 0 0;
  276. }
  277. .chip {
  278. background: rgba(255, 255, 255, 0.04);
  279. border: 1px solid rgba(255, 255, 255, 0.08);
  280. border-radius: 18px;
  281. padding: 6px 18px;
  282. font-size: 13px;
  283. color: rgba(255, 255, 255, 0.6);
  284. }
  285. .chip.active {
  286. background: rgba(251, 191, 36, 0.12);
  287. border-color: rgba(251, 191, 36, 0.25);
  288. color: #fbbf24;
  289. }
  290. .analyze-btn {
  291. width: 100%;
  292. height: 46px;
  293. background: linear-gradient(135deg, #f59e0b, #d97706);
  294. color: #fff;
  295. border: none;
  296. border-radius: 12px;
  297. font-size: 16px;
  298. font-weight: 600;
  299. line-height: 46px;
  300. letter-spacing: 2px;
  301. margin-top: 4px;
  302. }
  303. /* US-4.1: Upgrade guide banner */
  304. .upgrade-banner {
  305. margin-top: 16px;
  306. text-align: center;
  307. padding: 12px;
  308. background: rgba(251, 191, 36, 0.06);
  309. border: 1px solid rgba(251, 191, 36, 0.1);
  310. border-radius: 10px;
  311. }
  312. .upgrade-banner-text {
  313. font-size: 13px;
  314. color: #fbbf24;
  315. font-weight: 500;
  316. }
  317. </style>