vue版本2.6.12+Element+markdown-it,实现仿写DeepSeek,流试渲染输出,界面组件封装

1.创建一个vue2项目,引入UI组件Element

2.安装依赖这三个依赖库  npm install markdown-it markdown-it-highlightjs dompurify

3.组件代码

  可以直接复制进项目使用,在需要显示的界面引用就好。代码中的apiKey需要自己去deepseek官网获取,具体操作步骤如下:

  1. 访问DeepSeek官网 https://platform.deepseek.com
  2. 注册/登录后进入控制台
  3. 在「API Keys」模块点击「Create New Key」
  4. 复制生成的密钥字符串
  5. 在代码中定位以下位置进行替换:
  6. // 在<script>部分找到data配置项
    data() {
      return {
        // ...其他配置...
        apiKey: "在此替换为你的API密钥", // 👈 替换这里
      };
    },

<template>
  <div class="chat-container">
    <div class="chat-box" style="height: 1200px; overflow: auto;">
      <div v-for="(message, index) in messages" :key="index" class="message-item">
        <div v-if="message.role === 'user'" class="user-message">
          <div class="bubble user" v-html="message.content"></div>
          <!-- <div>
            <el-image src="/static/BNMap/images/user.png" style="width: 45px;height:45px" alt="用户头像"></el-image>
          </div> -->
        </div>

        <div v-else class="assistant-message">
          <div style="background-color:#fff;border-radius: 50%;width:40px;height:40px;margin-right:5px">
            <svg-icon icon-class="deepseek" style="font-size:46px"></svg-icon>
          </div>
          <div v-if="message.content" class="bubble assistant" v-html="parseMarkdown(message.content)"></div>
          <!-- 添加逻辑:仅在loading状态为true且没有内容时显示加载图标 -->
          <div v-if="message.loading && !message.content" style="position: absolute;left: 71px"> <i
              class="el-icon-loading" style="font-size:25px"></i></div>
        </div>
      </div>
    </div>
    <div class="input-container">
      <el-input type="textarea" :rows="4" placeholder="请输入您的问题..." class="message-input" v-model="content"
        @keyup.enter.native="submit"></el-input>
      <!-- 动态切换按钮 -->
      <el-button @click="handleButtonAction" class="send-button" :style="buttonStyle">
        <i v-if="!isSending" style="font-size: 20px" class="el-icon-position"></i>
        <i v-else style="font-size: 20px" class="el-icon-switch-button"></i>
        {{ buttonText }}
      </el-button>
    </div>
  </div>
</template>

<script>
import MarkdownIt from 'markdown-it'
import hljs from 'markdown-it-highlightjs'
import DOMPurify from 'dompurify'

export default {
  name: "ChatWindow",
  data() {
    return {
      content: "",
      messages: [],
      isButtonDisabled: false,
      apiKey: "your_deepseek_key", // 替换为你的API Key
      md: new MarkdownIt({
        html: true,
        linkify: true,
        typographer: true
      }).use(hljs),
      controller: null,
      isSending: false,    // 新增发送状态
      buttonText: '发送',  // 按钮文字
      buttonStyle: {       // 按钮样式
        backgroundColor: '#409eff'
      }
    };
  },
  mounted() { },
  methods: {
    // 统一按钮处理
    handleButtonAction() {
      if (this.isSending) {
        this.stopGenerating();
      } else {
        this.submit();
      }
    },
    // 停止生成
    stopGenerating() {
      if (this.controller) {
        this.controller.abort();
        this.controller = null;
        // this.updateLastAssistantMessage(this.fullResponse + "\n(已停止生成)");
        this.resetButtonState();
      }
    },
    parseMarkdown(raw) {
      return DOMPurify.sanitize(this.md.render(raw))
    },

    async submit() {
      if (!this.content.trim()) return;

      // 设置按钮状态
      this.isSending = true;
      this.buttonText = '停止';
      this.buttonStyle.backgroundColor = '#ff4d4f';
      this.isButtonDisabled = false;


      // 添加用户消息
      this.messages.push({
        role: "user",
        content: this.content
      });

      // 添加AI消息占位符
      this.messages.push({
        role: "assistant",
        content: "",
        loading: true
      });

      this.isButtonDisabled = true;
      this.content = "";

      try {
        this.controller = new AbortController();
        const response = await fetch("https://api.deepseek.com/v1/chat/completions", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            "Authorization": `Bearer ${this.apiKey}`,
          },
          body: JSON.stringify({
            model: "deepseek-chat",
            messages: this.formatMessages(),
            stream: true,
            temperature: 0.3,
            max_tokens: 2048
          }),
          signal: this.controller.signal
        });

        const reader = response.body.getReader();
        const decoder = new TextDecoder();
        let fullResponse = "";

        while (true) {
          const { done, value } = await reader.read();
          if (done) break;

          const chunk = decoder.decode(value);
          const lines = chunk.split('\n').filter(line => line.trim());

          // 添加首次响应处理
          let hasReceivedData = false;

          for (const line of lines) {
            const message = line.replace(/^data: /, '');
            if (message === "[DONE]") break;

            try {
              const parsed = JSON.parse(message);
              if (parsed.choices[0].delta.content) {
                // 首次收到数据时关闭loading
                if (!hasReceivedData) {
                  hasReceivedData = true;
                  this.messages[this.messages.length - 1].loading = false;
                }
                fullResponse += parsed.choices[0].delta.content;
                this.updateLastAssistantMessage(fullResponse);
              }
            } catch (e) {
              console.error("Error parsing message:", e);
            }
          }
        }
      } catch (error) {
        if (error.name !== 'AbortError') {
          console.error("请求错误:", error);
          this.updateLastAssistantMessage("请求出错,请稍后重试");
          this.messages[this.messages.length - 1].loading = false;
        }
      } finally {
        this.isButtonDisabled = false;
        this.controller = null;
        this.messages[this.messages.length - 1].loading = false;
        this.resetButtonState();

      }
    },
    // 重置按钮状态
    resetButtonState() {
      this.isSending = false;
      this.buttonText = '发送';
      this.buttonStyle.backgroundColor = '#409eff';
      this.isButtonDisabled = false;

      // 更新最后一条消息的加载状态
      const lastIndex = this.messages.length - 1;
      if (lastIndex >= 0 && this.messages[lastIndex].loading) {
        this.$set(this.messages, lastIndex, {
          ...this.messages[lastIndex],
          loading: false
        });
      }
    },

    formatMessages() {
      return this.messages
        .filter(m => m.role !== 'assistant' || m.content)
        .map(m => ({ role: m.role, content: m.content }));
    },

    updateLastAssistantMessage(content) {
      const lastIndex = this.messages.length - 1;
      if (lastIndex >= 0 && this.messages[lastIndex].role === 'assistant') {
        this.$set(this.messages, lastIndex, {
          ...this.messages[lastIndex],
          content: content
        });
      }
    }
  },
  beforeDestroy() {
    if (this.controller) {
      this.controller.abort();
    }
  }
};
</script>

<style scoped>
/* 新增停止按钮动画 */
.el-icon-switch-button {
  animation: pulse 1.5s infinite;
}

@keyframes pulse {
  0% {
    transform: scale(1);
  }

  50% {
    transform: scale(1.2);
  }

  100% {
    transform: scale(1);
  }
}

/* 按钮过渡效果 */
.send-button {
  transition: all 0.3s ease-in-out !important;
}

/* 保持原有样式不变 */
.chat-container {
  height: 700px;
  display: flex;
  flex-direction: column;
  max-width: 1000px;
  margin: 0 auto;
  padding: 10px;
  background-color: rgb(90, 79, 79);
  border-radius: 10px;
}

.chat-box {
  border-radius: 10px;
  width: 800px;
  background-color: #ccc;
  flex: 1;
  padding: 10px;
  margin-bottom: 8px;
  overflow-y: auto;
  overflow-x: hidden;
}

.message-item {
  margin-bottom: 10px;
}

.user-message {
  display: flex;
  justify-content: flex-end;
  align-items: center;
}

.assistant-message {
  display: flex;
  justify-content: flex-start;
  align-items: center;
}

.bubble {
  font-size: 15px;
  max-width: 80%;
  padding: 15px;
  padding-left: 25px;
  border-radius: 10px;
  overflow: hidden;
  word-wrap: break-word;
  box-sizing: border-box;
}

.user {
  background-color: #e0f7fa;
  margin-left: auto;
}

.assistant {
  background-color: #f5f5f5;
  margin-right: auto;
}

.input-container {
  display: flex;
  width: 800px;
  gap: 10px;
}

.send-button {
  padding: 0 20px;
  background: #409eff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  transition: background 0.3s;
}

.loading-icon {
  display: inline-block;
  width: 20px;
  height: 20px;
  border: 2px solid #ccc;
  border-top-color: #409eff;
  border-radius: 50%;
  animation: spin 1s linear infinite;
  margin-left: 10px;
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

@import '~highlight.js/styles/github.css';
</style>

3.效果界面展示

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值