仿Deepseek页面的聊天窗口

先到阿里云控制台注册token

https://bailian.console.aliyun.com/

在这里插入图片描述

后端代码

python Django

from django.http import JsonResponse
from openai import OpenAI
import json
from django.http import StreamingHttpResponse

def admin_only_api(request):
    if request.method == 'GET':
        content = request.GET.get('content')
        client = OpenAI(
            api_key="申请的token",
            base_url="https://dashscope.aliyuncs.com/compatible-mode/v1"
        )

        def event_stream():
            reasoning_content = ""
            answer_content = ""
            is_answering = False

            completion = client.chat.completions.create(
                model="deepseek-r1",
                messages=[
                    {"role": "user", "content": content}
                ],
                stream=True
            )

            for chunk in completion:
                if not chunk.choices:
                    continue
                delta = chunk.choices[0].delta
                if hasattr(delta, 'reasoning_content') and delta.reasoning_content is not None:
                    reasoning_content += delta.reasoning_content
                    data = {'code': 200, 'msg': '', 'msg1': delta.reasoning_content}
                    yield f"data: {json.dumps(data)}\n\n"
                else:
                    if delta.content != "" and not is_answering:
                        is_answering = True
                    if delta.content:
                        answer_content += delta.content
                        print(delta.content)
                        data = {'code': 200, 'msg': delta.content, 'msg1': ''}
                        yield f"data: {json.dumps(data)}\n\n"

        response = StreamingHttpResponse(event_stream(), content_type='text/event-stream')
        response['Cache-Control'] = 'no-cache'
        # 跨域
        response['Access-Control-Allow-Origin'] = '设置成你的前端地址'  # 设置允许的来源
        return response
    else:
        response = JsonResponse({"error": "Method not allowed"}, status=405)
        # 跨域
        response['Access-Control-Allow-Origin'] = '设置成你的前端地址'  # 设置允许的来源
        return response

java Springboot

  • Maven依赖
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>dashscope-sdk-java</artifactId>
			<exclusions>
				<exclusion>
					<groupId>org.slf4j</groupId>
					<artifactId>slf4j-simple</artifactId>
				</exclusion>
			</exclusions>
			<version>2.18.2</version>
		</dependency>
  • 代码

import com.alibaba.dashscope.aigc.generation.Generation;
import com.alibaba.dashscope.aigc.generation.GenerationParam;
import com.alibaba.dashscope.common.Message;
import com.alibaba.dashscope.common.Role;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * Author reisen7
 * Date 2025/4/9 1:12
 * Description 
 */

@RestController
@RequestMapping("/deepSeek")
public class DeepSeekController {
    private static final Logger logger = LoggerFactory.getLogger(DeepSeekController.class);
    private final ExecutorService executor = Executors.newCachedThreadPool();

    private static GenerationParam buildGenerationParam(Message userMsg) {
        return GenerationParam.builder()
                .apiKey("sk-xxxxxx")
                .model("deepseek-r1")
                .messages(Arrays.asList(userMsg))
                .resultFormat(GenerationParam.ResultFormat.MESSAGE)
                .incrementalOutput(true)
                .build();
    }

    @GetMapping(value = "/query", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter getBotContent(@RequestParam String question) {
        SseEmitter emitter = new SseEmitter(60_000L); // 60秒超时

        executor.execute(() -> {
            try {
                // 先发送思考中状态
                Map<String, Object> thinkingResponse = new HashMap<>();
                thinkingResponse.put("code", 200);
                thinkingResponse.put("msg1", "正在分析您的问题...");
                emitter.send(SseEmitter.event().data(thinkingResponse));

                Generation gen = new Generation();
                Message userMsg = Message.builder()
                        .role(Role.USER.getValue())
                        .content(question)
                        .build();

                GenerationParam param = buildGenerationParam(userMsg);

                // 处理流式响应
                gen.streamCall(param).blockingForEach(message -> {
                    Map<String, Object> response = new HashMap<>();
                    response.put("code", 200);

                    String reasoning = message.getOutput().getChoices().get(0).getMessage().getReasoningContent();
                    String content = message.getOutput().getChoices().get(0).getMessage().getContent();

                    if (reasoning != null && !reasoning.isEmpty()) {
                        response.put("msg1", reasoning);
                        emitter.send(SseEmitter.event().data(response));
                    }

                    if (content != null && !content.isEmpty()) {
                        response.remove("msg1");
                        response.put("msg", content);
                        emitter.send(SseEmitter.event().data(response));
                    }
                });

                emitter.complete();
            } catch (Exception e) {
                logger.error("Error in streaming response", e);
                Map<String, Object> errorResponse = new HashMap<>();
                errorResponse.put("code", 500);
                errorResponse.put("msg", "处理请求时发生错误: " + e.getMessage());
                try {
                    emitter.send(SseEmitter.event().data(errorResponse));
                } catch (IOException ex) {
                    throw new RuntimeException(ex);
                }
                emitter.completeWithError(e);
            }
        });

        return emitter;
    }
}

前端仿Deepseek页面

这只是实现了弹窗的内容,需要引入到使用的页面中,这里不再补充可以参考之前的代码

https://blog.youkuaiyun.com/qq_46150074/article/details/144575355?spm=1001.2014.3001.5502

<template>
  <div class="model-bg" v-show="show" @mousemove="modelMove" @mouseup="cancelMove">
    <div class="model-container">
      <div class="model-header" @mousedown="setStartingPoint">
        {{ title }}
      </div>
      <div class="model-main" ref="box">
        <div v-for="(item, i) in list" :key="i" :class="item.id == 2 ? 'atalk' : 'btalk'">
          <span>{{ item.content }}</span>
        </div>
      </div>
      <div style="width: 100%; display: flex;"><input type="text" v-model="wordone" class="inputword" @keyup.enter="sendmsg">
        <el-button type="primary" :disabled="isButtonDisabled" round @click="sendmsg"
          class="btnsend">发送</el-button>
      </div>

      <div class="model-footer">
        <el-button round @click="cancel">关闭</el-button>
      </div>
    </div>
  </div>
</template>
<script>
  export default {
    props: {
      show: {
        type: Boolean,
        default: false
      },
      title: {
        type: String,
        default: 'deepseek'
      },
    },
    data() {
      return {
        x: 0,
        y: 0,
        node: null,
        isCanMove: false,
        isButtonDisabled: false,
        list: [],
        wordone: '',
        wordtow: '',
        eventSource: null,
        currentIndex: 0, // 当前显示的字符索引
        currentMessage: null, // 当前正在显示的消息对象
        typingTimer: null // 逐字显示定时器
      }
    },
    components: {
    },
    mounted() {
      this.node = document.querySelector('.model-container')
    },
    methods: {
      sendmsg() {
        if (this.isButtonDisabled == false) {
          // 重置相关状态变量
          if (this.typingTimer) {
            clearInterval(this.typingTimer);
          }
          this.currentIndex = 0;
          this.currentMessage = null;
          this.typingTimer = null;

          this.list.push({ id: 1, name: 'sigtuna', content: this.wordone });
          this.scrollToBottom();
          this.getBotContent();
        } else {
          return;
        }
        this.isButtonDisabled = true;
        this.wordone = '';
      },
      getBotContent() {
        // 添加思考中的提示
        this.list.push({ id: 2, name: 'kanade', content: '正在思考中...', isThinking: true });
        // 改成后端的接口
        this.eventSource = new EventSource(`/api/ai?content=${this.wordone}`);

        this.eventSource.onmessage = (event) => {
          const data = JSON.parse(event.data);
          if (data.code === 200) {
            if (data.msg1) {
              // 处理思考问题消息
              if (!this.currentMessage || this.currentMessage.id !== 2) {
                // 如果当前没有正在显示的思考问题消息,或者当前消息不是思考问题消息,或者当前思考问题消息已结束
                this.currentMessage = { id: 2, name: 'kanade', content: '思考问题:' + data.msg1, isThinking: true };
                this.list.push(this.currentMessage);
                            this.currentIndex = 0;
                        } else {
                            // 如果当前正在显示思考问题消息,追加内容
                            this.currentMessage.content += data.msg1;
                        }
                    }
                    if (data.msg) {
                        // 处理回答消息
                        if (!this.currentMessage || this.currentMessage.id !== 2 || this.currentMessage.isThinking) {
                            // 如果当前没有正在显示的回答消息,或者当前消息不是回答消息,或者当前是思考问题消息
                            this.currentMessage = { id: 2, name: 'kanade', content: '回答问题:' + data.msg, isThinking: false };
                            this.list.push(this.currentMessage);
                            this.currentIndex = 0;
                        } else {
                            // 如果当前正在显示回答消息,追加内容
                            this.currentMessage.content += data.msg;
                        }
                    }
                    this.scrollToBottom();

                }
            };

            this.eventSource.onerror = (error) => {
                this.isButtonDisabled = false;
                this.eventSource.close();
            };
        },
        // 添加新的方法用于处理内容更新时的滚动
        scrollToBottom() {
            this.$nextTick(() => {
                const div = this.$refs.box;
                div.scrollTop = div.scrollHeight;
            });
        },
        startTyping() {
            if (this.typingTimer) {
                clearInterval(this.typingTimer);
            }
            this.typingTimer = setInterval(() => {
                if (this.currentIndex < this.currentMessage.content.length) {
                    this.currentMessage.content = this.currentMessage.content.slice(0, this.currentIndex + 1);
                    this.currentIndex++;
                    const div = this.$refs.box;
                    div.scrollTop = div.scrollHeight;
                } else {
                    clearInterval(this.typingTimer);
                }
            }, 50); // 控制逐字显示速度,可调整
        },
        cancel() {
            if (this.eventSource) {
                this.eventSource.close();
            }
            if (this.typingTimer) {
                clearInterval(this.typingTimer);
            }
            this.$emit('cancel');
        },
        submit() {
            this.$emit('submit');
        },
        setStartingPoint(e) {
            this.x = e.clientX - this.node.offsetLeft;
            this.y = e.clientY - this.node.offsetTop;
            this.isCanMove = true;
        },
        modelMove(e) {
            if (this.isCanMove) {
                this.node.style.left = e.clientX - this.x + 'px';
                this.node.style.top = e.clientY - this.y + 'px';
            }
        },
        cancelMove() {
            this.isCanMove = false;
        },
    }
}
</script>
<style scoped>
.model-bg {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.5);
    z-index: 1000;
    display: flex;
    justify-content: center;
    align-items: center;
}

.model-container {
    background: #1a1a1a;
    border-radius: 12px;
    width: 80%;
    max-width: 900px;
    height: 60vh;
    max-height: 800px;
    display: flex;
    flex-direction: column;
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
    overflow: hidden;
    border: 1px solid #333;
}

.model-header {
    height: 60px;
    background: linear-gradient(90deg, #2a2a2a, #1a1a1a);
    color: #fff;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: move;
    font-size: 18px;
    font-weight: 600;
    border-bottom: 1px solid #333;
    position: relative;
}

.model-header::after {
    content: "";
    position: absolute;
    bottom: 0;
    left: 0;
    width: 100%;
    height: 1px;
    background: linear-gradient(90deg, rgba(0, 0, 0, 0), #444, rgba(0, 0, 0, 0));
}

.model-main {
    flex: 1;
    padding: 20px;
    overflow-y: auto;
    background: #1a1a1a;
    scrollbar-width: thin;
    scrollbar-color: #444 #1a1a1a;
}

.model-main::-webkit-scrollbar {
    width: 6px;
}

.model-main::-webkit-scrollbar-track {
    background: #1a1a1a;
}

.model-main::-webkit-scrollbar-thumb {
    background-color: #444;
    border-radius: 3px;
}

.model-footer {
    padding: 15px;
    background: #1a1a1a;
    border-top: 1px solid #333;
    display: flex;
    align-items: center;
    justify-content: center;
}

.model-footer button {
    width: 100px;
    background: #333;
    color: #fff;
    border: none;
}

.model-footer button:hover {
    background: #444;
}

.atalk {
    margin: 15px 0;
    display: flex;
    align-items: flex-start;
}

.atalk::before {
    content: "🤖";
    margin-right: 10px;
    font-size: 20px;
}

.atalk span {
    background: #2a2a2a;
    color: #e0e0e0;
    border-radius: 12px;
    padding: 12px 16px;
    max-width: 80%;
    line-height: 1.5;
    border: 1px solid #333;
    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}

.btalk {
    margin: 15px 0;
    display: flex;
    justify-content: flex-end;
}

.btalk::after {
    content: "👤";
    margin-left: 10px;
    font-size: 20px;
}

.btalk span {
    background: #0078d4;
    color: white;
    border-radius: 12px;
    padding: 12px 16px;
    max-width: 80%;
    line-height: 1.5;
    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}

.input-container {
    display: flex;
    padding: 15px;
    background: #1a1a1a;
    border-top: 1px solid #333;
}

.inputword {
    flex: 1;
    height: 40px;
    border-radius: 20px;
    border: 1px solid #444;
    padding: 0 15px;
    background: #2a2a2a;
    color: #fff;
    outline: none;
    font-size: 14px;
}

.inputword:focus {
    border-color: #0078d4;
}

.btnsend {
    width: 100px;
    height: 40px;
    margin-left: 10px;
    background: #0078d4;
    color: white;
    border: none;
    border-radius: 20px;
    font-weight: 500;
    transition: all 0.2s;
}

.btnsend:hover {
    background: #0063b1;
}

.btnsend:disabled {
    background: #555;
    cursor: not-allowed;
}

/* 思考中样式 */
.atalk.thinking span {
    color: #aaa;
    font-style: italic;
    background: #2a2a2a;
}

/* 问题样式 */
.atalk.question span {
    color: #4fc3f7;
    font-weight: 500;
}

/* 打字效果 */
@keyframes blink {

    0%,
    100% {
        opacity: 1;
    }

    50% {
        opacity: 0.5;
    }
}

.typing-cursor::after {
    content: "|";
    animation: blink 1s infinite;
    color: #4fc3f7;
}
</style>

实际效果

在这里插入图片描述

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值