This commit is contained in:
yvan 2025-08-13 09:00:38 +08:00
parent bd1c7a713f
commit 4296226396
3 changed files with 319 additions and 769 deletions

View File

@ -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"
}
},

View File

@ -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 {

View File

@ -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>