1228 lines
28 KiB
Vue
1228 lines
28 KiB
Vue
<template>
|
||
<view class="page">
|
||
<view class="message-scroll">
|
||
<view class="message-content">
|
||
<view class="intro-card">
|
||
<image
|
||
v-if="aiConfig.aiAvatar"
|
||
class="intro-avatar"
|
||
:src="aiConfig.aiAvatar"
|
||
mode="aspectFill"
|
||
></image>
|
||
<view v-else class="intro-badge">AI</view>
|
||
<view class="intro-content">
|
||
<view class="intro-title">AI找房助手已就位</view>
|
||
<view class="intro-desc">{{ introDesc }}</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view
|
||
v-for="message in messages"
|
||
:key="message.id"
|
||
:id="message.id"
|
||
:class="['message-row', message.role === 'user' ? 'message-row-user' : 'message-row-ai']"
|
||
>
|
||
<image
|
||
v-if="message.role === 'ai'"
|
||
class="avatar avatar-image"
|
||
:src="resolvedAiAvatar"
|
||
mode="aspectFill"
|
||
></image>
|
||
<view :class="['message-bubble', message.role === 'user' ? 'bubble-user' : 'bubble-ai']">
|
||
<view v-if="message.type === 'form'" class="form-message">
|
||
<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"
|
||
/>
|
||
</view>
|
||
<view class="form-item">
|
||
<text class="form-label">多少楼层</text>
|
||
<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"
|
||
/>
|
||
</view>
|
||
<view class="form-item">
|
||
<text class="form-label">什么地段</text>
|
||
<input
|
||
v-model="form.location"
|
||
class="form-input"
|
||
type="text"
|
||
placeholder="请输入地段"
|
||
:disabled="loading"
|
||
/>
|
||
</view>
|
||
</view>
|
||
<view v-else-if="message.type === 'messageForm'" class="message-form-message">
|
||
<view v-if="messageSubmitted" class="message-form-success">
|
||
<view class="message-form-title">留言已提交</view>
|
||
<view class="message-form-desc">顾问收到后会尽快通过您留下的方式联系您。</view>
|
||
</view>
|
||
<view v-else-if="!messageFormOpened" class="message-form-entry" @click="openMessageForm">
|
||
<view class="message-form-title">留下联系方式</view>
|
||
<view class="message-form-desc">姓名必填,手机号和微信号二选一,方便顾问继续跟进。</view>
|
||
<view class="message-form-open">点击填写</view>
|
||
</view>
|
||
<view v-else class="message-form">
|
||
<view class="message-form-title">留言板</view>
|
||
<view class="message-form-desc">请填写联系方式,手机号和微信号至少填一项。</view>
|
||
<view class="form-item">
|
||
<text class="form-label">姓名</text>
|
||
<input
|
||
v-model="messageForm.realName"
|
||
class="form-input"
|
||
type="text"
|
||
placeholder="请输入姓名"
|
||
:disabled="messageSubmitting"
|
||
/>
|
||
</view>
|
||
<view class="form-item">
|
||
<text class="form-label">手机号</text>
|
||
<input
|
||
v-model="messageForm.phone"
|
||
class="form-input"
|
||
type="number"
|
||
maxlength="11"
|
||
placeholder="手机号/微信号二选一"
|
||
:disabled="messageSubmitting"
|
||
/>
|
||
</view>
|
||
<view class="form-item">
|
||
<text class="form-label">微信号</text>
|
||
<input
|
||
v-model="messageForm.wechat"
|
||
class="form-input"
|
||
type="text"
|
||
maxlength="20"
|
||
placeholder="手机号/微信号二选一"
|
||
:disabled="messageSubmitting"
|
||
/>
|
||
</view>
|
||
<view
|
||
class="message-submit"
|
||
:class="{ 'message-submit-disabled': messageSubmitting }"
|
||
@click="submitMessageForm"
|
||
>
|
||
{{ messageSubmitting ? '提交中...' : '提交留言' }}
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view v-else-if="message.type === 'text'" class="text-message">
|
||
<view user-select>{{ message.content }}</view>
|
||
<!-- <view-->
|
||
<!-- v-if="message.content"-->
|
||
<!-- class="copy-btn"-->
|
||
<!-- @click="copyText(message.content)"-->
|
||
<!-- >-->
|
||
<!-- <image-->
|
||
<!-- class="copy-icon"-->
|
||
<!-- src="https://oss.wsdns.cn/20260601/0d53634419a448919c90c99ab9779d8a.png?x-oss-process=image/resize,w_750/quality,Q_90"-->
|
||
<!-- mode="aspectFill"-->
|
||
<!-- ></image>-->
|
||
<!-- <text class="copy-text">复制</text>-->
|
||
<!-- </view>-->
|
||
</view>
|
||
<view v-else-if="message.type === 'contact'" class="contact-message">
|
||
<button class="contact-btn" open-type="contact">{{ message.content || '联系客服' }}</button>
|
||
</view>
|
||
<view v-else-if="message.type === 'faq'" class="faq-message">
|
||
<view class="faq-title">{{ message.content || '为您找到以下常见问题答案:' }}</view>
|
||
<view
|
||
v-for="item in message.faqs"
|
||
:key="item.faqId || item.question"
|
||
class="faq-card"
|
||
>
|
||
<view class="faq-question">{{ item.question }}</view>
|
||
<view class="faq-answer">{{ item.answer }}</view>
|
||
</view>
|
||
</view>
|
||
<view v-else-if="message.type === 'list'" class="result-message">
|
||
<view class="result-title">{{ message.content }}</view>
|
||
<view class="result-list">
|
||
<view
|
||
v-for="item in message.houses"
|
||
:key="item.houseId"
|
||
class="house-card"
|
||
@click="goHouseDetail(item)"
|
||
>
|
||
<image class="house-image" :src="item.files[0].url" mode="aspectFill"></image>
|
||
<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>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view v-if="message.role === 'user'" class="avatar avatar-user">我</view>
|
||
</view>
|
||
|
||
<view v-if="loading" id="typing-indicator" class="message-row message-row-ai">
|
||
<image
|
||
class="avatar avatar-image"
|
||
:src="resolvedAiAvatar"
|
||
mode="aspectFill"
|
||
></image>
|
||
<view class="message-bubble bubble-ai typing-bubble">
|
||
<view class="typing-dot"></view>
|
||
<view class="typing-dot"></view>
|
||
<view class="typing-dot"></view>
|
||
</view>
|
||
</view>
|
||
<view id="page-bottom-anchor" class="page-bottom-anchor"></view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="input-panel">
|
||
<view class="tag-row">
|
||
<view class="finder-tag" :class="{ active: showFinderForm }" @click="toggleFinderForm">找房</view>
|
||
</view>
|
||
<view class="input-row">
|
||
<input
|
||
v-model="inputText"
|
||
class="chat-input"
|
||
type="text"
|
||
placeholder="可补充户型、朝向、电梯等偏好"
|
||
:disabled="loading"
|
||
confirm-type="send"
|
||
@confirm="handlePrimaryAction"
|
||
/>
|
||
<view class="action-btn" :class="{ 'action-btn-stop': loading }" @click="handlePrimaryAction">
|
||
{{ loading ? '停止' : '发送' }}
|
||
</view>
|
||
</view>
|
||
<!-- <view class="input-copy-row">-->
|
||
<!-- <view class="copy-btn" @click="copyInputText">-->
|
||
<!-- <image-->
|
||
<!-- class="copy-icon"-->
|
||
<!-- src="https://oss.wsdns.cn/20260601/0d53634419a448919c90c99ab9779d8a.png?x-oss-process=image/resize,w_750/quality,Q_90"-->
|
||
<!-- mode="aspectFill"-->
|
||
<!-- ></image>-->
|
||
<!-- <text class="copy-text">复制提问</text>-->
|
||
<!-- </view>-->
|
||
<!-- </view>-->
|
||
</view>
|
||
</view>
|
||
</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'
|
||
|
||
export default {
|
||
data() {
|
||
return {
|
||
inputText: '',
|
||
loading: false,
|
||
messages: [],
|
||
showFinderForm: false,
|
||
messageFormOpened: false,
|
||
messageSubmitting: false,
|
||
messageSubmitted: false,
|
||
form: {
|
||
extent: '',
|
||
floor: '',
|
||
rent: '',
|
||
location: ''
|
||
},
|
||
messageForm: {
|
||
realName: '',
|
||
phone: '',
|
||
wechat: ''
|
||
},
|
||
aiConfig: {
|
||
aiAvatar: '',
|
||
welcomeMessage: ''
|
||
},
|
||
aiConfigLoaded: false,
|
||
conversationId: '',
|
||
socketTask: null,
|
||
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' }]
|
||
}
|
||
]
|
||
}
|
||
},
|
||
computed: {
|
||
introDesc() {
|
||
if (!this.aiConfigLoaded) {
|
||
return '正在加载AI助手配置...'
|
||
}
|
||
return this.aiConfig.welcomeMessage || '告诉我面积、楼层、预算和地段,我来帮您筛选更合适的房源。'
|
||
},
|
||
resolvedAiAvatar() {
|
||
return this.aiConfig.aiAvatar || 'https://oss.wsdns.cn/20260601/0d53634419a448919c90c99ab9779d8a.png?x-oss-process=image/resize,w_750/quality,Q_90'
|
||
}
|
||
},
|
||
onLoad() {
|
||
this.loginUserId = storage.get(USER_ID)
|
||
if (!this.loginUserId) {
|
||
uni.showToast({
|
||
title: '请先登录后再使用AI找房',
|
||
icon: 'none'
|
||
})
|
||
setTimeout(() => {
|
||
uni.navigateTo({
|
||
url: '/pages/login/index'
|
||
})
|
||
}, 300)
|
||
return
|
||
}
|
||
this.conversationId = `house-ai-${this.loginUserId || 'guest'}`
|
||
this.connectWebSocket()
|
||
this.loadAiConfig()
|
||
this.loadHouseList()
|
||
},
|
||
onUnload() {
|
||
this.clearMockTimer()
|
||
this.closeWebSocket()
|
||
},
|
||
methods: {
|
||
createMessage(role, type, extra = {}) {
|
||
const id = `msg-${this.nextMessageId++}`
|
||
return {
|
||
id,
|
||
role,
|
||
type,
|
||
content: '',
|
||
...extra
|
||
}
|
||
},
|
||
startNewChat() {
|
||
this.clearMockTimer()
|
||
this.loading = false
|
||
this.showFinderForm = false
|
||
this.messageFormOpened = false
|
||
this.messageSubmitting = false
|
||
this.messageSubmitted = false
|
||
this.inputText = ''
|
||
this.form = {
|
||
extent: '',
|
||
floor: '',
|
||
rent: '',
|
||
location: ''
|
||
}
|
||
this.messageForm = {
|
||
realName: '',
|
||
phone: '',
|
||
wechat: ''
|
||
}
|
||
this.messages = []
|
||
this.scrollToBottom()
|
||
},
|
||
appendWelcomeMessage() {
|
||
const welcomeMessage = this.aiConfig.welcomeMessage || '您好,我是AI找房助手,请告诉我面积、楼层、预算和地段,我来帮您筛选更合适的房源。'
|
||
this.messages = [
|
||
this.createMessage('ai', 'text', {
|
||
content: welcomeMessage
|
||
})
|
||
]
|
||
},
|
||
appendMessageFormCard() {
|
||
// this.messages.push(this.createMessage('ai', 'messageForm'))
|
||
},
|
||
async loadAiConfig() {
|
||
this.aiConfigLoaded = false
|
||
try {
|
||
const result = await HouseAiConfigApi.getCurrentHouseAiConfig()
|
||
const data = result && result.data ? result.data : result
|
||
this.aiConfig = {
|
||
aiAvatar: data?.aiAvatar || '',
|
||
welcomeMessage: data?.welcomeMessage || ''
|
||
}
|
||
} catch (error) {
|
||
this.aiConfig = {
|
||
aiAvatar: '',
|
||
welcomeMessage: ''
|
||
}
|
||
}
|
||
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() {
|
||
if (this.messageSubmitted || this.messageSubmitting) {
|
||
return
|
||
}
|
||
this.messageFormOpened = true
|
||
this.scrollToBottom()
|
||
},
|
||
validateMessageForm() {
|
||
const realName = (this.messageForm.realName || '').trim()
|
||
const phone = (this.messageForm.phone || '').trim()
|
||
const wechat = (this.messageForm.wechat || '').trim()
|
||
const phonePattern = /^1[3-9]\d{9}$/
|
||
const wechatPattern = /^[a-zA-Z][-_a-zA-Z0-9]{5,19}$/
|
||
|
||
if (!realName) {
|
||
return '请输入姓名'
|
||
}
|
||
if (realName.length > 30) {
|
||
return '姓名不能超过30个字符'
|
||
}
|
||
if (!phone && !wechat) {
|
||
return '手机号和微信号请至少填写一项'
|
||
}
|
||
if (phone && !phonePattern.test(phone)) {
|
||
return '手机号格式不正确'
|
||
}
|
||
if (wechat && !wechatPattern.test(wechat)) {
|
||
return '微信号格式不正确'
|
||
}
|
||
return ''
|
||
},
|
||
async submitMessageForm() {
|
||
if (this.messageSubmitting || this.messageSubmitted) {
|
||
return
|
||
}
|
||
const error = this.validateMessageForm()
|
||
if (error) {
|
||
uni.showToast({
|
||
title: error,
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
this.messageSubmitting = true
|
||
try {
|
||
await HouseMessageApi.addHouseMessage({
|
||
realName: this.messageForm.realName.trim(),
|
||
phone: (this.messageForm.phone || '').trim(),
|
||
wechat: (this.messageForm.wechat || '').trim(),
|
||
source: 'ai_house'
|
||
})
|
||
this.messageSubmitted = true
|
||
uni.showToast({
|
||
title: '提交成功',
|
||
icon: 'none'
|
||
})
|
||
} catch (error) {
|
||
uni.showToast({
|
||
title: error.message || '提交失败,请稍后再试',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
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
|
||
return
|
||
}
|
||
this.sendMessage()
|
||
},
|
||
async sendMessage() {
|
||
const question = this.buildQuestion()
|
||
if (!question) {
|
||
uni.showToast({
|
||
title: this.showFinderForm ? '请输入问题或填写找房条件' : '请输入您的找房需求',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
this.messages.push(
|
||
this.createMessage('user', 'text', {
|
||
content: question
|
||
})
|
||
)
|
||
this.inputText = ''
|
||
if (question.indexOf('人工') !== -1) {
|
||
this.loading = false
|
||
this.messages.push(
|
||
this.createMessage('ai', 'contact', {
|
||
content: '联系客服'
|
||
})
|
||
)
|
||
this.scrollToBottom()
|
||
return
|
||
}
|
||
this.loading = true
|
||
this.scrollToBottom()
|
||
try {
|
||
await HouseAiChatApi.sendHouseAiMessage({
|
||
conversationId: this.conversationId,
|
||
userId: this.loginUserId,
|
||
question
|
||
})
|
||
} catch (error) {
|
||
this.loading = false
|
||
this.messages.push(
|
||
this.createMessage('ai', 'text', {
|
||
content: error.message || 'AI服务暂时不可用,请稍后再试。'
|
||
})
|
||
)
|
||
this.scrollToBottom()
|
||
}
|
||
},
|
||
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 ''
|
||
},
|
||
buildFormQuestion() {
|
||
if (!this.showFinderForm) {
|
||
return ''
|
||
}
|
||
if (!this.form.extent || !this.form.floor || !this.form.rent || !this.form.location) {
|
||
return ''
|
||
}
|
||
return `帮我找房,想要${this.form.location}的房源,面积在${this.form.extent}左右,楼层要求${this.form.floor},预算租金控制在${this.form.rent}以内`
|
||
},
|
||
copyText(text) {
|
||
if (!text) {
|
||
uni.showToast({
|
||
title: '暂无可复制内容',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
uni.setClipboardData({
|
||
data: text,
|
||
success: () => {
|
||
uni.showToast({
|
||
title: '复制成功',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
})
|
||
},
|
||
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
|
||
}
|
||
if (item.files && typeof item.files === 'string') {
|
||
try {
|
||
const files = JSON.parse(item.files)
|
||
return files.length ? files : this.fallbackHouses[0].files
|
||
} catch (error) {
|
||
return this.fallbackHouses[0].files
|
||
}
|
||
}
|
||
return this.fallbackHouses[0].files
|
||
},
|
||
connectWebSocket() {
|
||
if (!this.loginUserId) {
|
||
return
|
||
}
|
||
const userId = this.loginUserId
|
||
const wsUrl = `${apiUrl.replace('https://', 'wss://').replace('http://', 'ws://')}/chat/${userId}`
|
||
this.socketTask = uni.connectSocket({
|
||
url: wsUrl
|
||
})
|
||
uni.onSocketOpen(() => {
|
||
this.socketConnected = true
|
||
this.startHeartbeat()
|
||
})
|
||
uni.onSocketMessage((res) => {
|
||
this.handleSocketMessage(res.data)
|
||
})
|
||
uni.onSocketClose(() => {
|
||
this.socketConnected = false
|
||
this.stopHeartbeat()
|
||
})
|
||
uni.onSocketError(() => {
|
||
this.socketConnected = false
|
||
this.stopHeartbeat()
|
||
})
|
||
},
|
||
closeWebSocket() {
|
||
this.stopHeartbeat()
|
||
uni.closeSocket({})
|
||
this.socketTask = null
|
||
this.socketConnected = false
|
||
},
|
||
startHeartbeat() {
|
||
this.stopHeartbeat()
|
||
this.sendHeartbeat()
|
||
this.heartbeatTimer = setInterval(() => {
|
||
this.sendHeartbeat()
|
||
}, 30000)
|
||
},
|
||
stopHeartbeat() {
|
||
if (this.heartbeatTimer) {
|
||
clearInterval(this.heartbeatTimer)
|
||
this.heartbeatTimer = null
|
||
}
|
||
},
|
||
sendHeartbeat() {
|
||
if (!this.socketConnected) {
|
||
return
|
||
}
|
||
uni.sendSocketMessage({
|
||
data: 'ping',
|
||
fail: () => {
|
||
this.socketConnected = false
|
||
this.stopHeartbeat()
|
||
}
|
||
})
|
||
},
|
||
handleSocketMessage(raw) {
|
||
if (!raw || raw === '连接成功' || raw === 'pong') {
|
||
return
|
||
}
|
||
let data = null
|
||
try {
|
||
data = JSON.parse(raw)
|
||
} catch (error) {
|
||
this.loading = false
|
||
this.messages.push(
|
||
this.createMessage('ai', 'text', {
|
||
content: raw
|
||
})
|
||
)
|
||
this.scrollToBottom()
|
||
return
|
||
}
|
||
if (data.type !== 'house_ai_result') {
|
||
return
|
||
}
|
||
this.loading = false
|
||
if (data.answer) {
|
||
this.messages.push(
|
||
this.createMessage('ai', 'text', {
|
||
content: data.answer || '已为您完成筛选。'
|
||
})
|
||
)
|
||
}
|
||
if (data.faqs && data.faqs.length) {
|
||
this.messages.push(
|
||
this.createMessage('ai', 'faq', {
|
||
content: '优先为您匹配到以下常见问题答案:',
|
||
faqs: data.faqs
|
||
})
|
||
)
|
||
}
|
||
if (data.houses && data.houses.length) {
|
||
const houses = data.houses.map((item, index) => ({
|
||
...item,
|
||
houseId: item.houseId || `mock-house-${index}`,
|
||
files: this.normalizeHouseFiles(item)
|
||
}))
|
||
const listTitle = data.matchType === 'approximate'
|
||
? '以下房源比较接近您的要求:'
|
||
: '为您推荐以下房源:'
|
||
this.messages.push(
|
||
this.createMessage('ai', 'list', {
|
||
content: listTitle,
|
||
houses
|
||
})
|
||
)
|
||
}
|
||
this.scrollToBottom()
|
||
},
|
||
goHouseDetail(item) {
|
||
if (!item.houseId || String(item.houseId).indexOf('mock-') === 0) {
|
||
return
|
||
}
|
||
uni.navigateTo({
|
||
url: `/sub_pages/house/detail?houseId=${item.houseId}`
|
||
})
|
||
},
|
||
scrollToBottom() {
|
||
this.$nextTick(() => {
|
||
setTimeout(() => {
|
||
uni.pageScrollTo({
|
||
selector: '#page-bottom-anchor',
|
||
duration: 300
|
||
})
|
||
}, 80)
|
||
})
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.page {
|
||
min-height: 100vh;
|
||
background: linear-gradient(180deg, #f2f7ff 0%, #f7f8fc 30%, #ffffff 100%);
|
||
padding-bottom: 260rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.message-scroll {
|
||
padding: 24rpx 24rpx 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.intro-card {
|
||
padding: 24rpx;
|
||
border-radius: 28rpx;
|
||
background: linear-gradient(135deg, rgba(79, 124, 255, 0.12), rgba(50, 197, 166, 0.12));
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
.intro-badge {
|
||
width: 78rpx;
|
||
height: 78rpx;
|
||
line-height: 78rpx;
|
||
text-align: center;
|
||
border-radius: 50%;
|
||
background: linear-gradient(135deg, #4f7cff, #32c5a6);
|
||
color: #ffffff;
|
||
font-size: 28rpx;
|
||
font-weight: 700;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.intro-avatar {
|
||
width: 78rpx;
|
||
height: 78rpx;
|
||
border-radius: 50%;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.intro-content {
|
||
margin-left: 20rpx;
|
||
}
|
||
|
||
.intro-title {
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
color: #1f2937;
|
||
}
|
||
|
||
.intro-desc {
|
||
margin-top: 10rpx;
|
||
font-size: 26rpx;
|
||
line-height: 1.6;
|
||
color: #5f6b7a;
|
||
}
|
||
|
||
.message-row {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
.message-row-user {
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.avatar {
|
||
width: 64rpx;
|
||
height: 64rpx;
|
||
line-height: 64rpx;
|
||
text-align: center;
|
||
border-radius: 50%;
|
||
font-size: 24rpx;
|
||
font-weight: 700;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.avatar-ai {
|
||
background: linear-gradient(135deg, #4f7cff, #32c5a6);
|
||
color: #ffffff;
|
||
}
|
||
|
||
.avatar-image {
|
||
overflow: hidden;
|
||
}
|
||
|
||
.avatar-user {
|
||
margin-left: 16rpx;
|
||
background: #eef2ff;
|
||
color: #3557d6;
|
||
}
|
||
|
||
.message-bubble {
|
||
max-width: 590rpx;
|
||
border-radius: 28rpx;
|
||
padding: 24rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.bubble-ai {
|
||
margin-left: 16rpx;
|
||
background: #ffffff;
|
||
box-shadow: 0 8rpx 24rpx rgba(31, 41, 55, 0.06);
|
||
}
|
||
|
||
.bubble-user {
|
||
background: linear-gradient(135deg, #4f7cff, #32c5a6);
|
||
color: #ffffff;
|
||
}
|
||
|
||
.text-message {
|
||
font-size: 28rpx;
|
||
line-height: 1.7;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.contact-message {
|
||
display: flex;
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
.faq-message {
|
||
padding: 8rpx 0 0;
|
||
}
|
||
|
||
.faq-title {
|
||
font-size: 28rpx;
|
||
color: #344054;
|
||
margin-bottom: 18rpx;
|
||
}
|
||
|
||
.faq-card {
|
||
padding: 20rpx;
|
||
border-radius: 20rpx;
|
||
background: #f8fbff;
|
||
border: 1rpx solid #e6eefc;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.faq-card:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.faq-question {
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
color: #1f2937;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.faq-answer {
|
||
margin-top: 12rpx;
|
||
font-size: 26rpx;
|
||
color: #475467;
|
||
line-height: 1.7;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.contact-btn {
|
||
margin: 0;
|
||
padding: 0 28rpx;
|
||
height: 72rpx;
|
||
line-height: 72rpx;
|
||
border-radius: 999rpx;
|
||
background: linear-gradient(135deg, #4f7cff, #32c5a6);
|
||
color: #ffffff;
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
border: none;
|
||
}
|
||
|
||
.contact-btn::after {
|
||
border: none;
|
||
}
|
||
|
||
.copy-btn {
|
||
margin-top: 16rpx;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
padding: 10rpx 18rpx;
|
||
border-radius: 999rpx;
|
||
background: #f4f7fb;
|
||
}
|
||
|
||
.copy-icon {
|
||
width: 28rpx;
|
||
height: 28rpx;
|
||
//border-radius: 50%;
|
||
}
|
||
|
||
.copy-text {
|
||
margin-left: 10rpx;
|
||
font-size: 22rpx;
|
||
color: #5f6b7a;
|
||
}
|
||
|
||
.form-title {
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
color: #1f2937;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.form-item {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 18rpx 0;
|
||
border-bottom: 1rpx solid #eef1f6;
|
||
}
|
||
|
||
.form-item:last-child {
|
||
border-bottom: none;
|
||
padding-bottom: 0;
|
||
}
|
||
|
||
.form-label {
|
||
width: 140rpx;
|
||
font-size: 28rpx;
|
||
color: #344054;
|
||
}
|
||
|
||
.form-input {
|
||
flex: 1;
|
||
height: 72rpx;
|
||
padding: 0 22rpx;
|
||
border-radius: 18rpx;
|
||
background: #f6f8fc;
|
||
font-size: 26rpx;
|
||
color: #1f2937;
|
||
}
|
||
|
||
.message-form-message {
|
||
width: 100%;
|
||
}
|
||
|
||
.message-form-entry,
|
||
.message-form,
|
||
.message-form-success {
|
||
padding: 4rpx 0;
|
||
}
|
||
|
||
.message-form-title {
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
color: #1f2937;
|
||
}
|
||
|
||
.message-form-desc {
|
||
margin-top: 10rpx;
|
||
font-size: 25rpx;
|
||
line-height: 1.6;
|
||
color: #667085;
|
||
}
|
||
|
||
.message-form-open {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-top: 20rpx;
|
||
padding: 12rpx 26rpx;
|
||
border-radius: 999rpx;
|
||
background: rgba(79, 124, 255, 0.1);
|
||
color: #3557d6;
|
||
font-size: 24rpx;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.message-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);
|
||
}
|
||
|
||
.message-submit-disabled {
|
||
opacity: 0.65;
|
||
}
|
||
|
||
.result-title {
|
||
font-size: 28rpx;
|
||
color: #344054;
|
||
margin-bottom: 18rpx;
|
||
}
|
||
|
||
.result-message {
|
||
padding: 20rpx;
|
||
border-radius: 24rpx;
|
||
background: #f8fbff;
|
||
border: 1rpx solid #e6eefc;
|
||
}
|
||
|
||
.result-list {
|
||
margin-top: 12rpx;
|
||
}
|
||
|
||
.house-card {
|
||
margin-bottom: 20rpx;
|
||
border-radius: 24rpx;
|
||
background: #ffffff;
|
||
overflow: hidden;
|
||
box-shadow: 0 10rpx 26rpx rgba(31, 41, 55, 0.08);
|
||
}
|
||
|
||
.house-card:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.house-image {
|
||
width: 100%;
|
||
height: 300rpx;
|
||
display: block;
|
||
}
|
||
|
||
.house-info {
|
||
padding: 22rpx 22rpx 24rpx;
|
||
}
|
||
|
||
.house-title {
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
color: #1f2937;
|
||
}
|
||
|
||
.house-match-reason {
|
||
margin-top: 10rpx;
|
||
padding: 10rpx 14rpx;
|
||
border-radius: 10rpx;
|
||
background: #eef6ff;
|
||
color: #2f5f9f;
|
||
font-size: 24rpx;
|
||
line-height: 1.45;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.house-location {
|
||
margin-top: 10rpx;
|
||
font-size: 24rpx;
|
||
color: #667085;
|
||
}
|
||
|
||
.house-desc {
|
||
margin-top: 8rpx;
|
||
font-size: 28rpx;
|
||
color: #98a2b3;
|
||
}
|
||
|
||
.house-price {
|
||
margin-top: 12rpx;
|
||
font-size: 34rpx;
|
||
font-weight: 600;
|
||
color: #ff3b30;
|
||
}
|
||
|
||
.typing-bubble {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.typing-dot {
|
||
width: 12rpx;
|
||
height: 12rpx;
|
||
border-radius: 50%;
|
||
background: #b7c0d4;
|
||
animation: typing 1.1s infinite ease-in-out;
|
||
margin-right: 10rpx;
|
||
}
|
||
|
||
.typing-dot:last-child {
|
||
margin-right: 0;
|
||
}
|
||
|
||
.typing-dot:nth-child(2) {
|
||
animation-delay: 0.15s;
|
||
}
|
||
|
||
.typing-dot:nth-child(3) {
|
||
animation-delay: 0.3s;
|
||
}
|
||
|
||
.input-panel {
|
||
position: fixed;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
z-index: 20;
|
||
padding: 18rpx 24rpx calc(24rpx + env(safe-area-inset-bottom));
|
||
background: rgba(255, 255, 255, 0.96);
|
||
box-shadow: 0 -8rpx 24rpx rgba(15, 23, 42, 0.06);
|
||
}
|
||
|
||
.tag-row {
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.finder-tag {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 12rpx 26rpx;
|
||
border-radius: 999rpx;
|
||
background: rgba(79, 124, 255, 0.1);
|
||
color: #3557d6;
|
||
font-size: 24rpx;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.finder-tag.active {
|
||
background: rgba(79, 124, 255, 0.16);
|
||
color: #274ed8;
|
||
}
|
||
|
||
.input-row {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.input-copy-row {
|
||
margin-top: 14rpx;
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.page-bottom-anchor {
|
||
height: 2rpx;
|
||
}
|
||
|
||
.chat-input {
|
||
flex: 1;
|
||
height: 84rpx;
|
||
padding: 0 28rpx;
|
||
border-radius: 999rpx;
|
||
background: #f5f7fb;
|
||
font-size: 26rpx;
|
||
color: #1f2937;
|
||
}
|
||
|
||
.action-btn {
|
||
margin-left: 16rpx;
|
||
min-width: 136rpx;
|
||
height: 84rpx;
|
||
line-height: 84rpx;
|
||
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.22);
|
||
}
|
||
|
||
.action-btn-stop {
|
||
background: linear-gradient(135deg, #ff7a59, #ff4d4f);
|
||
box-shadow: 0 12rpx 24rpx rgba(255, 77, 79, 0.22);
|
||
}
|
||
|
||
@keyframes typing {
|
||
0%, 80%, 100% {
|
||
transform: scale(0.85);
|
||
opacity: 0.5;
|
||
}
|
||
|
||
40% {
|
||
transform: scale(1);
|
||
opacity: 1;
|
||
}
|
||
}
|
||
</style>
|