feat: 新增ai助手静态页面;tabbar添加“ai助手”

master
chris 2 weeks ago
parent 13469fcde4
commit 37300e3745

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

@ -65,6 +65,15 @@ export const customTabbarList: CustomTabBarItem[] = [
icon: 'i-carbon-home',
// badge: 'dot',
},
{
pagePath: 'pages/ai/ai',
text: 'AI助手',
// 1在fg-tabbar.vue页面上引入一下并注释掉见tabbar/index.vue代码第2行
// 2配置到 unocss.config.ts 的 safelist 中
iconType: 'unocss',
icon: 'i-carbon-ai',
// badge: 10,
},
{
pagePath: 'pages/me/me',
text: '我的',

@ -1,3 +1,9 @@
/*
* @Author: chris
* @Date: 2025-10-20 10:45:46
* @LastEditors: chris
* @LastEditTime: 2025-11-03 10:14:17
*/
import type {
Preset,
} from 'unocss'
@ -51,7 +57,7 @@ export default defineConfig({
},
],
// 动态图标需要在这里配置或者写在vue页面中注释掉
safelist: ['i-carbon-code', 'i-carbon-home', 'i-carbon-user'],
safelist: ['i-carbon-code', 'i-carbon-home', 'i-carbon-user', 'i-carbon-ai'],
rules: [
[
'p-safe',

Loading…
Cancel
Save