feat: 添加微信按钮组件及优化AI聊天功能
refactor: 移除未使用的circle-btn组件引用 style: 更新图片资源路径及样式 fix: 修复axios请求配置及类型定义 docs: 添加mock-api.js模拟接口文件
This commit is contained in:
6
src/assets/img/publicImage/arrow-circular-orange-red.svg
Normal file
6
src/assets/img/publicImage/arrow-circular-orange-red.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="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>
|
||||
BIN
src/assets/img/publicImage/reopen-icon.png
Normal file
BIN
src/assets/img/publicImage/reopen-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.3 KiB |
6
src/assets/img/publicImage/star-half.svg
Normal file
6
src/assets/img/publicImage/star-half.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="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>
|
||||
6
src/assets/img/publicImage/star-icon.svg
Normal file
6
src/assets/img/publicImage/star-icon.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="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>
|
||||
16
src/assets/img/publicImage/wechat-pop-green.svg
Normal file
16
src/assets/img/publicImage/wechat-pop-green.svg
Normal 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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
167
src/components/ai/wechat-btn.vue
Normal file
167
src/components/ai/wechat-btn.vue
Normal 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>
|
||||
@@ -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
40
src/mock/mock-api.js
Normal 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
|
||||
@@ -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)
|
||||
|
||||
@@ -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不需要,只需要暴露出去get,post方法就可以
|
||||
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;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
487
src/views/ai.vue
487
src/views/ai.vue
@@ -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;
|
||||
|
||||
@@ -832,6 +832,8 @@ const cloaseImageShow = (list, index, type) => {
|
||||
imageIndex.value = index;
|
||||
imageType = type;
|
||||
}
|
||||
console.log(imageList.value);
|
||||
|
||||
imageShow.value = !imageShow.value;
|
||||
};
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user