pet/pages/pets/pet-chat.vue

645 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="pet-chat-container">
<!-- 聊天头部 -->
<view class="chat-header">
<view class="header-content">
<view class="back-button" @click="goBack">
<u-icon name="arrow-left" size="18" color="#ffffff"></u-icon>
</view>
<view class="pet-info">
<view class="pet-avatar">
<image :src="petInfo.avatar || '/static/default-pet.png'" class="avatar-img"></image>
</view>
<view class="pet-details">
<text class="pet-name">{{ petInfo.name }}聊天</text>
<text class="pet-status">在线</text>
</view>
</view>
<view class="header-actions">
<view class="action-button" @click="showChatMenu">
<u-icon name="more-dot-fill" size="18" color="#ffffff"></u-icon>
</view>
</view>
</view>
</view>
<!-- 聊天消息列表 -->
<scroll-view
class="chat-messages"
scroll-y
:scroll-top="scrollTop"
scroll-with-animation
@scrolltolower="loadMoreMessages"
>
<view class="message-list">
<!-- 加载更多提示 -->
<view class="load-more" v-if="hasMoreMessages">
<u-loading-icon v-if="loadingMore"></u-loading-icon>
<u-text text="上拉加载更多历史消息" type="tips" size="12" v-else></u-text>
</view>
<!-- 消息项 -->
<view
class="message-item"
v-for="message in messageList"
:key="message.id"
:class="message.type"
>
<view class="message-avatar" v-if="message.type === 'ai'">
<u-avatar :src="petInfo.avatar || '/static/default-pet.png'" size="32" shape="circle"></u-avatar>
</view>
<view class="message-content">
<view class="message-bubble" :class="message.type">
<u-text :text="message.content" size="14" :color="message.type === 'user' ? '#ffffff' : '#333333'"></u-text>
</view>
<view class="message-time">
<u-text :text="formatTime(message.timestamp)" type="tips" size="10"></u-text>
</view>
</view>
<view class="message-avatar" v-if="message.type === 'user'">
<u-avatar src="/static/user-avatar.png" size="32" shape="circle"></u-avatar>
</view>
</view>
<!-- AI正在输入提示 -->
<view class="message-item ai" v-if="aiTyping">
<view class="message-avatar">
<u-avatar :src="petInfo.avatar || '/static/default-pet.png'" size="32" shape="circle"></u-avatar>
</view>
<view class="message-content">
<view class="message-bubble ai typing">
<view class="typing-dots">
<view class="dot"></view>
<view class="dot"></view>
<view class="dot"></view>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
<!-- 输入区域 -->
<view class="chat-input-area">
<view class="input-container">
<u-input
v-model="inputMessage"
placeholder="和你的宠物说点什么..."
:border="false"
:custom-style="inputStyle"
@keyboardheightchange="onKeyboardHeightChange"
@confirm="sendMessage"
confirm-type="send"
></u-input>
<view class="input-actions">
<u-icon name="mic" size="20" color="#999" @click="startVoiceInput" v-if="!inputMessage"></u-icon>
<u-button
type="primary"
size="mini"
text="发送"
@click="sendMessage"
:disabled="!inputMessage.trim()"
v-else
></u-button>
</view>
</view>
</view>
<!-- 聊天菜单弹窗 -->
<u-popup v-model="showMenu" mode="bottom" border-radius="20">
<view class="chat-menu">
<view class="menu-title">
<u-text text="聊天设置" type="primary" size="16" bold></u-text>
</view>
<view class="menu-items">
<view class="menu-item" @click="clearHistory">
<u-icon name="delete" size="20" color="#ff6b6b"></u-icon>
<u-text text="清空聊天记录" size="14"></u-text>
</view>
<view class="menu-item" @click="exportHistory">
<u-icon name="download" size="20" color="#64B5F6"></u-icon>
<u-text text="导出聊天记录" size="14"></u-text>
</view>
<view class="menu-item" @click="togglePersonality">
<u-icon name="setting" size="20" color="#81C784"></u-icon>
<u-text text="个性化设置" size="14"></u-text>
</view>
</view>
<view class="menu-cancel" @click="showMenu = false">
<u-text text="取消" type="info" size="14"></u-text>
</view>
</view>
</u-popup>
</view>
</template>
<script>
import { ref, reactive, onMounted, nextTick } from 'vue'
export default {
name: 'PetChat',
setup() {
// 响应式数据
const state = reactive({
petId: '',
petInfo: {},
messageList: [],
inputMessage: '',
scrollTop: 0,
aiTyping: false,
showMenu: false,
hasMoreMessages: true,
loadingMore: false,
keyboardHeight: 0
})
// 样式配置
const inputStyle = {
backgroundColor: '#f8f9fa',
borderRadius: '20px',
padding: '10px 15px'
}
// 生命周期
onMounted(() => {
initPage()
})
// 初始化页面
const initPage = () => {
// 获取宠物ID从路由参数
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
state.petId = currentPage.options.petId || '1'
loadPetInfo()
loadChatHistory()
}
// 加载宠物信息
const loadPetInfo = () => {
// 模拟从本地存储获取宠物信息
const mockPets = [
{
id: '1',
name: '小橘',
breed: '橘猫',
personality: '活泼好动,喜欢撒娇',
avatar: '/static/cat-avatar.jpg'
},
{
id: '2',
name: '小白',
breed: '金毛',
personality: '温顺友好,聪明伶俐',
avatar: '/static/dog-avatar.jpg'
}
]
state.petInfo = mockPets.find(pet => pet.id === state.petId) || mockPets[0]
}
// 加载聊天历史
const loadChatHistory = () => {
// 模拟聊天历史数据
const mockMessages = [
{
id: '1',
type: 'ai',
content: `你好主人!我是${state.petInfo.name},今天感觉特别开心呢!有什么想和我聊的吗?`,
timestamp: Date.now() - 3600000
},
{
id: '2',
type: 'user',
content: '小橘今天吃饭怎么样?',
timestamp: Date.now() - 3500000
},
{
id: '3',
type: 'ai',
content: '今天的小鱼干特别香!我吃得可开心了,还想要更多呢~主人你也要记得按时吃饭哦!',
timestamp: Date.now() - 3400000
},
{
id: '4',
type: 'user',
content: '那你今天有没有调皮捣蛋?',
timestamp: Date.now() - 3300000
},
{
id: '5',
type: 'ai',
content: '嘿嘿,我今天把你的拖鞋藏起来了!不过是因为太想你了,想要你的味道陪着我~',
timestamp: Date.now() - 3200000
}
]
state.messageList = mockMessages
scrollToBottom()
}
// 发送消息
const sendMessage = () => {
if (!state.inputMessage.trim()) return
const userMessage = {
id: Date.now().toString(),
type: 'user',
content: state.inputMessage.trim(),
timestamp: Date.now()
}
state.messageList.push(userMessage)
const messageContent = state.inputMessage.trim()
state.inputMessage = ''
// 滚动到底部
scrollToBottom()
// 模拟AI回复
simulateAIReply(messageContent)
}
// 模拟AI回复
const simulateAIReply = (userMessage) => {
state.aiTyping = true
scrollToBottom()
setTimeout(() => {
const aiResponse = generateAIResponse(userMessage)
const aiMessage = {
id: (Date.now() + 1).toString(),
type: 'ai',
content: aiResponse,
timestamp: Date.now()
}
state.messageList.push(aiMessage)
state.aiTyping = false
scrollToBottom()
}, 1500 + Math.random() * 1000) // 1.5-2.5秒随机延迟
}
// 生成AI回复内容
const generateAIResponse = (userMessage) => {
const petName = state.petInfo.name
const responses = {
// 问候类
greeting: [
`${petName}最喜欢和主人聊天了!今天过得怎么样呀?`,
`主人好!${petName}今天心情特别好呢~`,
`哇!主人来找我聊天了,${petName}好开心!`
],
// 食物相关
food: [
`说到吃的,${petName}就兴奋了!今天的小鱼干特别香呢~`,
`${petName}最爱吃主人准备的美食了!什么时候再给我加餐呀?`,
`嘿嘿,${petName}今天偷偷多吃了一点,不要告诉其他人哦!`
],
// 健康相关
health: [
`${petName}今天身体棒棒的!谢谢主人这么关心我~`,
`${petName}觉得自己越来越健康了,都是主人照顾得好!`,
`主人真贴心!${petName}会好好保护自己的身体的!`
],
// 玩耍相关
play: [
`${petName}最喜欢和主人一起玩了!我们什么时候再玩游戏呀?`,
`玩耍时间到!${petName}已经准备好了,主人快来陪我!`,
`${petName}今天学会了新的小把戏,想不想看看?`
],
// 情感相关
emotion: [
`${petName}最爱主人了!每天都想和你在一起~`,
`主人不在的时候,${petName}会想念你的味道呢!`,
`${petName}觉得自己是世界上最幸福的小宝贝!`
],
// 默认回复
default: [
`${petName}听不太懂,但是很开心能和主人聊天!`,
`主人说的话${petName}都会认真听的!`,
`${petName}觉得主人说得很有道理呢!`,
`虽然${petName}不太明白,但是感觉很厉害的样子!`
]
}
// 简单的关键词匹配
const message = userMessage.toLowerCase()
let category = 'default'
if (message.includes('你好') || message.includes('hi') || message.includes('hello')) {
category = 'greeting'
} else if (message.includes('吃') || message.includes('食') || message.includes('饭') || message.includes('零食')) {
category = 'food'
} else if (message.includes('健康') || message.includes('身体') || message.includes('生病') || message.includes('医生')) {
category = 'health'
} else if (message.includes('玩') || message.includes('游戏') || message.includes('运动') || message.includes('散步')) {
category = 'play'
} else if (message.includes('爱') || message.includes('想') || message.includes('喜欢') || message.includes('亲亲')) {
category = 'emotion'
}
const categoryResponses = responses[category]
return categoryResponses[Math.floor(Math.random() * categoryResponses.length)]
}
// 滚动到底部
const scrollToBottom = () => {
nextTick(() => {
state.scrollTop = 99999
})
}
// 格式化时间
const formatTime = (timestamp) => {
const date = new Date(timestamp)
const now = new Date()
const diff = now - date
if (diff < 60000) { // 1分钟内
return '刚刚'
} else if (diff < 3600000) { // 1小时内
return `${Math.floor(diff / 60000)}分钟前`
} else if (diff < 86400000) { // 24小时内
return `${Math.floor(diff / 3600000)}小时前`
} else {
return `${date.getMonth() + 1}${date.getDate()}`
}
}
// 返回上一页
const goBack = () => {
uni.navigateBack()
}
// 显示聊天菜单
const showChatMenu = () => {
state.showMenu = true
}
// 加载更多消息
const loadMoreMessages = () => {
if (state.loadingMore || !state.hasMoreMessages) return
state.loadingMore = true
// 模拟加载历史消息
setTimeout(() => {
const moreMessages = [
{
id: `history_${Date.now()}`,
type: 'ai',
content: `主人,${state.petInfo.name}想你了!`,
timestamp: Date.now() - 7200000
}
]
state.messageList.unshift(...moreMessages)
state.loadingMore = false
// 模拟没有更多消息
if (state.messageList.length > 20) {
state.hasMoreMessages = false
}
}, 1000)
}
// 清空聊天记录
const clearHistory = () => {
uni.showModal({
title: '确认清空',
content: '确定要清空所有聊天记录吗?此操作不可恢复。',
success: (res) => {
if (res.confirm) {
state.messageList = []
state.showMenu = false
uni.showToast({
title: '已清空聊天记录',
icon: 'success'
})
}
}
})
}
// 导出聊天记录
const exportHistory = () => {
uni.showToast({
title: '导出功能开发中',
icon: 'none'
})
state.showMenu = false
}
// 个性化设置
const togglePersonality = () => {
uni.showToast({
title: '个性化设置开发中',
icon: 'none'
})
state.showMenu = false
}
// 语音输入
const startVoiceInput = () => {
uni.showToast({
title: '语音功能开发中',
icon: 'none'
})
}
// 键盘高度变化
const onKeyboardHeightChange = (e) => {
state.keyboardHeight = e.detail.height
if (e.detail.height > 0) {
scrollToBottom()
}
}
return {
...state,
inputStyle,
sendMessage,
formatTime,
goBack,
showChatMenu,
loadMoreMessages,
clearHistory,
exportHistory,
togglePersonality,
startVoiceInput,
onKeyboardHeightChange
}
}
}
</script>
<style lang="scss" scoped>
.pet-chat-container {
height: 100vh;
display: flex;
flex-direction: column;
background-color: #f8f9fa;
}
.pet-info-card {
display: flex;
align-items: center;
padding: 15px 20px;
background-color: #ffffff;
border-bottom: 1px solid #f0f0f0;
.pet-info-text {
flex: 1;
margin-left: 10px;
}
.chat-status {
margin-left: 10px;
}
}
.chat-messages {
flex: 1;
padding: 0 15px;
}
.message-list {
padding: 20px 0;
}
.load-more {
text-align: center;
padding: 20px 0;
}
.message-item {
display: flex;
margin-bottom: 20px;
&.user {
flex-direction: row-reverse;
.message-content {
align-items: flex-end;
}
.message-bubble {
background-color: #FF8A80;
color: #ffffff;
}
}
&.ai {
.message-bubble {
background-color: #ffffff;
border: 1px solid #f0f0f0;
}
}
}
.message-avatar {
margin: 0 10px;
}
.message-content {
display: flex;
flex-direction: column;
max-width: 70%;
}
.message-bubble {
padding: 12px 16px;
border-radius: 18px;
word-wrap: break-word;
&.typing {
padding: 16px;
}
}
.message-time {
margin-top: 5px;
text-align: center;
}
.typing-dots {
display: flex;
gap: 4px;
.dot {
width: 6px;
height: 6px;
border-radius: 50%;
background-color: #ccc;
animation: typing 1.4s infinite ease-in-out;
&:nth-child(1) { animation-delay: -0.32s; }
&:nth-child(2) { animation-delay: -0.16s; }
}
}
@keyframes typing {
0%, 80%, 100% {
transform: scale(0.8);
opacity: 0.5;
}
40% {
transform: scale(1);
opacity: 1;
}
}
.chat-input-area {
background-color: #ffffff;
border-top: 1px solid #f0f0f0;
padding: 15px 20px;
padding-bottom: calc(15px + env(safe-area-inset-bottom));
}
.input-container {
display: flex;
align-items: center;
gap: 10px;
}
.input-actions {
flex-shrink: 0;
}
.chat-menu {
padding: 20px;
.menu-title {
text-align: center;
padding-bottom: 20px;
border-bottom: 1px solid #f0f0f0;
}
.menu-items {
padding: 20px 0;
}
.menu-item {
display: flex;
align-items: center;
gap: 15px;
padding: 15px 0;
border-bottom: 1px solid #f8f9fa;
&:last-child {
border-bottom: none;
}
}
.menu-cancel {
text-align: center;
padding: 15px 0;
border-top: 1px solid #f0f0f0;
}
}
</style>