feat: 添加微信按钮组件及优化AI聊天功能

refactor: 移除未使用的circle-btn组件引用
style: 更新图片资源路径及样式
fix: 修复axios请求配置及类型定义
docs: 添加mock-api.js模拟接口文件
This commit is contained in:
DESKTOP-RQ919RC\Pc
2025-08-25 17:54:08 +08:00
parent 397cdd2a53
commit ca8185df91
37 changed files with 865 additions and 156 deletions

View 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="10px" height="10px" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1 0 0 1 -2066 -660 )">
<path d="M 4.375 8.37239583333333 C 4.48784722222222 8.37239583333333 4.58550347222222 8.33116319444444 4.66796875 8.24869791666667 L 7.62369791666667 5.29296875 C 7.70616319444445 5.21050347222222 7.74739583333333 5.11284722222222 7.74739583333333 5 C 7.74739583333333 4.88715277777778 7.70616319444445 4.78949652777778 7.62369791666667 4.70703125 L 4.66796875 1.75130208333333 C 4.58550347222222 1.66883680555555 4.48784722222222 1.62760416666667 4.375 1.62760416666667 C 4.26215277777778 1.62760416666667 4.16449652777778 1.66883680555555 4.08203125 1.75130208333333 L 3.41796875 2.41536458333333 C 3.33550347222222 2.49782986111111 3.29427083333333 2.59548611111111 3.29427083333333 2.70833333333333 C 3.29427083333333 2.82118055555555 3.33550347222222 2.91883680555555 3.41796875 3.00130208333333 L 5.41666666666667 5 L 3.41796875 6.99869791666667 C 3.33550347222222 7.08116319444444 3.29427083333333 7.17881944444444 3.29427083333333 7.29166666666667 C 3.29427083333333 7.40451388888889 3.33550347222222 7.50217013888889 3.41796875 7.58463541666667 L 4.08203125 8.24869791666667 C 4.16449652777778 8.33116319444444 4.26215277777778 8.37239583333333 4.375 8.37239583333333 Z M 9.32942708333333 2.490234375 C 9.77647569444444 3.25629340277778 10 4.09288194444444 10 5 C 10 5.90711805555556 9.77647569444444 6.74370659722222 9.32942708333333 7.509765625 C 8.88237847222222 8.27582465277778 8.27582465277778 8.88237847222222 7.509765625 9.32942708333333 C 6.74370659722222 9.77647569444444 5.90711805555556 10 5 10 C 4.09288194444444 10 3.25629340277778 9.77647569444444 2.490234375 9.32942708333333 C 1.72417534722222 8.88237847222222 1.11762152777778 8.27582465277778 0.670572916666667 7.509765625 C 0.223524305555556 6.74370659722222 0 5.90711805555556 0 5 C 0 4.09288194444444 0.223524305555556 3.25629340277778 0.670572916666667 2.490234375 C 1.11762152777778 1.72417534722222 1.72417534722222 1.11762152777778 2.490234375 0.670572916666666 C 3.25629340277778 0.223524305555555 4.09288194444444 0 5 0 C 5.90711805555556 0 6.74370659722222 0.223524305555555 7.509765625 0.670572916666666 C 8.27582465277778 1.11762152777778 8.88237847222222 1.72417534722222 9.32942708333333 2.490234375 Z " fill-rule="nonzero" fill="#d35110" stroke="none" transform="matrix(1 0 0 1 2066 660 )" />
</g>
</svg>

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

View 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="18px" height="18px" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1 0 0 1 -1980 -943 )">
<path d="M 12.2992788461538 11.0245746691871 L 12.8293269230769 10.4914933837429 L 15.609375 7.65595463137996 L 11.7584134615385 7.06616257088847 L 11.0444711538462 6.95274102079395 L 10.7199519230769 6.27221172022684 L 9 2.62003780718336 L 9 13.5425330812854 L 9.63822115384615 13.8941398865785 L 13.078125 15.7996219281664 L 12.4290865384615 11.773156899811 L 12.2992788461538 11.0245746691871 Z M 17.9783653846154 6.84499054820416 C 18.0432692307692 7.05293005671077 17.9567307692308 7.27788279773157 17.71875 7.51984877126654 L 13.7920673076923 11.5349716446125 L 14.7223557692308 17.2060491493384 C 14.7584134615385 17.4555765595463 14.7367788461538 17.6502835538752 14.6574519230769 17.7901701323251 C 14.578125 17.930056710775 14.4555288461538 18 14.2896634615385 18 C 14.1670673076923 18 14.0228365384615 17.9546313799622 13.8569711538462 17.8638941398866 L 9 15.187145557656 L 4.14302884615385 17.8638941398866 C 3.97716346153846 17.9546313799622 3.83293269230769 18 3.71033653846154 18 C 3.54447115384615 18 3.421875 17.930056710775 3.34254807692308 17.7901701323251 C 3.26322115384615 17.6502835538752 3.24158653846154 17.4555765595463 3.27764423076923 17.2060491493384 L 4.20793269230769 11.5349716446125 L 0.270432692307692 7.51984877126654 C 0.0396634615384615 7.27788279773157 -0.0432692307692308 7.05293005671077 0.0216346153846154 6.84499054820416 C 0.0865384615384615 6.63705103969754 0.28125 6.50661625708885 0.605769230769231 6.45368620037807 L 6.03605769230769 5.62570888468809 L 8.46995192307692 0.465028355387523 C 8.61418269230769 0.15500945179584 8.79086538461539 0 9 0 C 9.20192307692308 0 9.37860576923077 0.15500945179584 9.53004807692308 0.465028355387523 L 11.9639423076923 5.62570888468809 L 17.3942307692308 6.45368620037807 C 17.71875 6.50661625708885 17.9134615384615 6.63705103969754 17.9783653846154 6.84499054820416 Z " fill-rule="nonzero" fill="#fddf6d" stroke="none" transform="matrix(1 0 0 1 1980 943 )" />
</g>
</svg>

View 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="18px" height="18px" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1 0 0 1 -1957 -943 )">
<path d="M 17.3942307692308 6.45368620037807 C 17.7980769230769 6.52173913043478 18 6.69565217391304 18 6.97542533081285 C 18 7.14177693761815 17.90625 7.32325141776937 17.71875 7.51984877126654 L 13.7920673076923 11.5349716446125 L 14.7223557692308 17.2060491493384 C 14.7295673076923 17.2589792060492 14.7331730769231 17.3345935727788 14.7331730769231 17.4328922495274 C 14.7331730769231 17.5916824196597 14.6953125 17.7258979206049 14.6195913461538 17.8355387523629 C 14.5438701923077 17.945179584121 14.4338942307692 18 14.2896634615385 18 C 14.1526442307692 18 14.0084134615385 17.9546313799622 13.8569711538462 17.8638941398866 L 9 15.187145557656 L 4.14302884615385 17.8638941398866 C 3.984375 17.9546313799622 3.84014423076923 18 3.71033653846154 18 C 3.55889423076923 18 3.4453125 17.945179584121 3.36959134615385 17.8355387523629 C 3.29387019230769 17.7258979206049 3.25600961538462 17.5916824196597 3.25600961538462 17.4328922495274 C 3.25600961538462 17.3875236294896 3.26322115384615 17.3119092627599 3.27764423076923 17.2060491493384 L 4.20793269230769 11.5349716446125 L 0.270432692307692 7.51984877126654 C 0.0901442307692308 7.31568998109641 0 7.13421550094518 0 6.97542533081285 C 0 6.69565217391304 0.201923076923077 6.52173913043478 0.605769230769231 6.45368620037807 L 6.03605769230769 5.62570888468809 L 8.46995192307692 0.465028355387523 C 8.60697115384616 0.15500945179584 8.78365384615385 0 9 0 C 9.21634615384615 0 9.39302884615385 0.15500945179584 9.53004807692308 0.465028355387523 L 11.9639423076923 5.62570888468809 L 17.3942307692308 6.45368620037807 Z " fill-rule="nonzero" fill="#fddf6d" stroke="none" transform="matrix(1 0 0 1 1957 943 )" />
</g>
</svg>

View File

@@ -0,0 +1,16 @@
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" version="1.1" width="292px" height="149px">
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="150" y1="0" x2="150" y2="160" id="LinearGradient1149">
<stop id="Stop1150" stop-color="#50e3c2" stop-opacity="0.498039215686275" offset="0"/>
<stop id="Stop1151" stop-color="#ffffff" offset="1"/>
</linearGradient>
</defs>
<g transform="matrix(1 0 0 1 0 -150)">
<!-- 白色箭头(置于下层) -->
<path d="M 0 0 L 10 10 L 20 0 Z" fill="#ffffff" transform="translate(140, 289)"/>
<!-- 原有渐变图形(置于上层,覆盖下层箭头重合部分) -->
<path d="M 292 19 L 292 132 C 292 136.48 288.48 140 284 140 L 160 140 L 150 150 L 140 140 L 8 140 C 3.52 140 0 136.48 0 132 L 0 19 C 0 19 65.55 1.07 149 1 C 233.55 0.93 292 19 292 19 Z" fill-rule="nonzero" fill="url(#LinearGradient1149)" stroke="none" transform="matrix(1 0 0 1 0 150)"/>
<!-- 底部箭头(置于最上层,避免被覆盖) -->
</g>
</svg>

View File

@@ -1,7 +1,8 @@
<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="right message" :id="`item${item.id}`" v-if="item.role == 'user'">{{ item.content }}</div>
<div class="left" :id="`item${item.id}`" :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>
@@ -11,15 +12,15 @@
<div class="loading-item" style="--i: 5"></div>
<span class="text">圆同学正为您努力找房中</span>
</div>
<template v-if="item.payload.type == 'recommendation'">
<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 }}">
<a class="title flexacenter" :href="item.listing_url" target="_blank">
<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" />
<div class="full" v-if="!item.is_rentable">已租满</div>
<img class="arrows" src="@/assets/img/publicImage/arrow-circular-orange-red.svg" />
</a>
<div class="spot flexflex">
<div class="dot"></div>
@@ -45,8 +46,9 @@
<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" />
<img v-for="item in item.score" :key="item" class="star-item" src="@/assets/img/publicImage/star-icon.svg" />
<img v-if="item.scoreHalf" class="star-item" src="@/assets/img/publicImage//star-half.svg" />
</div>
</div>
<div class="spot flexflex" wx:if="{{ item.recommendation_reason }}">
@@ -55,7 +57,7 @@
</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" />
<img class="img" @click="openImageShow(item.images, index)" v-for="(source, index) in item.images" :key="index" :src="source" alt="图片" />
</div>
</div>
</div>
@@ -75,18 +77,24 @@
</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 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">
<wechat-btn>{{ item.payload.message.wechat }}</wechat-btn>
</span>{{ item.payload.message.after }}
</div>
<div v-else>{{ item.payload.message || item.message }}</div>
<div v-else>{{ item?.payload?.message || item.message }}</div>
</div>
<Contacts v-if="item.role == 'contacts'"></Contacts>
<image-watch arrow="never" :index="imageIndex" v-if="imageShow" :close="cloaseImageShow" :list="imageList"></image-watch>
</template>
<script setup>
import { reactive, onMounted, ref, nextTick, onBeforeUnmount, watch } from "vue";
import Contacts from "./contacts.vue";
import imageWatch from "@/components/detail/imageWatch.vue";
import wechatBtn from "@/components/ai/wechat-btn.vue";
const props = defineProps({
item: {
@@ -94,6 +102,30 @@ const props = defineProps({
default: () => {},
},
});
let imageShow = ref(false); // 查看大图弹窗的状态
let imageList = ref([]); // 查看大图弹窗的状态
let imageIndex = ref(0); // 查看大图弹窗的状态
const openImageShow = (list, index) => {
console.log("list", list);
let arr = [];
list.forEach((item) => {
arr.push({
imageurl: item,
thumbnail: item,
type: "attachment",
});
});
imageList.value = arr;
imageIndex.value = index;
imageShow.value = !imageShow.value;
};
const cloaseImageShow = () => {
imageShow.value = false;
}
</script>
<style lang="less" scoped>
@@ -114,6 +146,7 @@ const props = defineProps({
margin-left: auto;
margin-bottom: 30px;
font-family: "PingFangSC-Regular", "PingFang SC", sans-serif;
word-break: break-word;
}
.left {
@@ -122,6 +155,12 @@ const props = defineProps({
font-size: 16px;
line-height: 26px;
font-family: "PingFangSC-Regular", "PingFang SC", sans-serif;
word-break: break-word;
&.error {
color: #f84b37;
// border: 1rpx solid rgba(255, 204, 198, 1);
// background-color: rgba(255, 242, 240, 1);
}
.loading-container {
display: inline-flex;
@@ -322,6 +361,7 @@ const props = defineProps({
border-radius: 10px;
object-fit: cover;
display: inline-block;
cursor: pointer;
}
}

View File

@@ -1,18 +1,17 @@
<template>
<div class="contacts">
如需进一步了解具体房源信息或香港租房生活相关问题请添加方同学微信
<div class="text link" bind:tap="consultStateCut">gternet2</div>
<div class="text link">
<wechat-btn>gternet2</wechat-btn>
</div>
</div>
</template>
<script setup>
import { ref } from "vue";
import wechatBtn from "@/components/ai/wechat-btn.vue";
const consultState = ref(false);
const consultStateCut = () => {
consultState.value = true;
};
</script>
<style lang="less" scoped>

View File

@@ -0,0 +1,167 @@
<template>
<div class="circle-btn flexcenter">
<div @click="circleState = !circleState">
<slot></slot>
</div>
<div class="circle-pop flexacenter" v-if="circleState">
<img class="close-icon" src="@/assets/img/publicImage/circle-close.png" alt="" @click.stop="circleState = !circleState" />
<!-- <img class="circle-bj" src="@/assets/img/publicImage/circle-pop-bj.svg"> -->
<img class="circle-bj-green" src="@/assets/img/publicImage/wechat-pop-green.svg" />
<div class="circle-title flexacenter">
欢迎联系 <b>{{ wechat["nickname"] }}</b> 咨询公寓
</div>
<div class="circle-QRcode flexcenter">
<img class="circle-QRcode-img" :src="wechat['personalqrcode']" />
</div>
<div class="circle-hint">微信扫码添加好友</div>
</div>
</div>
</template>
<script setup>
import { ref, toRefs } from "vue";
import { useStore } from "vuex";
const store = useStore();
const { wechat } = toRefs(store.state);
let circleState = ref(false);
</script>
<style lang="less" scoped>
// @media screen and (max-width: 1360px) {
// .circle-btn {
// right: 20px !important;
// }
// }
.circle-btn {
position: relative;
// position: fixed;
// bottom: 158px;
// right: calc((100vw - 1200px) / 2 - 75px);
// width: 60px;
// height: 60px;
// background-color: #50e3c2;
// border-radius: 50%;
// cursor: pointer;
// z-index: 10000;
.circle-inside {
flex-direction: column;
background-color: #cbf7ed;
width: 50px;
height: 50px;
border-radius: 50%;
position: relative;
z-index: 1;
color: #000;
font-size: 14px;
.circle-bj {
position: absolute;
top: 0;
left: 0;
width: 50px;
height: 50px;
z-index: -1;
}
.circle-icon {
width: 20px;
height: 17px;
margin-bottom: 2px;
}
}
}
.circle-pop {
position: absolute;
bottom: 45px;
// right: 65px;
width: 300px;
height: 300px;
border-radius: 10px;
flex-direction: column;
z-index: 1100;
padding-top: 45px;
font-family: "PingFangSC-Regular", "PingFang SC", sans-serif;
// -moz-box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.101960784313725);
// -webkit-box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.101960784313725);
// box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.101960784313725);
background-color: #fff;
filter: drop-shadow(0px 0px 10px rgba(0, 0, 0, 0.50196078));
.close-icon {
position: absolute;
right: 10px;
top: 10px;
width: 16px;
height: 16px;
cursor: pointer;
}
.circle-bj {
position: absolute;
top: -15px;
left: -15px;
width: 341px;
height: 336px;
z-index: -1;
}
.circle-bj-green {
width: 300px;
// height: 156px;
position: absolute;
bottom: -9.5px;
left: 0;
z-index: -1;
}
.circle-title {
font-size: 15px;
color: #555555;
margin-bottom: 24px;
b {
color: #000;
font-weight: 650;
margin: 0 5px;
}
}
.circle-QRcode {
width: 120px;
height: 120px;
border-radius: 20px;
-moz-box-shadow: 0px 0px 15px rgba(0, 0, 0, 0.101960784313725);
-webkit-box-shadow: 0px 0px 15px rgba(0, 0, 0, 0.101960784313725);
box-shadow: 0px 0px 15px rgba(0, 0, 0, 0.101960784313725);
margin-bottom: 20px;
background: #fff;
.circle-QRcode-img {
width: 100px;
height: 100px;
}
}
.circle-hint {
margin-bottom: 6px;
}
.circle-hint,
.circle-remark {
font-size: 13px;
color: #555555;
line-height: 18px;
}
.circle-remark {
b {
font-weight: 650;
color: #000000;
}
}
}
</style>

View File

@@ -90,21 +90,21 @@ watchEffect(() => {
user.data = store.state.user;
// bannerLists.data = props.bannerList
if (!store.state.indexData.menu) return;
store.state.indexData.menu.map((res) => {
if (res.name === "首页") {
res.path = "/";
} else if (res.name === "个人房源") {
res.path = "/personHousing";
} else if (res.name === "中介房源") {
res.path = "/intermediaryHousing";
} else if (res.name === "品牌公寓") {
res.path = "/apartment";
} else if (res.name === "求房源") {
res.path = "/needHousing";
} else if (res.name === "我的") {
res.path = "/user";
}
});
// store.state.indexData.menu.map((res) => {
// if (res.name === "首页") {
// res.path = "/";
// } else if (res.name === "个人房源") {
// res.path = "/personHousing";
// } else if (res.name === "中介房源") {
// res.path = "/intermediaryHousing";
// } else if (res.name === "品牌公寓") {
// res.path = "/apartment";
// } else if (res.name === "求房源") {
// res.path = "/needHousing";
// } else if (res.name === "我的") {
// res.path = "/user";
// }
// });
seachTab.data = store.state.indexData.menu;
topTab.data = store.state.indexData.nav;
});

40
src/mock/mock-api.js Normal file
View File

@@ -0,0 +1,40 @@
const express = require("express");
const app = express();
app.use(express.json());
// 模拟POST请求接口/api/v1/chat
app.post("/api/v1/chat", (req, res) => {
res.json({
history: [
{
content: "您好啊我是寄托香港租房的AI找房助手圆同学只要您告诉我以下信息中的任意两样预算范围、位置比如哪所学校附近、房间类型比如单人间、双人间等我就可以为您找房啦因为我还是初代智能体只能根据上述要求提供找房服务哦。",
id: "812ede61-1524-440a-9035-356e3dd94c95",
payload: {
message: "您好啊我是寄托香港租房的AI找房助手圆同学只要您告诉我以下信息中的任意两样预算范围、位置比如哪所学校附近、房间类型比如单人间、双人间等我就可以为您找房啦因为我还是初代智能体只能根据上述要求提供找房服务哦。",
type: "text",
},
role: "assistant",
timestamp: "2025-08-25T10:40:21.445827+08:00",
},
{ content: "ffffff", id: "4856fc38-3489-4478-9577-7d9bfc9d43f8", payload: null, role: "user", timestamp: "2025-08-25T10:49:13.557769+08:00" },
{
content: "目前我只能提供找房服务,只要您告诉我以下信息中的任意两样:预算范围、位置(比如哪所学校附近)、房间类型(比如单人间、双人间等),我就可以为您找房啦!其他问题可以找方同学:<wechat>gternet2</wechat>。",
id: "37d0b325-a3dd-4cf1-8989-59f76180761b",
payload: {
message: "目前我只能提供找房服务,只要您告诉我以下信息中的任意两样:预算范围、位置(比如哪所学校附近)、房间类型(比如单人间、双人间等),我就可以为您找房啦!其他问题可以找方同学:<wechat>gternet2</wechat>。",
type: "text",
},
role: "assistant",
timestamp: "2025-08-25T10:49:13.558771+08:00",
},
],
});
});
// 启动服务器
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Mock server running on port ${PORT}`);
});
// 提示需要先安装express依赖运行npm install express

View File

@@ -53,10 +53,10 @@ export default{
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)
return axios.postV2('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)
return axios.postV2('https://fangchat.x-php.com/api/v1/chat/resume',params)
},
alEnd:(params={})=>{// AI 结束会话
return axios.post('https://api.gter.net/v1/chat/end',params)

View File

@@ -1,69 +1,68 @@
import axios from "axios"
import QS from "qs"
import { goTologin } from "@/utils/util.js"
import { showFullScreenLoading, tryHideFullScreenLoading } from "./loading"
import store from "@/store/index"
import axios from "axios";
import QS from "qs";
import { goTologin } from "@/utils/util.js";
import { showFullScreenLoading, tryHideFullScreenLoading } from "./loading";
import store from "@/store/index";
axios.defaults.baseURL = "https://app.gter.net"
axios.defaults.emulateJSON = true
axios.defaults.withCredentials = true
// content-type 为 multipart/form-data
axios.defaults.baseURL = "https://app.gter.net";
axios.defaults.emulateJSON = true;
axios.defaults.withCredentials = true;
// axios.defaults.headers["Content-Type"] = "application/json";
axios.interceptors.request.use(
//响应拦截
async config => {
async (config) => {
// 不需要遮罩
let noMask = false
let noMask = false;
if (config.method == "get") noMask = config.params?.noMask || false
noMask = config.params?.noMask || config.noMask || false;
if (config.url != "/tenement/pc/api/user/operation" && !noMask) showFullScreenLoading()
if (config.url != "/tenement/pc/api/user/operation" && !noMask) showFullScreenLoading();
// 开发时登录用的,可以直接替换小程序的 authorization
if (process.env.NODE_ENV !== "production") {
const miucms_session = "01346a38444d71aaadb3adad52b52c39";
document.cookie = "miucms_session=" + miucms_session;
config["headers"]["authorization"] = miucms_session;
}
// 当 noMask == true 和 confing.method == 'get' 时,删除 config.params['noMask']
if (noMask && config.method == "get") delete config.params["noMask"]
return config
// 当 noMask == true 和 confing.method == 'get' 时,删除 config.params['noMask']
if (noMask && config.params) delete config.params["noMask"];
return config;
},
error => {
return Promise.error(error)
(error) => {
return Promise.error(error);
}
)
);
// 响应拦截器
axios.interceptors.response.use(
response => {
tryHideFullScreenLoading()
if (response.status === 200) return Promise.resolve(response) //进行中
else return Promise.reject(response) //失败
(response) => {
tryHideFullScreenLoading();
if (response.status === 200) return Promise.resolve(response); //进行中
else return Promise.reject(response); //失败
},
// 服务器状态码不是200的情况
error => {
tryHideFullScreenLoading()
(error) => {
tryHideFullScreenLoading();
if (error.response.status) {
switch (error.response.status) {
// 401: 未登录
case 401:
// goTologin() // 跳转登录页面
store.state.showloginmodal = true
break
store.state.showloginmodal = true;
break;
case 403:
// router.push('/login')
break
break;
// 404请求不存在
case 404:
break
break;
// 其他错误,直接抛出错误提示
default:
}
return Promise.reject(error.response)
return Promise.reject(error.response);
}
}
)
);
/**
* get方法对应get请求
* @param {String} url [请求的url地址]
@@ -75,14 +74,14 @@ const $get = (url, params) => {
.get(url, {
params: params,
})
.then(res => {
resolve(res.data)
.then((res) => {
resolve(res.data);
})
.catch(err => {
reject(err.data)
})
})
}
.catch((err) => {
reject(err.data);
});
});
};
/**
* post方法对应post请求
* @param {String} url [请求的url地址]
@@ -93,23 +92,40 @@ const $post = (url, params) => {
//是将对象 序列化成URL的形式以&进行拼接
axios
.post(url, QS.stringify(params))
.then(res => {
resolve(res.data)
.then((res) => {
resolve(res.data);
})
.catch(err => {
if (err.data.code == 401) resolve(err.data)
else reject(err.data)
.catch((err) => {
if (err.data.code == 401) resolve(err.data);
else reject(err.data);
});
});
};
const $postV2 = (url, params) => {
return new Promise((resolve, reject) => {
console.log("params", params);
axios
.post(url, JSON.stringify(params), { headers: { "Content-Type": "application/json" }, timeout: 100000, ...(params.noMask !== undefined ? { noMask: params.noMask } : {}) })
.then((res) => {
resolve(res.data);
})
})
}
.catch((err) => {
if (err.data.code == 401) resolve(err.data);
else reject(err.data);
});
});
};
//下面是vue3必须加的vue2不需要只需要暴露出去getpost方法就可以
export default {
get: $get,
post: $post,
install: app => {
app.config.globalProperties["$get"] = $get
app.config.globalProperties["$post"] = $post
app.config.globalProperties["$axios"] = axios
postV2: $postV2,
install: (app) => {
app.config.globalProperties["$get"] = $get;
app.config.globalProperties["$post"] = $post;
app.config.globalProperties["$axios"] = axios;
},
}
};

View File

@@ -2,7 +2,7 @@
<div class="main">
<header class="header flexacenter">
<div class="title">寄托AI找房</div>
<div class="logo flex1 flexacenter">
<div class="logo flexacenter">
<img class="icon" src="../assets/img/publicImage/round-icon.gif" />
圆同学
</div>
@@ -24,14 +24,20 @@
<div class="history-line history-right"></div>
</div>
<AiItem v-for="(item, index) in list" :key="index" :item="item"></AiItem>
<AiItem v-for="(item, index) in list" :key="item.id" :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" />
<textarea class="input-text" placeholder="请输入" @input="handleInput" v-model="input" @keyup.enter.prevent="send()" ref="inputRef" maxlength="100"></textarea>
<div class="input-bottom flexacenter">
<div class="restart flexcenter" @click="restartAffirm">
<img class="icon" src="@/assets/img/publicImage/reopen-icon.png" />
<div class="text">新开会话</div>
</div>
<div class="send flexcenter" :class="{ pitch: input.length > 0 }" @click="send()">
<img class="icon" src="../assets/img/publicImage/arrows-deep-white.svg" />
</div>
</div>
</div>
</div>
@@ -40,15 +46,45 @@
</template>
<script setup>
import { reactive, onMounted, ref, nextTick, onBeforeUnmount, watch } from "vue";
import { reactive, onMounted, ref, nextTick, onBeforeUnmount, watch, getCurrentInstance, onUnmounted } 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";
import { ElMessage, ElMessageBox } from "element-plus";
const { proxy } = getCurrentInstance();
// 处理输入内容,去除末尾换行符
const handleInput = (e) => {
input.value = input.value.replace(/\n+$/, "");
autoResize();
};
// 全局滚动到顶部监听函数
const handleScrollToTop = () => {
const isAtTop = window.scrollY === 0;
if (isAtTop) {
console.log("页面已滚动到顶部");
// 这里可以添加滚动到顶部后的业务逻辑
getHistory();
}
};
onMounted(() => {
console.log("onMounted");
init();
document.addEventListener("visibilitychange", handleVisibilityChange);
window.addEventListener("beforeunload", endChat);
window.addEventListener("scroll", handleScrollToTop);
});
onBeforeUnmount(() => {
// 移除全局滚动事件监听
window.removeEventListener("scroll", handleScrollToTop);
document.removeEventListener("visibilitychange", handleVisibilityChange);
window.removeEventListener("beforeunload", endChat);
});
let initState = false;
@@ -94,21 +130,26 @@ const scrollEnd = async () => {
// 1. 获取当前水平滚动位置
const currentX = window.pageXOffset;
// 2. 设置新的滚动位置:水平位置不变,垂直滚动到 500px 处
window.scrollTo(currentX, 100000);
window.scrollTo(currentX, 1000000);
});
};
// 滚动到指定位置
const scrollItem = (id) => {
nextTick(() => {
const element = contentRef.value.querySelector(`[data-id="${id}"]`);
if (element) {
element.scrollIntoView({
behavior: "smooth",
block: "center",
});
}
});
console.log("id", id);
const element = contentRef.value.querySelector(`#item${id}`);
console.log("element", element);
if (element) {
// 计算元素顶部位置并减去50px偏移
const rect = element.getBoundingClientRect();
const top = rect.top + window.pageYOffset - 60;
window.scrollTo({
top: top,
behavior: "smooth",
});
}
};
let page = 1;
@@ -128,34 +169,53 @@ const getHistory = () => {
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();
})
.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 = history.concat(historyList.value);
history = calculateRecordTimeText(history);
history = calculateRecordTimeText(history);
if (history.length > 0 && page == 1) {
let obj = historyObj || {};
list.value.push(obj);
}
if (history.length > 0 && page == 1) {
let obj = historyObj || {};
list.value.push(obj);
}
console.log("history", history);
console.log("history", history);
historyList.value = history;
isLogo.value = history.length == 0 ? true : false;
historyList.value = history;
isLogo.value = history.length == 0 ? true : false;
if (page > 1) scrollItem(aiId);
else scrollEnd();
if (page > 1) {
// scrollItem(aiId);
const beforeScrollHeight = document.body.scrollHeight; // 加载前的总高度
page = data.page * limit >= data.count ? 0 : data.page + 1;
});
nextTick(() => {
const afterScrollHeight = document.body.scrollHeight; // 加载后的总高度
const heightDiff = afterScrollHeight - beforeScrollHeight;
// container.scrollTop = heightDiff; // 关键通过设置scrollTop抵消高度变化
window.scrollTo({
top: heightDiff,
// behavior: "smooth",
});
});
} else scrollEnd();
data.page = data.page * 1;
page = data.page * limit >= data.count ? 0 : data.page + 1;
})
.finally(() => {
loading = false;
});
};
let firstTimeStamp = null;
@@ -182,10 +242,201 @@ const calculateRecordTimeText = (list = []) => {
return list;
};
let generateState = false;
let input = ref("");
const send = () => {
console.log("send");
if (generateState) {
ElMessage.error("生成中,请等待!");
return;
}
const inputValue = input.value;
console.log("inputValue", inputValue);
if (!inputValue) {
ElMessage.error("请输入内容");
return;
}
let listValue = list.value || [];
const arr = [
{
content: inputValue,
role: "user",
timestamp: getFormattedTime(),
id: randomString(),
},
{
role: "assistant",
loading: true,
timestamp: getFormattedTime(),
id: randomString(),
},
];
listValue = listValue.concat(arr);
listValue = calculateRecordTimeText(listValue);
list.value = listValue;
input.value = "";
nextTick(() => autoResize());
generateState = true;
nextTick(() => scrollEnd());
api.alChat({
session_id,
message: inputValue,
noMask: true,
})
.then((res) => {
if (res.detail) {
chatError(listValue, inputValue, res.detail);
return;
}
let history = res.history || [];
let itemUser = history.findLast((item) => item.role === "user");
listValue[listValue.length - 2] = Object.assign(listValue[listValue.length - 2], itemUser);
let itemAssis = history.findLast((item) => item.role === "assistant");
console.log("history", history);
itemAssis = handleData(itemAssis);
let target = listValue.findLast((item) => item.role === "assistant");
target = Object.assign(target, itemAssis);
target["loading"] = false;
if (itemAssis?.payload?.type == "text") {
let message = itemAssis?.payload?.message || "";
if (message.includes("<wechat>")) {
const startTag = "<wechat>";
const endTag = "</wechat>";
const startIndex = message.indexOf(startTag);
const endIndex = message.indexOf(endTag);
itemAssis.payload.message = {
before: message.substring(0, startIndex),
wechat: message.substring(startIndex + startTag.length, endIndex),
after: message.substring(endIndex + endTag.length),
};
}
}
listValue[listValue.length - 1] = target;
if (itemAssis?.payload?.type == "recommendation") {
listValue.push({
role: "contacts",
timestamp: getFormattedTime(),
});
}
generateState = false;
list.value = listValue;
// 强制更新组件
// const instance = getCurrentInstance();
proxy.$forceUpdate();
nextTick(() => scrollItem(itemUser.id));
})
.catch((err) => {
chatError(listValue, inputValue);
});
};
// 处理数据
const handleData = (obj) => {
// 通用处理函数:检查数组每项是否有句号,无则补充,再用逗号连接
const processArrayField = (array) => {
if (!Array.isArray(array)) return array;
return array
.map((item) => {
let trimmed = item.trim();
if (!trimmed) return trimmed;
if (trimmed.endsWith("")) trimmed = trimmed.slice(0, -1) + "。";
else if (!trimmed.endsWith("。")) trimmed += "。";
return trimmed;
})
.join("");
};
if (obj.payload?.recommendations?.length > 0) {
const recommendations = obj.payload.recommendations;
recommendations.forEach((element) => {
element.advantages = processArrayField(element.advantages);
element.disadvantages = processArrayField(element.disadvantages);
if (Array.isArray(element.district)) element.district = element.district.join("、");
let url = `/apartmentDetail?uniqid=${element.uniqid}`;
if (["person", "agent"].includes(element.type)) url = `/detail?id=${element.uniqid}`;
console.log("url", url);
element["url"] = url;
element["score"] = Math.floor(element.recommendation_score);
element["scoreHalf"] = element.recommendation_score % 1 >= 0.5;
if (element.images?.length > 0) element.images.forEach((ele, index) => (element.images[index] = `https://oss.x-php.com/${ele}`));
});
}
return obj;
};
const chatError = (listValue, inputValue, hint) => {
let target = listValue[listValue.length - 1];
const obj = {
type: "text",
message: hint || "网络开小差了,圆同学现在很方,麻烦你再说一次吧",
loading: false,
iserror: true,
id: randomString(),
};
target = Object.assign(target, obj);
listValue[listValue.length - 1] = target;
console.log("target", listValue);
generateState = false;
list.value = [...listValue];
input.value = inputValue;
console.log(list.value);
nextTick(() => autoResize());
};
// 恢复会话
const handleResume = () => {
if (!session_id || !initState) return;
api.alResume({ session_id }).then((res) => {
console.log("res", res);
});
};
// 结束对话 上报历史
const endChat = () => {
let listValue = [...list.value];
listValue = listValue.filter((item) => {
return !("isHistory" in item) && item.role !== "contacts";
});
api.alEnd({
session_id,
history: listValue,
}).then((res) => {
console.log("res", res);
});
};
let inputRef = ref(null);
// 自动输入框增高
const autoResize = (e) => {
e.target.style.height = "auto"; // 重置高度
e.target.style.height = `${e.target.scrollHeight}px`; // 设置为内容高度
console.log("inputRef", inputRef.value);
inputRef.value.style.height = "auto"; // 重置高度
inputRef.value.style.height = `${inputRef.value.scrollHeight + 1}px`; // 设置为内容高度
};
const getTimestamp = (time) => {
@@ -193,6 +444,30 @@ const getTimestamp = (time) => {
return new Date(time.replace(" ", "T")).getTime();
};
// 输出类似 "2025-08-04T10:38:23.095600+08:00" 的字符串
const getFormattedTime = () => {
const date = new Date(); // 获取当前时间,也可传入时间戳指定时间
// 手动拼接带时区的ISO格式保留6位毫秒
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0"); // 月份从0开始
const day = String(date.getDate()).padStart(2, "0");
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
const seconds = String(date.getSeconds()).padStart(2, "0");
const milliseconds = String(date.getMilliseconds()).padStart(3, "0").padEnd(6, "0"); // 补全6位
// 获取时区偏移(分钟)并转换为 ±HH:MM 格式
const timeZoneOffset = date.getTimezoneOffset();
const offsetHours = Math.abs(Math.floor(timeZoneOffset / 60));
const offsetMinutes = Math.abs(timeZoneOffset % 60);
const offsetSign = timeZoneOffset > 0 ? "-" : "+";
const formattedOffset = `${offsetSign}${String(offsetHours).padStart(2, "0")}:${String(offsetMinutes).padStart(2, "0")}`;
// 拼接最终格式
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}.${milliseconds}${formattedOffset}`;
};
// 加载历史消息(插入到列表顶部)
const loadHistory = async () => {
isLoading.value = true;
@@ -251,6 +526,87 @@ const formatTimestamp = (timestamp) => {
return str;
};
// 页面可见性状态visible: 显示, hidden: 隐藏)
const visibilityState = ref(document.visibilityState);
// 上次状态变化时间
const lastChangeTime = ref("");
// 处理可见性变化的函数
const handleVisibilityChange = () => {
// 更新状态
visibilityState.value = document.visibilityState;
// 记录时间
lastChangeTime.value = new Date().toLocaleTimeString();
// 根据状态执行不同操作
if (document.visibilityState === "visible") {
console.log("页面已显示(用户切回标签页/窗口)");
// 可执行恢复操作:如重新请求数据、播放视频等
handleResume();
} else {
console.log("页面已隐藏(用户切换标签页/最小化窗口)");
// 可执行暂停操作:如暂停视频、保存临时数据等
endChat();
}
};
const randomString = () => {
const random = Math.random().toString(36).substring(2);
const timestamp = Date.now().toString(36);
return random + timestamp;
};
onUnmounted(() => {
// 组件卸载时移除事件监听(避免内存泄漏)
window.removeEventListener("beforeunload", endChat);
document.removeEventListener("visibilitychange", handleVisibilityChange);
});
// 新开会话 确认
const restartAffirm = () => {
if (generateState) {
ElMessage.error("生成中,请等待!");
return;
}
ElMessageBox.confirm("您是否希望离开当前对话,并开始新的聊天?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(() => {
endChat();
restart();
});
};
// 新开会话
const restart = () => {
api.alNew().then((res) => {
const data = res.data;
let history = data.history || [];
session_id = data.session_id;
let listValue = [...list.value] || [];
listValue = listValue.filter((item) => {
return !("isHistory" in item) && item.role !== "contacts";
});
historyList.value = historyList.value.concat(listValue);
let obj = historyObj || {};
const target = history[0] || {};
obj["payload"] = target["payload"];
obj["timestamp"] = target["timestamp"];
obj["timeText"] = formatTimestamp(getTimestamp(target["timestamp"])) || "";
isLogo.value = false;
list.value = [obj];
nextTick(() => scrollEnd());
});
};
</script>
<style scoped lang="less">
@@ -267,6 +623,7 @@ const formatTimestamp = (timestamp) => {
position: fixed;
top: 0;
left: 0;
z-index: 1;
.title {
font-family: "PingFangSC-Semibold", "PingFang SC Semibold", "PingFang SC", sans-serif;
@@ -303,7 +660,7 @@ const formatTimestamp = (timestamp) => {
flex: 1;
// background: aqua;
flex-direction: column;
padding-top: 50px;
padding-top: 60px;
padding-bottom: 150px;
.logo {
width: 110px;
@@ -351,31 +708,47 @@ const formatTimestamp = (timestamp) => {
max-height: 230px;
}
.send {
width: 32px;
height: 32px;
border-radius: 50%;
background-color: #d7d7d7;
margin-left: auto;
.input-bottom {
margin-left: 10px;
margin-right: 20px;
margin-bottom: 15px;
cursor: not-allowed;
&.pitch {
background-color: #7fddff;
cursor: pointer;
}
.icon {
width: 16px;
height: 16px;
transform: rotate(270deg);
margin-bottom: 15px;
.restart {
display: flex;
flex-direction: column;
cursor: pointer;
font-size: 10px;
.icon {
width: 20px;
height: 20px;
margin-bottom: 3px;
}
}
.send {
width: 32px;
height: 32px;
border-radius: 50%;
background-color: #d7d7d7;
margin-left: auto;
cursor: not-allowed;
&.pitch {
background-color: #7fddff;
cursor: pointer;
}
.icon {
width: 16px;
height: 16px;
transform: rotate(270deg);
}
}
}
}
}
.history-hint {
font-size: 16px;
font-size: 14px;
line-height: 26px;
color: #7f7f7f;
text-align: center;

View File

@@ -832,6 +832,8 @@ const cloaseImageShow = (list, index, type) => {
imageIndex.value = index;
imageType = type;
}
console.log(imageList.value);
imageShow.value = !imageShow.value;
};

View File

@@ -34,7 +34,6 @@ import seachModule from "../../components/seachModule/seachModule1.vue"
import biserialItem from "../../components/biserialListItem/biserialListItem.vue"
import listBtmPrompt from "../../components/public/have-questions.vue"
import noList from "../../components/public/empty-duck.vue"
import circleBtn from "@/components/public/circle-btn.vue"
import api from "../../utils/api"
import tool from "../../toolJs/downLoadMore"
import { ElMessage } from "element-plus"

View File

@@ -33,7 +33,6 @@ import seachModule from "../../components/seachModule/seachModule1.vue"
import biserialItem from "../../components/biserialListItem/biserialListItem.vue"
import listBtmPrompt from "../../components/public/have-questions.vue"
import noList from "../../components/public/empty-duck.vue"
import circleBtn from "@/components/public/circle-btn.vue"
import api from "../../utils/api"
import tool from "../../toolJs/downLoadMore"
import { ElMessage } from "element-plus"

View File

@@ -34,7 +34,6 @@ import seachModule from "../../components/seachModule/seachModule1.vue"
import biserialItem from "../../components/biserialListItem/biserialListItem.vue"
import listBtmPrompt from "../../components/public/have-questions.vue"
import noList from "../../components/public/empty-duck.vue"
import circleBtn from "@/components/public/circle-btn.vue"
import api from "../../utils/api"
import tool from "../../toolJs/downLoadMore"
import { useRouter } from "vue-router"