index.vue 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. <template>
  2. <view class="page-chart">
  3. <!-- Triangle chart -->
  4. <view class="chart-section">
  5. <TriangleChart
  6. :chartData="store.currentChart"
  7. :birthYear="store.currentBirthYear"
  8. :birthMonth="store.currentBirthMonth"
  9. :birthDay="store.currentBirthDay"
  10. />
  11. </view>
  12. <!-- AI Chat -->
  13. <view class="chat-section">
  14. <view class="section-title">
  15. <text class="title-icon">◈</text>
  16. AI 解读
  17. <text v-if="!userStore.isVipActive && maxChats > 0" class="quota-badge">
  18. 今日 {{ usedChats }}/{{ maxChats }} 轮
  19. </text>
  20. <text v-if="userStore.isVipActive" class="quota-badge vip">无限次</text>
  21. </view>
  22. <scroll-view class="chat-box" scroll-y :scroll-top="scrollTop">
  23. <view v-for="(msg, idx) in messages" :key="idx" class="msg-row" :class="msg.role">
  24. <view class="msg-bubble">
  25. <text>{{ msg.content }}</text>
  26. </view>
  27. </view>
  28. </scroll-view>
  29. <!-- Quota exceeded prompt for non-VIP -->
  30. <view v-if="quotaExceeded" class="upgrade-prompt">
  31. <text class="upgrade-text">今日 AI 解读次数已用完</text>
  32. <text class="upgrade-desc">开通能量师会员享无限次解读</text>
  33. <button class="upgrade-btn" @click="goSubscribe">立即开通 →</button>
  34. </view>
  35. <!-- Input area (disabled when quota exceeded) -->
  36. <template v-if="!quotaExceeded">
  37. <view class="input-bar">
  38. <input
  39. class="chat-input"
  40. v-model="chatInput"
  41. placeholder="输入您的问题..."
  42. @confirm="sendQuestion(chatInput)"
  43. :disabled="loading"
  44. />
  45. <button class="send-btn" @click="sendQuestion(chatInput)" :disabled="loading">
  46. {{ loading ? '···' : '发送' }}
  47. </button>
  48. </view>
  49. </template>
  50. </view>
  51. </view>
  52. </template>
  53. <script setup>
  54. import { ref, computed, onMounted } from 'vue'
  55. import { useChartStore } from '@/stores/chart'
  56. import { useUserStore } from '@/stores/user'
  57. import TriangleChart from '@/components/TriangleChart.vue'
  58. import { chatApi } from '@/utils/api'
  59. const store = useChartStore()
  60. const userStore = useUserStore()
  61. const chatInput = ref('')
  62. const scrollTop = ref(0)
  63. const messages = ref([])
  64. const loading = ref(false)
  65. const usedChats = ref(0)
  66. const maxChats = ref(0)
  67. const quotaExceeded = computed(() => {
  68. if (userStore.isVipActive) return false
  69. return maxChats.value > 0 && usedChats.value >= maxChats.value
  70. })
  71. onMounted(async () => {
  72. try {
  73. await userStore.fetchQuota()
  74. } catch (e) { /* ignore */ }
  75. // URL shareability — load from recordId if no store state (US-1.1 §14)
  76. const pages = getCurrentPages()
  77. const currentPage = pages[pages.length - 1]
  78. const recordId = currentPage?.options?.recordId
  79. if (!store.currentChart && recordId) {
  80. try {
  81. await store.loadChart(Number(recordId))
  82. } catch (e) {
  83. // fall through to "no data" message
  84. }
  85. }
  86. if (store.currentChart) {
  87. maxChats.value = userStore.maxChats || 3
  88. usedChats.value = userStore.usedChats || 0
  89. // Use consultationMessages from store (set during startConsultation) if available
  90. if (store.consultationMessages && store.consultationMessages.length > 0) {
  91. messages.value = store.consultationMessages.map(m => ({
  92. role: m.role === 'ai' ? 'ai' : 'user',
  93. content: m.content
  94. }))
  95. } else if (store.currentRecordId) {
  96. try {
  97. const history = await chatApi.history(store.currentRecordId)
  98. messages.value = history.map(m => ({
  99. role: m.role === 'ai' ? 'ai' : 'user',
  100. content: m.content
  101. }))
  102. const userMsgCount = history.filter(m => m.role === 'user').length
  103. usedChats.value = Math.max(usedChats.value, userMsgCount)
  104. } catch (e) {
  105. // fallback to generic greeting
  106. }
  107. }
  108. // Ensure at least a greeting message if still empty
  109. if (messages.value.length === 0) {
  110. messages.value = [{
  111. role: 'ai',
  112. content: `已为您生成命盘。主性格数字为${store.currentChart.O}。请点击下方问题或输入您想了解的内容。`
  113. }]
  114. }
  115. } else {
  116. messages.value = [{
  117. role: 'ai',
  118. content: '暂无命盘数据,请先返回首页输入出生信息。'
  119. }]
  120. }
  121. })
  122. async function sendQuestion(q) {
  123. if (!q.trim() || loading.value) return
  124. if (quotaExceeded.value) return
  125. messages.value.push({ role: 'user', content: q })
  126. chatInput.value = ''
  127. loading.value = true
  128. try {
  129. if (store.currentRecordId) {
  130. const res = await chatApi.send({ chartRecordId: store.currentRecordId, query: q })
  131. messages.value.push({ role: 'ai', content: res.reply })
  132. usedChats.value++
  133. } else {
  134. messages.value.push({
  135. role: 'ai',
  136. content: `数字能量分析显示,${q}方面您的能量组合表明有良好的发展潜力,请结合自身情况理性决策。`
  137. })
  138. }
  139. } catch (e) {
  140. if (e.code === 1008) {
  141. usedChats.value = maxChats.value
  142. messages.value.push({
  143. role: 'ai',
  144. content: '今日解读次数已用完,开通会员可享无限次解读。'
  145. })
  146. } else {
  147. messages.value.push({
  148. role: 'ai',
  149. content: e.message || '解读服务暂时不可用,请稍后再试。'
  150. })
  151. }
  152. }
  153. loading.value = false
  154. scrollTop.value = 99999
  155. }
  156. function goSubscribe() {
  157. uni.navigateTo({ url: '/pages/subscribe/index' })
  158. }
  159. </script>
  160. <style scoped lang="scss">
  161. .page-chart {
  162. height: 100vh;
  163. display: flex;
  164. flex-direction: column;
  165. overflow: hidden;
  166. background: linear-gradient(180deg, #0c0a1a 0%, #120d28 50%, #0f0c1e 100%);
  167. }
  168. .chart-section {
  169. flex-shrink: 0;
  170. margin: 0 14px;
  171. }
  172. .chat-section {
  173. flex: 1;
  174. display: flex;
  175. flex-direction: column;
  176. overflow: hidden;
  177. min-height: 0;
  178. margin: 0 14px;
  179. }
  180. .section-title {
  181. font-size: 17px;
  182. font-weight: 600;
  183. color: rgba(255, 255, 255, 0.9);
  184. margin: 20px 0 14px;
  185. display: flex;
  186. align-items: center;
  187. gap: 8px;
  188. }
  189. .title-icon {
  190. font-size: 14px;
  191. color: #fbbf24;
  192. opacity: 0.7;
  193. }
  194. .quota-badge {
  195. font-size: 11px;
  196. font-weight: 400;
  197. color: #fbbf24;
  198. background: rgba(251, 191, 36, 0.12);
  199. padding: 2px 10px;
  200. border-radius: 10px;
  201. border: 1px solid rgba(251, 191, 36, 0.15);
  202. }
  203. .quota-badge.vip {
  204. color: #34d399;
  205. background: rgba(52, 211, 153, 0.1);
  206. border-color: rgba(52, 211, 153, 0.15);
  207. }
  208. .chat-box {
  209. flex: 1;
  210. height: auto;
  211. overflow-y: auto;
  212. background: rgba(255, 255, 255, 0.04);
  213. border: 1px solid rgba(255, 255, 255, 0.06);
  214. border-radius: 14px;
  215. padding: 14px;
  216. margin-bottom: 10px;
  217. }
  218. .msg-row {
  219. margin-bottom: 14px;
  220. display: flex;
  221. }
  222. .msg-row.user {
  223. justify-content: flex-end;
  224. }
  225. .msg-bubble {
  226. max-width: 80%;
  227. padding: 10px 16px;
  228. border-radius: 14px;
  229. font-size: 14px;
  230. line-height: 1.5;
  231. color: rgba(255, 255, 255, 0.85);
  232. background: rgba(255, 255, 255, 0.06);
  233. border: 1px solid rgba(255, 255, 255, 0.06);
  234. }
  235. .msg-row.user .msg-bubble {
  236. background: rgba(251, 191, 36, 0.12);
  237. border-color: rgba(251, 191, 36, 0.15);
  238. color: #fbbf24;
  239. }
  240. .input-bar {
  241. display: flex;
  242. gap: 8px;
  243. }
  244. .chat-input {
  245. flex: 1;
  246. height: 42px;
  247. border: 1px solid rgba(255, 255, 255, 0.1);
  248. border-radius: 10px;
  249. padding: 0 14px;
  250. font-size: 14px;
  251. color: rgba(255, 255, 255, 0.85);
  252. background: rgba(255, 255, 255, 0.04);
  253. }
  254. .send-btn {
  255. height: 42px;
  256. padding: 0 24px;
  257. background: linear-gradient(135deg, #f59e0b, #d97706);
  258. color: #fff;
  259. border: none;
  260. border-radius: 10px;
  261. line-height: 42px;
  262. font-size: 14px;
  263. font-weight: 500;
  264. }
  265. .send-btn[disabled] {
  266. opacity: 0.4;
  267. }
  268. /* Upgrade prompt */
  269. .upgrade-prompt {
  270. background: linear-gradient(135deg, rgba(251, 191, 36, 0.08), rgba(217, 119, 6, 0.06));
  271. border: 1px solid rgba(251, 191, 36, 0.12);
  272. border-radius: 14px;
  273. padding: 22px;
  274. text-align: center;
  275. margin-bottom: 12px;
  276. }
  277. .upgrade-text {
  278. display: block;
  279. font-size: 16px;
  280. font-weight: 600;
  281. color: #fbbf24;
  282. }
  283. .upgrade-desc {
  284. display: block;
  285. font-size: 13px;
  286. color: rgba(255, 255, 255, 0.45);
  287. margin: 8px 0 16px;
  288. }
  289. .upgrade-btn {
  290. background: linear-gradient(135deg, #f59e0b, #d97706);
  291. color: #fff;
  292. border: none;
  293. border-radius: 10px;
  294. padding: 10px 36px;
  295. font-size: 15px;
  296. font-weight: 500;
  297. }
  298. </style>