This commit is contained in:
parent
bd1c7a713f
commit
4296226396
13
pages.json
13
pages.json
|
|
@ -64,19 +64,12 @@
|
|||
"navigationBarTextStyle": "black"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/pets/pet-chat",
|
||||
"style": {
|
||||
"navigationBarTitleText": "AI聊天",
|
||||
"navigationBarBackgroundColor": "#64B5F6",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"path": "pages/pets/pet-chat-simple",
|
||||
"style": {
|
||||
"navigationBarTitleText": "AI聊天",
|
||||
"navigationBarBackgroundColor": "#64B5F6",
|
||||
"navigationBarTitleText": "宠物会话",
|
||||
"navigationBarBackgroundColor": "#FF8A80",
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,93 +1,104 @@
|
|||
<template>
|
||||
<view class="pet-chat-container">
|
||||
<u-navbar :title="`与${petInfo.name}聊天`" left-icon="arrow-left" @left-click="goBack">
|
||||
<template #right>
|
||||
<u-icon name="more-circle" size="20" @click="showMenu = true"></u-icon>
|
||||
</template>
|
||||
</u-navbar>
|
||||
|
||||
<!-- 宠物信息卡片 -->
|
||||
<view class="pet-info-card">
|
||||
<u-avatar :src="petInfo.avatar || '/static/default-pet.png'" size="40" shape="circle"></u-avatar>
|
||||
<view class="pet-avatar-wrapper">
|
||||
<view class="pet-avatar">
|
||||
<text class="avatar-emoji">🐱</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="pet-info-text">
|
||||
<u-text :text="petInfo.name" type="primary" size="14" bold></u-text>
|
||||
<u-text :text="petInfo.personality" type="tips" size="12"></u-text>
|
||||
<text class="pet-name">{{ petInfo.name || '小橘' }}</text>
|
||||
<text class="pet-status">{{ petInfo.personality || '活泼好动,喜欢撒娇' }}</text>
|
||||
</view>
|
||||
<view class="chat-status">
|
||||
<u-tag text="在线" type="success" size="mini"></u-tag>
|
||||
<view class="status-dot"></view>
|
||||
<text class="status-text">在线</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 聊天消息列表 -->
|
||||
<scroll-view
|
||||
class="chat-messages"
|
||||
scroll-y
|
||||
:scroll-top="scrollTop"
|
||||
scroll-with-animation
|
||||
>
|
||||
<view class="chat-messages">
|
||||
<scroll-view
|
||||
class="chat-scroll"
|
||||
scroll-y
|
||||
:scroll-top="scrollTop"
|
||||
:scroll-into-view="scrollIntoView"
|
||||
scroll-with-animation
|
||||
>
|
||||
<view class="message-list">
|
||||
<!-- 消息项 -->
|
||||
<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>
|
||||
<template v-for="message in messageList" :key="message.id">
|
||||
<!-- AI消息 -->
|
||||
<view class="message-item ai" v-if="message.type === 'ai'">
|
||||
<view class="message-avatar">
|
||||
<view class="pet-avatar">
|
||||
<text class="avatar-emoji">🐱</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="message-time">
|
||||
<u-text :text="formatTime(message.timestamp)" type="tips" size="10"></u-text>
|
||||
<view class="message-content ai">
|
||||
<view class="message-bubble ai">
|
||||
<text class="message-text">{{ message.content }}</text>
|
||||
</view>
|
||||
<text class="message-time">{{ formatTime(message.timestamp) }}</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 class="message-item user" v-if="message.type === 'user'">
|
||||
<view class="message-content user">
|
||||
<view class="message-bubble user">
|
||||
<text class="message-text">{{ message.content }}</text>
|
||||
</view>
|
||||
<text class="message-time">{{ formatTime(message.timestamp) }}</text>
|
||||
</view>
|
||||
<view class="message-avatar">
|
||||
<view class="user-avatar">
|
||||
<text class="avatar-emoji">😊</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 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 class="pet-avatar">
|
||||
<text class="avatar-emoji">🐱</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="message-content">
|
||||
<view class="message-bubble ai typing">
|
||||
<view class="message-content ai">
|
||||
<view class="message-bubble ai typing-bubble">
|
||||
<view class="typing-dots">
|
||||
<view class="dot"></view>
|
||||
<view class="dot"></view>
|
||||
<view class="dot"></view>
|
||||
<view class="typing-dot"></view>
|
||||
<view class="typing-dot"></view>
|
||||
<view class="typing-dot"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 滚动锚点 -->
|
||||
<view id="scroll-bottom" style="height: 1rpx;"></view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 输入区域 -->
|
||||
<view class="chat-input-area">
|
||||
<view class="input-container">
|
||||
<u-input
|
||||
v-model="inputMessage"
|
||||
placeholder="和你的宠物说点什么..."
|
||||
:border="false"
|
||||
:custom-style="inputStyle"
|
||||
@confirm="sendMessage"
|
||||
confirm-type="send"
|
||||
></u-input>
|
||||
<view class="input-actions">
|
||||
<u-button
|
||||
type="primary"
|
||||
size="mini"
|
||||
text="发送"
|
||||
@click="sendMessage"
|
||||
:disabled="!inputMessage.trim()"
|
||||
></u-button>
|
||||
<view class="input-wrapper">
|
||||
<input
|
||||
v-model="inputMessage"
|
||||
placeholder="和你的宠物说点什么..."
|
||||
class="message-input"
|
||||
@confirm="sendMessage"
|
||||
confirm-type="send"
|
||||
maxlength="500"
|
||||
/>
|
||||
</view>
|
||||
<view class="send-button" @click="sendMessage" :class="{ active: inputMessage.trim() }">
|
||||
<text class="send-text">发送</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
|
@ -122,6 +133,7 @@ export default {
|
|||
messageList: [],
|
||||
inputMessage: '',
|
||||
scrollTop: 0,
|
||||
scrollIntoView: '',
|
||||
aiTyping: false,
|
||||
showMenu: false,
|
||||
inputStyle: {
|
||||
|
|
@ -242,7 +254,12 @@ export default {
|
|||
// 滚动到底部
|
||||
scrollToBottom() {
|
||||
this.$nextTick(() => {
|
||||
this.scrollTop = 99999
|
||||
// 使用scroll-into-view滚动到底部锚点
|
||||
this.scrollIntoView = 'scroll-bottom'
|
||||
// 同时设置scrollTop作为备用方案
|
||||
setTimeout(() => {
|
||||
this.scrollTop = Math.random() * 1000 + 99999 // 每次不同的值确保触发
|
||||
}, 100)
|
||||
})
|
||||
},
|
||||
|
||||
|
|
@ -287,99 +304,232 @@ export default {
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.pet-chat-container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #f8f9fa;
|
||||
background: linear-gradient(135deg, #FF8A80 0%, #FFB6C1 25%, #FECFEF 50%, #F8BBD9 100%);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.pet-info-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20rpx);
|
||||
margin: 20rpx 30rpx 0 30rpx;
|
||||
border-radius: 24rpx;
|
||||
padding: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 15px 20px;
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
box-shadow: 0 8rpx 32rpx rgba(255, 138, 128, 0.2);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.3);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
|
||||
.pet-avatar-wrapper {
|
||||
margin-right: 24rpx;
|
||||
|
||||
.pet-avatar {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
background: linear-gradient(135deg, #FF8A80, #FFB6C1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 8rpx 24rpx rgba(255, 138, 128, 0.3);
|
||||
|
||||
.avatar-emoji {
|
||||
font-size: 40rpx;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pet-info-text {
|
||||
flex: 1;
|
||||
margin-left: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
|
||||
.pet-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.pet-status {
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.chat-status {
|
||||
margin-left: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
|
||||
.status-dot {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 50%;
|
||||
background: #4CAF50;
|
||||
box-shadow: 0 0 0 4rpx rgba(76, 175, 80, 0.2);
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 24rpx;
|
||||
color: #4CAF50;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
padding: 0 15px;
|
||||
position: absolute;
|
||||
top: 140rpx; /* 宠物信息卡片的高度 */
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 120rpx; /* 输入框的高度 */
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chat-scroll {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.message-list {
|
||||
padding: 20px 0;
|
||||
padding: 20rpx 0;
|
||||
padding-bottom: 80rpx; /* 为输入框留出空间 */
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
margin-bottom: 24rpx;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.message-item.user {
|
||||
justify-content: flex-end;
|
||||
padding: 0 30rpx 0 80rpx; /* 右边距30rpx,左边距80rpx给AI消息留空间 */
|
||||
}
|
||||
|
||||
.message-item.ai {
|
||||
justify-content: flex-start;
|
||||
padding: 0 80rpx 0 30rpx; /* 左边距30rpx,右边距80rpx给用户消息留空间 */
|
||||
}
|
||||
|
||||
.message-avatar {
|
||||
margin: 0 10px;
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
margin: 0 16rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pet-avatar, .user-avatar {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.pet-avatar {
|
||||
background: linear-gradient(135deg, #FF8A80, #FFB6C1);
|
||||
box-shadow: 0 8rpx 24rpx rgba(255, 138, 128, 0.3);
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
background: linear-gradient(135deg, #81C784, #A5D6A7);
|
||||
box-shadow: 0 8rpx 24rpx rgba(129, 199, 132, 0.3);
|
||||
}
|
||||
|
||||
.avatar-emoji {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 70%;
|
||||
max-width: 65%;
|
||||
}
|
||||
|
||||
.message-content.user {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.message-content.ai {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.message-bubble {
|
||||
padding: 12px 16px;
|
||||
border-radius: 18px;
|
||||
padding: 20rpx 24rpx;
|
||||
border-radius: 24rpx;
|
||||
word-wrap: break-word;
|
||||
|
||||
&.typing {
|
||||
padding: 16px;
|
||||
}
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.message-bubble.ai {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10rpx);
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
|
||||
border-bottom-left-radius: 8rpx;
|
||||
}
|
||||
|
||||
.message-bubble.ai.typing-bubble {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.message-bubble.user {
|
||||
background: linear-gradient(135deg, #FF8A80 0%, #FFB6C1 100%);
|
||||
box-shadow: 0 4rpx 16rpx rgba(255, 138, 128, 0.3);
|
||||
border-bottom-right-radius: 8rpx;
|
||||
}
|
||||
|
||||
.message-text {
|
||||
font-size: 28rpx;
|
||||
line-height: 1.4;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.message-bubble.user .message-text {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
margin-top: 5px;
|
||||
text-align: center;
|
||||
margin-top: 8rpx;
|
||||
font-size: 20rpx;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.message-content.user .message-time {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.message-content.ai .message-time {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.typing-dots {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
|
||||
.dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
gap: 8rpx;
|
||||
justify-content: center;
|
||||
|
||||
.typing-dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
border-radius: 50%;
|
||||
background-color: #ccc;
|
||||
background-color: #FF8A80;
|
||||
animation: typing 1.4s infinite ease-in-out;
|
||||
|
||||
|
||||
&:nth-child(1) { animation-delay: -0.32s; }
|
||||
&:nth-child(2) { animation-delay: -0.16s; }
|
||||
}
|
||||
|
|
@ -397,20 +547,71 @@ export default {
|
|||
}
|
||||
|
||||
.chat-input-area {
|
||||
background-color: #ffffff;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
padding: 15px 20px;
|
||||
padding-bottom: calc(15px + env(safe-area-inset-bottom));
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20rpx);
|
||||
padding: 16rpx 20rpx;
|
||||
padding-bottom: calc(16rpx + env(safe-area-inset-bottom));
|
||||
border-top: 1rpx solid rgba(255, 255, 255, 0.2);
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
gap: 16rpx;
|
||||
|
||||
.input-actions {
|
||||
flex-shrink: 0;
|
||||
.input-wrapper {
|
||||
flex: 1;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 28rpx;
|
||||
padding: 0 20rpx;
|
||||
backdrop-filter: blur(10rpx);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.3);
|
||||
height: 56rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.message-input {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.send-button {
|
||||
padding: 12rpx 20rpx;
|
||||
border-radius: 24rpx;
|
||||
background: rgba(200, 200, 200, 0.5);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.send-button.active {
|
||||
background: linear-gradient(135deg, #FF8A80 0%, #FFB6C1 100%);
|
||||
box-shadow: 0 4rpx 16rpx rgba(255, 138, 128, 0.4);
|
||||
}
|
||||
|
||||
.send-button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.send-text {
|
||||
font-size: 26rpx;
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.send-button:not(.active) .send-text {
|
||||
color: #cccccc;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-menu {
|
||||
|
|
|
|||
|
|
@ -1,644 +0,0 @@
|
|||
<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>
|
||||
Loading…
Reference in New Issue