Files
aishangjia-uniapp/pages/ai-house/index.vue

1242 lines
29 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 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
class="finder-submit"
:class="{ 'finder-submit-disabled': loading }"
@click="submitFinderForm"
>
{{ 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 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 }}|{{ item.toward }}</view>
<view class="house-price">{{ item.monthlyRent ? item.monthlyRent + '元/月' : (item.totalPrice || item.salePrice || '价格待确认') }}</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>
<text class="typing-text">{{ progressText || '正在处理中' }}</text>
</view>
</view>
<view id="page-bottom-anchor" class="page-bottom-anchor"></view>
</view>
</view>
<view class="input-panel">
<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="输入找房需求或房源相关问题"
: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 HouseAiConfigApi from '@/api/house-ai-config.js'
import * as HouseAiChatApi from '@/api/house-ai-chat.js'
import storage from '@/utils/storage.js'
import { USER_ID } from '@/store/mutation-types.js'
import { apiUrl } from '@/config.js'
export default {
data() {
return {
quickQuestions: [
{
label: '面积和价格',
question: '这套房的面积和价格是多少?'
},
{
label: '物业和水电',
question: '这套房的物业费和水电怎么收费?'
},
{
label: '空调和停车',
question: '这套房有空调和停车位吗?'
}
],
inputText: '',
loading: false,
progressText: '',
messages: [],
showFinderForm: false,
form: {
extent: '',
floor: '',
rent: '',
location: ''
},
messageFormOpened: false,
messageSubmitting: false,
messageSubmitted: false,
messageForm: {
realName: '',
phone: '',
wechat: ''
},
aiConfig: {
aiAvatar: '',
welcomeMessage: ''
},
aiConfigLoaded: false,
conversationId: '',
socketTask: null,
socketConnected: false,
heartbeatTimer: null,
loginUserId: null,
nextMessageId: 0
}
},
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}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
this.connectWebSocket()
this.loadAiConfig()
},
onUnload() {
if (this.conversationId) {
HouseAiChatApi.clearHouseAiSession({
conversationId: this.conversationId
}).catch(() => {})
}
this.closeWebSocket()
},
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,
role,
type,
content: '',
...extra
}
},
startNewChat() {
this.loading = false
this.progressText = ''
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
})
]
},
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.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 HouseAiChatApi.submitHouseAiLead({
conversationId: this.conversationId,
realName: this.messageForm.realName.trim(),
phone: (this.messageForm.phone || '').trim(),
wechat: (this.messageForm.wechat || '').trim()
})
this.messageSubmitted = true
uni.showToast({
title: '提交成功',
icon: 'none'
})
} catch (error) {
uni.showToast({
title: error.message || '提交失败,请稍后再试',
icon: 'none'
})
}
this.messageSubmitting = false
this.scrollToBottom()
},
handlePrimaryAction() {
if (this.loading) {
this.loading = false
return
}
this.sendMessage()
},
async sendMessage(question = this.buildQuestion()) {
if (!question) {
uni.showToast({
title: '请输入您的找房需求',
icon: 'none'
})
return
}
this.messages.push(
this.createMessage('user', 'text', {
content: question
})
)
this.inputText = ''
this.loading = true
this.progressText = '正在分析您的需求'
this.scrollToBottom()
try {
const result = await HouseAiChatApi.sendHouseAiMessage({
conversationId: this.conversationId,
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服务暂时不可用请稍后再试。'
})
)
this.scrollToBottom()
}
},
buildQuestion() {
return (this.inputText || '').trim()
},
buildFormQuestion() {
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())
},
normalizeHouseFiles(item) {
if (item.files && Array.isArray(item.files)) {
return item.files
}
if (item.files && typeof item.files === 'string') {
try {
const files = JSON.parse(item.files)
return Array.isArray(files) ? files : []
} catch (error) {
return []
}
}
return []
},
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.progressText = ''
this.messages.push(
this.createMessage('ai', 'text', {
content: raw
})
)
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', {
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
})
)
}
if (data.showContactForm) {
this.messages.push(this.createMessage('ai', 'messageForm'))
}
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;
}
.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%;
}
.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-image-empty {
background: #eef2f6;
}
.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-text {
margin-left: 12rpx;
font-size: 24rpx;
color: #667085;
}
.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);
}
.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;
}
.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>