feat(ai): 优化AI聊天界面布局和交互逻辑

- 重构输入框布局,将"新开会话"按钮移至左侧
- 添加ResizeObserver监听输入框高度变化,动态调整内容区域padding
- 增加登录状态检查,未登录时提示并限制操作
- 优化文本显示样式,包括AI提示框的圆角和背景色
- 修复输入框自动高度调整逻辑
- 提取logo文字为变量,支持动态配置
This commit is contained in:
DESKTOP-RQ919RC\Pc
2025-08-26 12:09:06 +08:00
parent 5fd6f68a7c
commit 09f55e27f1
11 changed files with 153 additions and 63 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
dist/index.html vendored
View File

@@ -1,4 +1,4 @@
<!doctype html><html lang=""><head><meta charset="utf-8"/><meta http-equiv="X-UA-Compatible" content="IE=edge"/><meta name="viewport" content="width=device-width,initial-scale=1"/><link rel="icon" href="https://ansnid.oss-cn-shenzhen.aliyuncs.com/fang/favicon.ico"/><title>港校租房</title><script defer="defer" src="https://ansnid.oss-cn-shenzhen.aliyuncs.com/fang/js/chunk-vendors.65a8ec5d.js"></script><script defer="defer" src="https://ansnid.oss-cn-shenzhen.aliyuncs.com/fang/js/app.ecc66856.js"></script><link href="https://ansnid.oss-cn-shenzhen.aliyuncs.com/fang/css/chunk-vendors.7885d77e.css" rel="stylesheet"><link href="https://ansnid.oss-cn-shenzhen.aliyuncs.com/fang/css/app.e162f837.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but zufang doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><script src="https://app.gter.net/bottom?tpl=header&menukey=fang"></script><div id="app"></div><div style="display: none;"><script>var _hmt = _hmt || [] <!doctype html><html lang=""><head><meta charset="utf-8"/><meta http-equiv="X-UA-Compatible" content="IE=edge"/><meta name="viewport" content="width=device-width,initial-scale=1"/><link rel="icon" href="https://ansnid.oss-cn-shenzhen.aliyuncs.com/fang/favicon.ico"/><title>港校租房</title><script defer="defer" src="https://ansnid.oss-cn-shenzhen.aliyuncs.com/fang/js/chunk-vendors.65a8ec5d.js"></script><script defer="defer" src="https://ansnid.oss-cn-shenzhen.aliyuncs.com/fang/js/app.b40db703.js"></script><link href="https://ansnid.oss-cn-shenzhen.aliyuncs.com/fang/css/chunk-vendors.7885d77e.css" rel="stylesheet"><link href="https://ansnid.oss-cn-shenzhen.aliyuncs.com/fang/css/app.e162f837.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but zufang doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><script src="https://app.gter.net/bottom?tpl=header&menukey=fang"></script><div id="app"></div><div style="display: none;"><script>var _hmt = _hmt || []
;(function () { ;(function () {
var hm = document.createElement("script") var hm = document.createElement("script")
hm.src = "//hm.baidu.com/hm.js?4bd66cbe45a640b607fe46c48f658746" hm.src = "//hm.baidu.com/hm.js?4bd66cbe45a640b607fe46c48f658746"

1
dist/js/app.b40db703.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
dist/js/edit.e600b5f5.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/js/housing.a7d1dd9f.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -78,9 +78,10 @@
<div class="ai-hint">内容由AI生成请审慎参考</div> <div class="ai-hint">内容由AI生成请审慎参考</div>
</template> </template>
<div v-else-if="item?.payload?.type == 'text' && item?.payload?.message?.before"> <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.before
<wechat-btn>{{ item.payload.message.wechat }}</wechat-btn> }}<span class="text link" bind:tap="consultStateCut" v-if="item.payload.message.wechat">
</span>{{ item.payload.message.after }} <wechat-btn>{{ item.payload.message.wechat }}</wechat-btn> </span
>{{ item.payload.message.after }}
</div> </div>
<div v-else>{{ item?.payload?.message || item.message }}</div> <div v-else>{{ item?.payload?.message || item.message }}</div>
</div> </div>
@@ -95,7 +96,6 @@ import Contacts from "./contacts.vue";
import imageWatch from "@/components/detail/imageWatch.vue"; import imageWatch from "@/components/detail/imageWatch.vue";
import wechatBtn from "@/components/ai/wechat-btn.vue"; import wechatBtn from "@/components/ai/wechat-btn.vue";
const props = defineProps({ const props = defineProps({
item: { item: {
type: Object, type: Object,
@@ -123,8 +123,7 @@ const openImageShow = (list, index) => {
const cloaseImageShow = () => { const cloaseImageShow = () => {
imageShow.value = false; imageShow.value = false;
} };
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@@ -369,6 +368,10 @@ const cloaseImageShow = () => {
color: #aaaaaa; color: #aaaaaa;
font-size: 14px; font-size: 14px;
text-align: center; text-align: center;
height: 32px;
line-height: 32px;
background-color: rgba(246, 246, 246, 1);
border-radius: 5px;
} }
.link { .link {

View File

@@ -10,7 +10,7 @@
<div class="content flexflex" ref="contentRef"> <div class="content flexflex" ref="contentRef">
<template v-if="isLogo"> <template v-if="isLogo">
<img class="logo" src="../assets/img/publicImage/round-icon.gif" /> <img class="logo" src="../assets/img/publicImage/round-icon.gif" />
<div class="hint">Hi我是你的租房小助手圆同学初次见面很开心目前我还属于初代产品只能提供房源推荐服务暂时干不了其他事来吧告诉我你想找什么样的房子</div> <div class="hint">{{ logoText }}</div>
</template> </template>
<template v-if="historyList.length > 0"> <template v-if="historyList.length > 0">
@@ -26,18 +26,15 @@
<AiItem v-for="(item, index) in list" :key="item.id" :item="item"></AiItem> <AiItem v-for="(item, index) in list" :key="item.id" :item="item"></AiItem>
<!-- fixed --> <div class="input-box flexflex" :class="{ fixed: !isLogo }" ref="inputBoxRef">
<div class="input-box" :class="{ fixed: !isLogo }"> <div class="restart flexcenter" @click="restartAffirm">
<div class="input"> <img class="icon" src="@/assets/img/publicImage/reopen-icon.png" />
<div>新开会话</div>
</div>
<div class="input flex1" @click="inputRef.focus()">
<textarea class="input-text" placeholder="请输入" @input="handleInput" v-model="input" @keyup.enter.prevent="send()" ref="inputRef" maxlength="100"></textarea> <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="send flexcenter" :class="{ pitch: input.length > 0 }" @click.stop="send()">
<div class="restart flexcenter" @click="restartAffirm"> <img class="icon" src="../assets/img/publicImage/arrows-deep-white.svg" />
<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> </div>
</div> </div>
@@ -68,6 +65,19 @@ const handleScrollToTop = () => {
}; };
onMounted(() => { onMounted(() => {
// 初始化input-box高度监听
resizeObserver = new ResizeObserver((entries) => {
const inputBoxHeight = entries[0].contentRect.height;
console.log("entries", entries[0].contentRect);
if (contentRef.value) {
contentRef.value.style.paddingBottom = `${inputBoxHeight + 32 + 40}px`;
}
});
if (inputBoxRef.value) {
resizeObserver.observe(inputBoxRef.value);
contentRef.value.style.paddingBottom = `${inputBoxRef.value.offsetHeight + 32 + 40}px`;
}
init(); init();
document.addEventListener("visibilitychange", handleVisibilityChange); document.addEventListener("visibilitychange", handleVisibilityChange);
@@ -76,6 +86,10 @@ onMounted(() => {
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
// 移除高度监听
if (resizeObserver) {
resizeObserver.disconnect();
}
// 移除全局滚动事件监听 // 移除全局滚动事件监听
window.removeEventListener("scroll", handleScrollToTop); window.removeEventListener("scroll", handleScrollToTop);
document.removeEventListener("visibilitychange", handleVisibilityChange); document.removeEventListener("visibilitychange", handleVisibilityChange);
@@ -94,19 +108,28 @@ let historyObj = {
}, },
}; };
let isLogin = false; // 登录状态
let logoText = ref(""); // logo 文字
// 初始化 // 初始化
const init = () => { const init = () => {
api.alInit().then((res) => { api.alInit().then((res) => {
if (res.code == 401) store.state.showloginmodal = true; if (res.code == 401) {
store.state.showloginmodal = true;
isLogin = false;
}
if (res.code != 200) return; if (res.code != 200) return;
isLogin = true;
const data = res.data; const data = res.data;
let history = data.history || []; let history = data.history || [];
session_id = data.session_id; session_id = data.session_id;
initState = true; initState = true;
const target = history[0] || {}; const target = history[0] || {};
logoText.value = target["payload"]?.message || "";
historyObj["id"] = randomString();
historyObj["payload"] = target["payload"]; historyObj["payload"] = target["payload"];
historyObj["timestamp"] = target["timestamp"]; historyObj["timestamp"] = target["timestamp"];
historyObj["timeText"] = formatTimestamp(getTimestamp(target["timestamp"])) || ""; historyObj["timeText"] = formatTimestamp(getTimestamp(target["timestamp"])) || "";
@@ -116,6 +139,8 @@ const init = () => {
}; };
const contentRef = ref(null); const contentRef = ref(null);
const inputBoxRef = ref(null); // input-box的ref引用
let resizeObserver = null; // ResizeObserver实例
// 滚动到最后一条 // 滚动到最后一条
const scrollEnd = async () => { const scrollEnd = async () => {
@@ -151,6 +176,10 @@ let isLogo = ref(true);
// 获取历史记录 // 获取历史记录
const getHistory = () => { const getHistory = () => {
if (!isLogin) {
ElMessage.error("请先登录!");
return;
}
if (page == 0 || loading) return; if (page == 0 || loading) return;
loading = true; loading = true;
@@ -164,8 +193,8 @@ const getHistory = () => {
.then((res) => { .then((res) => {
if (res.code != 200) return; if (res.code != 200) return;
let data = res.data || {}; let data = res.data || {};
// data.count = 0 // data.count = 0;
// data.history = [] // data.history = [];
let history = data.history || []; let history = data.history || [];
history.reverse(); history.reverse();
@@ -175,6 +204,8 @@ const getHistory = () => {
if (history.length > 0 && page == 1) { if (history.length > 0 && page == 1) {
let obj = historyObj || {}; let obj = historyObj || {};
obj["id"] = randomString();
list.value.push(obj); list.value.push(obj);
} }
@@ -234,11 +265,21 @@ let generateState = false;
let input = ref(""); let input = ref("");
const send = () => { const send = () => {
if (!isLogin) {
ElMessage.error("请先登录!");
return;
}
if (generateState) { if (generateState) {
ElMessage.error("生成中,请等待!"); ElMessage.error("生成中,请等待!");
return; return;
} }
if (!session_id) {
ElMessage.error("出错了,请刷新页面重试");
return;
}
const inputValue = input.value; const inputValue = input.value;
if (!inputValue) { if (!inputValue) {
@@ -390,7 +431,7 @@ const chatError = (listValue, inputValue, hint) => {
// 恢复会话 // 恢复会话
const handleResume = () => { const handleResume = () => {
if (!session_id || !initState) return; if (!session_id || !initState || !isLogin) return;
api.alResume({ session_id }).then((res) => { api.alResume({ session_id }).then((res) => {
// console.log("res", res); // console.log("res", res);
}); });
@@ -398,6 +439,11 @@ const handleResume = () => {
// 结束对话 上报历史 // 结束对话 上报历史
const endChat = () => { const endChat = () => {
if (!isLogin) {
ElMessage.error("请先登录!");
return;
}
let listValue = [...list.value]; let listValue = [...list.value];
listValue = listValue.filter((item) => { listValue = listValue.filter((item) => {
return !("isHistory" in item) && item.role !== "contacts"; return !("isHistory" in item) && item.role !== "contacts";
@@ -415,6 +461,10 @@ let inputRef = ref(null);
// 自动输入框增高 // 自动输入框增高
const autoResize = (e) => { const autoResize = (e) => {
if (!input.value) {
inputRef.value.style.height = "41px"; // 重置高度
return;
}
inputRef.value.style.height = "auto"; // 重置高度 inputRef.value.style.height = "auto"; // 重置高度
inputRef.value.style.height = `${inputRef.value.scrollHeight + 1}px`; // 设置为内容高度 inputRef.value.style.height = `${inputRef.value.scrollHeight + 1}px`; // 设置为内容高度
}; };
@@ -521,7 +571,7 @@ const handleVisibilityChange = () => {
// 根据状态执行不同操作 // 根据状态执行不同操作
if (document.visibilityState === "visible") handleResume(); if (document.visibilityState === "visible") handleResume();
else endChat(); // 可执行暂停操作:如暂停视频、保存临时数据等 else endChat(); // 可执行暂停操作:如暂停视频、保存临时数据等
}; };
const randomString = () => { const randomString = () => {
@@ -554,6 +604,10 @@ const restartAffirm = () => {
// 新开会话 // 新开会话
const restart = () => { const restart = () => {
if (!isLogin) {
ElMessage.error("请先登录!");
return;
}
api.alNew().then((res) => { api.alNew().then((res) => {
const data = res.data; const data = res.data;
let history = data.history || []; let history = data.history || [];
@@ -569,7 +623,7 @@ const restart = () => {
let obj = historyObj || {}; let obj = historyObj || {};
const target = history[0] || {}; const target = history[0] || {};
obj["id"] = randomString();
obj["payload"] = target["payload"]; obj["payload"] = target["payload"];
obj["timestamp"] = target["timestamp"]; obj["timestamp"] = target["timestamp"];
obj["timeText"] = formatTimestamp(getTimestamp(target["timestamp"])) || ""; obj["timeText"] = formatTimestamp(getTimestamp(target["timestamp"])) || "";
@@ -648,7 +702,7 @@ const restart = () => {
font-size: 16px; font-size: 16px;
line-height: 26px; line-height: 26px;
color: #333333; color: #333333;
width: 430px; max-width: 430px;
margin: 0 auto 86px; margin: 0 auto 86px;
} }
@@ -659,8 +713,37 @@ const restart = () => {
position: fixed; position: fixed;
bottom: 0; bottom: 0;
z-index: 1; z-index: 1;
padding-bottom: 38px; padding-bottom: 32px;
background: #fff; background: #fff;
.restart {
display: flex;
}
.input {
// height: 90px;
.input-text {
height: 41px;
}
}
}
.restart {
display: none;
flex-direction: column;
cursor: pointer;
width: 90px;
// height: 90px;
background-color: rgba(246, 246, 246, 1);
border-radius: 20px;
color: #333333;
margin-right: 10px;
font-size: 14px;
.icon {
width: 20px;
height: 20px;
margin-bottom: 5px;
}
} }
.input { .input {
@@ -677,46 +760,50 @@ const restart = () => {
resize: none; resize: none;
display: block; display: block;
font-size: 16px; font-size: 16px;
height: 51px; // height: 51px;
max-height: 230px; max-height: 230px;
transition: all 0.3s;
} }
.input-bottom { // .input-bottom {
margin-left: 10px; // margin-left: 10px;
// margin-right: 20px;
// 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;
margin-right: 20px; margin-right: 20px;
cursor: not-allowed;
margin-bottom: 15px; margin-bottom: 15px;
.restart {
display: flex; &.pitch {
flex-direction: column; background-color: #7fddff;
cursor: pointer; 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 { .icon {
width: 16px; width: 16px;
height: 16px; height: 16px;
transform: rotate(270deg); transform: rotate(270deg);
}
} }
} }
// }
} }
} }