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

1228 lines
28 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>
<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 }}|{{ 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>