feat(ai): 新增AI找房功能模块及配套组件
添加AI找房功能页面、路由配置和API接口 实现聊天界面组件、历史记录加载和滚动定位功能 包含房源推荐展示、联系人组件和样式优化
This commit is contained in:
342
src/components/ai/aiItem.vue
Normal file
342
src/components/ai/aiItem.vue
Normal 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>
|
||||
32
src/components/ai/contacts.vue
Normal file
32
src/components/ai/contacts.vue
Normal 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>
|
||||
Reference in New Issue
Block a user