本周工作内容:
-
前端界面开发
- 完成了整个与ChatGPT对话界面相似的前端界面的开发工作。
- 具体工作包括:
- 搭建了界面的基本框架,设计了整体布局和结构。
- 实现了主要UI元素,包括文本输入框、发送按钮、消息气泡、用户头像等。
- 编写了详细的CSS,确保各元素在不同设备和分辨率下都能正确显示。
-
对话功能实现
- 实现了文本输入和发送功能,使用户能够输入并发送消息。
- 开发了消息显示区域,确保发送的消息能即时、准确地显示在界面上。
- 实现了对话气泡的样式,使其美观且易于阅读,用户消息和系统回复有清晰的区分。
-
响应式设计与优化
- 确保界面在各种设备(桌面、平板、手机)上都有良好的用户体验。
- 针对不同浏览器进行了测试,修复了兼容性问题,确保在主流浏览器(如Chrome、Firefox、Safari等)上表现一致。
- 优化了界面的加载速度和交互性能,提高了用户操作的流畅性。
-
功能调试与完善
- 进行了多轮功能测试,确保对话功能的稳定性和可靠性。
- 修复了测试过程中发现的若干问题,如消息顺序错乱、界面元素位置不对等。
- 根据用户反馈进行了部分功能和界面的调整和优化,提高了用户满意度。
-
文档编写与总结
- 编写了详细的技术文档,记录了界面的开发过程、技术选型和实现细节。
- 总结了本周的开发工作,为下周的功能扩展和优化提供了依据。
代码展示:
<template> <div class="container"> <div class="popup" ref="popup" @click="popupClose"> <div class="con_left" @click.stop> <button class="new_talk" @click="newTalk">开始新对话{{ type }}</button> <div class="talk_list"> <div v-for="item in talkList" @click="reTalk(item)" :class="{ 'border': tid == item.id }"> <span>{{ item.data[(item.data.length - 2)].content }}</span> <i class="el-icon-s-comment"></i> <!-- 下拉选项--> <el-dropdown style="height :22px; width: 30px ;position: relative; right: 0px; top: -5px;"> <span class="el-dropdown-link" style="position: absolute; left: 0px; top: 1px;" > <el-icon class="el-icon--right"><arrow-down /></el-icon> </span> <template #dropdown> <el-dropdown-menu> <el-dropdown-item @click="reTalk(item)">进入对话</el-dropdown-item> <el-dropdown-item @click="deleteTalk(item)">删除对话</el-dropdown-item> </el-dropdown-menu> </template> </el-dropdown> </div> </div> <div class="copyright"> <el-icon :size="20"><User /></el-icon> <div style="position: absolute ; left: 45px;"> <router-link to="/home">用户名</router-link> </div> </div> </div> </div> <div class="con_right" ref="con_right"> <div class="header" ref="header"> <div class="more_button" @click="popupShow"><i class="el-icon-s-operation"></i></div> <!-- <img src="../../assets/images/aslogo.png" alt="" srcset=""> --> <span>AI Scene</span> </div> <div class="content"> <div class="limit" v-if="messages.length <= 0"> <div class="primary_con"> <div class=""> <i class="el-icon-connection"></i> <h3>AI能力</h3> <p @click="assign('scene是什么意思')">scene是什么意思</p> <p @click="assign('莲藕排骨汤怎么做')">莲藕排骨汤怎么做</p> <p @click="assign('今天的世界有哪些新闻热点')">今天的新闻热点</p> <p @click="assign('查询xx星座运势')">查询星座运势</p> <p @click="assign('有趣的科学实验')">有趣的科学实验</p> <p @click="assign('全球高分电影推荐')">全球高分电影推荐</p> <p> <i class="el-icon-more"></i> </p> </div> <div class=""> <i class="el-icon-chat-line-square"></i> <h3>AI办公</h3> <p @click="assign('文章润色')">文章润色</p> <p @click="assign('写一篇方案报告')">写一篇方案报告</p> <p @click="assign('生成一篇关于xx的日报/周报')">生成一篇日报/周报</p> <p @click="assign('撰写一篇邮件/演讲稿')">撰写一篇邮件/演讲稿</p> <p @click="assign('代码报错解决')">代码报错解决</p> <p> <i class="el-icon-more"></i> </p> </div> </div> <div class="tip">更多AI能力等你来探索!</div> </div> <div class="content_list" ref="scrollDiv"> <div class="talk_con"> <contentList :contentList="messages"></contentList> </div> </div> </div> <div class="footer"> <div class="input_con"> <el-input type="textarea" :rows="3" :placeholder="disabled ? '获取中...' : '发送消息给AI'" v-model="content" @keyup.enter="send()"></el-input> <div class="sub_btn" @click="send()"> <i v-if="!disabled" class="el-icon-s-promotion"></i> <i v-else class="el-icon-loading"></i> </div> </div> <p>Based on OpenAI API (gpt-3.5-turbo) 仅供学习 AI 使用</p> </div> </div> </div> </template> <script> // import { openChat } from "@/api/api"; import md5 from 'js-md5' //是通过前台js加密的方式对密码等私密信息进行加密的工具 import contentList from "../components/content-list.vue" import { ArrowDown } from '@element-plus/icons-vue' const handleClick = () => { // eslint-disable-next-line no-alert alert('button click') } //这是一次尝试 很多依赖不匹配 本来的项目是python的 所以依赖不匹配 大概需要等后端的代码写好了 再来改 //目前是可以发送空白消息的 应该是空白的验证在后端进行 //整体的后端只需要完成send 方法的功能就行了 export default { name: "AI", components: { contentList }, data() { return { // 内容 content: "", messages: [], disabled: false, dialogVisible: false, // 打赏弹窗 reward: false, //大概用不着 看看删了就行 wxCode: false, talkList: [], tid: localStorage.getItem("talkId"), type: this.$route.query.type }; }, created() { this.messages = localStorage.getItem("messages") ? JSON.parse(localStorage.getItem("messages")) : [] this.talkList = localStorage.getItem("talkList") ? JSON.parse(localStorage.getItem("talkList")) : [] let arr = [{ "topic": null, "describe": null, "annotation": null, "fileUrl": null, "status": 0, "allScore": 0, "stuGraScoreList": [] }] console.log(arr[0].allScore); setTimeout(_ => { this.handleScrollBottom() //滚动至最底部 }, 100) }, mounted() { if (this.type == 'app') { this.$refs.header.style = "display:none" this.$refs.scrollDiv.style = "margin-top:-35px" } }, methods: { send() { this.handleScrollBottom() if (!this.content) { return } //下拉条直接拉到最下 this.disabled = true //发送按钮被禁用 let timeStamp = this.getTimeStamp() //获取时间戳 let user = { "role": "user", "content": this.content.trim() } //创建一个 user的对象 包含用户输入的内容 let system = { "role": "assistant", "content": "wait" } //创建一个 system的对象 包含系统回复的内容 //目前应该不会显示 当系统正常运行后在等待答案时 在答案部分会显示wait this.messages.push(user) this.messages.push(system) // 将用户消息和系统消息加入消息列表 let format = this.content.trim() + timeStamp let sign = md5(format) //将用户输入的消息内容和当前时间戳进行连接,并使用 MD5 加密算法进行签名,以确保消息的完整性和安全性。签名后的值 sign 可能会用于向后端发送消息以及验证消息的真实性。 this.content = "" //清空消息输入框的内容 this.handleScrollBottom() //滚动至最底部 let req = { messages: this.messages.slice(0, -1), //this.messages 表示当前对象的 messages 属性,可能是用于存储消息的数组。 //slice(0, -1) 是数组的一个内置方法,它用于创建一个新的数组,包括从原始数组中指定起始索引到指定结束索引之间的元素,但不包括结束索引对应的元素。在这里,slice(0, -1) 表示获取 messages 数组中的从第一个元素到倒数第二个元素的部分,排除了最后一个元素。 // model: "gpt-3.5-turbo", sign: sign, //上文的sign timestamp: timeStamp //时间戳 } //这行代码创建了一个名为 requestData 的对象,用于存储将要发送给后端的数据。 //messages 属性包含了准备发送的消息内容数组(可能是对消息历史的处理)。 // sign 属性包含了之前生成的消息内容和时间戳的签名。 // timestamp 属性包含了表示当前时间的时间戳。 openChat(req).then(res => { //成功的话 调用一下以下方法 let data if (res.data.indexOf('"error": {') != -1) { data = JSON.parse(res.data).error.message } else { data = res.data } // 对响应数据进行判断,以确定是否包含错误信息。如果响应数据中包含 "error": { 字符串,则说明后端返回了一个错误响应。 // 如果存在错误信息,则从响应中解析出错误消息,赋值给 data。 // 否则,直接将响应数据赋值给 data // data = res.data.indexOf('"error": {') != -1 ? "获取失败,请重试,或重新开启对话!" : res.data this.messages[(this.messages.length - 1)].content = data//更新前端消息列表中最后一条消息的内容为处理后的数据 data。 this.handleScrollBottom() //滚动至最底部 this.disabled = false//解除发送按钮禁用状态 //失败的话调用以下代码 }, error => { //错误的话 显示 服务器繁忙,请重新尝试 this.messages[(this.messages.length - 1)].content = "服务器繁忙,请重新尝试" this.handleScrollBottom() //滚动至最底部 this.disabled = false }) }, getTimeStamp() { let dateNow = new Date() let nowTime = dateNow.getTime() return nowTime }, //将下拉条拉到最下 handleScrollBottom() { this.$nextTick(() => { let scrollElem = this.$refs.scrollDiv; scrollElem.scrollTo({ top: scrollElem.scrollHeight, behavior: 'smooth' }); }); localStorage.setItem("messages", JSON.stringify(this.messages)) }, handleClose(done) { done(); }, // popupShow 方法的作用是通过设置样式,将弹出窗口水平居中显示,并且添加了半透明的黑色背景。这个方法可能在用户触发某个事件(比如点击按钮)后被调用,以显示弹出窗口 popupShow() { this.$refs.popup.style = 'left:0;background: rgba(0, 0, 0, 0.5)' }, popupClose() { this.$refs.popup.style = 'left:-100vw' }, assign(text) { this.content = text }, // 保存历史数据据 saveHistory() { let obj = { id: 'scene' + this.getTimeStamp(), data: this.messages } // 创建一个对象 obj,包含两个属性:id 和 data。 // id 属性由字符串 'scene' 和当前时间戳组成,用于标识保存的对话历史数据。 // data 属性则保存了当前的消息列表 this.messages。 if (this.messages.length > 0) { if (localStorage.getItem("talkId")) { this.talkList.map(item => { if (item.id == localStorage.getItem("talkId")) { item.data = this.messages } }) } else { this.talkList.unshift(obj) } localStorage.removeItem("talkId") this.tid = '' this.talkList = this.talkList.slice(0, 8) localStorage.setItem("talkList", JSON.stringify(this.talkList)) // 如果本地存储中存在名为 "talkId" 的项,执行相应逻辑;否则执行另一套逻辑。 // 当本地存储中存在名为 "talkId" 的项时: // 遍历 talkList 数组,找到对应的对话历史数据项(根据 "talkId" 进行匹配),并更新对应的消息数据为当前的消息列表 this.messages。 // 当本地存储中不存在名为 "talkId" 的项时: // 将新的对话历史数据项 obj 添加到 talkList 数组的开头。 // 移除本地存储中的 "talkId" 项,并重置 tid 属性。 // 将 talkList 数组保留最新的 8 个对话历史数据项,并将更新后的 talkList 保存到本地存储中。 } }, newTalk() { this.saveHistory() this.messages = [] localStorage.setItem("messages", JSON.stringify([])) this.popupClose() }, // newTalk 方法用于开始一个新的对话。 // 首先调用了 saveHistory 方法,将当前的对话历史数据保存到本地存储中。 // 然后将当前消息列表清空。 // 将空的消息列表保存到本地存储中,以便在页面刷新或重新加载后能够恢复空的对话历史记录。 // 最后调用了 popupClose 方法,关闭弹出窗口。 deleteTalk() { // 删除对话的前端逻辑 this.messages = []; // 清空消息列表 localStorage.removeItem("messages"); // 移除本地存储中的对话数据 this.popupClose(); // 关闭弹出窗口 // 与后端通信,删除对应用户的对话数据 deleteTalkOnBackend().then(() => { // 删除成功的处理 console.log("对话数据已成功删除"); }).catch((error) => { // 删除失败的处理 console.error("删除对话数据时出错: ", error); }); window.location.reload(); }, //目前的删除出现了问题 我还没解决 后端先对应完成删除方法 我计划用talkid作为值传递回去 然后删除对应对话 再在页面上进行刷新来达到删除的效果 reTalk(item) { this.saveHistory() localStorage.setItem("talkId", item.id) this.tid = item.id this.messages = item.data this.popupClose() setTimeout(_ => { this.handleScrollBottom() //滚动至最底部 }, 100) } // reTalk 方法用于重新加载之前保存的对话历史记录。 // 首先调用了 saveHistory 方法,将当前的对话历史数据保存到本地存储中。 // 然后将 talkId 设置为传入 item 参数的 id 属性。 // 将 tid 设置为 item 的 id。 // 将消息列表设置为传入 item 参数的 data 属性,即重新加载之前保存的对话历史数据。 // 调用 popupClose 方法,关闭弹出窗口。 // 通过 setTimeout 方法,延迟一段时间后调用 handleScrollBottom 方法,确保消息列表滚动至底部。 } }; </script> <style lang="less" scoped> @contentWidth: 800px; @themeColor: #4684ff; // @themeColor: #4684ff; @commonColor: #eee; @themeRadius: 6px; .container { display: flex; justify-content: space-between; height: 100vh; width: 100vw; overflow: hidden; min-width: 100px; transition: 0.6s; background-size: cover; background-repeat: no-repeat; background-position: center; backdrop-filter: blur(10px); } .con_left { position: relative; display: flex; flex-direction: column; align-items: center; // justify-content: center; min-width: 250px; max-width: 250px; background: rgba(255, 255, 255, 1); height: 100%; padding: 20px 0; box-sizing: border-box; transition: 0.5s; .talk_list { width: 80%; margin-top: 10px; color: #4c4c4c; .border { border: 1px solid #778dfc; background: #f2f6ff; color: #6e86ff; &:hover { background: #f0f4ff; } // font-weight: bold; } div { display: flex; justify-content: space-between; align-items: center; height: 45px; border: 1px solid #eee; border-radius: 6px; padding: 0 15px 0 15px; font-size: 14px; box-sizing: border-box; cursor: pointer; margin-top: 10px; color: #595959; span { display: inline-block; text-align: left; width: 80%; overflow: hidden; word-break: break-all; text-overflow: ellipsis; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 1; } &:hover { background-image: linear-gradient(to right, #eee, #eee); } } } .copyright { position: absolute; bottom: 15px; width: 90%; border: 1px solid #eee; display: flex; align-items: center; padding: 10px; box-sizing: border-box; border-radius: 8px; img { width: 40px; height: 40px; border-radius: 50%; margin-right: 10px; } div { color: #424242; h2 { font-size: 15px; } } p { font-size: 13px; margin-top: 2px; } a { color: #266dfb; font-weight: normal !important; } } .new_talk { border: none; width: 80%; height: 45px; font-weight: bold; letter-spacing: 1px; margin-bottom: 16px; background-image: linear-gradient(to right, #778dfc, #3D73E9); border-radius: @themeRadius; box-shadow: 0px 3px 8px 0px rgba(0, 0, 0, 0.08); color: #fff; cursor: pointer; transition: 0.2s; &:hover { background-image: linear-gradient(to right, #6b83fb, #3d8de4); } &:active { background-image: linear-gradient(to right, #6b83fb, #3d8de4); } } .other { position: absolute; bottom: 90px; width: 90%; border-radius: 6px; overflow: hidden; .bar { line-height: 45px; // border-bottom: 1px solid #ddd; font-weight: bold; // background: #eeeeee; color: #5a5a5a; box-sizing: border-box; cursor: pointer; font-size: 14px; padding: 0 15px; display: flex; align-items: center; justify-content: space-between; &:hover { background: #eee; } &:nth-child(3) { border: none; } } } } .con_right { position: relative; min-width: 800px; width: 100%; height: 100%; background: #ECEFF6 linear-gradient(45deg, #f6f8ff, #E2E8FF); .header { position: absolute; z-index: 1; width: 100%; display: flex; display: none; align-items: center; justify-content: center; background-image: linear-gradient(to left, #5185ff, 1px, #5185ff); backdrop-filter: blur(8px); height: 45px; span { font-size: 16px; text-align: center; color: #ffffff; font-weight: bold; } img { width: 140px; } .more_button { display: none; position: absolute; left: 15px; font-size: 23px; width: 35px; height: 35px; color: #fff; text-align: center; line-height: 35px; &:active { background: rgba(0, 0, 0, 0.1); border-radius: 3px; } } } .content { width: 100%; .limit { margin: auto; width: @contentWidth; margin-top: 60px; } .tip { padding-left: 6px; border-left: 3px solid #266dfb; margin-left: 20px; box-sizing: border-box; font-size: 14px; color: #4a4a4a; } .primary_con { padding: 20px 10px; box-sizing: border-box; display: flex; justify-content: center; text-align: center; h3 { margin-bottom: 15px; color: #303030; } div { width: 50%; padding: 10px; box-sizing: border-box; } p { line-height: 35px; margin-top: 10px; background: #f7f7f8; border-radius: 5px; font-size: 13px; color: #4a4a4a; cursor: pointer; &:hover { background: #e9e9ea; } &:active { background: #d6d6d6; } i { font-size: 15px; } } i { font-size: 26px; } } .content_list { width: calc(100%); padding-top: 50px; padding-bottom: 170px; // padding-left: 3px; box-sizing: border-box; overflow: scroll; height: calc(100vh); overflow-x: hidden; } .talk_con { margin: auto; width: calc(@contentWidth); } } // footer总高度175px .footer { position: absolute; // background: #ECEFF6; // padding-top: 30px; // background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0), 3%, #ECEFF6); backdrop-filter: blur(20px); bottom: 0px; width: 100%; .input_con { position: relative; margin: auto; width: @contentWidth + 100px; .sub_btn { position: absolute; right: 10px; line-height: 38px; border-radius: 5px; bottom: 10px; width: 38px; text-align: center; color: #fff; cursor: pointer; transition: 0.2s; font-size: 21px; background-image: linear-gradient(to right, #778dfc, #3D73E9); &:hover { background-image: linear-gradient(to right, #778dfc, #6178ec); } } // .el-input { // position: absolute; // top: 0; // left: 0; // box-sizing: border-box; // border: 1px solid #e9e9e9; // width: 100%; // line-height: 45px; // padding: 0 45px 0 15px; // border-radius: 6px; // // height: 50px; // transition: 0.3s; // letter-spacing: 0.5px; // color: #383838; // box-shadow: 0px 3px 8px 0px rgba(0, 0, 0, 0.02); // &:focus { // outline: none; // border: 1px solid @themeColor; // box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.03); // } // } /deep/ .el-textarea__inner { box-shadow: 0px 3px 8px 0px rgba(0, 0, 0, 0.02); border-radius: 8px; font-family: 'black'; padding: 10px 10px; box-sizing: border-box; &:focus { outline: none; border: 1px solid @themeColor; box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.03); } } } p { margin-top: 15px; margin-bottom: 20px; box-sizing: border-box; font-size: 10px; color: #909090; text-align: center; span { text-decoration: underline; color: @themeColor; cursor: pointer; } } } } /* android适配css 从下开始 */ @media (max-width: 800px) { .popup { position: absolute; left: -100vw; z-index: 4; height: 100%; width: 100vw; transition: 0.5s; } .con_left { max-width: 200px; } .con_right { min-width: 100%; .limit { max-width: 100%; } .header { display: block; display: flex; background-image: linear-gradient(to right, #778dfc, #3D73E9); } .more_button { display: block !important; color: #fff !important; } .talk_con { max-width: 100%; } } .input_con { max-width: 95%; } .content_list { max-width: calc(100% - 30px); margin: auto; } /deep/ .el-dialog { width: 85% !important; } } </style>