feat(ai-house): 重构找房聊天交互
This commit is contained in:
@@ -3,6 +3,14 @@ import http from './index.js';
|
||||
// AI找房问答
|
||||
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 {
|
||||
sendHouseAiMessage
|
||||
sendHouseAiMessage,
|
||||
clearHouseAiSession,
|
||||
submitHouseAiLead
|
||||
}
|
||||
|
||||
@@ -33,43 +33,26 @@
|
||||
<view class="form-title">您想要找什么样的房型?</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">多少平方</text>
|
||||
<input
|
||||
v-model="form.extent"
|
||||
class="form-input"
|
||||
type="text"
|
||||
placeholder="请输入面积"
|
||||
:disabled="loading"
|
||||
/>
|
||||
<input v-model="form.extent" class="form-input" type="text" placeholder="请输入面积" :disabled="loading" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">多少楼层</text>
|
||||
<input
|
||||
v-model="form.floor"
|
||||
class="form-input"
|
||||
type="text"
|
||||
placeholder="请输入楼层"
|
||||
:disabled="loading"
|
||||
/>
|
||||
<input v-model="form.floor" class="form-input" type="text" placeholder="请输入楼层" :disabled="loading" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">多少租金</text>
|
||||
<input
|
||||
v-model="form.rent"
|
||||
class="form-input"
|
||||
type="text"
|
||||
placeholder="请输入租金"
|
||||
:disabled="loading"
|
||||
/>
|
||||
<input v-model="form.rent" class="form-input" type="text" placeholder="请输入租金" :disabled="loading" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">什么地段</text>
|
||||
<input
|
||||
v-model="form.location"
|
||||
class="form-input"
|
||||
type="text"
|
||||
placeholder="请输入地段"
|
||||
:disabled="loading"
|
||||
/>
|
||||
<input v-model="form.location" class="form-input" type="text" placeholder="请输入地段" :disabled="loading" />
|
||||
</view>
|
||||
<view
|
||||
class="finder-submit"
|
||||
:class="{ 'finder-submit-disabled': loading }"
|
||||
@click="submitFinderForm"
|
||||
>
|
||||
{{ loading ? '提交中...' : '提交找房需求' }}
|
||||
</view>
|
||||
</view>
|
||||
<view v-else-if="message.type === 'messageForm'" class="message-form-message">
|
||||
@@ -164,13 +147,14 @@
|
||||
class="house-card"
|
||||
@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-title">{{ item.houseTitle }}</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-desc">{{ item.extent }}m²|{{ 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>
|
||||
@@ -189,6 +173,7 @@
|
||||
<view class="typing-dot"></view>
|
||||
<view class="typing-dot"></view>
|
||||
<view class="typing-dot"></view>
|
||||
<text class="typing-text">{{ progressText || '正在处理中' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view id="page-bottom-anchor" class="page-bottom-anchor"></view>
|
||||
@@ -196,15 +181,24 @@
|
||||
</view>
|
||||
|
||||
<view class="input-panel">
|
||||
<view class="tag-row">
|
||||
<view class="quick-question-row">
|
||||
<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 class="input-row">
|
||||
<input
|
||||
v-model="inputText"
|
||||
class="chat-input"
|
||||
type="text"
|
||||
placeholder="可补充户型、朝向、电梯等偏好"
|
||||
placeholder="输入找房需求或房源相关问题"
|
||||
:disabled="loading"
|
||||
confirm-type="send"
|
||||
@confirm="handlePrimaryAction"
|
||||
@@ -228,10 +222,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as HouseInfoApi from '@/api/house-info.js'
|
||||
import * as HouseAiConfigApi from '@/api/house-ai-config.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 { USER_ID } from '@/store/mutation-types.js'
|
||||
import { apiUrl } from '@/config.js'
|
||||
@@ -239,19 +231,34 @@
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
quickQuestions: [
|
||||
{
|
||||
label: '面积和价格',
|
||||
question: '这套房的面积和价格是多少?'
|
||||
},
|
||||
{
|
||||
label: '物业和水电',
|
||||
question: '这套房的物业费和水电怎么收费?'
|
||||
},
|
||||
{
|
||||
label: '空调和停车',
|
||||
question: '这套房有空调和停车位吗?'
|
||||
}
|
||||
],
|
||||
inputText: '',
|
||||
loading: false,
|
||||
progressText: '',
|
||||
messages: [],
|
||||
showFinderForm: false,
|
||||
messageFormOpened: false,
|
||||
messageSubmitting: false,
|
||||
messageSubmitted: false,
|
||||
form: {
|
||||
extent: '',
|
||||
floor: '',
|
||||
rent: '',
|
||||
location: ''
|
||||
},
|
||||
messageFormOpened: false,
|
||||
messageSubmitting: false,
|
||||
messageSubmitted: false,
|
||||
messageForm: {
|
||||
realName: '',
|
||||
phone: '',
|
||||
@@ -267,29 +274,7 @@
|
||||
socketConnected: false,
|
||||
heartbeatTimer: null,
|
||||
loginUserId: null,
|
||||
timer: null,
|
||||
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' }]
|
||||
}
|
||||
]
|
||||
nextMessageId: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -317,17 +302,59 @@
|
||||
}, 300)
|
||||
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.loadAiConfig()
|
||||
this.loadHouseList()
|
||||
},
|
||||
onUnload() {
|
||||
this.clearMockTimer()
|
||||
if (this.conversationId) {
|
||||
HouseAiChatApi.clearHouseAiSession({
|
||||
conversationId: this.conversationId
|
||||
}).catch(() => {})
|
||||
}
|
||||
this.closeWebSocket()
|
||||
},
|
||||
methods: {
|
||||
createMessage(role, type, extra = {}) {
|
||||
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 = {}) {
|
||||
const id = `msg-${this.nextMessageId++}`
|
||||
return {
|
||||
id,
|
||||
@@ -338,8 +365,8 @@
|
||||
}
|
||||
},
|
||||
startNewChat() {
|
||||
this.clearMockTimer()
|
||||
this.loading = false
|
||||
this.progressText = ''
|
||||
this.showFinderForm = false
|
||||
this.messageFormOpened = false
|
||||
this.messageSubmitting = false
|
||||
@@ -367,9 +394,6 @@
|
||||
})
|
||||
]
|
||||
},
|
||||
appendMessageFormCard() {
|
||||
// this.messages.push(this.createMessage('ai', 'messageForm'))
|
||||
},
|
||||
async loadAiConfig() {
|
||||
this.aiConfigLoaded = false
|
||||
try {
|
||||
@@ -388,26 +412,6 @@
|
||||
this.aiConfigLoaded = true
|
||||
this.startNewChat()
|
||||
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()
|
||||
},
|
||||
openMessageForm() {
|
||||
@@ -455,11 +459,11 @@
|
||||
}
|
||||
this.messageSubmitting = true
|
||||
try {
|
||||
await HouseMessageApi.addHouseMessage({
|
||||
await HouseAiChatApi.submitHouseAiLead({
|
||||
conversationId: this.conversationId,
|
||||
realName: this.messageForm.realName.trim(),
|
||||
phone: (this.messageForm.phone || '').trim(),
|
||||
wechat: (this.messageForm.wechat || '').trim(),
|
||||
source: 'ai_house'
|
||||
wechat: (this.messageForm.wechat || '').trim()
|
||||
})
|
||||
this.messageSubmitted = true
|
||||
uni.showToast({
|
||||
@@ -475,30 +479,6 @@
|
||||
this.messageSubmitting = false
|
||||
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() {
|
||||
if (this.loading) {
|
||||
this.loading = false
|
||||
@@ -506,11 +486,10 @@
|
||||
}
|
||||
this.sendMessage()
|
||||
},
|
||||
async sendMessage() {
|
||||
const question = this.buildQuestion()
|
||||
async sendMessage(question = this.buildQuestion()) {
|
||||
if (!question) {
|
||||
uni.showToast({
|
||||
title: this.showFinderForm ? '请输入问题或填写找房条件' : '请输入您的找房需求',
|
||||
title: '请输入您的找房需求',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
@@ -522,26 +501,23 @@
|
||||
})
|
||||
)
|
||||
this.inputText = ''
|
||||
if (question.indexOf('人工') !== -1) {
|
||||
this.loading = false
|
||||
this.messages.push(
|
||||
this.createMessage('ai', 'contact', {
|
||||
content: '联系客服'
|
||||
})
|
||||
)
|
||||
this.scrollToBottom()
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
this.progressText = '正在分析您的需求'
|
||||
this.scrollToBottom()
|
||||
try {
|
||||
await HouseAiChatApi.sendHouseAiMessage({
|
||||
const result = await HouseAiChatApi.sendHouseAiMessage({
|
||||
conversationId: this.conversationId,
|
||||
userId: this.loginUserId,
|
||||
question
|
||||
})
|
||||
const fallbackResponse = result && result.data
|
||||
? result.data
|
||||
: null
|
||||
if (fallbackResponse && fallbackResponse.type === 'house_ai_result') {
|
||||
this.handleSocketMessage(JSON.stringify(fallbackResponse))
|
||||
}
|
||||
} catch (error) {
|
||||
this.loading = false
|
||||
this.progressText = ''
|
||||
this.messages.push(
|
||||
this.createMessage('ai', 'text', {
|
||||
content: error.message || 'AI服务暂时不可用,请稍后再试。'
|
||||
@@ -551,26 +527,9 @@
|
||||
}
|
||||
},
|
||||
buildQuestion() {
|
||||
const inputQuestion = (this.inputText || '').trim()
|
||||
const formQuestion = this.buildFormQuestion()
|
||||
|
||||
if (this.showFinderForm && formQuestion) {
|
||||
if (inputQuestion) {
|
||||
return `${formQuestion},补充要求:${inputQuestion}`
|
||||
}
|
||||
return formQuestion
|
||||
}
|
||||
|
||||
if (inputQuestion) {
|
||||
return inputQuestion
|
||||
}
|
||||
|
||||
return ''
|
||||
return (this.inputText || '').trim()
|
||||
},
|
||||
buildFormQuestion() {
|
||||
if (!this.showFinderForm) {
|
||||
return ''
|
||||
}
|
||||
if (!this.form.extent || !this.form.floor || !this.form.rent || !this.form.location) {
|
||||
return ''
|
||||
}
|
||||
@@ -597,25 +556,19 @@
|
||||
copyInputText() {
|
||||
this.copyText((this.inputText || '').trim())
|
||||
},
|
||||
clearMockTimer() {
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer)
|
||||
this.timer = null
|
||||
}
|
||||
},
|
||||
normalizeHouseFiles(item) {
|
||||
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') {
|
||||
try {
|
||||
const files = JSON.parse(item.files)
|
||||
return files.length ? files : this.fallbackHouses[0].files
|
||||
return Array.isArray(files) ? files : []
|
||||
} catch (error) {
|
||||
return this.fallbackHouses[0].files
|
||||
return []
|
||||
}
|
||||
}
|
||||
return this.fallbackHouses[0].files
|
||||
return []
|
||||
},
|
||||
connectWebSocket() {
|
||||
if (!this.loginUserId) {
|
||||
@@ -682,6 +635,7 @@
|
||||
data = JSON.parse(raw)
|
||||
} catch (error) {
|
||||
this.loading = false
|
||||
this.progressText = ''
|
||||
this.messages.push(
|
||||
this.createMessage('ai', 'text', {
|
||||
content: raw
|
||||
@@ -690,10 +644,15 @@
|
||||
this.scrollToBottom()
|
||||
return
|
||||
}
|
||||
if (data.type === 'house_ai_progress') {
|
||||
this.progressText = data.message || '正在处理中'
|
||||
return
|
||||
}
|
||||
if (data.type !== 'house_ai_result') {
|
||||
return
|
||||
}
|
||||
this.loading = false
|
||||
this.progressText = ''
|
||||
if (data.answer) {
|
||||
this.messages.push(
|
||||
this.createMessage('ai', 'text', {
|
||||
@@ -725,6 +684,9 @@
|
||||
})
|
||||
)
|
||||
}
|
||||
if (data.showContactForm) {
|
||||
this.messages.push(this.createMessage('ai', 'messageForm'))
|
||||
}
|
||||
this.scrollToBottom()
|
||||
},
|
||||
goHouseDetail(item) {
|
||||
@@ -983,6 +945,23 @@
|
||||
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 {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -1071,6 +1050,10 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
.house-image-empty {
|
||||
background: #eef2f6;
|
||||
}
|
||||
|
||||
.house-info {
|
||||
padding: 22rpx 22rpx 24rpx;
|
||||
}
|
||||
@@ -1129,6 +1112,12 @@
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.typing-text {
|
||||
margin-left: 12rpx;
|
||||
font-size: 24rpx;
|
||||
color: #667085;
|
||||
}
|
||||
|
||||
.typing-dot:nth-child(2) {
|
||||
animation-delay: 0.15s;
|
||||
}
|
||||
@@ -1148,6 +1137,31 @@
|
||||
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 {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
@@ -101,6 +101,19 @@
|
||||
</view>
|
||||
<view v-if="isManager" class="item col-2" @click="makePhoneCall">业主电话:{{ form.phone || '' }}</view>
|
||||
<view class="item col-2">物业费(元/m²):{{ 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 class="item col-2" v-if="isManager">
|
||||
佣金:{{ form.commission || '' }}
|
||||
|
||||
Reference in New Issue
Block a user