element-ui + vue开发一个openai的聊天框

element-ui + vue开发一个openai的聊天框

效果演示

  • 支持流式响应渲染
  • 支持渲染思考内容
  • 支持渲染工具调用
  • 支持markdown内容解析
    在这里插入图片描述
    在这里插入图片描述

代码

<template>
    <div class="chat-wrapper">
        <el-container>
            <el-header>
                <strong>调试和预览</strong>
                <el-tooltip class="item" effect="dark" content="清空对话记录" placement="bottom">
                    <i class="el-icon-refresh-right"
                       :class="{ 'rotate-animation': control.clearChatRecord }"
                        @click="clearChatRecordEvent"/>
                </el-tooltip>
            </el-header>

            <el-main ref="messagesContainer">
                <template v-for="(message, index) in messages">
                    <div v-if="message.role == 'user'" class="user-message">
                        <el-avatar>
                            <div class="message-avatar">
                                <svg-icon icon-class="chat-user"/>
                            </div>
                        </el-avatar>
                        <div class="message-content">
                            <div class="content">
                                {{message.content}}
                            </div>
                        </div>
                    </div>
                    <div v-if="message.role == 'assistant'" class="ai-message">
                        <el-avatar style="background: white !important;">
                            <div class="message-avatar">
                                <svg-icon icon-class="chat-ai"/>
                            </div>
                        </el-avatar>
                        <dot-flashing v-if="!message.content && !message.error && !(message.aiMsgs && message.aiMsgs.length > 0)"
                                      class="chat-load"/>
                        <div v-if="message.content || (message.aiMsgs && message.aiMsgs.length > 0)" class="message-content">
                            <div>
                                <template
                                        v-if="message.aiMsgs && message.aiMsgs.length > 0"
                                        v-for="(aiMsg, index) in message.aiMsgs">
                                    <template v-if="aiMsg.type == 'think'">
                                        <div class="message-title">
                                            <el-button v-if="aiMsg.endTime" @click="clickThinkEvent(aiMsg)">
                                                深度思考(用时
                                                {{ (aiMsg.endTime - aiMsg.startTime) / 1000 }})
                                                <i class="el-icon-arrow-down arrow-icon" :class="{'up': aiMsg.showThink}"/>
                                            </el-button>
                                        </div>
                                        <div v-if="aiMsg.showThink" class="think-content">
                                            {{aiMsg.think}}
                                        </div>
                                    </template>
                                    <template v-if="aiMsg.type == 'function'">
                                        <div class="message-title">
                                            <el-button @click="clickToolCallEvent(aiMsg)">
                                                工具调用(find_user)
                                            </el-button>
                                        </div>
                                    </template>
                                </template>
                                <template v-if="message.content">
                                    <div
                                       class="content markdown-body"
                                       v-html="message.htmlContent">
                                    </div>
                                </template>
                            </div>
                        </div>
                        <div v-if="message.error" class="message-content error-message-content">
                            <div class="content">
                                {{message.error}}
                            </div>
                        </div>
                    </div>
                </template>

            </el-main>

            <el-footer>
                <el-input
                    :rows="1"
                    v-model="inputMessage"
                    :disabled="control.waitAiResponseFinish"
                    @keydown.enter.native.prevent="sendMessage"
                    type="textarea"
                    resize="none"/>
                <el-button type="primary" size="small" :disabled="control.waitAiResponseFinish" @click="sendMessage">发送</el-button>
            </el-footer>

            <div v-if="control.waitAiResponseFinish" class="stop-response-container">
                <el-button
                        type="danger"
                        round
                        size="small"
                        class="stop-response-btn"
                >
                    <i class="el-icon-warning-outline"></i> 停止响应
                </el-button>
            </div>

        </el-container>
    </div>
</template>

<script>
    import MarkdownIt from 'markdown-it'
    import hljs from 'highlight.js'
    import 'highlight.js/styles/github.css'

    import dotFlashing from "./dot_flashing"
    import setter from "@/setting/setter";

    let that;

    export default {
        name: "OpenAiChat",
        components: {
            dotFlashing
        },
        props: {
            agentId: {
                type: String,
                require: true,
            },
        },
        data() {
            return {
                /* 对话消息 */
                messages: [
                    // {
                    //     role: "assistant",
                    //     aiMsgs: [
                    //         {
                    //             type: "think",
                    //             think: "dsa",
                    //             showThink: true
                    //         },
                    //         {
                    //             type: "function",
                    //             function: "{\"arguments\":{\"type\":\"PRODUCT\",\"keywords\":\"\"},\"name\":\"test_platform_data_query\"}",
                    //         }
                    //     ],
                    //     content: "当前查询产品数据时遇到连接异常(错误码500:连接被拒绝)。可能原因如下:\n" +
                    //         "\n" +
                    //         "1. **平台服务未启动**:请确认性能压测平台服务是否正常运行\n" +
                    //         "2. **网络隔离**:请检查当前网络环境是否具备访问平台API的权限\n" +
                    //         "3. **地址配置错误**:若为自建平台,请确认服务地址配置是否正确(当前请求地址:http://127.0.0.1:18090)\n" +
                    //         "\n" +
                    //         "建议您:\n" +
                    //         "- 等待30秒后重试\n" +
                    //         "- 联系平台管理员确认服务状态\n" +
                    //         "- 检查本地hosts文件是否配置正确\n" +
                    //         "\n" +
                    //         "需要进一步协助请提供:\n" +
                    //         "1. 当前使用的访问入口(如:测试管理门户/命令行工具)\n" +
                    //         "2. 是否为首次使用该平台",
                    // }
                ],
                /* 聊天框输入消息 */
                inputMessage: "",
                /*
                 * 控制页面元素状态
                 */
                control: {
                    // 点击清理按钮,设置 true。清理结束设置false
                    clearChatRecord: false,
                    // 等待AI响应完成
                    waitAiResponseFinish: false,
                },
                // markdown
                md: null,
            }
        },
        methods: {
            // Markdown 渲染
            renderMarkdown(text) {
                let content = text;
                return this.md.render(content || '')
            },

            sendMessage() {
                if (checkSendMessage()) {
                    let input = this.inputMessage;
                    // 添加到消息列表
                    this.messages.push({role: "user", content: input});
                    this.control.waitAiResponseFinish = true;
                    this.messages.push({role: "assistant", content: "", htmlContent: "", aiMsgs: []});
                    this.inputMessage = "";

                    // 发起对话
                    chat(setter.server.request.baseURL + "/chat/completions", input, (data, down) => {
                        if (down) {
                            if (this.control.waitAiResponseFinish) {
                                this.control.waitAiResponseFinish = false;

                            }
                            return;
                        }

                        // 获取 ai message
                        let aiMessage = this.messages[this.messages.length - 1];

                        // AI响应内容
                        if (data.content) {
                            if (aiMessage.aiMsgs.length > 0
                                && aiMessage.aiMsgs[aiMessage.aiMsgs.length - 1].type == "think"
                                && !aiMessage.aiMsgs[aiMessage.aiMsgs.length - 1].endTime) {
                                aiMessage.aiMsgs[aiMessage.aiMsgs.length - 1].endTime = new Date().getTime();
                                aiMessage.aiMsgs[aiMessage.aiMsgs.length - 1].showThink = false;
                            }
                            aiMessage.content += data.content;
                            aiMessage.htmlContent = this.renderMarkdown(aiMessage.content);
                            //console.log("aiMessage", aiMessage)
                        }

                        // 思考内容
                        if (data.think) {
                            let aiMsgs = aiMessage.aiMsgs;
                            if (aiMsgs.length == 0 || aiMsgs[aiMsgs.length - 1].type != "think") {
                                aiMsgs.push(
                                    {
                                        type: "think",
                                        think: data.think,
                                        startTime: new Date().getTime(),
                                        endTime: 0,
                                        showThink: true
                                    }
                                );
                            } else {
                                // 获取最后一条数据填入
                                let aiMsg = aiMsgs[aiMsgs.length - 1];
                                aiMsg.think += data.think;
                            }
                        }

                        // 工具调用
                        if (data.function) {
                            if (aiMessage.aiMsgs.length > 0
                                && aiMessage.aiMsgs[aiMessage.aiMsgs.length - 1].type == "think"
                                && !aiMessage.aiMsgs[aiMessage.aiMsgs.length - 1].endTime) {
                                aiMessage.aiMsgs[aiMessage.aiMsgs.length - 1].endTime = new Date().getTime();
                                aiMessage.aiMsgs[aiMessage.aiMsgs.length - 1].showThink = false;
                            }
                            aiMessage.aiMsgs.push(
                                {
                                    type: "function",
                                    function: data.function,
                                }
                            );
                        }

                        // 设置滚动到底部
                        this.messagesContainerScrollToBottom();
                    }, error => {
                        let aiMessage = this.messages[this.messages.length - 1];
                        this.$set(aiMessage, "error", "系统服务异常, 请稍后再试");
                        if (this.control.waitAiResponseFinish) {
                            this.control.waitAiResponseFinish = false;
                        }
                    });
                }
            },

            /**
             * 点击深度思考事件
             */
            clickThinkEvent(aiMsg) {
                this.$set(aiMsg, "showThink", aiMsg.showThink ? false : true);
            },

            /**
             * 点击工具调用事件
             */
             clickToolCallEvent(aiMsg) {
                this.$alert(JSON.stringify(JSON.stringify(aiMsg.function), null, 2), "工具调用详情", {
                    showConfirmButton: false,
                    closeOnPressEscape: true,
                    callback: action => {}
                });
            },

            /**
             * 点击清空聊天记录事件
             */
            clearChatRecordEvent() {
                if (this.control.waitAiResponseFinish) {
                    that.$message.error("当前有未完成的对话, 请稍后再试");
                    return;
                }
                this.control.clearChatRecord = true;
                this.messages = [];

                setTimeout(() => {
                    this.control.clearChatRecord = false;
                }, 3000);
            },

            /**
             * message聊天框滚动到最低部
             */
            messagesContainerScrollToBottom() {
                this.$nextTick(() => {
                    const container = this.$refs.messagesContainer.$el;
                    container.scrollTop = container.scrollHeight;
                })
            },
        },
        created() {
            // 初始化Markdown渲染器
            this.md = new MarkdownIt({
                html: true,
                linkify: true,
                typographer: true,
                highlight: function (str, lang) {
                    if (lang && hljs.getLanguage(lang)) {
                        try {
                            return hljs.highlight(str, { language: lang }).value
                        } catch (__) {}
                    }
                    return '' // 使用额外的默认转义
                }
            })
        },
        mounted() {
            that = this;
            this.messagesContainerScrollToBottom();
        },
    }

    /**
     * 检查是否可以发送消息
     */
    function checkSendMessage() {
        if (!that.inputMessage) {
            that.$message.error("输入消息为空");
            return false;
        } else if (that.control.waitAiResponseFinish) {
            that.$message.error("当前有未完成的对话");
            return false;
        }
        return true;
    }

    /**
     * 发起聊天, 服务端是SSE协议的响应内容
     *
     * @param url
     */
    function chat(url, input, callback, errorCallback) {
        fetch(url, {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "cookie": "grafana_session=9026800badf33e22d50e868892ff97b0; _bl_uid=aama4bqet6X1nIkRe6jjhI8a213n; locale=zh-Hans; X-XSHELTER-ACCESS-TOKEN=957c62ee-e319-4615-a67b-5234e1aa2a58; sys_token=ea5367a3df814e5899618e5da79706bc; sys_env_id=1022400244723744768; sys_env_code=Init",
            },
            body: JSON.stringify({
                agentId: that.agentId,
                username: "admin",
                variables: {
                    "cookie": "sys_token=ea5367a3df814e5899618e5da79706bc; sys_env_id=1022400244723744768; sys_env_code=Init"
                },
                content: input
            })
        })
            .then(response => {
                if (response.ok && response.body) {
                    return response.body;
                } else {
                    throw new Error('SSE connection failed');
                }
            })
            .then(body => {
                const reader = body.getReader();
                const decoder = new TextDecoder();

                // 读取流数据粘包后存储起来的缓冲区
                let chunkBuffer = "";

                // 读取流数据
                function readStream() {
                    reader.read().then(({ done, value }) => {
                        if (done) {
                            //console.log('SSE stream closed');
                            return;
                        }
                        // 接收流数据
                        let chunk = decoder.decode(value, { stream: true });

                        // 数据块需要加上未处理完的缓冲区数据
                        let tmpBuf = chunkBuffer + chunk;

                        // 解析data数据
                        let data = "";
                        for (let i = 0; i < tmpBuf.length; i++) {
                            if (tmpBuf[i] == "\n" && tmpBuf[i + 1] == "\n") {
                                //console.log("data=[" + data + "]");
                                if (callback) {
                                    let chunkData = data.replace("data: ", "");
                                    if (chunkData == "[DONE]") {
                                        callback(null, true);
                                    } else {
                                        callback(JSON.parse(chunkData));
                                    }
                                }
                                data = "";
                                i = i + 1;
                            } else {
                                data += tmpBuf[i];
                            }
                        }

                        // 如果data数据是不完整的,加载到缓冲区
                        chunkBuffer = data;

                        // // 继续读取下一段
                        readStream();
                    }).catch(err => {
                        console.error('Error reading stream:', err);
                    });
                }

                // 读取流数据
                readStream();
            })
            .catch(error => {
                if (errorCallback) {
                    errorCallback(error);
                }
                console.error('Fetch error:', error);
            });
    }

</script>

<style scoped>
    .chat-wrapper {
        display: flex;
        height: 100%;
        width: 100%;
    }

    .el-container .el-header {
        height: 40px !important;
        display: flex;
        justify-content: space-between;
        align-items: center;
        background-color: #ece6e6;
        padding: 10px !important;
    }

    .el-container {
        position: relative;
    }

    .el-container .el-header i {
        font-size: 20px;
    }
    .el-container .el-header i:hover {
        color: #ee9090;
        cursor: pointer;
    }

    .el-container .el-main {
        background: rgba(231, 230, 230, 0.50);
        overflow: auto !important;
        overflow-x: hidden !important;
    }
    .el-container .el-main::-webkit-scrollbar {
        display: none !important; /* Chrome/Safari/Opera */
    }
    .el-container .el-footer {
        height: 50px !important;
        display: flex;
        justify-content: space-between;
        align-items: center;
        gap: 16px;
        background-color: #f1eeee;
        padding: 0 10px 0 10px !important;
    }

    .stop-response-container {
        position: absolute;
        bottom: 64px;
        left: 50%;
        transform: translateX(-50%);
        z-index: 1000;
    }

    .chat-load {
        display: flex;
        align-items: center;
    }

    .user-message {
        display: flex;
        flex-direction: row-reverse;
        margin: 20px 10px 20px 10px;
    }

    .ai-message {
        display: flex;
        margin: 20px 10px 20px 10px;
    }

    .message-content {
        padding: 0 10px 0 10px;
        display: flex;
        align-items: center;
        line-height: 1.6;
        word-break: break-word;
        max-width: 70%;
        margin: 0 10px 0 10px;
        border-radius: 8px;
        font-size: 16px;
    }

    .message-avatar {
        width: 100%;
        height: 100%;
        display: flex;
        align-items: center;
        justify-content: center;
    }

    .message-avatar svg {
        width: 80%;
        height: 80%;
    }

    .user-message .message-content {
        background-color: #c4d8f5;
    }

    .ai-message .message-content {
        background-color: white;
    }

    .error-message-content {
        background-color: #bd6079 !important;
        color: white;
    }

    .message-title {
        margin-top: 6px;
    }
    .message-title .el-button {
        background: rgba(230, 230, 230, 0.7) !important;
        color: rgb(38 38 38) !important;
        border: 0px !important;
        padding: 8px 6px 8px 6px !important;
        font-size: 14px !important;
    }
    .message-title .el-button:hover {
        background: rgba(230, 230, 230, 1) !important;
    }

    .think-content {
        color: #767474;
        margin: 10px 0px 10px 0px;
        padding: 0 8px 0 8px;
        border-left: 1px solid #767474; /* 宽度 样式 颜色 */
        font-size: 14px;
    }

    .content {
        margin: 6px 0 6px 0;
    }

    .arrow-icon {
        transition: transform 0.3s ease;
    }
    .arrow-icon.up {
        transform: rotate(180deg);
    }
    .collapse-content {
        max-height: 0;
        transition: max-height 0.3s ease, padding 0.3s ease;
    }

    .rotate-animation {
        animation: rotate 1.5s linear infinite;
    }

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

    /* Markdown内容样式 */
    .markdown-body {
        font-size: 16px;
        line-height: 1.5;
        word-wrap: break-word;
    }

    .markdown-body >>> h1,
    .markdown-body >>> h2,
    .markdown-body >>> h3,
    .markdown-body >>> h4,
    .markdown-body >>> h5,
    .markdown-body >>> h6 {
        margin-top: 24px;
        margin-bottom: 16px;
        font-weight: 600;
        line-height: 1.25;
    }

    .markdown-body >>> h1 {
        padding-bottom: 0.3em;
        font-size: 2em;
        border-bottom: 1px solid #eaecef;
    }

    .markdown-body >>> h2 {
        padding-bottom: 0.3em;
        font-size: 1.5em;
        border-bottom: 1px solid #eaecef;
    }

    .markdown-body >>> h3 {
        font-size: 1.25em;
    }

    .markdown-body >>> p {
        margin-top: 0;
        margin-bottom: 16px;
    }

    .markdown-body >>> ul,
    .markdown-body >>> ol {
        padding-left: 2em;
        margin-top: 0;
        margin-bottom: 16px;
    }

    .markdown-body >>> li {
        margin-bottom: 0.25em;
    }

    .markdown-body >>> blockquote {
        padding: 0 1em;
        color: #6a737d;
        border-left: 0.25em solid #dfe2e5;
        margin: 0 0 16px 0;
    }

    .markdown-body >>> pre {
        padding: 16px;
        overflow: auto;
        font-size: 85%;
        line-height: 1.45;
        background-color: #f6f8fa;
        border-radius: 6px;
        margin-bottom: 16px;
    }

    .markdown-body >>> code {
        font-size: 85%;
        background-color: rgba(175, 184, 193, 0.2);
        border-radius: 3px;
        padding: 0.2em 0.4em;
    }

    .markdown-body >>> pre code {
        background-color: transparent;
        padding: 0;
    }

    .markdown-body >>> a {
        color: #0366d6;
        text-decoration: none;
    }

    .markdown-body >>> a:hover {
        text-decoration: underline;
    }

    .markdown-body >>> strong {
        font-weight: 600;
    }

    .markdown-body >>> em {
        font-style: italic;
    }

    .markdown-body >>> img {
        max-width: 100%;
        box-sizing: content-box;
        background-color: #fff;
    }

    .markdown-body >>> table {
        border-spacing: 0;
        border-collapse: collapse;
        display: block;
        width: 100%;
        overflow: auto;
        margin-bottom: 16px;
    }

    .markdown-body >>> th {
        font-weight: 600;
        padding: 6px 13px;
        border: 1px solid #dfe2e5;
    }

    .markdown-body >>> td {
        padding: 6px 13px;
        border: 1px solid #dfe2e5;
    }

    .markdown-body >>> tr {
        background-color: #fff;
        border-top: 1px solid #c6cbd1;
    }

    .markdown-body >>> tr:nth-child(2n) {
        background-color: #f6f8fa;
    }
</style>

loading 代码

<template>
    <div class="dot-flashing">
        <div class="dot"></div>
        <div class="dot"></div>
        <div class="dot"></div>
    </div>
</template>

<script>
    export default {
        name: "dot_flashing"
    }
</script>

<style>
    .dot-flashing {
        display: inline-flex;
        margin: 0px 10px 0px 10px;
        gap: 5px;
    }
    .dot {
        width: 8px;
        height: 8px;
        border-radius: 50%;
        background: #3dd91b;
        opacity: 0.3;
        animation: dot-flash 1s infinite alternate;
    }
    .dot:nth-child(1) { animation-delay: 0s; }
    .dot:nth-child(2) { animation-delay: 0.3s; }
    .dot:nth-child(3) { animation-delay: 0.6s; }
    @keyframes dot-flash {
        to { opacity: 1; }
    }
</style>

使用

import openAiChat from "../chat/openai_chat";

<open-ai-cha/>
### Vue3 中使用 Element-Plus-X 实现 AI 聊天功能 为了在 Vue3 项目中集成 Element-Plus-X 并实现 AI 聊天功能,可以按照以下方法构建应用。以下是详细的说明以及示例代码。 #### 安装依赖项 首先,在项目中安装必要的依赖包 `element-plus` 和 `vue-element-plus-x`: ```bash pnpm install element-plus vue-element-plus-x ``` 如果未初始化 Vue3 项目,则可以通过 Vue CLI 创建一个新的 Vue3 项目[^1]。 --- #### 配置 Element-Plus 在项目的入口文件(通常是 `main.js` 或 `main.ts`),引入并注册 Element-Plus 组件库: ```javascript import { createApp } from 'vue'; import App from './App.vue'; import ElementPlus from 'element-plus'; import 'element-plus/dist/index.css'; const app = createApp(App); app.use(ElementPlus); app.mount('#app'); ``` 这一步确保了整个应用程序能够正常使用 Element-Plus 的 UI 组件[^2]。 --- #### 构建聊天界面 创建一个简单的聊天窗口布局,利用 Element-Plus 提供的组件来增强用户体验。例如,使用 `<el-scrollbar>` 来处理滚动条,<el-input> 处理用户输入等。 ```html <template> <div class="chat-container"> <!-- 对话历史 --> <el-scrollbar max-height="400px" class="message-list"> <div v-for="(msg, index) in messages" :key="index" class="message-item"> {{ msg }} </div> </el-scrollbar> <!-- 用户输入 --> <div class="input-area"> <el-input v-model="newMessage" placeholder="请输入消息..." @keyup.enter="sendMessage"></el-input> <el-button type="primary" @click="sendMessage">发送</el-button> </div> </div> </template> <script> export default { data() { return { newMessage: '', messages: [], }; }, methods: { sendMessage() { if (this.newMessage.trim()) { this.messages.push(`用户: ${this.newMessage}`); this.simulateAIAPIResponse(); // 模拟调用 API 获取回复 this.newMessage = ''; } }, simulateAIAPIResponse() { setTimeout(() => { const aiReply = '这是来自AI的模拟回复'; // 替换为实际接口返回的数据 this.messages.push(`AI: ${aiReply}`); }, 1000); // 延迟一秒模拟网络请求时间 }, }, }; </script> <style scoped> .chat-container { display: flex; flex-direction: column; height: 500px; } .message-list { margin-bottom: 10px; border: 1px solid #ccc; padding: 10px; } .input-area { display: flex; } </style> ``` 此模板定义了一个基本的聊天室结构,其中包含了对话列表和输入区域[^1]。 --- #### 整合 WebSocket 进行实时通信 对于即时通讯场景,WebSocket 是一种高效的解决方案。可以在 Vue3 应用中通过 Composition API 设置 WebSocket 连接逻辑。 ```javascript import { ref, onMounted, onUnmounted } from 'vue'; export function useWebSocket(url) { const socketRef = ref(null); onMounted(() => { socketRef.value = new WebSocket(url); socketRef.value.onopen = () => console.log('连接已建立...'); socketRef.value.onmessage = (event) => handleIncomingMessage(event.data); socketRef.value.onerror = (error) => handleError(error); socketRef.value.onclose = () => console.log('连接关闭...'); }); onUnmounted(() => { if (socketRef.value && socketRef.value.readyState === WebSocket.OPEN) { socketRef.value.close(); } }); function send(message) { if (socketRef.value?.readyState === WebSocket.OPEN) { socketRef.value.send(JSON.stringify({ message })); } } function handleIncomingMessage(data) { console.log('收到服务器消息:', JSON.parse(data)); } function handleError(error) { console.error('WebSocket 错误:', error); } return { send }; } ``` 将上述函数导入到聊天组件中,并替换掉 `simulateAIAPIResponse()` 方法中的静态数据获取部分,改为动态接收 WebSocket 数据。 --- #### 使用 Axios 请求远程服务 当需要向后端发起 HTTP 请求时,可借助 Axios 发送异步请求并与 Spring Boot 后台交互。例如: ```javascript import axios from 'axios'; async function fetchAIMessage(userInput) { try { const response = await axios.post('/api/chat', { userInput }); return response.data.aiResponse; // 返回 AI 接口响应的内容 } catch (err) { console.error(err); throw err; } } ``` 将其嵌入至聊天组件的方法中即可完成前后端联动[^1]。 --- ### 总结 以上展示了如何基于 Vue3 和 Element-Plus-X 构造具备基础功能的 AI 聊天页面,同时介绍了 WebSockets 及 Axios 的具体运用方式。开发者可以根据需求进一步扩展样式设计和服务对接细节。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Me_Liu_Q

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值