| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351 |
- <template>
- <view class="page-index">
- <!-- Hero header -->
- <view class="hero">
- <text class="hero-title">数字能量</text>
- <text class="hero-sub">输入出生信息,解锁专属命盘</text>
- </view>
- <!-- Birth input section -->
- <view class="input-section">
- <view class="input-card">
- <text class="label">姓名(选填)</text>
- <input
- class="input-field"
- v-model="name"
- placeholder="请输入姓名"
- />
- <text class="label mt-16">出生日期</text>
- <picker class="date-picker" mode="date" :value="birthday" :start="minDate" :end="maxDate" @change="onBirthdayChange">
- <view class="input-field date-field">
- <text class="date-val">{{ birthday }}</text>
- <text class="picker-arrow">▼</text>
- </view>
- </picker>
- <text class="label mt-16">想了解的问题(选填)</text>
- <view class="question-wrap">
- <textarea
- class="question-input"
- v-model="questions"
- maxlength="100"
- placeholder="例如:我的财运如何?最近适合换工作吗?感情方面有什么建议?"
- />
- <text class="char-counter">{{ questions.length }}/100</text>
- </view>
- <view class="quick-chips">
- <view
- v-for="q in quickQuestions"
- :key="q"
- class="chip"
- :class="{ active: questions === q }"
- @click="selectQuick(q)"
- >
- <text>{{ q }}</text>
- </view>
- </view>
- <button class="analyze-btn" @click="onAnalyze">开始咨询</button>
- <!-- US-4.1: Upgrade guide for free users -->
- <view v-if="!userStore.isVipActive" class="upgrade-banner" @click="goSubscribe">
- <text class="upgrade-banner-text">解锁无限解读 · 开通会员仅 ¥131/年 ›</text>
- </view>
- </view>
- </view>
- </view>
- </template>
- <script setup>
- import { ref, computed } from 'vue'
- import { useChartStore } from '@/stores/chart'
- import { useUserStore } from '@/stores/user'
- import { consultationApi } from '@/utils/api'
- const store = useChartStore()
- const userStore = useUserStore()
- const name = ref('')
- const birthday = ref('1990-01-01')
- const questions = ref('')
- const confirming = ref(false) // prevent double-click
- const quickQuestions = ['职业发展', '财务分析', '感情运势', '健康建议']
- // Capture referral code from shared link
- onLoad((options) => {
- if (options.ref) {
- uni.setStorageSync('pending_referrer', options.ref.toUpperCase())
- }
- })
- // Date picker range: 1900-01-01 ~ today (US-1.2 §6)
- const minDate = '1900-01-01'
- const maxDate = computed(() => {
- const d = new Date()
- const y = d.getFullYear()
- const m = String(d.getMonth() + 1).padStart(2, '0')
- const day = String(d.getDate()).padStart(2, '0')
- return `${y}-${m}-${day}`
- })
- function selectQuick(q) {
- questions.value = questions.value === q ? '' : q
- }
- function onBirthdayChange(e) {
- birthday.value = e.detail.value
- }
- function parseBirthday(str) {
- const parts = str.split('-')
- return {
- year: parseInt(parts[0], 10),
- month: parseInt(parts[1], 10),
- day: parseInt(parts[2], 10)
- }
- }
- async function onAnalyze() {
- if (confirming.value) return
- const { year, month, day } = parseBirthday(birthday.value)
- if (!year || !month || !day) {
- uni.showToast({ title: '请选择出生日期', icon: 'none' })
- return
- }
- if (year < 1900 || year > new Date().getFullYear()) {
- uni.showToast({ title: '请输入正确的出生年份', icon: 'none' })
- return
- }
- if (month < 1 || month > 12) {
- uni.showToast({ title: '请输入正确的月份(1-12)', icon: 'none' })
- return
- }
- if (day < 1 || day > 31) {
- uni.showToast({ title: '请输入正确的日期(1-31)', icon: 'none' })
- return
- }
- const birthdayStr = birthday.value
- confirming.value = true
- try {
- // Check if this birthday has existing records — US-1.1 §7-9
- const isNew = await checkNewBirthday(birthdayStr)
- if (!isNew) {
- // Existing birthday — proceed directly (US-1.1 §7-8: returns existing record)
- confirming.value = false
- await proceedConsultation(year, month, day, birthdayStr)
- return
- }
- } catch (_e) {
- // If check fails (e.g. network), allow proceed without confirm
- }
- // New birthday — show confirm dialog (US-1.1 §9-11)
- uni.showModal({
- title: '确认咨询',
- content: '此生日将开启全新的命盘咨询,确认吗?',
- confirmText: '确认开启',
- cancelText: '再想想',
- success: async (res) => {
- confirming.value = false
- if (res.confirm) {
- await proceedConsultation(year, month, day, birthdayStr)
- }
- // res.cancel → stay on page, do nothing (US-1.1 §11)
- },
- fail: () => { confirming.value = false }
- })
- }
- async function checkNewBirthday(birthday) {
- try {
- const result = await consultationApi.check({ birthday })
- return !result.exists
- } catch {
- return false
- }
- }
- async function proceedConsultation(year, month, day, birthday) {
- const result = await store.startConsultation(name.value, birthday, questions.value)
- // Parse backend chart data into birth props for TriangleChart
- if (store.currentChart) {
- store.currentBirthYear = year
- store.currentBirthMonth = month
- store.currentBirthDay = day
- }
- // Navigate with recordId for URL shareability (US-1.1 §14)
- const recordId = store.currentRecordId || (result.record && result.record.id)
- const url = recordId
- ? `/pages/chart/index?recordId=${recordId}`
- : '/pages/chart/index'
- uni.navigateTo({ url })
- }
- function goSubscribe() {
- uni.navigateTo({ url: '/pages/subscribe/index' })
- }
- </script>
- <style scoped lang="scss">
- .page-index {
- min-height: 100vh;
- padding-bottom: 40px;
- background: linear-gradient(180deg, #0c0a1a 0%, #120d28 60%, #0f0c1e 100%);
- }
- /* Hero */
- .hero {
- padding: 40px 20px 20px;
- text-align: center;
- }
- .hero-title {
- font-size: 32px;
- font-weight: 700;
- color: #fbbf24;
- text-shadow: 0 0 30px rgba(251, 191, 36, 0.2);
- letter-spacing: 4px;
- }
- .hero-sub {
- display: block;
- margin-top: 10px;
- font-size: 14px;
- color: rgba(255, 255, 255, 0.4);
- letter-spacing: 2px;
- }
- /* Input card */
- .input-section {
- padding: 0 16px;
- }
- .input-card {
- background: rgba(255, 255, 255, 0.03);
- border: 1px solid rgba(255, 255, 255, 0.06);
- border-radius: 16px;
- padding: 24px 20px;
- backdrop-filter: blur(10px);
- }
- .label {
- font-size: 13px;
- color: rgba(255, 255, 255, 0.5);
- margin-bottom: 8px;
- display: block;
- letter-spacing: 1px;
- }
- .input-field {
- height: 44px;
- border: 1px solid rgba(255, 255, 255, 0.08);
- border-radius: 10px;
- padding: 0 14px;
- font-size: 16px;
- width: 100%;
- color: rgba(255, 255, 255, 0.85);
- background: rgba(255, 255, 255, 0.04);
- box-sizing: border-box;
- }
- .mt-16 {
- margin-top: 16px;
- display: block;
- }
- /* Date picker */
- .date-picker {
- width: 100%;
- display: block;
- }
- .date-field {
- display: flex;
- align-items: center;
- justify-content: space-between;
- }
- .date-val {
- font-size: 18px;
- color: rgba(255, 255, 255, 0.85);
- letter-spacing: 2px;
- }
- .picker-arrow {
- font-size: 10px;
- color: rgba(255, 255, 255, 0.3);
- }
- .question-wrap {
- position: relative;
- }
- .question-input {
- width: 100%;
- min-height: 80px;
- border: 1px solid rgba(255, 255, 255, 0.08);
- border-radius: 10px;
- padding: 12px 14px 28px;
- font-size: 14px;
- line-height: 1.5;
- box-sizing: border-box;
- color: rgba(255, 255, 255, 0.85);
- background: rgba(255, 255, 255, 0.04);
- }
- .char-counter {
- position: absolute;
- right: 10px;
- bottom: 8px;
- font-size: 11px;
- color: rgba(255, 255, 255, 0.3);
- }
- .quick-chips {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- margin: 12px 0 0;
- }
- .chip {
- background: rgba(255, 255, 255, 0.04);
- border: 1px solid rgba(255, 255, 255, 0.08);
- border-radius: 18px;
- padding: 6px 18px;
- font-size: 13px;
- color: rgba(255, 255, 255, 0.6);
- }
- .chip.active {
- background: rgba(251, 191, 36, 0.12);
- border-color: rgba(251, 191, 36, 0.25);
- color: #fbbf24;
- }
- .analyze-btn {
- width: 100%;
- height: 46px;
- background: linear-gradient(135deg, #f59e0b, #d97706);
- color: #fff;
- border: none;
- border-radius: 12px;
- font-size: 16px;
- font-weight: 600;
- line-height: 46px;
- letter-spacing: 2px;
- margin-top: 4px;
- }
- /* US-4.1: Upgrade guide banner */
- .upgrade-banner {
- margin-top: 16px;
- text-align: center;
- padding: 12px;
- background: rgba(251, 191, 36, 0.06);
- border: 1px solid rgba(251, 191, 36, 0.1);
- border-radius: 10px;
- }
- .upgrade-banner-text {
- font-size: 13px;
- color: #fbbf24;
- font-weight: 500;
- }
- </style>
|