1
This commit is contained in:
+259
-118
@@ -15,8 +15,13 @@
|
||||
<view class="status-dot"></view>
|
||||
<text class="status-text">在线</text>
|
||||
</view>
|
||||
<view class="knowledge-icon" @click="openKnowledge">
|
||||
<text class="icon-text">📚</text>
|
||||
<view class="action-buttons">
|
||||
<view class="action-btn" @click="clearHistory" title="清空历史">
|
||||
<text class="icon-text">🗑️</text>
|
||||
</view>
|
||||
<view class="action-btn" @click="openKnowledge" title="知识库">
|
||||
<text class="icon-text">📚</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -114,6 +119,17 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
askPetAssistant,
|
||||
streamAskPetAssistant,
|
||||
getAssistantHistory,
|
||||
clearAssistantHistory,
|
||||
validateMessage,
|
||||
formatChatMessage,
|
||||
formatMessageTime
|
||||
} from '@/http/api/assistant.js'
|
||||
import { createPetAssistantSSE } from '@/utils/sse.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
@@ -121,82 +137,46 @@ export default {
|
||||
scrollTop: 0,
|
||||
isThinking: false,
|
||||
userAvatar: '/static/user-avatar.png',
|
||||
messageList: [
|
||||
{
|
||||
type: 'ai',
|
||||
content: '您好!我是您的宠物AI助手🐾\n\n我可以为您解答关于宠物饲养、健康、训练、营养等方面的问题。\n\n**我能帮你做什么:**\n- 🍖 **饮食建议**:营养搭配、食物选择\n- 🏥 **健康咨询**:症状分析、预防措施 \n- 🎾 **训练指导**:行为纠正、技能训练\n- 💡 **日常护理**:清洁、美容、环境\n\n有什么想了解的吗?',
|
||||
time: this.getCurrentTime()
|
||||
}
|
||||
],
|
||||
quickQuestions: [
|
||||
{ id: 1, text: '猫咪不吃饭怎么办?' },
|
||||
{ id: 2, text: '狗狗需要多久洗一次澡?' },
|
||||
{ id: 3, text: '宠物疫苗接种时间' },
|
||||
{ id: 4, text: '如何训练宠物定点上厕所?' },
|
||||
{ id: 5, text: '宠物发烧的症状' },
|
||||
{ id: 6, text: '幼猫喂养注意事项' }
|
||||
],
|
||||
// 知识库数据
|
||||
knowledgeBase: {
|
||||
'喂食': {
|
||||
keywords: ['喂食', '吃饭', '食物', '饮食', '营养'],
|
||||
responses: [
|
||||
'定时定量喂食很重要,成年猫每天2-3次,幼猫3-4次。',
|
||||
'选择优质的宠物食品,避免给宠物吃人类食物。',
|
||||
'确保宠物随时有清洁的饮用水。'
|
||||
]
|
||||
},
|
||||
'健康': {
|
||||
keywords: ['健康', '生病', '症状', '发烧', '呕吐', '腹泻'],
|
||||
responses: [
|
||||
'定期体检很重要,建议每年至少一次。',
|
||||
'注意观察宠物的食欲、精神状态和排便情况。',
|
||||
'如果出现异常症状,请及时就医。'
|
||||
]
|
||||
},
|
||||
'疫苗': {
|
||||
keywords: ['疫苗', '接种', '免疫', '预防'],
|
||||
responses: [
|
||||
'幼猫幼犬8-12周开始接种疫苗。',
|
||||
'成年宠物每年需要加强免疫。',
|
||||
'常见疫苗包括三联疫苗和狂犬疫苗。'
|
||||
]
|
||||
},
|
||||
'训练': {
|
||||
keywords: ['训练', '教育', '行为', '定点', '上厕所'],
|
||||
responses: [
|
||||
'训练需要耐心和一致性。',
|
||||
'使用正向强化,奖励好行为。',
|
||||
'避免体罚,这会让宠物产生恐惧。'
|
||||
]
|
||||
},
|
||||
'洗澡': {
|
||||
keywords: ['洗澡', '清洁', '卫生'],
|
||||
responses: [
|
||||
'狗狗一般1-2周洗一次澡,猫咪通常不需要经常洗澡。',
|
||||
'使用宠物专用洗浴用品。',
|
||||
'洗澡后要及时吹干,避免感冒。'
|
||||
]
|
||||
}
|
||||
}
|
||||
messageList: []
|
||||
}
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
// 加载历史记录
|
||||
this.loadChatHistory()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
// 页面显示时滚动到底部
|
||||
this.$nextTick(() => {
|
||||
this.scrollToBottom()
|
||||
})
|
||||
},
|
||||
|
||||
methods: {
|
||||
getCurrentTime() {
|
||||
const now = new Date()
|
||||
return `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`
|
||||
},
|
||||
|
||||
askQuestion(question) {
|
||||
this.inputMessage = question
|
||||
this.sendMessage()
|
||||
},
|
||||
|
||||
sendMessage() {
|
||||
|
||||
async sendMessage() {
|
||||
if (!this.inputMessage.trim() || this.isThinking) return
|
||||
|
||||
// 验证消息内容
|
||||
const validation = validateMessage(this.inputMessage)
|
||||
if (!validation.valid) {
|
||||
uni.showToast({
|
||||
title: validation.error,
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 保存用户输入内容
|
||||
const userInput = this.inputMessage
|
||||
const userInput = this.inputMessage.trim()
|
||||
|
||||
// 添加用户消息
|
||||
const userMessage = {
|
||||
@@ -224,46 +204,203 @@ export default {
|
||||
}, 500)
|
||||
})
|
||||
|
||||
// 模拟AI回复
|
||||
setTimeout(() => {
|
||||
const aiMessage = {
|
||||
type: 'ai',
|
||||
content: this.getAIResponse(userInput),
|
||||
time: this.getCurrentTime()
|
||||
}
|
||||
this.messageList.push(aiMessage)
|
||||
this.isThinking = false
|
||||
|
||||
// AI回复完成后强制滚动到底部
|
||||
this.$nextTick(() => {
|
||||
this.forceScrollToBottom()
|
||||
// 延迟再次滚动,确保消息完全显示
|
||||
setTimeout(() => {
|
||||
this.forceScrollToBottom()
|
||||
}, 100)
|
||||
try {
|
||||
// 使用SSE流式响应
|
||||
const response = await this.handleStreamResponse({
|
||||
message: userInput,
|
||||
temperature: 0.7,
|
||||
maxTokens: 1000
|
||||
})
|
||||
}, 1500 + Math.random() * 1000) // 1.5-2.5秒随机延迟
|
||||
|
||||
// 检查是否包含敏感词
|
||||
if (response && response.data && response.data.isSensitive) {
|
||||
uni.showToast({
|
||||
title: '消息包含敏感内容',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
|
||||
// 结束思考状态
|
||||
this.isThinking = false
|
||||
} catch (error) {
|
||||
console.error('SSE流式请求失败,尝试普通请求:', error)
|
||||
|
||||
// 降级处理:使用普通API
|
||||
try {
|
||||
const fallbackResponse = await askPetAssistant({
|
||||
message: userInput,
|
||||
temperature: 0.7,
|
||||
maxTokens: 1000
|
||||
})
|
||||
|
||||
if (fallbackResponse && fallbackResponse.data) {
|
||||
const aiMessage = {
|
||||
type: 'ai',
|
||||
content: fallbackResponse.data.message || fallbackResponse.data.content || '抱歉,我现在无法回答您的问题。',
|
||||
time: this.getCurrentTime(),
|
||||
isSensitive: fallbackResponse.data.isSensitive || false,
|
||||
tokenCount: fallbackResponse.data.tokenCount || 0,
|
||||
responseTime: fallbackResponse.data.responseTime || 0
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 检查敏感词
|
||||
if (fallbackResponse.data.isSensitive) {
|
||||
uni.showToast({
|
||||
title: '消息包含敏感内容',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
|
||||
this.messageList.push(aiMessage)
|
||||
this.forceScrollToBottom()
|
||||
} else {
|
||||
this.handleApiError('API响应格式异常')
|
||||
}
|
||||
} catch (fallbackError) {
|
||||
console.error('普通请求也失败:', fallbackError)
|
||||
this.handleApiError(fallbackError.message || '网络请求失败')
|
||||
}
|
||||
} finally {
|
||||
// 确保思考状态结束(防止异常情况)
|
||||
this.isThinking = false
|
||||
}
|
||||
},
|
||||
|
||||
getAIResponse(question) {
|
||||
// 智能匹配知识库
|
||||
for (let category in this.knowledgeBase) {
|
||||
const { keywords, responses } = this.knowledgeBase[category]
|
||||
if (keywords.some(keyword => question.includes(keyword))) {
|
||||
const randomResponse = responses[Math.floor(Math.random() * responses.length)]
|
||||
return `${randomResponse}\n\n💡 如果您需要更详细的建议,建议咨询专业的宠物医生。`
|
||||
}
|
||||
/**
|
||||
* 处理API错误
|
||||
* @param {string} errorMessage 错误信息
|
||||
*/
|
||||
handleApiError(errorMessage) {
|
||||
const errorMessage_display = {
|
||||
type: 'ai',
|
||||
content: `抱歉,我遇到了一些问题:${errorMessage}\n\n请稍后再试,或者检查网络连接。`,
|
||||
time: this.getCurrentTime(),
|
||||
isError: true
|
||||
}
|
||||
|
||||
// 默认回复
|
||||
const defaultResponses = [
|
||||
'这是一个很好的问题!建议您咨询专业的宠物医生获得更准确的建议。',
|
||||
'每只宠物的情况都不同,建议根据具体情况来处理。您可以查看我们的知识库了解更多信息。',
|
||||
'感谢您的提问!这个问题比较复杂,建议您带宠物去专业的宠物医院检查。',
|
||||
'我理解您的担心。建议您记录宠物的具体症状,然后咨询专业医生。'
|
||||
]
|
||||
// 直接添加错误消息
|
||||
this.messageList.push(errorMessage_display)
|
||||
this.forceScrollToBottom()
|
||||
},
|
||||
|
||||
return defaultResponses[Math.floor(Math.random() * defaultResponses.length)]
|
||||
/**
|
||||
* 加载聊天历史记录
|
||||
*/
|
||||
async loadChatHistory() {
|
||||
try {
|
||||
const response = await getAssistantHistory({
|
||||
page: 1,
|
||||
pageSize: 50
|
||||
})
|
||||
|
||||
if (response && response.data && response.data.list && response.data.list.length > 0) {
|
||||
// 格式化并设置历史消息
|
||||
const historyMessages = response.data.list.map(msg => formatChatMessage(msg))
|
||||
this.messageList = historyMessages
|
||||
|
||||
// 滚动到底部
|
||||
this.$nextTick(() => {
|
||||
this.scrollToBottom()
|
||||
})
|
||||
} else {
|
||||
// 没有历史记录,保持空列表
|
||||
this.messageList = []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载历史记录失败:', error)
|
||||
// 静默处理,保持空列表
|
||||
this.messageList = []
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 清空聊天历史
|
||||
*/
|
||||
async clearHistory() {
|
||||
try {
|
||||
const result = await uni.showModal({
|
||||
title: '确认清空',
|
||||
content: '确定要清空聊天记录吗?此操作不可恢复。',
|
||||
confirmText: '清空',
|
||||
cancelText: '取消'
|
||||
})
|
||||
|
||||
if (!result.confirm) return
|
||||
|
||||
uni.showLoading({
|
||||
title: '正在清空...'
|
||||
})
|
||||
|
||||
await clearAssistantHistory()
|
||||
|
||||
// 清空消息列表
|
||||
this.messageList = []
|
||||
|
||||
uni.showToast({
|
||||
title: '历史记录已清空',
|
||||
icon: 'success',
|
||||
duration: 1500
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('清空历史记录失败:', error)
|
||||
uni.showToast({
|
||||
title: '清空失败',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 处理SSE流式响应
|
||||
* @param {Object} messageData 消息数据
|
||||
* @returns {Promise} 返回流式响应处理结果
|
||||
*/
|
||||
async handleStreamResponse(messageData) {
|
||||
// 创建AI消息对象
|
||||
const aiMessage = {
|
||||
type: 'ai',
|
||||
content: '',
|
||||
time: this.getCurrentTime(),
|
||||
isSensitive: false,
|
||||
tokenCount: 0,
|
||||
responseTime: 0
|
||||
}
|
||||
|
||||
// 添加到消息列表
|
||||
this.messageList.push(aiMessage)
|
||||
|
||||
// 使用SSE工具类处理流式响应
|
||||
return createPetAssistantSSE(messageData, {
|
||||
onChunk: (chunk) => {
|
||||
// 实时更新消息内容
|
||||
aiMessage.content = chunk.totalContent
|
||||
this.forceScrollToBottom()
|
||||
},
|
||||
onComplete: (result) => {
|
||||
// 更新消息的元数据
|
||||
if (result.data.isSensitive !== undefined) {
|
||||
aiMessage.isSensitive = result.data.isSensitive
|
||||
}
|
||||
if (result.data.tokenCount !== undefined) {
|
||||
aiMessage.tokenCount = result.data.tokenCount
|
||||
}
|
||||
if (result.data.responseTime !== undefined) {
|
||||
aiMessage.responseTime = result.data.responseTime
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('SSE流式响应失败:', error)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
scrollToBottom() {
|
||||
@@ -302,19 +439,11 @@ export default {
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
startVoiceInput() {
|
||||
uni.showToast({
|
||||
title: '语音功能开发中',
|
||||
icon: 'none'
|
||||
})
|
||||
},
|
||||
|
||||
openKnowledge() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/assistant/knowledge'
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -398,18 +527,30 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.knowledge-icon {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 30rpx;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
|
||||
gap: 16rpx;
|
||||
|
||||
.icon-text {
|
||||
font-size: 28rpx;
|
||||
.action-btn {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 28rpx;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.icon-text {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user