feat(ai): 新增AI找房功能模块及配套组件
添加AI找房功能页面、路由配置和API接口 实现聊天界面组件、历史记录加载和滚动定位功能 包含房源推荐展示、联系人组件和样式优化
This commit is contained in:
6
src/assets/img/publicImage/arrows-deep-white.svg
Normal file
6
src/assets/img/publicImage/arrows-deep-white.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="17px" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g transform="matrix(1 0 0 1 -3899 -1199 )">
|
||||||
|
<path d="M 15.5978260869565 7.51797175866495 C 15.8659420289855 7.77257167308515 16 8.09991442019683 16 8.5 C 16 8.89281129653402 15.8659420289855 9.22379118528027 15.5978260869565 9.49293966623877 L 8.52173913043478 16.5962772785623 C 8.23913043478261 16.8654257595208 7.90942028985507 17 7.53260869565217 17 C 7.16304347826087 17 6.83695652173913 16.8654257595208 6.55434782608696 16.5962772785623 L 5.73913043478261 15.7779204107831 C 5.46376811594203 15.5014976465554 5.32608695652174 15.1705177578092 5.32608695652174 14.7849807445443 C 5.32608695652174 14.3994437312794 5.46376811594203 14.0684638425332 5.73913043478261 13.7920410783055 L 8.92391304347826 10.5949935815148 L 1.27173913043478 10.5949935815148 C 0.894927536231884 10.5949935815148 0.588768115942029 10.4586007702182 0.353260869565217 10.1858151476252 C 0.117753623188406 9.91302952503209 0 9.58386820710312 0 9.19833119383826 L 0 7.80166880616175 C 0 7.41613179289688 0.117753623188406 7.08697047496791 0.353260869565217 6.81418485237484 C 0.588768115942029 6.54139922978177 0.894927536231884 6.40500641848524 1.27173913043478 6.40500641848524 L 8.92391304347826 6.40500641848524 L 5.73913043478261 3.19704749679076 C 5.46376811594203 2.93517329910141 5.32608695652174 2.60783055198973 5.32608695652174 2.21501925545571 C 5.32608695652174 1.8222079589217 5.46376811594203 1.49486521181001 5.73913043478261 1.23299101412067 L 6.55434782608696 0.414634146341464 C 6.82971014492754 0.13821138211382 7.15579710144928 0 7.53260869565217 0 C 7.91666666666667 0 8.2463768115942 0.13821138211382 8.52173913043478 0.414634146341464 L 15.5978260869565 7.51797175866495 Z " fill-rule="nonzero" fill="#ffffff" stroke="none" transform="matrix(1 0 0 1 3899 1199 )" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
BIN
src/assets/img/publicImage/round-icon.gif
Normal file
BIN
src/assets/img/publicImage/round-icon.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
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>
|
||||||
@@ -121,6 +121,14 @@ const routes = [
|
|||||||
path: "/apartmentDetail",
|
path: "/apartmentDetail",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/ai",
|
||||||
|
component: () => import(/* webpackChunkName: "housing" */ "@/views/ai.vue"),
|
||||||
|
meta: {
|
||||||
|
title: "AI找房",
|
||||||
|
path: "/ai",
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
|
|||||||
@@ -45,6 +45,23 @@ export default{
|
|||||||
},
|
},
|
||||||
apartmentCollection:(params={})=>{// 公寓列表 - 收藏
|
apartmentCollection:(params={})=>{// 公寓列表 - 收藏
|
||||||
return axios.post('/tenement/pc/api/user/apartmentCollection',params)
|
return axios.post('/tenement/pc/api/user/apartmentCollection',params)
|
||||||
}
|
},
|
||||||
|
alInit:(params={})=>{// AI 初始化
|
||||||
|
return axios.post('https://api.gter.net/v1/chat/init',params)
|
||||||
|
},
|
||||||
|
alHistory:(params={})=>{// AI 历史记录
|
||||||
|
return axios.post('https://api.gter.net/v1/chat/history',params)
|
||||||
|
},
|
||||||
|
alChat:(params={})=>{// AI 发送信息
|
||||||
|
return axios.post('https://fangchat.x-php.com/api/v1/chat',params)
|
||||||
|
},
|
||||||
|
alResume:(params={})=>{// AI 恢复会话
|
||||||
|
return axios.post('https://fangchat.x-php.com/api/v1/chat/resume',params)
|
||||||
|
},
|
||||||
|
alEnd:(params={})=>{// AI 结束会话
|
||||||
|
return axios.post('https://api.gter.net/v1/chat/end',params)
|
||||||
|
},
|
||||||
|
alNew:(params={})=>{// AI 新建会话
|
||||||
|
return axios.post('https://api.gter.net/v1/chat/new',params)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
@@ -20,7 +20,7 @@ axios.interceptors.request.use(
|
|||||||
if (config.url != "/tenement/pc/api/user/operation" && !noMask) showFullScreenLoading()
|
if (config.url != "/tenement/pc/api/user/operation" && !noMask) showFullScreenLoading()
|
||||||
// 开发时登录用的,可以直接替换小程序的 authorization
|
// 开发时登录用的,可以直接替换小程序的 authorization
|
||||||
if (process.env.NODE_ENV !== "production") {
|
if (process.env.NODE_ENV !== "production") {
|
||||||
const miucms_session = "fb685339c8ec5030749c4d85d3c1a7fd";
|
const miucms_session = "01346a38444d71aaadb3adad52b52c39";
|
||||||
document.cookie = "miucms_session=" + miucms_session;
|
document.cookie = "miucms_session=" + miucms_session;
|
||||||
config["headers"]["authorization"] = miucms_session;
|
config["headers"]["authorization"] = miucms_session;
|
||||||
}
|
}
|
||||||
@@ -97,9 +97,8 @@ const $post = (url, params) => {
|
|||||||
resolve(res.data)
|
resolve(res.data)
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
if (err.data.code == 401) {
|
if (err.data.code == 401) resolve(err.data)
|
||||||
resolve(err.data)
|
else reject(err.data)
|
||||||
} else reject(err.data)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
410
src/views/ai.vue
Normal file
410
src/views/ai.vue
Normal file
@@ -0,0 +1,410 @@
|
|||||||
|
<template>
|
||||||
|
<div class="main">
|
||||||
|
<header class="header flexacenter">
|
||||||
|
<div class="title">寄托AI找房</div>
|
||||||
|
<div class="logo flex1 flexacenter">
|
||||||
|
<img class="icon" src="../assets/img/publicImage/round-icon.gif" />
|
||||||
|
圆同学
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class="content flexflex" ref="contentRef">
|
||||||
|
<template v-if="isLogo">
|
||||||
|
<img class="logo" src="../assets/img/publicImage/round-icon.gif" />
|
||||||
|
<div class="hint">Hi,我是你的租房小助手圆同学。初次见面很开心!目前我还属于初代产品,只能提供房源推荐服务,暂时干不了其他事。来吧,告诉我你想找什么样的房子…</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="historyList.length > 0">
|
||||||
|
<AiItem v-for="(item, index) in historyList" :key="index" :item="item"></AiItem>
|
||||||
|
<Contacts></Contacts>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-if="historyList.length > 0" class="history-hint flexcenter flex1">
|
||||||
|
<div class="history-line history-left"></div>
|
||||||
|
以上是历史对话
|
||||||
|
<div class="history-line history-right"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AiItem v-for="(item, index) in list" :key="index" :item="item"></AiItem>
|
||||||
|
|
||||||
|
<!-- fixed -->
|
||||||
|
<div class="input-box fixed">
|
||||||
|
<div class="input">
|
||||||
|
<textarea class="input-text" placeholder="请输入" @input="autoResize"></textarea>
|
||||||
|
<div class="send pitch flexcenter">
|
||||||
|
<img class="icon" src="../assets/img/publicImage/arrows-deep-white.svg" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { reactive, onMounted, ref, nextTick, onBeforeUnmount, watch } from "vue";
|
||||||
|
import api from "../utils/api";
|
||||||
|
import store from "@/store/index";
|
||||||
|
import AiItem from "../components/ai/aiItem.vue";
|
||||||
|
import Contacts from "../components/ai/contacts.vue";
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
console.log("onMounted");
|
||||||
|
init();
|
||||||
|
});
|
||||||
|
|
||||||
|
let initState = false;
|
||||||
|
let session_id = "";
|
||||||
|
|
||||||
|
let historyObj = {
|
||||||
|
isHistory: true,
|
||||||
|
role: "assistant",
|
||||||
|
payload: {
|
||||||
|
type: "text",
|
||||||
|
message: "很高兴又见面了,快快告诉我你的租房需求吧",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
const init = () => {
|
||||||
|
api.alInit().then((res) => {
|
||||||
|
if (res.code == 401) store.state.showloginmodal = true;
|
||||||
|
if (res.code != 200) return;
|
||||||
|
|
||||||
|
const data = res.data;
|
||||||
|
let history = data.history || [];
|
||||||
|
|
||||||
|
session_id = data.session_id;
|
||||||
|
initState = true;
|
||||||
|
|
||||||
|
const target = history[0] || {};
|
||||||
|
historyObj["payload"] = target["payload"];
|
||||||
|
historyObj["timestamp"] = target["timestamp"];
|
||||||
|
historyObj["timeText"] = formatTimestamp(getTimestamp(target["timestamp"])) || "";
|
||||||
|
|
||||||
|
getHistory();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const contentRef = ref(null);
|
||||||
|
|
||||||
|
// 滚动到最后一条
|
||||||
|
const scrollEnd = async () => {
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
// 1. 获取当前水平滚动位置
|
||||||
|
const currentX = window.pageXOffset;
|
||||||
|
// 2. 设置新的滚动位置:水平位置不变,垂直滚动到 500px 处
|
||||||
|
window.scrollTo(currentX, 100000);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 滚动到指定位置
|
||||||
|
const scrollItem = (id) => {
|
||||||
|
nextTick(() => {
|
||||||
|
const element = contentRef.value.querySelector(`[data-id="${id}"]`);
|
||||||
|
if (element) {
|
||||||
|
element.scrollIntoView({
|
||||||
|
behavior: "smooth",
|
||||||
|
block: "center",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let page = 1;
|
||||||
|
let loading = false;
|
||||||
|
let list = ref([]);
|
||||||
|
let historyList = ref([]);
|
||||||
|
let isLogo = ref(true);
|
||||||
|
|
||||||
|
// 获取历史记录
|
||||||
|
const getHistory = () => {
|
||||||
|
if (page == 0 || loading) return;
|
||||||
|
loading = true;
|
||||||
|
|
||||||
|
const limit = 10;
|
||||||
|
const aiId = historyList.value[0]?.id || "";
|
||||||
|
|
||||||
|
api.alHistory({
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
}).then((res) => {
|
||||||
|
console.log(res);
|
||||||
|
if (res.code != 200) return;
|
||||||
|
let data = res.data || {};
|
||||||
|
// data.count = 0
|
||||||
|
// data.history = []
|
||||||
|
let history = data.history || [];
|
||||||
|
history.reverse();
|
||||||
|
|
||||||
|
history = history.concat(historyList.value);
|
||||||
|
|
||||||
|
history = calculateRecordTimeText(history);
|
||||||
|
|
||||||
|
if (history.length > 0 && page == 1) {
|
||||||
|
let obj = historyObj || {};
|
||||||
|
list.value.push(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("history", history);
|
||||||
|
|
||||||
|
historyList.value = history;
|
||||||
|
isLogo.value = history.length == 0 ? true : false;
|
||||||
|
|
||||||
|
if (page > 1) scrollItem(aiId);
|
||||||
|
else scrollEnd();
|
||||||
|
|
||||||
|
page = data.page * limit >= data.count ? 0 : data.page + 1;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let firstTimeStamp = null;
|
||||||
|
|
||||||
|
// 计算 聊天记录时间显示
|
||||||
|
const calculateRecordTimeText = (list = []) => {
|
||||||
|
let interval = 5 * 60 * 1000; // 5 分钟
|
||||||
|
// interval = 1000
|
||||||
|
list = list.map((item) => {
|
||||||
|
const currentTimeStamp = getTimestamp(item.timestamp || "") || 0;
|
||||||
|
if (firstTimeStamp === null || Math.abs(currentTimeStamp - firstTimeStamp) > interval) {
|
||||||
|
firstTimeStamp = currentTimeStamp;
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
timeText: formatTimestamp(currentTimeStamp) || "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return list;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 自动输入框增高
|
||||||
|
const autoResize = (e) => {
|
||||||
|
e.target.style.height = "auto"; // 重置高度
|
||||||
|
e.target.style.height = `${e.target.scrollHeight}px`; // 设置为内容高度
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTimestamp = (time) => {
|
||||||
|
if (!time || typeof time !== "string" || time.trim() === "") return null;
|
||||||
|
return new Date(time.replace(" ", "T")).getTime();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载历史消息(插入到列表顶部)
|
||||||
|
const loadHistory = async () => {
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 步骤1:记录加载前的滚动状态(关键!用于后续校准位置)
|
||||||
|
const container = chatContainer.value;
|
||||||
|
const beforeScrollHeight = container.scrollHeight; // 加载前的总高度
|
||||||
|
|
||||||
|
// 步骤2:模拟请求历史消息(实际项目替换为API调用)
|
||||||
|
const historyMsgs = await fetchHistory(page.value);
|
||||||
|
if (historyMsgs.length === 0) {
|
||||||
|
hasMore.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 步骤3:插入新消息到列表顶部(历史消息比现有消息更早)
|
||||||
|
messages.value = [...historyMsgs, ...messages.value];
|
||||||
|
|
||||||
|
// 步骤4:等待DOM更新后,校准滚动位置(核心逻辑)
|
||||||
|
await nextTick();
|
||||||
|
const afterScrollHeight = container.scrollHeight; // 加载后的总高度
|
||||||
|
// 计算新消息增加的高度,让滚动条保持在加载前的相对位置
|
||||||
|
const heightDiff = afterScrollHeight - beforeScrollHeight;
|
||||||
|
container.scrollTop = heightDiff; // 关键:通过设置scrollTop抵消高度变化
|
||||||
|
|
||||||
|
page.value++; // 页数+1
|
||||||
|
} catch (err) {
|
||||||
|
console.error("加载失败:", err);
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTimestamp = (timestamp) => {
|
||||||
|
// 创建时间对象(处理毫秒级时间戳)
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
// 获取年、月、日、时、分
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = date.getMonth() + 1; // 月份从0开始,需要加1
|
||||||
|
const day = date.getDate();
|
||||||
|
const hours = String(date.getHours()).padStart(2, "0"); // 确保两位数
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, "0");
|
||||||
|
|
||||||
|
// 获取当前日期的年、月、日
|
||||||
|
const currentYear = now.getFullYear();
|
||||||
|
const currentMonth = now.getMonth() + 1;
|
||||||
|
const currentDay = now.getDate();
|
||||||
|
|
||||||
|
let str = "";
|
||||||
|
if (year === currentYear && month === currentMonth && day === currentDay) str = `${hours}:${minutes}`; // 检查是否是今天
|
||||||
|
else if (year === currentYear) str = `${month}月${day}日 ${hours}:${minutes}`; // 检查是否是同一年
|
||||||
|
else str = `${year}年${month}月${day}日 ${hours}:${minutes}`; // 不同年
|
||||||
|
|
||||||
|
return str;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.main {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #fff;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
.header {
|
||||||
|
width: 100%;
|
||||||
|
height: 50px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-bottom: 1px solid #ebebeb;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
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;
|
||||||
|
padding-left: 11px;
|
||||||
|
padding-right: 15px;
|
||||||
|
border-right: 1px solid #d7d7d7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
font-family: "PingFangSC-Regular", "PingFang SC", sans-serif;
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #6e6e96;
|
||||||
|
padding-left: 18px;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 37px;
|
||||||
|
height: 40px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
height: 800px;
|
||||||
|
flex: 1;
|
||||||
|
// background: aqua;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-top: 50px;
|
||||||
|
padding-bottom: 150px;
|
||||||
|
.logo {
|
||||||
|
width: 110px;
|
||||||
|
height: 119px;
|
||||||
|
margin: 122px auto 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
font-family: "PingFangSC-Regular", "PingFang SC", sans-serif;
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 26px;
|
||||||
|
color: #333333;
|
||||||
|
width: 430px;
|
||||||
|
margin: 0 auto 86px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-box {
|
||||||
|
width: 800px;
|
||||||
|
|
||||||
|
&.fixed {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 1;
|
||||||
|
padding-bottom: 38px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
background-color: rgba(255, 255, 255, 1);
|
||||||
|
border: 1px solid rgba(235, 235, 235, 1);
|
||||||
|
border-radius: 20px;
|
||||||
|
|
||||||
|
.input-text {
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 15px 20px 0 15px;
|
||||||
|
resize: none;
|
||||||
|
display: block;
|
||||||
|
font-size: 16px;
|
||||||
|
height: 51px;
|
||||||
|
max-height: 230px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #d7d7d7;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: 20px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
cursor: not-allowed;
|
||||||
|
&.pitch {
|
||||||
|
background-color: #7fddff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
transform: rotate(270deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-hint {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 26px;
|
||||||
|
color: #7f7f7f;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
position: relative;
|
||||||
|
padding: 10px 0;
|
||||||
|
|
||||||
|
.history-line {
|
||||||
|
width: 150px;
|
||||||
|
height: 2px;
|
||||||
|
margin: 0 20px;
|
||||||
|
|
||||||
|
&.history-left {
|
||||||
|
background: linear-gradient(to left, #ebebeb, #f7f8fa);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.history-right {
|
||||||
|
background: linear-gradient(to right, #ebebeb, #f7f8fa);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
header.page-header,
|
||||||
|
section.index-footer,
|
||||||
|
.left-side-bar1 {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user