Files
aishangjia-uniapp/components/ai-chat-popup/ai-chat-popup.vue
赵忠林 4d3503f47e feat(house): 添加 AI 找房助手功能
- 新增 AI 找房对话弹窗组件,实现房源信息智能问答
- 房源详情页面新增 AI 找房入口按钮,点击弹出聊天窗口
- AI 聊天弹窗支持用户输入提问和快速问题按钮
- 集成豆包 Seed 2.0 Pro 模型调用,实现房源详情智能解答
- 增加聊天消息列表和打字动画,提升交互体验
- 关闭按钮和输入框交互优化,确保流畅使用体验
- 代码结构和样式规范化,保持界面美观一致
2026-05-01 11:33:22 +08:00

456 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view>
<u-popup v-model="showPopup" mode="bottom" border-radius="24" :safe-area-inset-bottom="true" height="75%">
<view class="ai-chat-container">
<!-- 顶部标题栏 -->
<view class="ai-header">
<view class="header-left">
<u-icon name="chat" color="#3f72f4" size="20"></u-icon>
<text class="header-title">AI找房助手</text>
</view>
<view class="header-right" @click="closePopup">
<u-icon name="close" color="#999999" size="20"></u-icon>
</view>
</view>
<!-- 消息列表 -->
<scroll-view
scroll-y
class="message-list"
:scroll-into-view="lastMsgId"
:show-scrollbar="false"
>
<!-- 欢迎消息 -->
<view v-if="messages.length === 0" class="welcome-box">
<view class="welcome-icon">🏠</view>
<view class="welcome-title">您好我是AI找房助手</view>
<view class="welcome-desc">我可以帮您了解这套房源的详细信息分析性价比解答入住疑问等请问有什么可以帮您</view>
<view class="quick-questions">
<view class="quick-btn" @click="quickAsk('这套房性价比怎么样?')">性价比分析</view>
<view class="quick-btn" @click="quickAsk('周边的交通方便吗?')">交通情况</view>
<view class="quick-btn" @click="quickAsk('附近有什么配套设施?')">配套设施</view>
</view>
</view>
<!-- 消息气泡 -->
<view
v-for="(msg, index) in messages"
:key="index"
:id="'msg-' + index"
:class="['msg-row', msg.role === 'user' ? 'msg-right' : 'msg-left']"
>
<!-- AI头像 -->
<view v-if="msg.role !== 'user'" class="msg-avatar">
<text class="avatar-text">AI</text>
</view>
<!-- 气泡 -->
<view :class="['msg-bubble', msg.role === 'user' ? 'bubble-user' : 'bubble-ai']">
<text class="bubble-text" user-select>{{ msg.content }}</text>
</view>
<!-- 用户头像 -->
<view v-if="msg.role === 'user'" class="msg-avatar user-avatar">
<text class="avatar-text"></text>
</view>
</view>
<!-- AI 打字指示器 -->
<view v-if="loading" class="msg-row msg-left">
<view class="msg-avatar">
<text class="avatar-text">AI</text>
</view>
<view class="msg-bubble bubble-ai">
<view class="typing-indicator">
<view class="dot"></view>
<view class="dot"></view>
<view class="dot"></view>
</view>
</view>
</view>
<view style="height: 20rpx;"></view>
</scroll-view>
<!-- 底部输入栏 -->
<view class="input-bar">
<view class="input-wrap">
<input
class="chat-input"
v-model="inputText"
placeholder="问我关于这套房的任何问题..."
confirm-type="send"
@confirm="sendMessage"
:adjust-position="true"
:cursor-spacing="20"
/>
</view>
<view class="send-btn" @click="sendMessage" :class="{ 'send-btn-active': inputText.trim() && !loading }">
<text class="send-text">发送</text>
</view>
</view>
</view>
</u-popup>
</view>
</template>
<script>
export default {
name: 'ai-chat-popup',
props: {
value: {
type: Boolean,
default: false
},
houseInfo: {
type: Object,
default: () => ({})
},
houseId: {
type: [String, Number],
default: ''
}
},
data() {
return {
inputText: '',
messages: [],
loading: false,
lastMsgId: '',
apiKey: 'ark-9cf70df3-0b36-4f0f-be3b-5d4f572c48b4-d0735',
apiUrl: 'https://ark.cn-beijing.volces.com/api/v3/chat/completions',
model: 'doubao-seed-2-0-pro-260215' // 豆包 Seed 2.0 Pro 模型
}
},
computed: {
showPopup: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
},
systemPrompt() {
const info = this.houseInfo || {}
return `你是一名专业的房产找房助手,名叫"AI找房助手",语气亲切专业。
当前用户正在查看的房源信息如下:
- 房源标题:${info.houseTitle || '暂无'}
- 月租:${info.monthlyRent || 0}元/月
- 建筑面积:${info.extent || 0}
- 户型:${info.houseType || '暂无'}
- 楼层:${info.floor || '暂无'}
- 朝向:${info.toward || '暂无'}
- 押付方式:${info.leaseMethod || '暂无'}
- 城市:${info.city || '暂无'}
- 区/县:${info.region || '暂无'}
- 详细地址:${info.address || '暂无'}
- 物业费:${info.propertyFees || '暂无'}元/m²
${info.salePrice ? '- 卖价:' + info.salePrice + '元/m²' : ''}
${info.totalPrice ? '- 总价:' + info.totalPrice + '万元' : ''}
请根据以上信息帮助用户:
1. 解答关于这套房源的任何问题
2. 分析房源性价比
3. 介绍周边配套和交通情况(基于地址信息给出合理建议)
4. 回答入住、签约相关问题
5. 如果用户问其他房源,说明你只能帮助分析当前这套房源。
回答要简洁专业不超过300字。`
}
},
watch: {
showPopup(val) {
if (val) {
// 弹窗打开时可做初始化
this.$nextTick(() => {
this.scrollToBottom()
})
}
}
},
methods: {
closePopup() {
this.showPopup = false
},
quickAsk(text) {
this.inputText = text
this.$nextTick(() => {
this.sendMessage()
})
},
async sendMessage() {
const text = this.inputText.trim()
if (!text || this.loading) return
// 添加用户消息
this.messages.push({ role: 'user', content: text })
this.inputText = ''
this.loading = true
this.scrollToBottom()
try {
await this.callDoubaoAPI(text)
} catch (e) {
this.messages.push({
role: 'assistant',
content: '抱歉,网络出了点问题,请稍后再试~'
})
} finally {
this.loading = false
this.scrollToBottom()
}
},
callDoubaoAPI(userMessage) {
return new Promise((resolve, reject) => {
const messages = [
{ role: 'system', content: this.systemPrompt },
...this.messages.map(m => ({ role: m.role, content: m.content }))
]
uni.request({
url: this.apiUrl,
method: 'POST',
header: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
},
data: {
model: this.model,
messages: messages,
temperature: 0.7,
max_tokens: 500
},
success: (res) => {
if (res.statusCode === 200 && res.data.choices && res.data.choices.length > 0) {
const aiContent = res.data.choices[0].message.content
this.messages.push({ role: 'assistant', content: aiContent })
resolve(aiContent)
} else {
const errMsg = res.data?.error?.message || '请求失败'
this.messages.push({ role: 'assistant', content: `抱歉,${errMsg}` })
reject(new Error(errMsg))
}
},
fail: (err) => {
this.messages.push({
role: 'assistant',
content: '网络连接失败,请检查网络后重试~'
})
reject(err)
}
})
})
},
scrollToBottom() {
this.$nextTick(() => {
const len = this.messages.length
if (len > 0) {
this.lastMsgId = 'msg-' + (len - 1)
}
})
}
}
}
</script>
<style scoped lang="scss">
.ai-chat-container {
display: flex;
flex-direction: column;
height: 100%;
background-color: #f5f6fa;
}
/* 顶部标题栏 */
.ai-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 30rpx;
background-color: #ffffff;
border-bottom: 1rpx solid #eeeeee;
border-radius: 24rpx 24rpx 0 0;
}
.header-left {
display: flex;
align-items: center;
gap: 12rpx;
}
.header-title {
font-size: 32rpx;
font-weight: 500;
color: #333333;
}
.header-right {
padding: 10rpx;
}
/* 消息列表 */
.message-list {
flex: 1;
padding: 20rpx 24rpx;
overflow-y: auto;
}
/* 欢迎界面 */
.welcome-box {
display: flex;
flex-direction: column;
align-items: center;
padding: 40rpx 30rpx;
}
.welcome-icon {
font-size: 80rpx;
margin-bottom: 20rpx;
}
.welcome-title {
font-size: 34rpx;
font-weight: 500;
color: #333333;
margin-bottom: 16rpx;
}
.welcome-desc {
font-size: 26rpx;
color: #888888;
text-align: center;
line-height: 1.6;
margin-bottom: 30rpx;
}
.quick-questions {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 16rpx;
}
.quick-btn {
background-color: #eef4ff;
color: #3f72f4;
font-size: 24rpx;
padding: 12rpx 24rpx;
border-radius: 24rpx;
}
/* 消息行 */
.msg-row {
display: flex;
align-items: flex-start;
margin-bottom: 24rpx;
}
.msg-right {
justify-content: flex-end;
}
.msg-left {
justify-content: flex-start;
}
/* 头像 */
.msg-avatar {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
background: linear-gradient(135deg, #3f72f4, #6a9eff);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.user-avatar {
background: linear-gradient(135deg, #ff8a4c, #ffb347);
}
.avatar-text {
color: #ffffff;
font-size: 22rpx;
font-weight: 500;
}
/* 气泡 */
.msg-bubble {
max-width: 520rpx;
padding: 20rpx 24rpx;
border-radius: 20rpx;
margin: 0 16rpx;
word-break: break-all;
}
.bubble-user {
background-color: #3f72f4;
border-top-right-radius: 6rpx;
}
.bubble-ai {
background-color: #ffffff;
border-top-left-radius: 6rpx;
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04);
}
.bubble-text {
font-size: 28rpx;
line-height: 1.6;
color: #333333;
}
.bubble-user .bubble-text {
color: #ffffff;
}
/* 打字指示器 */
.typing-indicator {
display: flex;
align-items: center;
gap: 8rpx;
padding: 8rpx 0;
}
.dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background-color: #cccccc;
animation: typing 1.2s infinite ease-in-out;
}
.dot:nth-child(2) {
animation-delay: 0.2s;
}
.dot:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes typing {
0%, 60%, 100% {
transform: translateY(0);
opacity: 0.4;
}
30% {
transform: translateY(-10rpx);
opacity: 1;
}
}
/* 底部输入栏 */
.input-bar {
display: flex;
align-items: center;
padding: 16rpx 20rpx;
padding-bottom: calc(16rpx + env(safe-area-inset-bottom));
background-color: #ffffff;
border-top: 1rpx solid #eeeeee;
}
.input-wrap {
flex: 1;
background-color: #f5f6fa;
border-radius: 36rpx;
padding: 12rpx 24rpx;
margin-right: 16rpx;
}
.chat-input {
font-size: 28rpx;
width: 100%;
}
.send-btn {
padding: 14rpx 28rpx;
border-radius: 36rpx;
background-color: #cccccc;
transition: background-color 0.2s;
}
.send-btn-active {
background-color: #3f72f4;
}
.send-text {
font-size: 26rpx;
color: #ffffff;
font-weight: 500;
}
</style>