|
|
|
|
@ -0,0 +1,508 @@
|
|
|
|
|
<!--
|
|
|
|
|
* @Author: chris
|
|
|
|
|
* @Date: 2025-11-03 09:54:47
|
|
|
|
|
* @LastEditors: chris
|
|
|
|
|
* @LastEditTime: 2025-11-05 10:23:37
|
|
|
|
|
-->
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { nextTick, onMounted, ref } from 'vue'
|
|
|
|
|
|
|
|
|
|
// 聊天消息类型定义
|
|
|
|
|
interface Message {
|
|
|
|
|
id: number
|
|
|
|
|
type: 'user' | 'ai'
|
|
|
|
|
content: string
|
|
|
|
|
timestamp: string
|
|
|
|
|
isTyping?: boolean
|
|
|
|
|
isImage?: boolean
|
|
|
|
|
imageUrl?: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { proxy } = getCurrentInstance()
|
|
|
|
|
const query = uni.createSelectorQuery().in(proxy)
|
|
|
|
|
|
|
|
|
|
// 聊天状态管理
|
|
|
|
|
const messages = ref<Message[]>([])
|
|
|
|
|
const inputMessage = ref('')
|
|
|
|
|
const isTyping = ref(false)
|
|
|
|
|
const chatContainerRef = ref<HTMLElement>(null)
|
|
|
|
|
const scrollTop = ref(0)
|
|
|
|
|
|
|
|
|
|
// 系统提示信息
|
|
|
|
|
const systemPrompts = [
|
|
|
|
|
'我是您的农业AI助手,请问有什么可以帮助您的?',
|
|
|
|
|
'您可以咨询关于农作物病虫害防治、种植技术或土壤改良等问题',
|
|
|
|
|
'需要了解特定作物的生长周期或最佳种植时间吗?',
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
// 模拟AI回复
|
|
|
|
|
const aiResponses = [
|
|
|
|
|
'根据您的描述,这可能是水稻稻瘟病的症状。建议使用75%三环唑可湿性粉剂进行防治,每亩用量约20-30克,加水喷雾。',
|
|
|
|
|
'当前季节适合种植的作物有:小麦、油菜、豌豆等越冬作物。建议根据当地土壤条件选择适合的品种。',
|
|
|
|
|
'土壤改良的方法有多种,包括:增施有机肥、合理轮作、种植绿肥作物等。您可以根据土壤检测结果制定具体改良方案。',
|
|
|
|
|
'病虫害防治应遵循"预防为主,综合防治"的原则。建议加强田间管理,定期巡查,发现问题及时处理。',
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
// 滚动到底部
|
|
|
|
|
function scrollToBottom() {
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
query.select('#chat-scroll-container').scrollOffset((res) => {
|
|
|
|
|
const scrollHeight = res?.scrollHeight || 0
|
|
|
|
|
console.log('scrollHeight', scrollHeight)
|
|
|
|
|
scrollTop.value = scrollHeight
|
|
|
|
|
}).exec()
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 发送消息
|
|
|
|
|
function sendMessage() {
|
|
|
|
|
if (!inputMessage.value.trim())
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
const newMessage: Message = {
|
|
|
|
|
id: Date.now(),
|
|
|
|
|
type: 'user',
|
|
|
|
|
content: inputMessage.value.trim(),
|
|
|
|
|
timestamp: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
messages.value.push(newMessage)
|
|
|
|
|
inputMessage.value = ''
|
|
|
|
|
scrollToBottom()
|
|
|
|
|
|
|
|
|
|
// 模拟AI回复
|
|
|
|
|
simulateAIResponse()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 模拟AI回复过程
|
|
|
|
|
async function simulateAIResponse() {
|
|
|
|
|
isTyping.value = true
|
|
|
|
|
|
|
|
|
|
// 创建打字中消息
|
|
|
|
|
const typingMessage: Message = {
|
|
|
|
|
id: Date.now() + 1,
|
|
|
|
|
type: 'ai',
|
|
|
|
|
content: '',
|
|
|
|
|
timestamp: '',
|
|
|
|
|
isTyping: true,
|
|
|
|
|
}
|
|
|
|
|
messages.value.push(typingMessage)
|
|
|
|
|
scrollToBottom()
|
|
|
|
|
|
|
|
|
|
// 模拟思考时间
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 1000 + Math.random() * 2000))
|
|
|
|
|
|
|
|
|
|
// 替换为实际回复
|
|
|
|
|
const responseMessage: Message = {
|
|
|
|
|
id: typingMessage.id,
|
|
|
|
|
type: 'ai',
|
|
|
|
|
content: aiResponses[Math.floor(Math.random() * aiResponses.length)],
|
|
|
|
|
timestamp: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
messages.value = messages.value.map(msg =>
|
|
|
|
|
msg.id === typingMessage.id ? responseMessage : msg,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
isTyping.value = false
|
|
|
|
|
scrollToBottom()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 发送图片消息
|
|
|
|
|
function sendImg() {
|
|
|
|
|
// #ifdef MP-WEIXIN || MP-ALIPAY || APP
|
|
|
|
|
uni.chooseImage({
|
|
|
|
|
count: 1,
|
|
|
|
|
sizeType: ['compressed'],
|
|
|
|
|
sourceType: ['album', 'camera'],
|
|
|
|
|
success: (res) => {
|
|
|
|
|
if (res.tempFilePaths && res.tempFilePaths.length > 0) {
|
|
|
|
|
const imagePath = res.tempFilePaths[0]
|
|
|
|
|
const newMessage: Message = {
|
|
|
|
|
id: Date.now(),
|
|
|
|
|
type: 'user',
|
|
|
|
|
content: '[图片]',
|
|
|
|
|
timestamp: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }),
|
|
|
|
|
isImage: true,
|
|
|
|
|
imageUrl: imagePath,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
messages.value.push(newMessage)
|
|
|
|
|
// scrollToBottom()
|
|
|
|
|
|
|
|
|
|
// 模拟AI回复图片
|
|
|
|
|
simulateAIResponse()
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
fail: (err) => {
|
|
|
|
|
console.log('选择图片失败:', err)
|
|
|
|
|
// #ifdef MP-WEIXIN
|
|
|
|
|
wx.showToast({
|
|
|
|
|
title: '选择图片失败',
|
|
|
|
|
icon: 'none',
|
|
|
|
|
})
|
|
|
|
|
// #endif
|
|
|
|
|
// #ifdef MP-ALIPAY
|
|
|
|
|
my.showToast({
|
|
|
|
|
content: '选择图片失败',
|
|
|
|
|
type: 'none',
|
|
|
|
|
})
|
|
|
|
|
// #endif
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
// #endif
|
|
|
|
|
// #ifdef H5
|
|
|
|
|
uni.showToast({
|
|
|
|
|
title: 'H5暂不支持发送图片',
|
|
|
|
|
icon: 'none',
|
|
|
|
|
})
|
|
|
|
|
// #endif
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 初始化聊天界面
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
// 显示欢迎消息
|
|
|
|
|
systemPrompts.forEach((prompt, index) => {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
const welcomeMessage: Message = {
|
|
|
|
|
id: index + 1,
|
|
|
|
|
type: 'ai',
|
|
|
|
|
content: prompt,
|
|
|
|
|
timestamp: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }),
|
|
|
|
|
}
|
|
|
|
|
messages.value.push(welcomeMessage)
|
|
|
|
|
scrollToBottom()
|
|
|
|
|
}, index * 800)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
<view class="ai-chat-page">
|
|
|
|
|
<!-- 标题栏 -->
|
|
|
|
|
<view class="chat-header">
|
|
|
|
|
<view class="header-left">
|
|
|
|
|
<wd-icon name="chat" size="24" color="#007aff" />
|
|
|
|
|
<text class="header-title">农业AI助手</text>
|
|
|
|
|
</view>
|
|
|
|
|
<wd-icon name="ellipsis" size="24" color="#666" />
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
<!-- 聊天内容区域 -->
|
|
|
|
|
<scroll-view id="chat-scroll-container" :scroll-y="true" :scroll-with-animation="true" :scroll-top="scrollTop">
|
|
|
|
|
<view ref="chatContainerRef" class="chat-container">
|
|
|
|
|
<view v-if="messages.length === 0" class="no-messages">
|
|
|
|
|
<text>开始与AI助手对话</text>
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
<!-- 消息列表 -->
|
|
|
|
|
<view
|
|
|
|
|
v-for="message in messages"
|
|
|
|
|
:key="message.id"
|
|
|
|
|
class="message-wrapper"
|
|
|
|
|
:class="message.type"
|
|
|
|
|
>
|
|
|
|
|
<!-- 消息内容 -->
|
|
|
|
|
<view class="message-content">
|
|
|
|
|
<view
|
|
|
|
|
class="message-bubble"
|
|
|
|
|
:class="message.type"
|
|
|
|
|
>
|
|
|
|
|
<!-- 打字中动画 -->
|
|
|
|
|
<view v-if="message.isTyping" class="typing-indicator">
|
|
|
|
|
<view class="typing-dot" />
|
|
|
|
|
<view class="typing-dot" />
|
|
|
|
|
<view class="typing-dot" />
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
<!-- 消息文本 -->
|
|
|
|
|
<text v-else-if="!message.isImage" class="message-text">{{ message.content }}</text>
|
|
|
|
|
|
|
|
|
|
<!-- 消息图片 -->
|
|
|
|
|
<wd-img
|
|
|
|
|
v-else
|
|
|
|
|
class="message-image"
|
|
|
|
|
:src="message.imageUrl"
|
|
|
|
|
mode="aspectFill"
|
|
|
|
|
:enable-preview="true"
|
|
|
|
|
:preview-src="message.imageUrl"
|
|
|
|
|
/>
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
<!-- 时间戳 -->
|
|
|
|
|
<text v-if="!message.isTyping" class="message-time">{{ message.timestamp }}</text>
|
|
|
|
|
</view>
|
|
|
|
|
</view>
|
|
|
|
|
</view>
|
|
|
|
|
</scroll-view>
|
|
|
|
|
|
|
|
|
|
<!-- 输入区域 -->
|
|
|
|
|
<view class="input-container">
|
|
|
|
|
<view class="input-wrapper">
|
|
|
|
|
<textarea
|
|
|
|
|
v-model="inputMessage"
|
|
|
|
|
class="chat-input"
|
|
|
|
|
placeholder="请输入您的问题..."
|
|
|
|
|
placeholder-style="color: #999"
|
|
|
|
|
:auto-height="true"
|
|
|
|
|
maxlength="500"
|
|
|
|
|
@confirm="sendMessage"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<wd-button
|
|
|
|
|
class="send-img-button"
|
|
|
|
|
icon="image"
|
|
|
|
|
type="text"
|
|
|
|
|
@click="sendImg"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<!-- 发送按钮 -->
|
|
|
|
|
<wd-button
|
|
|
|
|
custom-class="send-button"
|
|
|
|
|
:round="true"
|
|
|
|
|
:disabled="!inputMessage.trim() || isTyping"
|
|
|
|
|
@click="sendMessage"
|
|
|
|
|
>
|
|
|
|
|
发送
|
|
|
|
|
<!-- <wd-icon name="login" size="20" color="#fff" /> -->
|
|
|
|
|
</wd-button>
|
|
|
|
|
</view>
|
|
|
|
|
</view>
|
|
|
|
|
</view>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
.ai-chat-page {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
height: calc(100vh - 50px - var(--safe-area-inset-bottom));
|
|
|
|
|
background-color: #f8f8f8;
|
|
|
|
|
width: 100%;
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 标题栏样式 */
|
|
|
|
|
.chat-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
background-color: #fff;
|
|
|
|
|
border-bottom: 1px solid #e8e8e8;
|
|
|
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header-left {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header-title {
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: #333;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-container::-webkit-scrollbar {
|
|
|
|
|
width: 6px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-container::-webkit-scrollbar-track {
|
|
|
|
|
background: transparent;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-container::-webkit-scrollbar-thumb {
|
|
|
|
|
background: rgba(0, 0, 0, 0.2);
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 无消息提示 */
|
|
|
|
|
.no-messages {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
align-items: center;
|
|
|
|
|
height: 200px;
|
|
|
|
|
color: #999;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 消息包装器 */
|
|
|
|
|
.message-wrapper {
|
|
|
|
|
display: flex;
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
align-items: flex-end;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message-wrapper.user {
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message-wrapper.ai {
|
|
|
|
|
justify-content: flex-start;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 发送图片按钮样式 */
|
|
|
|
|
.send-img-button {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
width: 40px;
|
|
|
|
|
height: 40px;
|
|
|
|
|
color: #007aff;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message-wrapper.user .avatar {
|
|
|
|
|
order: 2;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message-wrapper.ai .avatar {
|
|
|
|
|
order: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 消息内容区域 */
|
|
|
|
|
.message-content {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
max-width: 70%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message-wrapper.user .message-content {
|
|
|
|
|
align-items: flex-end;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message-wrapper.ai .message-content {
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 消息气泡样式 */
|
|
|
|
|
.message-bubble {
|
|
|
|
|
padding: 10px 14px;
|
|
|
|
|
border-radius: 18px;
|
|
|
|
|
word-wrap: break-word;
|
|
|
|
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
|
|
|
position: relative;
|
|
|
|
|
display: inline-block;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message-bubble.user {
|
|
|
|
|
background-color: #007aff;
|
|
|
|
|
color: #fff;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message-bubble.ai {
|
|
|
|
|
background-color: #fff;
|
|
|
|
|
color: #333;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 消息文本样式 */
|
|
|
|
|
.message-text {
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 消息图片样式 */
|
|
|
|
|
.message-image {
|
|
|
|
|
width: 200px;
|
|
|
|
|
height: 200px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
display: block;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 消息时间样式 */
|
|
|
|
|
.message-time {
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
color: #999;
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
padding: 0 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 打字动画 */
|
|
|
|
|
.typing-indicator {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 4px;
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.typing-dot {
|
|
|
|
|
width: 6px;
|
|
|
|
|
height: 6px;
|
|
|
|
|
background-color: #666;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
animation: typing 1.4s infinite ease-in-out both;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.typing-dot:nth-child(1) {
|
|
|
|
|
animation-delay: -0.32s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.typing-dot:nth-child(2) {
|
|
|
|
|
animation-delay: -0.16s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@keyframes typing {
|
|
|
|
|
0%,
|
|
|
|
|
80%,
|
|
|
|
|
100% {
|
|
|
|
|
transform: scale(0);
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
}
|
|
|
|
|
40% {
|
|
|
|
|
transform: scale(1);
|
|
|
|
|
opacity: 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 输入区域样式 */
|
|
|
|
|
.input-container {
|
|
|
|
|
background-color: #fff;
|
|
|
|
|
border-top: 1px solid #e8e8e8;
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.input-wrapper {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 输入框样式 */
|
|
|
|
|
.chat-input {
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-height: 36px;
|
|
|
|
|
max-height: 100px;
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
background-color: #f5f5f5;
|
|
|
|
|
border: none;
|
|
|
|
|
border-radius: 18px;
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
color: #333;
|
|
|
|
|
resize: none;
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-input:focus {
|
|
|
|
|
outline: none;
|
|
|
|
|
background-color: #e8e8e8;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.send-button) {
|
|
|
|
|
min-width: 120rpx !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 响应式设计 */
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
.message-content {
|
|
|
|
|
max-width: 85%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-container {
|
|
|
|
|
padding: 12px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|