在Vue3项目当中,嵌入一个deepseek/Kimi对话框

注意:因为当前deepseek服务器资源紧张, API 服务充值被停用,所以deepseek的接口是无法使用的,本篇文章的Kimi是可以正常使用的。


第一步:去官网申请API keys

deepseek api:DeepSeek 开放平台

kimi api Moonshot AI - 开放平台

注意!!!只有第一次创建的时候可以复制api key ,自己要保存好

点击创建,就可以获取api key了


第二步:配置环境变量

找到我们的.env.development 或者 .env文件

在文件里面设置我们的API keys 和 接口访问地址

# deepseek
VITE_APP_DEEPSEEK_API_URL=https://api.deepseek.com/v1/chat/completions
VITE_APP_DEEPSEEK_API_KEY=your api keys

#kimi
VITE_APP_KIMI_API_URL=https://api.moonshot.cn/v1/chat/completions
VITE_APP_KIMI_API_KEY=your api keys

把自己的api keys替换上去就可以了,我用的构建工具是vite,所以环境变量的名称必须以VITE_APP开头


第三步:封装请求方法

在我们文件的src\http目录下创建aiRequest.ts

可以直接使用我封装的代码,代码都有注释我就不过多的阐述,大家可以自己看一下。

需要提醒的是,我的这个封装方法实现的是流式输出,打字机的动画效果,所以用的是fetch而不是axios封装。

我是做了Kimi和deepseek两个可以切换的请求。

// request.ts 修改后的请求文件
import { AxiosError } from 'axios';
import { ChatMessage } from '@/types/ai';

/** 历史消息长度 */
const MAX_HISTORY = 10;

/**
 * 封装API请求,用于获取AI的流式响应
 * @param messages - 历史消息列表
 * @param aiType - AI类型,可选值为 'KIMI' 或 'DEEPSEEK'
 * @param onStream - 流式数据回调函数,接收到新的数据块时调用
 */
const fetchAIResponse = async (
    messages: ChatMessage[],
    aiType: 'KIMI' | 'DEEPSEEK',
    onStream?: (chunk: string) => void
) => {
    const url = import.meta.env[`VITE_APP_${aiType}_API_URL`];
    const key = import.meta.env[`VITE_APP_${aiType}_API_KEY`];

    try {
        const response = await fetch(url, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                Authorization: `Bearer ${key}`
            },
            body: JSON.stringify({
                model: aiType === 'KIMI' ? 'moonshot-v1-8k' : 'deepseek-chat',
                messages: messages.slice(-MAX_HISTORY),
                temperature: 0.7,
                stream: true
            })
        });
        // 检查响应状态是否正常
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        // 获取响应体的读取器
        const reader = response.body?.getReader();
        // 创建文本解码器
        const decoder = new TextDecoder('utf-8');
        // 用于存储未处理完的流式数据
        let buffer = '';

        // eslint-disable-next-line no-constant-condition
        while (true) {
            // 读取流式数据的下一个块
            const { done, value } = await reader!.read();
            if (done) break;
            // 解码并追加到缓冲区
            buffer += decoder.decode(value, { stream: true });

            // 按行分割缓冲区的数据
            const chunks = buffer.split('data: ');
            // 保留未完成的行
            buffer = chunks.pop() || '';

            for (let i = 1; i < chunks.length; i++) {
                const chunk = chunks[i].trim();
                // 忽略空行和结束标记[DONE]
                if (!chunk) continue;

                try {
                    const json = JSON.parse(chunk);
                    const content = json.choices[0]?.delta?.content || '';
                    if (content) {
                        // 调用回调函数处理数据块
                        onStream?.(content);
                    }
                } catch (e) {
                    console.error('Failed to parse JSON chunk:', chunk);
                }
            }
        }
    } catch (error) {
        handleApiError(error as AxiosError<unknown, any>);
        throw error;
    }
};

/**
 * 统一处理API请求错误
 * @param error - Axios错误对象
 */
const handleApiError = (error: AxiosError) => {
    let errorMsg = '请求失败,请稍后重试';
    if (error.response) {
        errorMsg = `API错误: ${error.response.status} ${error.response.data}`;
    } else if (error.request) {
        errorMsg = '网络连接异常,请检查网络';
    }
    ElMessage.error(errorMsg);
};

export default fetchAIResponse;

 添加类型

因为我们用的是TS,所以我们还需要在src\types文件夹下创建ai.ts。

类型放在哪就看自己的项目要求,也可以直接写在请求文件里面,因为后面Vue文件里面也会用到,所以我就把他提出来了

export type MessageRole = 'user' | 'assistant' | 'system';

export interface ChatMessage {
    id: string;
    //角色
    role: MessageRole;
    //内容
    content: string;
    //时间戳
    timestamp: number;
}

第四步:写vue文件

画页面

这是自己做路过简单的页面以及动画,大家可以根据自己的需要自行调整

<template>
    <div class="chat-container">
        <div class="message-area">
            <div v-for="msg in messages" :key="msg.id" :class="['message', msg.role]">
                <div class="role-tag">{{ msg.role === 'user' ? '我' : '助手' }}</div>
                <div class="content">{{ msg.content || '' }}</div>
                <div class="timestamp">
                    {{ new Date(msg.timestamp).toLocaleTimeString() }}
                </div>
            </div>
            <div v-if="isLoading" class="loading-indicator">思考中...</div>
        </div>

        <div class="input-area">
            <el-input
                v-model="inputMessage"
                type="textarea"
                :rows="2"
                :disabled="isLoading"
                placeholder="输入消息(Enter发送,Shift+Enter换行)"
                @keyup.enter.prevent="handleKeyPress"
                @keydown.enter.prevent
            />
            <el-button type="primary" :loading="isLoading" class="send-button" @click="sendMessage"> 发送 </el-button>
        </div>
    </div>
</template>

<style scoped>
.chat-container {
    display: flex;
    padding: 20px;
    margin: 0 auto;
    max-width: 1200px;
    height: 80vh;
    background: #fff;
    flex-direction: column;
}

.message-area {
    flex: 1;
    overflow-y: auto;
    padding: 20px;
    margin-bottom: 20px;
    background: white;
    border-radius: 12px;
    box-shadow: 0 2px 12px rgb(0 0 0 / 10%);
}

.message {
    display: flex;
    padding: 12px 16px;
    margin: 12px 0;
    max-width: 75%;
    border-radius: 12px;
    flex-direction: column;
    gap: 8px;

    &.user {
        align-self: flex-end;
        background: #e8f4ff;
        border: 1px solid #c2d9ff;
    }

    &.assistant {
        align-self: flex-start;
        background: #f6f6f7;
        border: 1px solid #e4e4e7;
    }
}

.role-tag {
    font-size: 0.8em;
    color: #666;
    font-weight: 500;
}

.timestamp {
    font-size: 0.75em;
    text-align: right;
    color: #999;
}

.input-area {
    display: flex;
    gap: 12px;
    padding: 16px;
    background: white;
    border-radius: 12px;
    box-shadow: 0 2px 12px rgb(0 0 0 / 10%);

    .send-button {
        width: 70px;
        height: 50px;
    }
}

.loading-indicator {
    padding: 12px;
    text-align: center;
    color: #666;
}

.message.assistant .content {
    overflow: hidden;
    white-space: pre-wrap;
    animation:
        typing 0.1s steps(4, end),
        blink 0.75s step-end infinite;
}

@keyframes typing {
    from {
        max-width: 0;
    }

    to {
        max-width: 100%;
    }
}

@keyframes blink {
    50% {
        border-color: transparent;
    }
}
</style>

TS逻辑

代码里面也有注释我就也不过多阐述了。

值得一提的是,在封装请求的时候,我们做了deepseek和Kimi的切换,我们只需要在调用fetchAIResponse请求的时候把第二个参数把 'KIMI' 换成 'DEEPSEEK'就可以了,如果有需要的话,大家可以在页面添加一个按钮做切换。

<script lang="ts" setup>
import { ChatMessage } from '@/types/ai';
import fetchAIResponse from '@/http/aiRequest';

/** 输入消息 */
const inputMessage = ref('');
/** 消息列表 */
const messages = ref<ChatMessage[]>([]);
/** 加载状态 */
const isLoading = ref(false);
/** 定时器 ID */
const currentStreamMessage = ref<ChatMessage | null>(null);

/**
 * 消息处理函数,将新消息添加到消息列表
 * @param message - 要添加的消息,不包含 id 和 timestamp
 */
const addMessage = (message: Omit<ChatMessage, 'id' | 'timestamp'>) => {
    messages.value.push({
        ...message,
        id: crypto.randomUUID(),
        timestamp: Date.now()
    });
};

/** 发送消息 */
async function sendMessage() {
    // 如果输入为空或正在加载,则不执行操作
    if (!inputMessage.value.trim() || isLoading.value) return;
    // 清空输入框前保存用户输入的消息
    const userMessage = inputMessage.value.trim();
    inputMessage.value = '';

    // 添加用户消息到消息列表
    addMessage({
        role: 'user',
        content: userMessage
    });

    // 更新消息列表,触发响应式更新
    const tempMessage: ChatMessage = {
        id: crypto.randomUUID(),
        role: 'assistant',
        content: '',
        timestamp: Date.now()
    };
    // 更新消息列表,触发响应式更新
    messages.value = [...messages.value, tempMessage];
    // 设置当前流式消息
    currentStreamMessage.value = tempMessage;

    try {
        isLoading.value = true;

        await fetchAIResponse(
            messages.value.filter((msg) => msg.content),
            'KIMI',
            (chunk: string) => {
                if (currentStreamMessage.value) {
                    currentStreamMessage.value.content += chunk;
                    // 触发 DOM 更新
                    requestAnimationFrame(autoScroll);
                }
            }
        );
    } catch (error) {
        if (currentStreamMessage.value) {
            currentStreamMessage.value.content += '(响应中断)';
        }
    } finally {
        isLoading.value = false;
        // 清空当前流式消息对象
        currentStreamMessage.value = null;
    }
}

/** 自动滚动函数 */
const autoScroll = () => {
    const container = document.querySelector('.message-area');
    if (container) {
        requestAnimationFrame(() => {
            const lastMessage = container.lastElementChild;
            if (lastMessage) {
                lastMessage.scrollIntoView({
                    behavior: 'smooth',
                    block: 'nearest'
                });
            }
        });
    }
};

/** 设置键盘事件(设置 Shift+Enter换行,Enter发送)*/
const handleKeyPress = (event: KeyboardEvent) => {
    if (event.key === 'Enter' && !event.shiftKey) {
        event.preventDefault();
        sendMessage();
    }
};
</script>

五、总结配图流程

 


六、扩展实现方案


七、AI 对话框简易实现流程图

了解原理和流程后,根据官网的示例,也可以去实现别的AI

### 如何在 UniApp Vue3 项目中调用 DeepSeek API #### 准备工作 为了能够在 UniApp 和 Vue3 环境下成功调用 DeepSeek API,需先完成必要的准备工作。这包括安装所需的依赖库以及配置环境变量。 #### 安装依赖包 确保已安装 `axios` 或其他 HTTP 请求库来发起网络请求: ```bash npm install axios ``` #### 配置环境变量 创建 `.env.development` 文件用于存储敏感信息如 API 密钥等,并将其加入到 `.gitignore` 中防止泄露[^1]。 ```plaintext VUE_APP_DEEPEEK_API_KEY=your_api_key_here VUE_APP_BASE_URL=https://api.deepseek.com/v1/ ``` #### 创建 API 调用服务 建立一个新的文件夹 `/services/deepseek.js` 来封装所有的 API 方法: ```javascript // services/deepseek.js import axios from &#39;axios&#39;; const instance = axios.create({ baseURL: process.env.VUE_APP_BASE_URL, headers: { Authorization: `Bearer ${process.env.VUE_APP_DEEPEEK_API_KEY}` } }); export const getSearchResults = async (query) => { try { const response = await instance.get(&#39;/search&#39;, { params: { q: query } }); return response.data; } catch (error) { console.error(&#39;Error fetching data:&#39;, error); throw new Error(&#39;Failed to fetch search results&#39;); } }; ``` #### 使用组件内调用 API 下面是一个简单的例子展示如何在一个页面里使用上述定义的服务函数获取数据并显示出来。 ```html <template> <view class="container"> <input v-model="searchQuery" placeholder="Enter your query..." /> <button @click="fetchData">Search</button> <!-- 显示结果 --> <ul v-if="results.length > 0"> <li v-for="(item, index) in results" :key="index">{{ item.title }}</li> </ul> {{ errorMessage }} </view> </template> <script setup> import { ref } from &#39;vue&#39;; import { onLoad } from &#39;@dcloudio/uni-app&#39;; import { getSearchResults } from &#39;@/services/deepseek&#39;; let searchQuery = ref(&#39;&#39;); let results = ref([]); let errorMessage = ref(&#39;&#39;); async function fetchData() { try { const result = await getSearchResults(searchQuery.value); results.value = result.items || []; errorMessage.value = &#39;&#39;; } catch (err) { errorMessage.value = err.message; } } </script> ``` 通过这种方式,在 UniApp 的 Vue3 项目中就可以方便地集成和调用 DeepSeek 提供的各种功能和服务了。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值