|
@@ -5,7 +5,7 @@
|
|
|
<view class="nav-back" @click="goBack">
|
|
<view class="nav-back" @click="goBack">
|
|
|
<text class="nav-back-icon">‹</text>
|
|
<text class="nav-back-icon">‹</text>
|
|
|
</view>
|
|
</view>
|
|
|
- <text class="nav-title">AI 家庭助手</text>
|
|
|
|
|
|
|
+ <text class="nav-title">{{ isNutrition ? '精准营养助手' : 'AI 家庭助手' }}</text>
|
|
|
<view class="nav-actions">
|
|
<view class="nav-actions">
|
|
|
<text class="nav-action" @click="showConversations = !showConversations">☰</text>
|
|
<text class="nav-action" @click="showConversations = !showConversations">☰</text>
|
|
|
</view>
|
|
</view>
|
|
@@ -34,22 +34,30 @@
|
|
|
@scrolltoupper="loadMoreMessages" id="messageList">
|
|
@scrolltoupper="loadMoreMessages" id="messageList">
|
|
|
<view class="message-list-inner">
|
|
<view class="message-list-inner">
|
|
|
<view class="welcome-card" v-if="messages.length === 0 && !sending">
|
|
<view class="welcome-card" v-if="messages.length === 0 && !sending">
|
|
|
- <view class="welcome-icon">🤖</view>
|
|
|
|
|
- <text class="welcome-title">你好!我是你的家庭助手</text>
|
|
|
|
|
- <text class="welcome-desc">我可以帮你了解孩子的成长情况,比如:</text>
|
|
|
|
|
|
|
+ <view class="welcome-icon">{{ isNutrition ? '🥗' : '🤖' }}</view>
|
|
|
|
|
+ <text class="welcome-title">{{ isNutrition ? '你好!我是精准营养助手' : '你好!我是你的家庭助手' }}</text>
|
|
|
|
|
+ <text class="welcome-desc">{{ isNutrition ? '我可以基于菌群检测报告,为你提供个性化的营养建议和推荐' : '我可以帮你了解孩子的成长情况,比如:' }}</text>
|
|
|
<view class="welcome-suggestions">
|
|
<view class="welcome-suggestions">
|
|
|
- <view class="welcome-suggestion" @click="quickQuestion('小明最近任务完成怎么样?')">
|
|
|
|
|
|
|
+ <view class="welcome-suggestion" v-if="!isNutrition" @click="quickQuestion('小明最近任务完成怎么样?')">
|
|
|
<text class="suggestion-icon">📋</text>
|
|
<text class="suggestion-icon">📋</text>
|
|
|
<text class="suggestion-text">小明最近任务完成怎么样?</text>
|
|
<text class="suggestion-text">小明最近任务完成怎么样?</text>
|
|
|
</view>
|
|
</view>
|
|
|
- <view class="welcome-suggestion" @click="quickQuestion('我家孩子的能量评分怎么样?')">
|
|
|
|
|
|
|
+ <view class="welcome-suggestion" v-if="!isNutrition" @click="quickQuestion('我家孩子的能量评分怎么样?')">
|
|
|
<text class="suggestion-icon">⚡</text>
|
|
<text class="suggestion-icon">⚡</text>
|
|
|
<text class="suggestion-text">孩子的能量评分怎么样?</text>
|
|
<text class="suggestion-text">孩子的能量评分怎么样?</text>
|
|
|
</view>
|
|
</view>
|
|
|
- <view class="welcome-suggestion" @click="quickQuestion('今天有什么需要我做的事情?')">
|
|
|
|
|
|
|
+ <view class="welcome-suggestion" v-if="!isNutrition" @click="quickQuestion('今天有什么需要我做的事情?')">
|
|
|
<text class="suggestion-icon">✅</text>
|
|
<text class="suggestion-icon">✅</text>
|
|
|
<text class="suggestion-text">今天有什么需要我做的事情?</text>
|
|
<text class="suggestion-text">今天有什么需要我做的事情?</text>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
+ <view class="welcome-suggestion" v-if="isNutrition" @click="quickQuestion('根据报告,我的孩子需要补充哪些营养?')">
|
|
|
|
|
+ <text class="suggestion-icon">🥦</text>
|
|
|
|
|
+ <text class="suggestion-text">根据报告,我的孩子需要补充哪些营养?</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="welcome-suggestion" v-if="isNutrition" @click="quickQuestion('有哪些食物或产品推荐?')">
|
|
|
|
|
+ <text class="suggestion-icon">🛒</text>
|
|
|
|
|
+ <text class="suggestion-text">有哪些食物或产品推荐?</text>
|
|
|
|
|
+ </view>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
@@ -61,7 +69,21 @@
|
|
|
<!-- AI 回复 -->
|
|
<!-- AI 回复 -->
|
|
|
<view class="message message-ai" v-if="msg.answer">
|
|
<view class="message message-ai" v-if="msg.answer">
|
|
|
<view class="message-avatar">🤖</view>
|
|
<view class="message-avatar">🤖</view>
|
|
|
- <view class="message-bubble ai-bubble">{{ msg.answer }}</view>
|
|
|
|
|
|
|
+ <view class="message-bubble ai-bubble">
|
|
|
|
|
+ <text class="ai-answer-text">{{ msg.answer }}</text>
|
|
|
|
|
+ <!-- 推荐卡片 -->
|
|
|
|
|
+ <view class="rec-list" v-if="msg.recommendations && msg.recommendations.length > 0">
|
|
|
|
|
+ <view class="rec-card" v-for="(rec, ri) in msg.recommendations" :key="ri" @click="gotoRec(rec)">
|
|
|
|
|
+ <image class="rec-cover" v-if="rec.coverImage" :src="rec.coverImage" mode="aspectFill" />
|
|
|
|
|
+ <view class="rec-info">
|
|
|
|
|
+ <text class="rec-type-tag">{{ rec.type === 'product' ? '商品' : rec.type === 'activity' ? '活动' : '文章' }}</text>
|
|
|
|
|
+ <text class="rec-name">{{ rec.name }}</text>
|
|
|
|
|
+ <text class="rec-desc">{{ rec.description }}</text>
|
|
|
|
|
+ <text class="rec-price" v-if="rec.price">¥{{ rec.price }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
@@ -94,7 +116,7 @@
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script>
|
|
<script>
|
|
|
-import { aiSendMessage, aiGetConversations, aiGetMessages, aiDeleteConversation } from '../../utils/api.js'
|
|
|
|
|
|
|
+import { aiSendMessage, aiGetConversations, aiGetMessages, aiDeleteConversation, aiSendNutritionMessage } from '../../utils/api.js'
|
|
|
import BaseEmpty from '../../components/BaseEmpty.vue'
|
|
import BaseEmpty from '../../components/BaseEmpty.vue'
|
|
|
|
|
|
|
|
export default {
|
|
export default {
|
|
@@ -111,12 +133,20 @@ export default {
|
|
|
pageLoaded: false,
|
|
pageLoaded: false,
|
|
|
reportId: null,
|
|
reportId: null,
|
|
|
surveyId: null,
|
|
surveyId: null,
|
|
|
- pendingFirstMessage: ''
|
|
|
|
|
|
|
+ pendingFirstMessage: '',
|
|
|
|
|
+ mode: '',
|
|
|
|
|
+ recommendations: []
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ computed: {
|
|
|
|
|
+ isNutrition: function() {
|
|
|
|
|
+ return this.mode === 'nutrition'
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
onLoad: function(options) {
|
|
onLoad: function(options) {
|
|
|
this.reportId = options.reportId ? parseInt(options.reportId) : null
|
|
this.reportId = options.reportId ? parseInt(options.reportId) : null
|
|
|
this.surveyId = options.surveyId ? parseInt(options.surveyId) : null
|
|
this.surveyId = options.surveyId ? parseInt(options.surveyId) : null
|
|
|
|
|
+ this.mode = options.mode || ''
|
|
|
if (options.firstMessage) {
|
|
if (options.firstMessage) {
|
|
|
this.pendingFirstMessage = decodeURIComponent(options.firstMessage)
|
|
this.pendingFirstMessage = decodeURIComponent(options.firstMessage)
|
|
|
}
|
|
}
|
|
@@ -204,13 +234,26 @@ export default {
|
|
|
if (this.surveyId) {
|
|
if (this.surveyId) {
|
|
|
params.surveyId = this.surveyId
|
|
params.surveyId = this.surveyId
|
|
|
}
|
|
}
|
|
|
- const res = await aiSendMessage(params)
|
|
|
|
|
- const data = res.data || {}
|
|
|
|
|
|
|
|
|
|
- // 更新最后一条消息的回复
|
|
|
|
|
- const lastMsg = this.messages[this.messages.length - 1]
|
|
|
|
|
- if (lastMsg) {
|
|
|
|
|
- lastMsg.answer = data.answer || ''
|
|
|
|
|
|
|
+ var res, data
|
|
|
|
|
+ if (this.isNutrition && this.reportId) {
|
|
|
|
|
+ // 营养模式:调精准营养助手
|
|
|
|
|
+ res = await aiSendNutritionMessage(params)
|
|
|
|
|
+ data = res.data || {}
|
|
|
|
|
+ // 更新最后一条消息的回复 + 推荐列表
|
|
|
|
|
+ const lastMsg = this.messages[this.messages.length - 1]
|
|
|
|
|
+ if (lastMsg) {
|
|
|
|
|
+ lastMsg.answer = data.answer || ''
|
|
|
|
|
+ lastMsg.recommendations = data.recommendations || []
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ res = await aiSendMessage(params)
|
|
|
|
|
+ data = res.data || {}
|
|
|
|
|
+ // 更新最后一条消息的回复
|
|
|
|
|
+ const lastMsg = this.messages[this.messages.length - 1]
|
|
|
|
|
+ if (lastMsg) {
|
|
|
|
|
+ lastMsg.answer = data.answer || ''
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 更新 conversationId(新建会话时 Dify 返回)
|
|
// 更新 conversationId(新建会话时 Dify 返回)
|
|
@@ -238,6 +281,12 @@ export default {
|
|
|
this.sendMessage()
|
|
this.sendMessage()
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
|
|
+ gotoRec(rec) {
|
|
|
|
|
+ if (rec.url) {
|
|
|
|
|
+ uni.navigateTo({ url: rec.url })
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
loadMoreMessages() {
|
|
loadMoreMessages() {
|
|
|
// 分页加载历史(当前简化)
|
|
// 分页加载历史(当前简化)
|
|
|
},
|
|
},
|
|
@@ -572,4 +621,78 @@ export default {
|
|
|
0%, 60%, 100% { opacity: 0.3; transform: translateY(0); }
|
|
0%, 60%, 100% { opacity: 0.3; transform: translateY(0); }
|
|
|
30% { opacity: 1; transform: translateY(-8rpx); }
|
|
30% { opacity: 1; transform: translateY(-8rpx); }
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+/* 推荐卡片 */
|
|
|
|
|
+.ai-answer-text {
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ margin-bottom: 16rpx;
|
|
|
|
|
+ line-height: 1.7;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.rec-list {
|
|
|
|
|
+ border-top: 1rpx solid var(--border-light, #E2E8F0);
|
|
|
|
|
+ padding-top: 16rpx;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 12rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.rec-card {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: row;
|
|
|
|
|
+ background: var(--bg, #FFF7ED);
|
|
|
|
|
+ border-radius: 12rpx;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ padding: 12rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.rec-cover {
|
|
|
|
|
+ width: 120rpx;
|
|
|
|
|
+ height: 120rpx;
|
|
|
|
|
+ border-radius: 8rpx;
|
|
|
|
|
+ margin-right: 12rpx;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ background: #E2E8F0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.rec-info {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 4rpx;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.rec-type-tag {
|
|
|
|
|
+ display: inline-block;
|
|
|
|
|
+ font-size: 18rpx;
|
|
|
|
|
+ color: var(--color-primary, #5B9BD5);
|
|
|
|
|
+ background: rgba(91, 155, 213, 0.1);
|
|
|
|
|
+ padding: 2rpx 12rpx;
|
|
|
|
|
+ border-radius: 8rpx;
|
|
|
|
|
+ align-self: flex-start;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.rec-name {
|
|
|
|
|
+ font-size: 26rpx;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: var(--text, #1E293B);
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.rec-desc {
|
|
|
|
|
+ font-size: 22rpx;
|
|
|
|
|
+ color: var(--text-secondary, #64748B);
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.rec-price {
|
|
|
|
|
+ font-size: 26rpx;
|
|
|
|
|
+ color: #F97316;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+}
|
|
|
</style>
|
|
</style>
|