创新实训第七周周报

本周工作内容:
  1. 前端界面开发

    • 完成了整个与ChatGPT对话界面相似的前端界面的开发工作。
    • 具体工作包括:
      • 搭建了界面的基本框架,设计了整体布局和结构。
      • 实现了主要UI元素,包括文本输入框、发送按钮、消息气泡、用户头像等。
      • 编写了详细的CSS,确保各元素在不同设备和分辨率下都能正确显示。
  2. 对话功能实现

    • 实现了文本输入和发送功能,使用户能够输入并发送消息。
    • 开发了消息显示区域,确保发送的消息能即时、准确地显示在界面上。
    • 实现了对话气泡的样式,使其美观且易于阅读,用户消息和系统回复有清晰的区分。
  3. 响应式设计与优化

    • 确保界面在各种设备(桌面、平板、手机)上都有良好的用户体验。
    • 针对不同浏览器进行了测试,修复了兼容性问题,确保在主流浏览器(如Chrome、Firefox、Safari等)上表现一致。
    • 优化了界面的加载速度和交互性能,提高了用户操作的流畅性。
  4. 功能调试与完善

    • 进行了多轮功能测试,确保对话功能的稳定性和可靠性。
    • 修复了测试过程中发现的若干问题,如消息顺序错乱、界面元素位置不对等。
    • 根据用户反馈进行了部分功能和界面的调整和优化,提高了用户满意度。
  5. 文档编写与总结

    • 编写了详细的技术文档,记录了界面的开发过程、技术选型和实现细节。
    • 总结了本周的开发工作,为下周的功能扩展和优化提供了依据。

代码展示:

<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>
  • 18
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值