feat(ai): 新增AI找房功能模块及配套组件

添加AI找房功能页面、路由配置和API接口
实现聊天界面组件、历史记录加载和滚动定位功能
包含房源推荐展示、联系人组件和样式优化
This commit is contained in:
DESKTOP-RQ919RC\Pc
2025-08-22 18:56:17 +08:00
parent 005441895b
commit 397cdd2a53
8 changed files with 820 additions and 6 deletions

View File

@@ -0,0 +1,342 @@
<template>
<div class="date" v-if="item.timeText">{{ item.timeText }}</div>
<div class="right message" v-if="item.role == 'user'">{{ item.content }}</div>
<div class="left" :class="{ error: item.iserror }" v-if="item.role == 'assistant'">
<div class="loading-container" v-if="item.loading">
<div class="loading-item" style="--i: 0"></div>
<div class="loading-item" style="--i: 1"></div>
<div class="loading-item" style="--i: 2"></div>
<div class="loading-item" style="--i: 3"></div>
<div class="loading-item" style="--i: 4"></div>
<div class="loading-item" style="--i: 5"></div>
<span class="text">圆同学正为您努力找房中</span>
</div>
<template v-if="item.payload.type == 'recommendation'">
<div class="intro">{{ item.payload.intro }}</div>
<div class="point">推荐房源</div>
<div class="point-item" v-for="(item, ii) in item.payload.recommendations" :key="ii">
<a class="title flexacenter" url="{{ item.url }}">
<div class="serial flexcenter">{{ ii + 1 }}</div>
<div class="name one-line-display">{{ item.name }}</div>
<div class="full">已租满</div>
<img class="arrows" src="https://app.gter.net/image/miniApp/HKRenting/arrow-circular-orange-red.svg" />
</a>
<div class="spot flexflex">
<div class="dot"></div>
<div class="text flex1"><span class="spot-title">区域</span>{{ item.district }}</div>
</div>
<div class="spot flexflex">
<div class="dot"></div>
<div class="rent flex1 flexacenter">
<span class="spot-title">租金</span>
<div class="number">{{ item.price }}</div>
HKD/{{ item.rent_type == "per_person" ? "/人" : "/房" }}
</div>
</div>
<div class="spot flexflex">
<div class="dot"></div>
<div class="text flex1"><span class="spot-title">优势</span>{{ item.advantages }}</div>
</div>
<div class="spot flexflex">
<div class="dot"></div>
<div class="text flex1"><span class="spot-title">不足</span>{{ item.disadvantages }}</div>
</div>
<div class="spot flexflex">
<div class="dot"></div>
<div class="star-box flex1 flexacenter">
<span class="spot-title">推荐星级</span>
<img v-for="item in item.score" :key="item" class="star-item" src="https://app.gter.net/image/miniApp/HKRenting/star-icon.svg" />
<img v-if="item.scoreHalf" class="star-item" src="https://app.gter.net/image/miniApp/HKRenting/star-half.svg" />
</div>
</div>
<div class="spot flexflex" wx:if="{{ item.recommendation_reason }}">
<div class="dot"></div>
<div class="text flex1"><span class="spot-title">推荐理由</span>{{ item.recommendation_reason }}</div>
</div>
<div class="spot" v-if="item.images.length > 0">
<div class="scroll-view">
<img class="img" v-for="(source, index) in item.images" :key="index" :src="source" />
</div>
</div>
</div>
<template v-if="item?.payload?.notes?.length > 0">
<div class="point">注意事项</div>
<div class="point-item">
<div class="spot flexflex" v-for="(item, index) in item.payload.notes" :key="index">
<div class="dot"></div>
<div v-if="item.content" class="text flex1">
{{ item.content }}
<div v-if="item.copy" class="text link" bind:tap="consultStateCut" data-text="{{ item.copy }}">{{ item.copy }}</div>
{{ item.end }}
</div>
<div v-else class="text flex1">{{ item }}</div>
</div>
</div>
</template>
<div class="ai-hint">内容由AI生成请审慎参考</div>
</template>
<div v-else-if="item.payload.type == 'text' && item.payload.message.before">
{{ item.payload.message.before }}<span class="text link" bind:tap="consultStateCut" v-if="item.payload.message.wechat">{{ item.payload.message.wechat }}</span
>{{ item.payload.message.after }}
</div>
<div v-else>{{ item.payload.message || item.message }}</div>
</div>
<Contacts v-if="item.role == 'contacts'"></Contacts>
</template>
<script setup>
import { reactive, onMounted, ref, nextTick, onBeforeUnmount, watch } from "vue";
import Contacts from "./contacts.vue";
const props = defineProps({
item: {
type: Object,
default: () => {},
},
});
</script>
<style lang="less" scoped>
.date {
font-size: 14px;
color: #7f7f7f;
height: 24px;
margin: 0 auto 20px;
}
.right {
background-color: rgba(213, 245, 255, 1);
border-radius: 20px;
padding: 11px 15px;
font-size: 17px;
line-height: 26px;
color: #333333;
margin-left: auto;
margin-bottom: 30px;
font-family: "PingFangSC-Regular", "PingFang SC", sans-serif;
}
.left {
margin-bottom: 30px;
color: #333333;
font-size: 16px;
line-height: 26px;
font-family: "PingFangSC-Regular", "PingFang SC", sans-serif;
.loading-container {
display: inline-flex;
align-items: center;
justify-content: center;
color: #333333;
.loading-item {
width: 20px;
height: 20px;
margin-right: 4px;
border-radius: 4px;
animation: colorShift 1.2s infinite linear;
animation-delay: calc(var(--i) * 0.2s);
display: inline-flex;
}
.text {
margin-left: 5px;
}
/* 初始化默认颜色 */
.loading-item:nth-child(1) {
background-color: rgba(163, 230, 251, 1);
}
.loading-item:nth-child(2) {
background-color: rgba(180, 234, 251, 1);
}
.loading-item:nth-child(3) {
background-color: rgba(194, 238, 251, 1);
}
.loading-item:nth-child(4) {
background-color: rgba(213, 245, 255, 1);
}
.loading-item:nth-child(5) {
background-color: rgba(230, 249, 255, 1);
}
.loading-item:nth-child(6) {
background-color: rgba(241, 252, 255, 1);
}
/* 定义颜色向左移动的关键帧 */
@keyframes colorShift {
0% {
background-color: rgba(163, 230, 251, 1);
}
16.67% {
background-color: rgba(241, 252, 255, 1);
}
33.33% {
background-color: rgba(230, 249, 255, 1);
}
50% {
background-color: rgba(213, 245, 255, 1);
}
66.67% {
background-color: rgba(194, 238, 251, 1);
}
83.33% {
background-color: rgba(180, 234, 251, 1);
}
100% {
background-color: rgba(163, 230, 251, 1);
}
}
}
.intro {
font-family: "PingFangSC-Regular", "PingFang SC", sans-serif;
font-weight: 400;
font-style: normal;
font-size: 16px;
line-height: 26px;
color: #333333;
margin-bottom: 20px;
}
.point {
font-family: "PingFangSC-Semibold", "PingFang SC Semibold", "PingFang SC", sans-serif;
font-weight: 650;
font-style: normal;
font-size: 18px;
color: #000000;
line-height: 26px;
margin-bottom: 10px;
}
.point-item {
margin-bottom: 30px;
.title {
margin-bottom: 14px;
.serial {
width: 20px;
height: 20px;
line-height: 20px;
background-color: rgba(127, 221, 255, 1);
border-radius: 6px;
font-size: 14px;
color: #ffffff;
margin-right: 10px;
}
.name {
font-family: "PingFangSC-Regular", "PingFang SC", sans-serif;
font-weight: 400;
font-style: normal;
font-size: 16px;
text-decoration: underline;
color: #026277;
line-height: 26px;
margin-right: 10px;
}
.full {
color: #fff;
background-color: #333333;
padding: 0 6px;
border-radius: 6px;
height: 20px;
line-height: 20px;
margin-right: 10px;
font-size: 14px;
}
.arrows {
width: 14px;
height: 14px;
}
}
.spot {
margin-bottom: 10px;
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #76c45e;
border: 1px solid #599146;
margin-top: 8px;
margin-left: 12px;
margin-right: 10px;
}
.text {
color: #333333;
font-size: 16px;
line-height: 26px;
}
.spot-title {
font-family: "PingFangSC-Semibold", "PingFang SC Semibold", "PingFang SC", sans-serif;
font-weight: 650;
color: #000000;
}
.star-item {
width: 18px;
height: 18px;
&:not(:last-of-type) {
margin-right: 4px;
}
}
.rent {
.number {
font-family: "Arial-Black", "Arial Black", sans-serif;
font-weight: 900;
color: #d35110;
margin-right: 5px;
}
}
}
}
.scroll-view {
width: 769px;
margin-left: 30px;
white-space: nowrap;
overflow: auto;
.img {
width: 165px;
height: 110px;
margin-right: 10px;
border-radius: 10px;
object-fit: cover;
display: inline-block;
}
}
.ai-hint {
margin: 0 auto;
color: #aaaaaa;
font-size: 14px;
text-align: center;
}
.link {
color: #026277;
text-decoration: underline;
display: inline-block;
cursor: pointer;
}
}
</style>

View File

@@ -0,0 +1,32 @@
<template>
<div class="contacts">
如需进一步了解具体房源信息或香港租房生活相关问题请添加方同学微信
<div class="text link" bind:tap="consultStateCut">gternet2</div>
</div>
</template>
<script setup>
import { ref } from "vue";
const consultState = ref(false);
const consultStateCut = () => {
consultState.value = true;
};
</script>
<style lang="less" scoped>
.contacts {
color: #333333;
font-size: 16px;
line-height: 26px;
margin-bottom: 30px;
.text {
color: #026277;
text-decoration: underline;
display: inline-block;
cursor: pointer;
}
}
</style>