feat(ai-house): 重构找房聊天交互

This commit is contained in:
2026-07-31 20:21:08 +08:00
parent ab32a41bd8
commit c93aac0aa9
3 changed files with 195 additions and 160 deletions

View File

@@ -3,6 +3,14 @@ import http from './index.js';
// AI找房问答 // AI找房问答
export const sendHouseAiMessage = (data) => http.post('/house/ai-chat/message', data) export const sendHouseAiMessage = (data) => http.post('/house/ai-chat/message', data)
// 清空当前AI找房会话
export const clearHouseAiSession = (data) => http.post('/house/ai-chat/session/clear', data)
// 无候选时提交找房咨询线索
export const submitHouseAiLead = (data) => http.post('/house/house-message/ai-agent', data)
export default { export default {
sendHouseAiMessage sendHouseAiMessage,
clearHouseAiSession,
submitHouseAiLead
} }

View File

@@ -33,43 +33,26 @@
<view class="form-title">您想要找什么样的房型</view> <view class="form-title">您想要找什么样的房型</view>
<view class="form-item"> <view class="form-item">
<text class="form-label">多少平方</text> <text class="form-label">多少平方</text>
<input <input v-model="form.extent" class="form-input" type="text" placeholder="请输入面积" :disabled="loading" />
v-model="form.extent"
class="form-input"
type="text"
placeholder="请输入面积"
:disabled="loading"
/>
</view> </view>
<view class="form-item"> <view class="form-item">
<text class="form-label">多少楼层</text> <text class="form-label">多少楼层</text>
<input <input v-model="form.floor" class="form-input" type="text" placeholder="请输入楼层" :disabled="loading" />
v-model="form.floor"
class="form-input"
type="text"
placeholder="请输入楼层"
:disabled="loading"
/>
</view> </view>
<view class="form-item"> <view class="form-item">
<text class="form-label">多少租金</text> <text class="form-label">多少租金</text>
<input <input v-model="form.rent" class="form-input" type="text" placeholder="请输入租金" :disabled="loading" />
v-model="form.rent"
class="form-input"
type="text"
placeholder="请输入租金"
:disabled="loading"
/>
</view> </view>
<view class="form-item"> <view class="form-item">
<text class="form-label">什么地段</text> <text class="form-label">什么地段</text>
<input <input v-model="form.location" class="form-input" type="text" placeholder="请输入地段" :disabled="loading" />
v-model="form.location" </view>
class="form-input" <view
type="text" class="finder-submit"
placeholder="请输入地段" :class="{ 'finder-submit-disabled': loading }"
:disabled="loading" @click="submitFinderForm"
/> >
{{ loading ? '提交中...' : '提交找房需求' }}
</view> </view>
</view> </view>
<view v-else-if="message.type === 'messageForm'" class="message-form-message"> <view v-else-if="message.type === 'messageForm'" class="message-form-message">
@@ -164,13 +147,14 @@
class="house-card" class="house-card"
@click="goHouseDetail(item)" @click="goHouseDetail(item)"
> >
<image class="house-image" :src="item.files[0].url" mode="aspectFill"></image> <image v-if="item.files && item.files.length" class="house-image" :src="item.files[0].url" mode="aspectFill"></image>
<view v-else class="house-image house-image-empty"></view>
<view class="house-info"> <view class="house-info">
<view class="house-title">{{ item.houseTitle }}</view> <view class="house-title">{{ item.houseTitle }}</view>
<view v-if="item.matchReason" class="house-match-reason">{{ item.matchReason }}</view> <view v-if="item.matchReason" class="house-match-reason">{{ item.matchReason }}</view>
<view class="house-location">{{ item.address || item.region || item.city || '位置待补充' }}</view> <view class="house-location">{{ item.address || item.region || item.city || '位置待补充' }}</view>
<view class="house-desc">{{ item.extent }}|{{ item.toward }}</view> <view class="house-desc">{{ item.extent }}|{{ item.toward }}</view>
<view class="house-price">{{ item.monthlyRent }}/</view> <view class="house-price">{{ item.monthlyRent ? item.monthlyRent + '元/月' : (item.totalPrice || item.salePrice || '价格待确认') }}</view>
</view> </view>
</view> </view>
</view> </view>
@@ -189,6 +173,7 @@
<view class="typing-dot"></view> <view class="typing-dot"></view>
<view class="typing-dot"></view> <view class="typing-dot"></view>
<view class="typing-dot"></view> <view class="typing-dot"></view>
<text class="typing-text">{{ progressText || '正在处理中' }}</text>
</view> </view>
</view> </view>
<view id="page-bottom-anchor" class="page-bottom-anchor"></view> <view id="page-bottom-anchor" class="page-bottom-anchor"></view>
@@ -196,15 +181,24 @@
</view> </view>
<view class="input-panel"> <view class="input-panel">
<view class="tag-row"> <view class="quick-question-row">
<view class="finder-tag" :class="{ active: showFinderForm }" @click="toggleFinderForm">找房</view> <view class="finder-tag" :class="{ active: showFinderForm }" @click="toggleFinderForm">找房</view>
<view
v-for="item in quickQuestions"
:key="item.label"
class="quick-question"
:class="{ 'quick-question-disabled': loading }"
@click="askQuickQuestion(item.question)"
>
{{ item.label }}
</view>
</view> </view>
<view class="input-row"> <view class="input-row">
<input <input
v-model="inputText" v-model="inputText"
class="chat-input" class="chat-input"
type="text" type="text"
placeholder="可补充户型、朝向、电梯等偏好" placeholder="输入找房需求或房源相关问题"
:disabled="loading" :disabled="loading"
confirm-type="send" confirm-type="send"
@confirm="handlePrimaryAction" @confirm="handlePrimaryAction"
@@ -228,10 +222,8 @@
</template> </template>
<script> <script>
import * as HouseInfoApi from '@/api/house-info.js'
import * as HouseAiConfigApi from '@/api/house-ai-config.js' import * as HouseAiConfigApi from '@/api/house-ai-config.js'
import * as HouseAiChatApi from '@/api/house-ai-chat.js' import * as HouseAiChatApi from '@/api/house-ai-chat.js'
import * as HouseMessageApi from '@/api/house-message.js'
import storage from '@/utils/storage.js' import storage from '@/utils/storage.js'
import { USER_ID } from '@/store/mutation-types.js' import { USER_ID } from '@/store/mutation-types.js'
import { apiUrl } from '@/config.js' import { apiUrl } from '@/config.js'
@@ -239,19 +231,34 @@
export default { export default {
data() { data() {
return { return {
quickQuestions: [
{
label: '面积和价格',
question: '这套房的面积和价格是多少?'
},
{
label: '物业和水电',
question: '这套房的物业费和水电怎么收费?'
},
{
label: '空调和停车',
question: '这套房有空调和停车位吗?'
}
],
inputText: '', inputText: '',
loading: false, loading: false,
progressText: '',
messages: [], messages: [],
showFinderForm: false, showFinderForm: false,
messageFormOpened: false,
messageSubmitting: false,
messageSubmitted: false,
form: { form: {
extent: '', extent: '',
floor: '', floor: '',
rent: '', rent: '',
location: '' location: ''
}, },
messageFormOpened: false,
messageSubmitting: false,
messageSubmitted: false,
messageForm: { messageForm: {
realName: '', realName: '',
phone: '', phone: '',
@@ -267,29 +274,7 @@
socketConnected: false, socketConnected: false,
heartbeatTimer: null, heartbeatTimer: null,
loginUserId: null, loginUserId: null,
timer: null, nextMessageId: 0
nextMessageId: 0,
allHouses: [],
fallbackHouses: [
{
houseId: 'mock-1',
houseTitle: '富雅国际金融中心',
extent: '75.03',
toward: '东',
monthlyRent: '3376.35',
address: '五象新区总部基地',
files: [{ url: 'https://oss.wsdns.cn/20240708/9e1f58e7fc7b4bad92edd852ff34dbcb.png?x-oss-process=image/resize,w_750/quality,Q_90' }]
},
{
houseId: 'mock-2',
houseTitle: '五象航洋城3号楼',
extent: '180',
toward: '西',
monthlyRent: '5508',
address: '五象新区航洋商圈',
files: [{ url: 'https://oss.wsdns.cn/20240708/c95838b78b5746eb934daffb12a2efdc.png?x-oss-process=image/resize,w_750/quality,Q_90' }]
}
]
} }
}, },
computed: { computed: {
@@ -317,16 +302,58 @@
}, 300) }, 300)
return return
} }
this.conversationId = `house-ai-${this.loginUserId || 'guest'}` this.conversationId = `house-ai-${this.loginUserId}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
this.connectWebSocket() this.connectWebSocket()
this.loadAiConfig() this.loadAiConfig()
this.loadHouseList()
}, },
onUnload() { onUnload() {
this.clearMockTimer() if (this.conversationId) {
HouseAiChatApi.clearHouseAiSession({
conversationId: this.conversationId
}).catch(() => {})
}
this.closeWebSocket() this.closeWebSocket()
}, },
methods: { methods: {
askQuickQuestion(question) {
if (this.loading) {
return
}
this.showFinderForm = false
this.inputText = question
this.sendMessage()
},
toggleFinderForm() {
if (this.loading) {
return
}
this.showFinderForm = !this.showFinderForm
if (this.showFinderForm) {
this.messages = [
this.createMessage('ai', 'text', {
content: this.aiConfig.welcomeMessage || '您好我是AI找房助手请告诉我您的找房需求。'
}),
this.createMessage('ai', 'form')
]
} else {
this.appendWelcomeMessage()
}
this.scrollToBottom()
},
submitFinderForm() {
if (this.loading) {
return
}
const question = this.buildFormQuestion()
if (!question) {
uni.showToast({
title: '请完整填写找房条件',
icon: 'none'
})
return
}
this.sendMessage(question)
},
createMessage(role, type, extra = {}) { createMessage(role, type, extra = {}) {
const id = `msg-${this.nextMessageId++}` const id = `msg-${this.nextMessageId++}`
return { return {
@@ -338,8 +365,8 @@
} }
}, },
startNewChat() { startNewChat() {
this.clearMockTimer()
this.loading = false this.loading = false
this.progressText = ''
this.showFinderForm = false this.showFinderForm = false
this.messageFormOpened = false this.messageFormOpened = false
this.messageSubmitting = false this.messageSubmitting = false
@@ -367,9 +394,6 @@
}) })
] ]
}, },
appendMessageFormCard() {
// this.messages.push(this.createMessage('ai', 'messageForm'))
},
async loadAiConfig() { async loadAiConfig() {
this.aiConfigLoaded = false this.aiConfigLoaded = false
try { try {
@@ -388,26 +412,6 @@
this.aiConfigLoaded = true this.aiConfigLoaded = true
this.startNewChat() this.startNewChat()
this.appendWelcomeMessage() this.appendWelcomeMessage()
this.appendMessageFormCard()
this.scrollToBottom()
},
toggleFinderForm() {
if (this.loading) {
return
}
this.showFinderForm = !this.showFinderForm
if (this.showFinderForm) {
this.messages = [
this.createMessage('ai', 'text', {
content: this.aiConfig.welcomeMessage || '您好我是AI找房助手请告诉我您的找房需求。'
}),
this.createMessage('ai', 'messageForm'),
this.createMessage('ai', 'form')
]
} else {
this.appendWelcomeMessage()
this.appendMessageFormCard()
}
this.scrollToBottom() this.scrollToBottom()
}, },
openMessageForm() { openMessageForm() {
@@ -455,11 +459,11 @@
} }
this.messageSubmitting = true this.messageSubmitting = true
try { try {
await HouseMessageApi.addHouseMessage({ await HouseAiChatApi.submitHouseAiLead({
conversationId: this.conversationId,
realName: this.messageForm.realName.trim(), realName: this.messageForm.realName.trim(),
phone: (this.messageForm.phone || '').trim(), phone: (this.messageForm.phone || '').trim(),
wechat: (this.messageForm.wechat || '').trim(), wechat: (this.messageForm.wechat || '').trim()
source: 'ai_house'
}) })
this.messageSubmitted = true this.messageSubmitted = true
uni.showToast({ uni.showToast({
@@ -475,30 +479,6 @@
this.messageSubmitting = false this.messageSubmitting = false
this.scrollToBottom() this.scrollToBottom()
}, },
async loadHouseList() {
try {
const result = await HouseInfoApi.pageHouseInfo({
page: 1,
recommend: 1,
status: 0
})
const list = (result.data.list || []).map(item => {
let files = []
try {
files = JSON.parse(item.files) || []
} catch (error) {
files = []
}
return {
...item,
files: files.length ? files : this.fallbackHouses[0].files
}
})
this.allHouses = list.length ? list : this.fallbackHouses
} catch (error) {
this.allHouses = this.fallbackHouses
}
},
handlePrimaryAction() { handlePrimaryAction() {
if (this.loading) { if (this.loading) {
this.loading = false this.loading = false
@@ -506,11 +486,10 @@
} }
this.sendMessage() this.sendMessage()
}, },
async sendMessage() { async sendMessage(question = this.buildQuestion()) {
const question = this.buildQuestion()
if (!question) { if (!question) {
uni.showToast({ uni.showToast({
title: this.showFinderForm ? '请输入问题或填写找房条件' : '请输入您的找房需求', title: '请输入您的找房需求',
icon: 'none' icon: 'none'
}) })
return return
@@ -522,26 +501,23 @@
}) })
) )
this.inputText = '' this.inputText = ''
if (question.indexOf('人工') !== -1) {
this.loading = false
this.messages.push(
this.createMessage('ai', 'contact', {
content: '联系客服'
})
)
this.scrollToBottom()
return
}
this.loading = true this.loading = true
this.progressText = '正在分析您的需求'
this.scrollToBottom() this.scrollToBottom()
try { try {
await HouseAiChatApi.sendHouseAiMessage({ const result = await HouseAiChatApi.sendHouseAiMessage({
conversationId: this.conversationId, conversationId: this.conversationId,
userId: this.loginUserId,
question question
}) })
const fallbackResponse = result && result.data
? result.data
: null
if (fallbackResponse && fallbackResponse.type === 'house_ai_result') {
this.handleSocketMessage(JSON.stringify(fallbackResponse))
}
} catch (error) { } catch (error) {
this.loading = false this.loading = false
this.progressText = ''
this.messages.push( this.messages.push(
this.createMessage('ai', 'text', { this.createMessage('ai', 'text', {
content: error.message || 'AI服务暂时不可用请稍后再试。' content: error.message || 'AI服务暂时不可用请稍后再试。'
@@ -551,26 +527,9 @@
} }
}, },
buildQuestion() { buildQuestion() {
const inputQuestion = (this.inputText || '').trim() return (this.inputText || '').trim()
const formQuestion = this.buildFormQuestion()
if (this.showFinderForm && formQuestion) {
if (inputQuestion) {
return `${formQuestion},补充要求:${inputQuestion}`
}
return formQuestion
}
if (inputQuestion) {
return inputQuestion
}
return ''
}, },
buildFormQuestion() { buildFormQuestion() {
if (!this.showFinderForm) {
return ''
}
if (!this.form.extent || !this.form.floor || !this.form.rent || !this.form.location) { if (!this.form.extent || !this.form.floor || !this.form.rent || !this.form.location) {
return '' return ''
} }
@@ -597,25 +556,19 @@
copyInputText() { copyInputText() {
this.copyText((this.inputText || '').trim()) this.copyText((this.inputText || '').trim())
}, },
clearMockTimer() {
if (this.timer) {
clearTimeout(this.timer)
this.timer = null
}
},
normalizeHouseFiles(item) { normalizeHouseFiles(item) {
if (item.files && Array.isArray(item.files)) { if (item.files && Array.isArray(item.files)) {
return item.files.length ? item.files : this.fallbackHouses[0].files return item.files
} }
if (item.files && typeof item.files === 'string') { if (item.files && typeof item.files === 'string') {
try { try {
const files = JSON.parse(item.files) const files = JSON.parse(item.files)
return files.length ? files : this.fallbackHouses[0].files return Array.isArray(files) ? files : []
} catch (error) { } catch (error) {
return this.fallbackHouses[0].files return []
} }
} }
return this.fallbackHouses[0].files return []
}, },
connectWebSocket() { connectWebSocket() {
if (!this.loginUserId) { if (!this.loginUserId) {
@@ -682,6 +635,7 @@
data = JSON.parse(raw) data = JSON.parse(raw)
} catch (error) { } catch (error) {
this.loading = false this.loading = false
this.progressText = ''
this.messages.push( this.messages.push(
this.createMessage('ai', 'text', { this.createMessage('ai', 'text', {
content: raw content: raw
@@ -690,10 +644,15 @@
this.scrollToBottom() this.scrollToBottom()
return return
} }
if (data.type === 'house_ai_progress') {
this.progressText = data.message || '正在处理中'
return
}
if (data.type !== 'house_ai_result') { if (data.type !== 'house_ai_result') {
return return
} }
this.loading = false this.loading = false
this.progressText = ''
if (data.answer) { if (data.answer) {
this.messages.push( this.messages.push(
this.createMessage('ai', 'text', { this.createMessage('ai', 'text', {
@@ -725,6 +684,9 @@
}) })
) )
} }
if (data.showContactForm) {
this.messages.push(this.createMessage('ai', 'messageForm'))
}
this.scrollToBottom() this.scrollToBottom()
}, },
goHouseDetail(item) { goHouseDetail(item) {
@@ -983,6 +945,23 @@
color: #1f2937; color: #1f2937;
} }
.finder-submit {
margin-top: 24rpx;
height: 76rpx;
line-height: 76rpx;
text-align: center;
border-radius: 999rpx;
background: linear-gradient(to right, #3f72f4, #7a9cf3);
color: #ffffff;
font-size: 28rpx;
font-weight: 600;
box-shadow: 0 12rpx 24rpx rgba(63, 114, 244, 0.18);
}
.finder-submit-disabled {
opacity: 0.65;
}
.message-form-message { .message-form-message {
width: 100%; width: 100%;
} }
@@ -1071,6 +1050,10 @@
display: block; display: block;
} }
.house-image-empty {
background: #eef2f6;
}
.house-info { .house-info {
padding: 22rpx 22rpx 24rpx; padding: 22rpx 22rpx 24rpx;
} }
@@ -1129,6 +1112,12 @@
margin-right: 0; margin-right: 0;
} }
.typing-text {
margin-left: 12rpx;
font-size: 24rpx;
color: #667085;
}
.typing-dot:nth-child(2) { .typing-dot:nth-child(2) {
animation-delay: 0.15s; animation-delay: 0.15s;
} }
@@ -1148,6 +1137,31 @@
box-shadow: 0 -8rpx 24rpx rgba(15, 23, 42, 0.06); box-shadow: 0 -8rpx 24rpx rgba(15, 23, 42, 0.06);
} }
.quick-question-row {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
margin-bottom: 16rpx;
}
.quick-question {
display: inline-flex;
align-items: center;
height: 56rpx;
padding: 0 18rpx;
box-sizing: border-box;
border: 1rpx solid #d6e2f7;
border-radius: 12rpx;
background: #f7faff;
color: #3557d6;
font-size: 23rpx;
line-height: 1;
}
.quick-question-disabled {
opacity: 0.55;
}
.tag-row { .tag-row {
margin-bottom: 16rpx; margin-bottom: 16rpx;
} }

View File

@@ -101,6 +101,19 @@
</view> </view>
<view v-if="isManager" class="item col-2" @click="makePhoneCall">业主电话{{ form.phone || '' }}</view> <view v-if="isManager" class="item col-2" @click="makePhoneCall">业主电话{{ form.phone || '' }}</view>
<view class="item col-2">物业费(/){{ form.propertyFees || '' }}</view> <view class="item col-2">物业费(/){{ form.propertyFees || '' }}</view>
<view v-if="form.propertyCompany" class="item col-2">物业公司{{ form.propertyCompany }}</view>
<view v-if="form.waterBillingType || form.waterUnitPrice" class="item col-2">
水费{{ form.waterBillingType || '未提供' }}{{ form.waterUnitPrice ? '' + form.waterUnitPrice + ' /单位' : '' }}
</view>
<view v-if="form.electricityBillingType || form.electricityUnitPrice" class="item col-2">
电费{{ form.electricityBillingType || '未提供' }}{{ form.electricityUnitPrice ? '' + form.electricityUnitPrice + ' /单位' : '' }}
</view>
<view v-if="form.airConditioningAvailable !== null && form.airConditioningAvailable !== undefined" class="item col-2">
空调{{ form.airConditioningAvailable ? '提供' : '不提供' }}{{ form.airConditioningFee ? '' + form.airConditioningFee : '' }}
</view>
<view v-if="form.parkingAvailable !== null && form.parkingAvailable !== undefined" class="item col-2">
停车{{ form.parkingAvailable ? '可停车' : '不可停车' }}{{ form.parkingFee ? '' + form.parkingFee : '' }}
</view>
<!-- <view v-if="isManager" class="item col-2">租期{{ form.tenancy || '' }}</view> --> <!-- <view v-if="isManager" class="item col-2">租期{{ form.tenancy || '' }}</view> -->
<view class="item col-2" v-if="isManager"> <view class="item col-2" v-if="isManager">
佣金{{ form.commission || '' }} 佣金{{ form.commission || '' }}