|
|
@@ -0,0 +1,346 @@
|
|
|
+<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>
|
|
|
+ <input
|
|
|
+ class="input-field"
|
|
|
+ type="digit"
|
|
|
+ maxlength="4"
|
|
|
+ v-model="birthYear"
|
|
|
+ placeholder="例如 1990"
|
|
|
+ />
|
|
|
+ <view class="row-inputs">
|
|
|
+ <view class="half-input">
|
|
|
+ <text class="label">月份</text>
|
|
|
+ <picker class="picker-wrap" mode="selector" :range="monthRange" @change="onMonthChange">
|
|
|
+ <view class="input-field picker-field">
|
|
|
+ <text :class="birthMonth ? 'picker-val' : 'picker-ph'">{{ birthMonth || '选择' }}</text>
|
|
|
+ <text class="picker-arrow">▼</text>
|
|
|
+ </view>
|
|
|
+ </picker>
|
|
|
+ </view>
|
|
|
+ <view class="half-input">
|
|
|
+ <text class="label">日期</text>
|
|
|
+ <picker class="picker-wrap" mode="selector" :range="dayRange" @change="onDayChange">
|
|
|
+ <view class="input-field picker-field">
|
|
|
+ <text :class="birthDay ? 'picker-val' : 'picker-ph'">{{ birthDay || '选择' }}</text>
|
|
|
+ <text class="picker-arrow">▼</text>
|
|
|
+ </view>
|
|
|
+ </picker>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <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>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref, computed } from 'vue'
|
|
|
+import { useChartStore } from '@/stores/chart'
|
|
|
+import { consultationApi } from '@/utils/api'
|
|
|
+
|
|
|
+const store = useChartStore()
|
|
|
+const name = ref('')
|
|
|
+const birthYear = ref('')
|
|
|
+const birthMonth = ref('')
|
|
|
+const birthDay = ref('')
|
|
|
+const questions = ref('')
|
|
|
+const confirming = ref(false) // prevent double-click
|
|
|
+const quickQuestions = ['职业发展', '财务分析', '感情运势', '健康建议']
|
|
|
+
|
|
|
+function selectQuick(q) {
|
|
|
+ questions.value = questions.value === q ? '' : q
|
|
|
+}
|
|
|
+
|
|
|
+const monthRange = Array.from({ length: 12 }, (_, i) => String(i + 1))
|
|
|
+const dayRange = Array.from({ length: 31 }, (_, i) => String(i + 1))
|
|
|
+
|
|
|
+function onMonthChange(e) {
|
|
|
+ birthMonth.value = monthRange[e.detail.value]
|
|
|
+}
|
|
|
+function onDayChange(e) {
|
|
|
+ birthDay.value = dayRange[e.detail.value]
|
|
|
+}
|
|
|
+
|
|
|
+function formatBirthday(year, month, day) {
|
|
|
+ return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`
|
|
|
+}
|
|
|
+
|
|
|
+async function onAnalyze() {
|
|
|
+ if (confirming.value) return
|
|
|
+ const year = parseInt(birthYear.value, 10)
|
|
|
+ const month = parseInt(birthMonth.value, 10)
|
|
|
+ const day = parseInt(birthDay.value, 10)
|
|
|
+
|
|
|
+ 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 birthday = formatBirthday(year, month, day)
|
|
|
+ confirming.value = true
|
|
|
+
|
|
|
+ try {
|
|
|
+ // Check if this birthday has existing records — US-1.1 §7-9
|
|
|
+ const isNew = await checkNewBirthday(birthday)
|
|
|
+ if (!isNew) {
|
|
|
+ // Existing birthday — proceed directly (US-1.1 §7-8: returns existing record)
|
|
|
+ confirming.value = false
|
|
|
+ await proceedConsultation(year, month, day, birthday)
|
|
|
+ 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, birthday)
|
|
|
+ }
|
|
|
+ // 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 })
|
|
|
+}
|
|
|
+</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;
|
|
|
+}
|
|
|
+
|
|
|
+.row-inputs {
|
|
|
+ display: flex;
|
|
|
+ gap: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.half-input {
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+
|
|
|
+/* Picker */
|
|
|
+.picker-wrap {
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+.picker-field {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+}
|
|
|
+.picker-val {
|
|
|
+ color: rgba(255, 255, 255, 0.85);
|
|
|
+ font-size: 16px;
|
|
|
+}
|
|
|
+.picker-ph {
|
|
|
+ color: rgba(255, 255, 255, 0.25);
|
|
|
+ font-size: 16px;
|
|
|
+}
|
|
|
+.picker-arrow {
|
|
|
+ font-size: 10px;
|
|
|
+ color: rgba(255, 255, 255, 0.3);
|
|
|
+ margin-left: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.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;
|
|
|
+}
|
|
|
+</style>
|