feat(ai-house): 重构找房聊天交互
This commit is contained in:
+349
-79
@@ -1,5 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="page">
|
<view class="page">
|
||||||
|
<view class="page-toolbar">
|
||||||
|
<view class="page-toolbar-title">AI找房助手</view>
|
||||||
|
<view
|
||||||
|
class="clear-session-btn"
|
||||||
|
:class="{ 'clear-session-btn-disabled': loading }"
|
||||||
|
@click="confirmClearSession"
|
||||||
|
>
|
||||||
|
<u-icon name="trash" size="18" color="#667085"></u-icon>
|
||||||
|
<text class="clear-session-text">清空</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
<view class="message-scroll">
|
<view class="message-scroll">
|
||||||
<view class="message-content">
|
<view class="message-content">
|
||||||
<view class="intro-card">
|
<view class="intro-card">
|
||||||
@@ -30,22 +41,19 @@
|
|||||||
></image>
|
></image>
|
||||||
<view :class="['message-bubble', message.role === 'user' ? 'bubble-user' : 'bubble-ai']">
|
<view :class="['message-bubble', message.role === 'user' ? 'bubble-user' : 'bubble-ai']">
|
||||||
<view v-if="message.type === 'form'" class="form-message">
|
<view v-if="message.type === 'form'" class="form-message">
|
||||||
<view class="form-title">您想要找什么样的房型?</view>
|
<view class="form-title">{{ message.content || '您好,请填写您的找房需求' }}</view>
|
||||||
|
<view class="form-desc">填写楼盘、面积和预算,我来帮您筛选合适的房源。</view>
|
||||||
<view class="form-item">
|
<view class="form-item">
|
||||||
<text class="form-label">多少平方</text>
|
<text class="form-label">楼盘</text>
|
||||||
<input v-model="form.extent" class="form-input" type="text" placeholder="请输入面积" :disabled="loading" />
|
<input v-model="form.building" class="form-input" type="text" placeholder="请输入楼盘名称" :disabled="loading" />
|
||||||
</view>
|
</view>
|
||||||
<view class="form-item">
|
<view class="form-item">
|
||||||
<text class="form-label">多少楼层</text>
|
<text class="form-label">面积</text>
|
||||||
<input v-model="form.floor" class="form-input" type="text" placeholder="请输入楼层" :disabled="loading" />
|
<input v-model="form.area" class="form-input" type="text" placeholder="请输入面积" :disabled="loading" />
|
||||||
</view>
|
</view>
|
||||||
<view class="form-item">
|
<view class="form-item">
|
||||||
<text class="form-label">多少租金</text>
|
<text class="form-label">预算</text>
|
||||||
<input v-model="form.rent" class="form-input" type="text" placeholder="请输入租金" :disabled="loading" />
|
<input v-model="form.budget" 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
|
<view
|
||||||
class="finder-submit"
|
class="finder-submit"
|
||||||
@@ -124,6 +132,25 @@
|
|||||||
<!-- <text class="copy-text">复制</text>-->
|
<!-- <text class="copy-text">复制</text>-->
|
||||||
<!-- </view>-->
|
<!-- </view>-->
|
||||||
</view>
|
</view>
|
||||||
|
<view v-else-if="message.type === 'location'" class="location-message">
|
||||||
|
<view class="result-title">{{ message.content }}</view>
|
||||||
|
<view v-for="item in message.locationCards" :key="item.locationId" class="location-card">
|
||||||
|
<view class="location-card-title">{{ item.locationName }}</view>
|
||||||
|
<view v-if="item.parentLocationName || item.city" class="location-card-path">
|
||||||
|
{{ [item.city, item.parentLocationName].filter(Boolean).join(' / ') }}
|
||||||
|
</view>
|
||||||
|
<view v-if="item.tags && item.tags.length" class="location-tag-row">
|
||||||
|
<text v-for="tag in item.tags" :key="tag" class="location-tag">{{ tag }}</text>
|
||||||
|
</view>
|
||||||
|
<view v-for="knowledge in item.knowledgeItems" :key="knowledge.topic" class="location-knowledge-item">
|
||||||
|
<view class="location-knowledge-title">{{ knowledge.title }}</view>
|
||||||
|
<view v-if="locationKnowledgeFacts(knowledge)" class="location-knowledge-facts">
|
||||||
|
{{ locationKnowledgeFacts(knowledge) }}
|
||||||
|
</view>
|
||||||
|
<view v-if="knowledge.content" class="location-knowledge-content">{{ knowledge.content }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
<view v-else-if="message.type === 'contact'" class="contact-message">
|
<view v-else-if="message.type === 'contact'" class="contact-message">
|
||||||
<button class="contact-btn" open-type="contact">{{ message.content || '联系客服' }}</button>
|
<button class="contact-btn" open-type="contact">{{ message.content || '联系客服' }}</button>
|
||||||
</view>
|
</view>
|
||||||
@@ -182,7 +209,6 @@
|
|||||||
|
|
||||||
<view class="input-panel">
|
<view class="input-panel">
|
||||||
<view class="quick-question-row">
|
<view class="quick-question-row">
|
||||||
<view class="finder-tag" :class="{ active: showFinderForm }" @click="toggleFinderForm">找房</view>
|
|
||||||
<view
|
<view
|
||||||
v-for="item in quickQuestions"
|
v-for="item in quickQuestions"
|
||||||
:key="item.label"
|
:key="item.label"
|
||||||
@@ -249,12 +275,10 @@
|
|||||||
loading: false,
|
loading: false,
|
||||||
progressText: '',
|
progressText: '',
|
||||||
messages: [],
|
messages: [],
|
||||||
showFinderForm: false,
|
|
||||||
form: {
|
form: {
|
||||||
extent: '',
|
building: '',
|
||||||
floor: '',
|
area: '',
|
||||||
rent: '',
|
budget: ''
|
||||||
location: ''
|
|
||||||
},
|
},
|
||||||
messageFormOpened: false,
|
messageFormOpened: false,
|
||||||
messageSubmitting: false,
|
messageSubmitting: false,
|
||||||
@@ -274,7 +298,8 @@
|
|||||||
socketConnected: false,
|
socketConnected: false,
|
||||||
heartbeatTimer: null,
|
heartbeatTimer: null,
|
||||||
loginUserId: null,
|
loginUserId: null,
|
||||||
nextMessageId: 0
|
nextMessageId: 0,
|
||||||
|
sessionRestored: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -282,7 +307,7 @@
|
|||||||
if (!this.aiConfigLoaded) {
|
if (!this.aiConfigLoaded) {
|
||||||
return '正在加载AI助手配置...'
|
return '正在加载AI助手配置...'
|
||||||
}
|
}
|
||||||
return this.aiConfig.welcomeMessage || '告诉我面积、楼层、预算和地段,我来帮您筛选更合适的房源。'
|
return this.aiConfig.welcomeMessage || '填写楼盘、面积和预算,我来帮您筛选更合适的房源。'
|
||||||
},
|
},
|
||||||
resolvedAiAvatar() {
|
resolvedAiAvatar() {
|
||||||
return this.aiConfig.aiAvatar || 'https://oss.wsdns.cn/20260601/0d53634419a448919c90c99ab9779d8a.png?x-oss-process=image/resize,w_750/quality,Q_90'
|
return this.aiConfig.aiAvatar || 'https://oss.wsdns.cn/20260601/0d53634419a448919c90c99ab9779d8a.png?x-oss-process=image/resize,w_750/quality,Q_90'
|
||||||
@@ -302,44 +327,97 @@
|
|||||||
}, 300)
|
}, 300)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.conversationId = `house-ai-${this.loginUserId}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
this.conversationId = this.createConversationId()
|
||||||
|
this.sessionRestored = this.restoreSession()
|
||||||
this.connectWebSocket()
|
this.connectWebSocket()
|
||||||
this.loadAiConfig()
|
this.loadAiConfig()
|
||||||
},
|
},
|
||||||
onUnload() {
|
onUnload() {
|
||||||
if (this.conversationId) {
|
this.persistSession()
|
||||||
HouseAiChatApi.clearHouseAiSession({
|
|
||||||
conversationId: this.conversationId
|
|
||||||
}).catch(() => {})
|
|
||||||
}
|
|
||||||
this.closeWebSocket()
|
this.closeWebSocket()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
getSessionStorageKey(userId = this.loginUserId) {
|
||||||
|
return userId ? `house-ai-session-${userId}` : ''
|
||||||
|
},
|
||||||
|
createConversationId() {
|
||||||
|
return `house-ai-${this.loginUserId}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
||||||
|
},
|
||||||
|
restoreSession() {
|
||||||
|
const storageKey = this.getSessionStorageKey()
|
||||||
|
if (!storageKey || !storage || typeof storage.get !== 'function') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const savedSession = storage.get(storageKey)
|
||||||
|
if (!savedSession || typeof savedSession !== 'object' || !savedSession.conversationId) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
this.conversationId = savedSession.conversationId
|
||||||
|
this.messages = Array.isArray(savedSession.messages) ? savedSession.messages : []
|
||||||
|
this.messageSubmitted = Boolean(savedSession.messageSubmitted)
|
||||||
|
this.nextMessageId = this.messages.reduce((nextId, message) => {
|
||||||
|
const match = String(message?.id || '').match(/^msg-(\d+)$/)
|
||||||
|
return match ? Math.max(nextId, Number(match[1]) + 1) : nextId
|
||||||
|
}, 0)
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
persistSession() {
|
||||||
|
const storageKey = this.getSessionStorageKey()
|
||||||
|
if (!storageKey || !this.conversationId || !storage || typeof storage.set !== 'function') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
storage.set(storageKey, {
|
||||||
|
conversationId: this.conversationId,
|
||||||
|
messages: JSON.parse(JSON.stringify(this.messages)),
|
||||||
|
messageSubmitted: this.messageSubmitted
|
||||||
|
})
|
||||||
|
},
|
||||||
|
confirmClearSession() {
|
||||||
|
if (this.loading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.showModal({
|
||||||
|
title: '清空会话',
|
||||||
|
content: '确定清空当前聊天记录吗?',
|
||||||
|
confirmText: '清空',
|
||||||
|
cancelText: '取消',
|
||||||
|
success: (result) => {
|
||||||
|
if (result.confirm) {
|
||||||
|
this.clearSession()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async clearSession() {
|
||||||
|
if (this.loading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const previousConversationId = this.conversationId
|
||||||
|
this.startNewChat()
|
||||||
|
this.conversationId = this.createConversationId()
|
||||||
|
this.appendWelcomeMessage()
|
||||||
|
try {
|
||||||
|
await HouseAiChatApi.clearHouseAiSession({
|
||||||
|
conversationId: previousConversationId
|
||||||
|
})
|
||||||
|
uni.showToast({
|
||||||
|
title: '会话已清空',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '本地已清空,服务端同步失败',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
askQuickQuestion(question) {
|
askQuickQuestion(question) {
|
||||||
if (this.loading) {
|
if (this.loading) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.showFinderForm = false
|
|
||||||
this.inputText = question
|
this.inputText = question
|
||||||
this.sendMessage()
|
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() {
|
submitFinderForm() {
|
||||||
if (this.loading) {
|
if (this.loading) {
|
||||||
return
|
return
|
||||||
@@ -367,32 +445,30 @@
|
|||||||
startNewChat() {
|
startNewChat() {
|
||||||
this.loading = false
|
this.loading = false
|
||||||
this.progressText = ''
|
this.progressText = ''
|
||||||
this.showFinderForm = false
|
|
||||||
this.messageFormOpened = false
|
this.messageFormOpened = false
|
||||||
this.messageSubmitting = false
|
this.messageSubmitting = false
|
||||||
this.messageSubmitted = false
|
this.messageSubmitted = false
|
||||||
this.inputText = ''
|
this.inputText = ''
|
||||||
this.form = {
|
this.form = {
|
||||||
extent: '',
|
building: '',
|
||||||
floor: '',
|
area: '',
|
||||||
rent: '',
|
budget: ''
|
||||||
location: ''
|
|
||||||
}
|
}
|
||||||
this.messageForm = {
|
this.messageForm = {
|
||||||
realName: '',
|
realName: '',
|
||||||
phone: '',
|
phone: '',
|
||||||
wechat: ''
|
wechat: ''
|
||||||
}
|
}
|
||||||
|
this.nextMessageId = 0
|
||||||
this.messages = []
|
this.messages = []
|
||||||
this.scrollToBottom()
|
this.scrollToBottom()
|
||||||
},
|
},
|
||||||
appendWelcomeMessage() {
|
appendWelcomeMessage() {
|
||||||
const welcomeMessage = this.aiConfig.welcomeMessage || '您好,我是AI找房助手,请告诉我面积、楼层、预算和地段,我来帮您筛选更合适的房源。'
|
const welcomeMessage = this.aiConfig.welcomeMessage || '您好,我是AI找房助手,请填写楼盘、面积和预算,我来帮您筛选更合适的房源。'
|
||||||
this.messages = [
|
this.messages = [this.createMessage('ai', 'form', {
|
||||||
this.createMessage('ai', 'text', {
|
content: welcomeMessage
|
||||||
content: welcomeMessage
|
})]
|
||||||
})
|
this.persistSession()
|
||||||
]
|
|
||||||
},
|
},
|
||||||
async loadAiConfig() {
|
async loadAiConfig() {
|
||||||
this.aiConfigLoaded = false
|
this.aiConfigLoaded = false
|
||||||
@@ -410,8 +486,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.aiConfigLoaded = true
|
this.aiConfigLoaded = true
|
||||||
this.startNewChat()
|
if (!this.sessionRestored) {
|
||||||
this.appendWelcomeMessage()
|
this.startNewChat()
|
||||||
|
this.appendWelcomeMessage()
|
||||||
|
} else if (!this.messages.length) {
|
||||||
|
this.appendWelcomeMessage()
|
||||||
|
}
|
||||||
this.scrollToBottom()
|
this.scrollToBottom()
|
||||||
},
|
},
|
||||||
openMessageForm() {
|
openMessageForm() {
|
||||||
@@ -419,6 +499,7 @@
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.messageFormOpened = true
|
this.messageFormOpened = true
|
||||||
|
this.persistSession()
|
||||||
this.scrollToBottom()
|
this.scrollToBottom()
|
||||||
},
|
},
|
||||||
validateMessageForm() {
|
validateMessageForm() {
|
||||||
@@ -466,6 +547,7 @@
|
|||||||
wechat: (this.messageForm.wechat || '').trim()
|
wechat: (this.messageForm.wechat || '').trim()
|
||||||
})
|
})
|
||||||
this.messageSubmitted = true
|
this.messageSubmitted = true
|
||||||
|
this.persistSession()
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '提交成功',
|
title: '提交成功',
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
@@ -500,6 +582,7 @@
|
|||||||
content: question
|
content: question
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
this.persistSession()
|
||||||
this.inputText = ''
|
this.inputText = ''
|
||||||
this.loading = true
|
this.loading = true
|
||||||
this.progressText = '正在分析您的需求'
|
this.progressText = '正在分析您的需求'
|
||||||
@@ -523,6 +606,7 @@
|
|||||||
content: error.message || 'AI服务暂时不可用,请稍后再试。'
|
content: error.message || 'AI服务暂时不可用,请稍后再试。'
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
this.persistSession()
|
||||||
this.scrollToBottom()
|
this.scrollToBottom()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -530,10 +614,10 @@
|
|||||||
return (this.inputText || '').trim()
|
return (this.inputText || '').trim()
|
||||||
},
|
},
|
||||||
buildFormQuestion() {
|
buildFormQuestion() {
|
||||||
if (!this.form.extent || !this.form.floor || !this.form.rent || !this.form.location) {
|
if (!this.form.building || !this.form.area || !this.form.budget) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
return `帮我找房,想要${this.form.location}的房源,面积在${this.form.extent}左右,楼层要求${this.form.floor},预算租金控制在${this.form.rent}以内`
|
return `帮我找房,想要${this.form.building}的房源,面积在${this.form.area}左右,预算控制在${this.form.budget}以内`
|
||||||
},
|
},
|
||||||
copyText(text) {
|
copyText(text) {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
@@ -570,6 +654,84 @@
|
|||||||
}
|
}
|
||||||
return []
|
return []
|
||||||
},
|
},
|
||||||
|
normalizeLocationCards(value) {
|
||||||
|
return this.normalizeStructuredArray(value)
|
||||||
|
.map((item, index) => {
|
||||||
|
if (!item || typeof item !== 'object') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
locationId: item.locationId ?? item.location_id ?? `location-${index}`,
|
||||||
|
locationName: item.locationName ?? item.location_name ?? item.name ?? '地点资料',
|
||||||
|
parentLocationName: item.parentLocationName ?? item.parent_location_name ?? '',
|
||||||
|
tags: this.normalizeLocationTags(item.tags),
|
||||||
|
knowledgeItems: this.normalizeLocationKnowledgeItems(
|
||||||
|
item.knowledgeItems !== undefined ? item.knowledgeItems : item.knowledge_items
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
},
|
||||||
|
normalizeStructuredArray(value) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(value)
|
||||||
|
return Array.isArray(parsed) ? parsed : (parsed && typeof parsed === 'object' ? [parsed] : [])
|
||||||
|
} catch (error) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value && typeof value === 'object' ? [value] : []
|
||||||
|
},
|
||||||
|
normalizeLocationTags(value) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.filter(Boolean).map((item) => String(item))
|
||||||
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const tags = this.normalizeStructuredArray(value)
|
||||||
|
if (tags.length) {
|
||||||
|
return tags.filter(Boolean).map((item) => String(item))
|
||||||
|
}
|
||||||
|
return value.split(/[,,]/).map((item) => item.trim()).filter(Boolean)
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
normalizeLocationKnowledgeItems(value) {
|
||||||
|
return this.normalizeStructuredArray(value)
|
||||||
|
.map((item, index) => {
|
||||||
|
if (!item || typeof item !== 'object') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
topic: item.topic || `knowledge-${index}`,
|
||||||
|
name: item.name || '',
|
||||||
|
title: item.title || '补充信息',
|
||||||
|
propertyCompany: item.propertyCompany ?? item.property_company,
|
||||||
|
propertyFees: item.propertyFees ?? item.property_fees,
|
||||||
|
waterBillingType: item.waterBillingType ?? item.water_billing_type,
|
||||||
|
waterUnitPrice: item.waterUnitPrice ?? item.water_unit_price,
|
||||||
|
electricityBillingType: item.electricityBillingType ?? item.electricity_billing_type,
|
||||||
|
electricityUnitPrice: item.electricityUnitPrice ?? item.electricity_unit_price,
|
||||||
|
parkingAvailable: this.normalizeBoolean(item.parkingAvailable ?? item.parking_available),
|
||||||
|
parkingFee: item.parkingFee ?? item.parking_fee
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
},
|
||||||
|
normalizeBoolean(value) {
|
||||||
|
if (value === 'true' || value === 1 || value === '1') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (value === 'false' || value === 0 || value === '0') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
},
|
||||||
connectWebSocket() {
|
connectWebSocket() {
|
||||||
if (!this.loginUserId) {
|
if (!this.loginUserId) {
|
||||||
return
|
return
|
||||||
@@ -641,6 +803,7 @@
|
|||||||
content: raw
|
content: raw
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
this.persistSession()
|
||||||
this.scrollToBottom()
|
this.scrollToBottom()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -684,9 +847,21 @@
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
const locationCards = this.normalizeLocationCards(
|
||||||
|
data.locationCards !== undefined ? data.locationCards : data.location_cards
|
||||||
|
)
|
||||||
|
if (locationCards.length) {
|
||||||
|
this.messages.push(
|
||||||
|
this.createMessage('ai', 'location', {
|
||||||
|
content: '以下地点资料来自已维护的知识库:',
|
||||||
|
locationCards
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
if (data.showContactForm) {
|
if (data.showContactForm) {
|
||||||
this.messages.push(this.createMessage('ai', 'messageForm'))
|
this.messages.push(this.createMessage('ai', 'messageForm'))
|
||||||
}
|
}
|
||||||
|
this.persistSession()
|
||||||
this.scrollToBottom()
|
this.scrollToBottom()
|
||||||
},
|
},
|
||||||
goHouseDetail(item) {
|
goHouseDetail(item) {
|
||||||
@@ -697,6 +872,19 @@
|
|||||||
url: `/sub_pages/house/detail?houseId=${item.houseId}`
|
url: `/sub_pages/house/detail?houseId=${item.houseId}`
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
locationKnowledgeFacts(item) {
|
||||||
|
const facts = []
|
||||||
|
if (item.propertyCompany) facts.push(`物业公司:${item.propertyCompany}`)
|
||||||
|
if (item.propertyFees !== null && item.propertyFees !== undefined) facts.push(`物业费:${item.propertyFees}`)
|
||||||
|
if (item.waterBillingType) facts.push(`水费:${item.waterBillingType}`)
|
||||||
|
if (item.waterUnitPrice !== null && item.waterUnitPrice !== undefined) facts.push(`水费单价:${item.waterUnitPrice}`)
|
||||||
|
if (item.electricityBillingType) facts.push(`电费:${item.electricityBillingType}`)
|
||||||
|
if (item.electricityUnitPrice !== null && item.electricityUnitPrice !== undefined) facts.push(`电费单价:${item.electricityUnitPrice}`)
|
||||||
|
if (item.parkingAvailable === true) facts.push('停车:可用')
|
||||||
|
if (item.parkingAvailable === false) facts.push('停车:不可用')
|
||||||
|
if (item.parkingFee) facts.push(`停车费用:${item.parkingFee}`)
|
||||||
|
return facts.join(';')
|
||||||
|
},
|
||||||
scrollToBottom() {
|
scrollToBottom() {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -719,6 +907,38 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 20rpx 24rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-toolbar-title {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #344054;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-session-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
padding: 10rpx 14rpx;
|
||||||
|
border: 1rpx solid #d0d5dd;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-session-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #667085;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-session-btn-disabled {
|
||||||
|
opacity: 0.45;
|
||||||
|
}
|
||||||
|
|
||||||
.message-scroll {
|
.message-scroll {
|
||||||
padding: 24rpx 24rpx 0;
|
padding: 24rpx 24rpx 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -835,6 +1055,69 @@
|
|||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.location-message {
|
||||||
|
padding: 20rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: #f8fbff;
|
||||||
|
border: 1rpx solid #e6eefc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-card {
|
||||||
|
margin-top: 16rpx;
|
||||||
|
padding: 20rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1rpx solid #e9edf5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-card-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-card-path {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 23rpx;
|
||||||
|
color: #667085;
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-tag-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8rpx;
|
||||||
|
margin-top: 14rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-tag {
|
||||||
|
padding: 4rpx 12rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
background: #eef6ff;
|
||||||
|
color: #2f5f9f;
|
||||||
|
font-size: 21rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-knowledge-item {
|
||||||
|
margin-top: 16rpx;
|
||||||
|
padding-top: 16rpx;
|
||||||
|
border-top: 1rpx solid #eef1f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-knowledge-title {
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #344054;
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-knowledge-facts,
|
||||||
|
.location-knowledge-content {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.55;
|
||||||
|
color: #667085;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
.faq-message {
|
.faq-message {
|
||||||
padding: 8rpx 0 0;
|
padding: 8rpx 0 0;
|
||||||
}
|
}
|
||||||
@@ -917,6 +1200,14 @@
|
|||||||
margin-bottom: 20rpx;
|
margin-bottom: 20rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-desc {
|
||||||
|
margin-top: -8rpx;
|
||||||
|
margin-bottom: 10rpx;
|
||||||
|
font-size: 25rpx;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #667085;
|
||||||
|
}
|
||||||
|
|
||||||
.form-item {
|
.form-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1162,27 +1453,6 @@
|
|||||||
opacity: 0.55;
|
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 {
|
.input-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
const assert = require('node:assert/strict')
|
||||||
|
const fs = require('node:fs')
|
||||||
|
const path = require('node:path')
|
||||||
|
|
||||||
|
const pagePath = path.resolve(__dirname, '../pages/ai-house/index.vue')
|
||||||
|
const source = fs.readFileSync(pagePath, 'utf8')
|
||||||
|
const scriptMatch = source.match(/<script>([\s\S]*?)<\/script>/)
|
||||||
|
|
||||||
|
assert.ok(scriptMatch, '未找到 AI 找房页面脚本')
|
||||||
|
|
||||||
|
const componentScript = scriptMatch[1]
|
||||||
|
.replace(/^\s*import .*$/gm, '')
|
||||||
|
.replace('export default {', 'return {')
|
||||||
|
const component = new Function(
|
||||||
|
'HouseAiConfigApi',
|
||||||
|
'HouseAiChatApi',
|
||||||
|
'storage',
|
||||||
|
'USER_ID',
|
||||||
|
'apiUrl',
|
||||||
|
componentScript
|
||||||
|
)({}, {}, {}, 'userId', 'http://localhost')
|
||||||
|
|
||||||
|
function createPageVm() {
|
||||||
|
const vm = component.data()
|
||||||
|
Object.entries(component.methods).forEach(([name, method]) => {
|
||||||
|
vm[name] = method.bind(vm)
|
||||||
|
})
|
||||||
|
vm.scrollToBottom = () => {}
|
||||||
|
return vm
|
||||||
|
}
|
||||||
|
|
||||||
|
function receiveLocationResponse(fieldName, locationCards) {
|
||||||
|
const vm = createPageVm()
|
||||||
|
const response = {
|
||||||
|
type: 'house_ai_result',
|
||||||
|
answer: '已按已维护的地点资料筛选,以下地点可供参考。'
|
||||||
|
}
|
||||||
|
response[fieldName] = locationCards
|
||||||
|
vm.handleSocketMessage(JSON.stringify(response))
|
||||||
|
return vm.messages.find((message) => message.type === 'location')
|
||||||
|
}
|
||||||
|
|
||||||
|
const locationCard = {
|
||||||
|
location_id: 101,
|
||||||
|
city: '南宁市',
|
||||||
|
location_name: '五象航洋城',
|
||||||
|
parent_location_name: '五象新区',
|
||||||
|
tags: ['停车方便'],
|
||||||
|
knowledge_items: [{
|
||||||
|
topic: 'parking',
|
||||||
|
title: '停车',
|
||||||
|
parking_available: true,
|
||||||
|
parking_fee: '月租 300 元'
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
const locationMessage = receiveLocationResponse('location_cards', JSON.stringify([locationCard]))
|
||||||
|
|
||||||
|
assert.ok(locationMessage, '地点咨询回包应创建地点卡片消息')
|
||||||
|
assert.ok(Array.isArray(locationMessage.locationCards), '地点卡片应转换为数组')
|
||||||
|
assert.equal(locationMessage.locationCards[0].locationId, 101)
|
||||||
|
assert.equal(locationMessage.locationCards[0].locationName, '五象航洋城')
|
||||||
|
assert.equal(locationMessage.locationCards[0].knowledgeItems[0].parkingAvailable, true)
|
||||||
|
|
||||||
|
const backendLocationMessage = receiveLocationResponse('locationCards', [{
|
||||||
|
locationId: 102,
|
||||||
|
city: '南宁市',
|
||||||
|
locationName: '会展航洋城',
|
||||||
|
knowledgeItems: []
|
||||||
|
}])
|
||||||
|
|
||||||
|
assert.ok(backendLocationMessage, '后端驼峰地点卡片回包应保持兼容')
|
||||||
|
assert.equal(backendLocationMessage.locationCards[0].locationName, '会展航洋城')
|
||||||
|
|
||||||
|
console.log('地点咨询回包能够创建可渲染的地点卡片消息')
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
const assert = require('node:assert/strict')
|
||||||
|
const fs = require('node:fs')
|
||||||
|
const path = require('node:path')
|
||||||
|
|
||||||
|
const pagePath = path.resolve(__dirname, '../pages/ai-house/index.vue')
|
||||||
|
const source = fs.readFileSync(pagePath, 'utf8')
|
||||||
|
const scriptMatch = source.match(/<script>([\s\S]*?)<\/script>/)
|
||||||
|
|
||||||
|
assert.ok(scriptMatch, 'AI house page script is missing')
|
||||||
|
|
||||||
|
const savedValues = new Map()
|
||||||
|
const storage = {
|
||||||
|
get(key) {
|
||||||
|
return savedValues.get(key) || false
|
||||||
|
},
|
||||||
|
set(key, value) {
|
||||||
|
savedValues.set(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const clearCalls = []
|
||||||
|
const componentScript = scriptMatch[1]
|
||||||
|
.replace(/^\s*import .*$/gm, '')
|
||||||
|
.replace('export default {', 'return {')
|
||||||
|
const component = new Function(
|
||||||
|
'HouseAiConfigApi',
|
||||||
|
'HouseAiChatApi',
|
||||||
|
'storage',
|
||||||
|
'USER_ID',
|
||||||
|
'apiUrl',
|
||||||
|
componentScript
|
||||||
|
)(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
clearHouseAiSession(payload) {
|
||||||
|
clearCalls.push(payload)
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
storage,
|
||||||
|
'userId',
|
||||||
|
'http://localhost'
|
||||||
|
)
|
||||||
|
|
||||||
|
global.uni = {
|
||||||
|
showToast() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPageVm() {
|
||||||
|
const vm = component.data()
|
||||||
|
Object.entries(component.methods).forEach(([name, method]) => {
|
||||||
|
vm[name] = method.bind(vm)
|
||||||
|
})
|
||||||
|
vm.scrollToBottom = () => {}
|
||||||
|
return vm
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstPage = createPageVm()
|
||||||
|
firstPage.loginUserId = 1001
|
||||||
|
firstPage.conversationId = 'house-ai-1001-existing'
|
||||||
|
firstPage.messages = [firstPage.createMessage('user', 'text', { content: 'Need a two-bedroom home' })]
|
||||||
|
firstPage.messageSubmitted = true
|
||||||
|
firstPage.form = {
|
||||||
|
building: '阳光小区',
|
||||||
|
area: '90',
|
||||||
|
budget: '3000'
|
||||||
|
}
|
||||||
|
const finderQuestion = firstPage.buildFormQuestion()
|
||||||
|
assert.ok(finderQuestion.includes('阳光小区'))
|
||||||
|
assert.ok(finderQuestion.includes('90'))
|
||||||
|
assert.ok(finderQuestion.includes('3000'))
|
||||||
|
firstPage.persistSession()
|
||||||
|
|
||||||
|
const restoredPage = createPageVm()
|
||||||
|
restoredPage.loginUserId = 1001
|
||||||
|
assert.equal(restoredPage.restoreSession(), true)
|
||||||
|
assert.equal(restoredPage.conversationId, 'house-ai-1001-existing')
|
||||||
|
assert.equal(restoredPage.messages[0].content, 'Need a two-bedroom home')
|
||||||
|
assert.equal(restoredPage.messageSubmitted, true)
|
||||||
|
assert.equal(restoredPage.createMessage('ai', 'text').id, 'msg-1')
|
||||||
|
|
||||||
|
restoredPage.aiConfig.welcomeMessage = 'Welcome back'
|
||||||
|
restoredPage.clearSession().then(() => {
|
||||||
|
assert.deepEqual(clearCalls, [{ conversationId: 'house-ai-1001-existing' }])
|
||||||
|
assert.notEqual(restoredPage.conversationId, 'house-ai-1001-existing')
|
||||||
|
assert.equal(restoredPage.messages.length, 1)
|
||||||
|
assert.equal(restoredPage.messages[0].type, 'form')
|
||||||
|
assert.equal(restoredPage.messages[0].content, 'Welcome back')
|
||||||
|
assert.equal(restoredPage.messageSubmitted, false)
|
||||||
|
|
||||||
|
const storedSession = storage.get('house-ai-session-1001')
|
||||||
|
assert.equal(storedSession.conversationId, restoredPage.conversationId)
|
||||||
|
assert.equal(storedSession.messages.length, 1)
|
||||||
|
|
||||||
|
console.log('AI house session persistence and clearing checks passed')
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error(error)
|
||||||
|
process.exitCode = 1
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user