<script setup lang="ts">
import {ref, nextTick, computed} from 'vue'
definePage({
name: 'AI',
style: {
navigationBarTitleText: 'AI',
navigationStyle: 'custom'
},
})
// 弹出框
const show = ref(false)
const onClickLeft = () => {
uni.navigateBack()
}
// ai 模型选择
const aiForm = ref({
model: 'DeepSeek',
iconUrl: '/static/AI/deepseek.png',
width: 35,
height: 22,
})
// ai 模型列表
const aiModel = ref([{
id: 1,
name: 'DeepSeek V3.2 Think',
iconUrl: '/static/AI/deepseek.png',
width: 35,
height: 22,
}, {
id: 2,
name: 'DeepSeek V3.1 Think',
iconUrl: '/static/AI/deepseek.png',
width: 35,
height: 22,
}, {
id: 3,
name: 'ERNIE 4.5 Turbo VL',
iconUrl: '/static/AI/ernie.png',
width: 22,
height: 22,
}, {
id: 4,
name: 'ERNIE X1.1',
iconUrl: '/static/AI/ernie.png',
width: 22,
height: 22,
}])
// 选择模型
const selectModel = () => {
show.value = true
}
// 变更模型
const changeModel = (value: any) => {
aiForm.value.model = value.name
aiForm.value.iconUrl = value.iconUrl
aiForm.value.width = value.width
aiForm.value.height = value.height
show.value = false
}
// 聊天消息列表
const messages = ref<any>([
{role: 'ai', content: '你好!我是你的 AI 助手,请问有什么可以帮您?'}
])
// 输入框内容
const inputMessage = ref('')
// 是否正在加载 AI 回复
const loading = ref(false)
// 自动滚动到最新消息
const scrollIntoView = computed(() => {
return `msg-${messages.value.length - 1}`
})
// 发送消息
const handleSend = async () => {
if (!inputMessage.value.trim() || loading.value) return
// 添加用户消息
messages.value.push({
role: 'user',
content: inputMessage.value
})
inputMessage.value = ''
// 显示加载状态
loading.value = true
// 确保视图更新后滚动到底部
await nextTick()
scrollToBottom()
// 模拟调用 AI 接口
try {
const response = await callAiApi(messages.value.slice(-1)[0].content)
messages.value.push({
role: 'ai',
content: response
})
} catch (error) {
messages.value.push({
role: 'ai',
content: '抱歉,AI 服务暂时不可用,请稍后再试。'
})
} finally {
loading.value = false
await nextTick()
scrollToBottom()
}
}
// 模拟 AI 接口请求(实际项目中替换为真实 API)
const callAiApi = (prompt: any) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(`这是对“${prompt}”的回答。AI正在模拟中...`)
}, 1200)
})
// 实际开发中应使用如下方式:
// return new Promise((resolve, reject) => {
// uni.request({
// url: 'https://your-ai-api.com/chat',
// method: 'POST',
// data: {prompt},
// success: (res) => {
// if (res.statusCode === 200) {
// resolve(res.data.response)
// } else {
// reject(new Error('AI request failed'))
// }
// },
// fail: reject
// })
// })
}
// 滚动到底部
const scrollToBottom = () => {
return nextTick()
}
// 弹出框关闭时清空
const handleClose = () => {
show.value = false
}
</script>
<template>
<wd-navbar custom-class="ai-navbar" fixed
custom-style="background-color: transparent !important; height: 140rpx !important;"
safeAreaInsetTop>
<template #left>
<view class="left-button" @click="onClickLeft">
<wd-icon name="arrow-left1" size="22px"></wd-icon>
</view>
</template>
<template #title>
<view class="title-box" @click="selectModel">
<wd-img :width="aiForm.width" :height="aiForm.height" :src="aiForm.iconUrl"/>
<span class="title-text">{{ aiForm.model }}</span>
<wd-icon name="caret-down-small" size="18px"></wd-icon>
</view>
</template>
</wd-navbar>
<view class="container">
<view class="chat-container">
<!-- 消息列表 -->
<scroll-view
class="message-list"
scroll-y
:scroll-into-view="scrollIntoView"
>
<!-- 单条消息 -->
<view
v-for="(msg, index) in messages"
:key="index"
:id="'msg-' + index"
class="message-item"
>
<!-- 用户消息:右对齐 -->
<view v-if="msg.role === 'user'" class="user-message">
<text class="message-text">{{ msg.content }}</text>
</view>
<!-- AI 消息:左对齐,带头像 -->
<view v-else class="ai-message">
<image src="/static/avatar.png" class="avatar"/>
<text class="message-text">{{ msg.content }}</text>
</view>
</view>
<!-- AI 正在输入状态 -->
<view v-if="loading" class="ai-message">
<image src="/static/ai-avatar.png" class="avatar"/>
<text class="message-text">AI思考中...</text>
</view>
</scroll-view>
<!-- 输入区域 -->
<view class="input-area">
<input
v-model="inputMessage"
class="input-field"
type="text"
placeholder="请输入您的问题..."
@confirm="handleSend"
/>
<!-- <wd-input type="text" v-model="inputMessage" placeholder="请输入用户名" @change="handleSend"/>-->
<!-- <button class="send-btn" size="mini" type="primary" @click="handleSend">发送</button>-->
</view>
</view>
</view>
<wd-popup v-model="show" position="top" transition="fade-down"
custom-style="border-radius:32rpx;top:200rpx;left:40rpx;right:40rpx;padding-bottom: 30rpx;"
@close="handleClose">
<wd-card title="模型选择" class="model-container">
<view v-for="item in aiModel" :key="item.id" class="model-item" @click="changeModel(item)">
<wd-img :width="item.width" :height="item.height" :src="item.iconUrl"/>
<span class="title-text">{{ item.name }}</span>
</view>
</wd-card>
</wd-popup>
</template>
<style scoped lang="scss">
:deep(.wd-navbar.is-border:after) {
background: none !important;
}
.left-button {
border-radius: 50px;
height: 80rpx;
width: 80rpx;
background-color: rgba(172, 172, 172, 0.13);
display: flex;
align-items: center;
justify-content: center;
}
.title-box {
display: flex;
height: 100%;
align-items: center;
justify-content: center;
}
.title-text {
margin-left: 10rpx;
font-size: 28rpx;
}
.model-container {
width: 500rpx;
height: 460rpx;
display: flex;
justify-content: center;
align-items: center;
font-size: 40rpx;
border-radius: 32rpx;
}
.model-item {
height: 100rpx;
border-radius: 15rpx;
margin-bottom: 20rpx;
border: #9e56f6 1rpx solid;
display: flex;
align-items: center;
justify-content: center;
}
.container {
margin-top: calc(140rpx + 54px);
}
:deep(.page-wraper) {
min-height: 0 !important;
}
/* 聊天区域 */
/* 根容器:全屏高度,弹性布局垂直排列 */
.chat-container {
display: flex;
flex-direction: column;
width: 100%;
height: calc(100vh - 140rpx - 54px);
background-color: #f5f5f5;
overflow-x: hidden;
box-sizing: border-box;
}
/* 消息列表区域:自动填充剩余空间 */
.message-list {
flex: 1; /* 自动伸缩以填满父容器减去输入框的高度 */
padding: 20rpx;
overflow-y: auto; /* 垂直滚动 */
overflow-x: hidden; /* 防止意外横向滚动 */
box-sizing: border-box;
}
/* 每一条消息容器 */
.message-item {
display: flex;
margin-bottom: 15rpx;
width: 100%; /* 防止溢出 */
box-sizing: border-box;
}
/* 用户消息气泡:右对齐 */
.user-message {
margin-left: auto; /* 推到右侧 */
color: white;
padding: 16rpx 24rpx;
border-radius: 30rpx 30rpx 0 30rpx; /* 右下角无弧度 */
max-width: 70%; /* 最大宽度限制 */
word-wrap: break-word; /* 自动换行 */
word-break: break-word; /* 强制拆分长单词 */
font-size: 28rpx;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.1);
}
/* AI 消息整体容器:左对齐,含头像和文本 */
.ai-message {
display: flex;
align-items: flex-start;
gap: 16rpx; /* 头像与文字间距 */
max-width: 100%;
}
/* AI 头像样式 */
.avatar {
width: 60rpx;
height: 60rpx;
border-radius: 50%; /* 圆形 */
background-color: #ddd;
flex-shrink: 0; /* 防止被压缩 */
}
/* 所有消息文本统一类名 */
.message-text {
background-color: white;
padding: 16rpx 24rpx;
border-radius: 30rpx 30rpx 30rpx 0; /* 左下角无弧度 */
max-width: 70%;
word-wrap: break-word;
word-break: break-word;
font-size: 28rpx;
color: #333;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.05);
line-height: 1.5;
}
/* 输入区域:固定在底部,留出安全区 */
.input-area {
display: flex;
align-items: center;
padding: 20rpx;
position: relative;
z-index: 999;
width: 100%;
box-sizing: border-box;
/* 安全区适配:iOS 设备底部留白 */
padding-bottom: calc(env(safe-area-inset-bottom) + 20rpx);
/* 或使用 constant,在老版本 iOS 中兼容 */
padding-bottom: calc(constant(safe-area-inset-bottom) + 20rpx);
padding-bottom: calc(env(safe-area-inset-bottom, 20rpx) + 20rpx);
}
/* 输入框样式(可自由调整) */
.input-field {
flex: 1;
height: 150rpx;
line-height: 80rpx;
border: 1rpx solid #da34ff;
border-radius: 25rpx;
padding: 0 30rpx;
font-size: 28rpx;
color: #333;
max-width: 100%;
margin-right: 20rpx;
overflow: hidden;
word-break: break-word;
}
/* 发送按钮 */
.send-btn {
width: 120rpx;
height: 80rpx;
font-size: 28rpx;
background-color: #007aff;
color: white;
border-radius: 40rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0; /* 防止被挤压 */
}
</style>
添加用户头像,用户消息用一层气泡,不要使用多层,输入框输入后由键盘的完成键发送消息