首页 前端知识 【VUE页面】简单实现基于本地部署的Deepseek-R1对话页面仿微信聊天

【VUE页面】简单实现基于本地部署的Deepseek-R1对话页面仿微信聊天

2025-02-25 13:02:09 前端知识 前端哥 247 713 我要收藏

在使用 DeepSeek 时,你是否总被 “服务器繁忙,请稍后再试” 这句话困扰?明明你的需求并不复杂,只是想借助它梳理思路,却总是碰壁。其实,本地化部署是个很好的解决方案。无需高昂成本,就能实现高效的思路梳理,以最低的代价创造高附加值。

DeepSeek本地部署在Mac M系列芯片,步骤实际就两个:
1、安装 Ollama,从官网下载符合芯片的安装包,按步骤安装;

2、下载DeepSeek模型,根据自己的配置和需求,不建议使用过大的,7b基本上能满足80%的场景, 如果配置杠杠的,那就32B吧。

接下来就是具体展示VUE代码,在这个基础上可以完全使用一个全新的VUE脚手架工程,这里包括css样式也一并纳入页面中,轻轻松松就能自产自用:

<template>
  <!-- 新增的外层容器,用于添加渐变背景 -->
  <div class="background-container">
    <!-- 整体页面容器,添加边框 -->
    <div class="chat-container">
      <!-- 新增的 el-switch 组件 -->
      <div class="robot-title" slot="title">
        <div class="robot-title-right">
          <!-- 用两张图片和一个“x”替换原来的文字 -->
          <img src="../../assets/picLeft.png" alt="图片1" class="robot-title-image1">
          <span class="robot-title-x blink">X</span>
          <img src="../../assets/picRight.png" alt="图片2" class="robot-title-image2">
          <div class="robot-title-right-bottom"></div>
        </div>
      </div>
      <!-- 对话页面容器,仿微信聊天对话框 -->
      <div class="chat-dialog" ref="chatDialog">
        <!-- 显示对话消息,按时间顺序穿插展示 -->
        <div v-for="message in allMessages" :key="message.id" :class="message.type === 'user' ? 'user-message' : 'ai-message'">
          {{ message.text }}
          <!-- 如果有思考过程,显示查看思考过程的图标 -->
          <span v-if="message.type === 'ai' && message.thinkContent" class="think-icon" @click="showThinkContent = message.thinkContent">
            查看思考过程
          </span>
        </div>
        <!-- 遮罩层,当 AI 还未回答返回时显示 -->
        <div v-if="isLoading" class="loading-mask">
          <div class="loading-spinner"></div>
        </div>
        <!-- 思考过程弹框 -->
        <div v-if="showThinkContent" class="think-popup">
          {{ showThinkContent }}
          <button @click="showThinkContent = null">关闭</button>
        </div>
      </div>
      <!-- 用户输入区域,多行文本输入框和发送按钮 -->
      <div class="input-container">
        <input v-model="userInput" placeholder="输入你的问题" class="input-textarea" @keydown.enter="sendMessageBasedOnApi"></input>
        <button @click="sendMessageBasedOnApi" class="send-button">发送</button>
        <div class="switch-container">
          <el-switch v-model="isLocalApi" @change="toggleApiUrl"
                     active-text="私有化API"
                     inactive-text="远程API"
                     inactive-color = "#C0CC1A">
            {{ isLocalApi ? '私有化 API' : '远程 API' }}
          </el-switch>
        </div>
      </div>
    </div>

  </div>
</template>

<script>
  export default {
    name: "index",
    data() {
      return {
        // 用户输入的内容
        userInput: "",
        // AI 回复的内容
        aiResponse: "",
        // 用户发送的消息列表
        userMessages: [],
        // AI 回复的消息列表
        aiMessages: [],
        // 所有消息列表,用于穿插展示
        allMessages: [],
        // 鼠标悬停时显示的思考过程内容
        showThinkContent: null,
        // 是否正在加载 AI 回复
        isLoading: false,
        // 是否使用本地 API
        isLocalApi: false,
        // API 接口地址
        apiUrl: "https://api.deepseek.com/chat/completions"
      };
    },
    methods: {
      /**
       * 发送用户输入的消息给 API 并获取回复
       */
      sendMessage() {
        if (this.userInput.trim() === "") {
          return;
        }
        // 构造请求数据
        const requestData = {
          messages: [
            {
              content: this.userInput,
              role: "user"
            }
          ],
          model: "deepseek-chat",
          frequency_penalty: 0,
          max_tokens: 2048,
          presence_penalty: 0,
          response_format: {
            type: "text"
          },
          stop: null,
          stream: false,
          stream_options: null,
          temperature: 1,
          top_p: 1,
          tools: null,
          tool_choice: "none",
          logprobs: false,
          top_logprobs: null
        };
        // 将用户输入的消息添加到用户消息列表中
        const userMessage = {
          id: Date.now(),
          text: this.userInput,
          type: 'user'
        };
        this.userMessages.push(userMessage);
        this.allMessages.push(userMessage);
        // 显示加载遮罩层
        this.isLoading = true;
        // 发送请求
        fetch(this.apiUrl, {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            "Authorization": "Bearer 这里填自己的ApiKey",
            "Accept": "application/json"
          },
          body: JSON.stringify(requestData)
        })
          .then(response => response.json())
          .then(data => {
            // 将 AI 回复赋值给 aiResponse 以便显示
            this.aiResponse = data.choices[0].message.content

            let thinkContent = null;

            // 将 AI 回复的消息添加到 AI 消息列表中
            const aiMessage = {
              id: Date.now(),
              text:  this.aiResponse,
              thinkContent: thinkContent,
              type: 'ai'
            };
            this.aiMessages.push(aiMessage);
            this.allMessages.push(aiMessage);
            // 清空用户输入框
            this.userInput = "";
            // 隐藏加载遮罩层
            this.isLoading = false;

            // 滚动到最新消息
            this.$nextTick(() => {
              const chatDialog = this.$refs.chatDialog;
              chatDialog.scrollTop = chatDialog.scrollHeight;
            });
          })
          .catch(error => {
            console.error("请求出错:", error);
            // 隐藏加载遮罩层
            this.isLoading = false;
          });
      },
      sendMessageLocal() {
        if (this.userInput.trim() === "") {
          return;
        }
        // 这是本地部署Ollama提供的接口地址,配置采用的模型,可以是ds,也可以是其他的模型,如Qwen等
        const apiUrl = "http://localhost:11434/api/generate";
        // 构造请求数据
        const requestData = {
          model: "deepseek-r1:7b",
          prompt: this.userInput,
          stream: false
        };
        // 将用户输入的消息添加到用户消息列表中
        const userMessage = {
          id: Date.now(),
          text: this.userInput,
          type: 'user'
        };
        this.userMessages.push(userMessage);
        this.allMessages.push(userMessage);
        // 显示加载遮罩层
        this.isLoading = true;
        // 发送请求
        fetch(apiUrl, {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify(requestData)
        })
          .then(response => response.json())
          .then(data => {
            // 将 AI 回复赋值给 aiResponse 以便显示
            this.aiResponse = data.response;

            let thinkContent = null;
            // 检查 AI 回复中是否包含 <think> 节点
            if (data.response.includes("<think>")) {
              // 提取 <think> 节点的内容
              const startIndex = data.response.indexOf("<think>") + 7;
              const endIndex = data.response.indexOf("</think>");
              thinkContent = data.response.substring(startIndex, endIndex);
            }
            let aiContent = String(this.aiResponse).replace(thinkContent, '').replace("<think>", '').replace("</think>", '');

            // 将 AI 回复的消息添加到 AI 消息列表中
            const aiMessage = {
              id: Date.now(),
              text:  aiContent,
              thinkContent: thinkContent,
              type: 'ai'
            };
            this.aiMessages.push(aiMessage);
            this.allMessages.push(aiMessage);
            // 清空用户输入框
            this.userInput = "";
            // 隐藏加载遮罩层
            this.isLoading = false;

            // 滚动到最新消息
            this.$nextTick(() => {
              const chatDialog = this.$refs.chatDialog;
              chatDialog.scrollTop = chatDialog.scrollHeight;
            });
          })
          .catch(error => {
            console.error("请求出错:", error);
            // 隐藏加载遮罩层
            this.isLoading = false;
          });
      },
      /**
       * 根据 el-switch 的切换结果调用相应的发送消息方法
       */
      sendMessageBasedOnApi() {
        if (this.isLocalApi) {
          this.sendMessageLocal();
        } else {
          this.sendMessage();
        }
      },
      /**
       * 切换 API 地址
       */
      toggleApiUrl() {
        this.apiUrl = this.isLocalApi ? "http://localhost:11434/api/generate" : "https://api.deepseek.com/chat/completions";
      },
      /**
       * 处理内网取数点击事件,方法预留
       */
      handleInternalDataFetch() {
        // 这里添加内网取数的具体逻辑
      },
      /**
       * 处理组装格式点击事件,方法预留
       */
      handleFormatAssembly() {
        // 这里添加组装格式的具体逻辑
      },
      /**
       * 处理构建图表点击事件,方法预留
       */
      handleChartBuilding() {
        // 这里添加构建图表的具体逻辑
      }
    }
  };
</script>

<style scoped>
  /* 新增的外层容器样式,设置浅蓝色渐变背景 */
  .background-container {
    background: linear-gradient(to bottom, #c0ebdd, #e0e6c8);
    min-height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
  }

  /* 整体页面容器样式 */
  .chat-container {
    border: 0px solid #ccc;
    padding: 10px;
    width: 80%;
    margin: 0 auto;
    box-sizing: border-box;
    position: relative;
  }

  /* 新增的 switch 容器样式 */
  .switch-container {
    position: absolute;
    /*top: 10px;*/
    left: 10px;
  }

  /* 对话页面容器样式 */
  .chat-dialog {
    height: 560px;
    overflow-y: auto;
    padding: 10px;
  }

  /* 用户发送的消息样式 */
  .user-message {
    background-color: #4CAF50;
    color: white;
    padding: 10px;
    border-radius: 10px;
    margin-bottom: 10px;
    float: right;
    clear: both;
    max-width: 70%;
  }

  /* AI 回复的消息样式 */
  .ai-message {
    background-color: #f1f1f1;
    color: black;
    padding: 10px;
    border-radius: 10px;
    margin-bottom: 10px;
    float: left;
    clear: both;
    max-width: 70%;
    position: relative;
  }

  /* 查看思考过程的图标样式 */
  .think-icon {
    cursor: pointer;
    color: blue;
    margin-left: 10px;
  }

  /* 思考过程的详细内容样式(弹框) */
  .think-popup {
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    background-color: white;
    border: 1px solid #ccc;
    padding: 20px;
    z-index: 2;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
  }

  .think-popup button {
    margin-top: 10px;
    padding: 5px 10px;
    background-color: #4CAF50;
    color: white;
    border: none;
    border-radius: 5px;
    cursor: pointer;
  }

  /* 加载遮罩层样式 */
  .loading-mask {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: transparent; /* 修改为透明 */
    display: flex;
    justify-content: center;
    align-items: center;
    z-index: 1;
  }

  .loading-spinner {
    border: 4px solid #f3f3f3;
    border-top: 4px solid #3498db;
    border-radius: 50%;
    width: 30px;
    height: 30px;
    animation: spin 1s linear infinite;
  }

  @keyframes spin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
  }

  /* 用户输入区域容器样式 */
  .input-container {
    display: flex;
    justify-content: center;
    align-items: center;
    width: 50%;
    margin: 0 auto;
  }

  /* 多行文本输入框样式 */
  .input-textarea {
    flex-grow: 1;
    padding: 10px;
    border: 1px solid #ccc;
    border-radius: 5px;
    resize: none;
  }

  /* 发送按钮样式 */
  .send-button {
    padding: 10px 20px;
    margin-left: 10px;
    background-color: #4CAF50;
    color: white;
    border: none;
    border-radius: 5px;
    cursor: pointer;
  }

  /* 构建图表样式 */
  .build-button {
    padding: 10px 20px;
    margin-left: 10px;
    background-color: #285eaf;
    color: white;
    border: none;
    border-radius: 5px;
    cursor: pointer;
  }
  /* 新增的图片样式 */
  .robot-title-image1 {
    width: 30%;
    /*height: 80px;*/
    object-fit: cover;
  }

  .robot-title-image2 {
    /*width: 140px;*/
    /*height: 80px;*/
    /*object-fit: cover;*/
    width: 30%; /* 让图片宽度自适应 */
    height: auto; /* 高度自动调整,保持图片宽高比 */
    object-fit: cover;
  }

  /* 新增的“x”样式 */
  .robot-title-x {
    margin: 0 30px;
    font-size: 20px;
  }

  .robot-title-right{
    width: 800px;
    display: flex;
    align-items: center; /* 垂直居中 */
  }

  /* 闪烁高亮效果 */
  .blink {
    animation: blink 1s infinite;
    color: #2b6aff; /* 高亮颜色 */
  }

  @keyframes blink {
    0% { opacity: 1; }
    50% { opacity: 0.5; }
    100% { opacity: 1; }
  }

  /* 步骤条容器样式 */
  .step-container {
    display: flex;
    flex-direction: column;
    align-items: center;
    margin-right: 50px;
    height: 500px;
    width: 160px;
  }

  /* 步骤条样式 */
  .step {
    padding: 10px 20px;
    border: 1px solid #ccc;
    border-radius: 5px;
    margin-bottom: 10px;
    cursor: pointer;
  }
  /* 步骤条字体颜色变为白色 */
  .el-steps .el-step__title {
    color: white;
  }
</style>

没有繁杂冗余的元素堆砌,左下角可以切换调用远程API,也可以本地化的接口进行切换,总体页面简洁,反正总比在终端看着舒服,要是你还有其他修改意见,欢迎指导互相学习~
在这里插入图片描述

转载请注明出处或者链接地址:https://www.qianduange.cn//article/21444.html
标签
评论
发布的文章

python调用ollama库详解

2025-02-25 13:02:30

大家推荐的文章
会员中心 联系我 留言建议 回顶部
复制成功!