Ant Desgin X 联调 Dify
Ant Desgin X 联调 Dify
0.前提条件
Ant Desogin X要求vue版本不低于3.5,因此这里直接选择vite进行项目的创建。- 创建项目时,指定项目类型为
Vue+TypeScript。 - 所使用的
node.js版本信息为20.17.0。
1.环境准备
1.1 创建一个vue项目
使用 vite 进行项目的创建,创建命令如下,在创建项目的时候声明项目类型为 Vue + TypeScript。
npm create vite@latest vue-typescript
创建操作执行完成后,进入项目目录下,依次执行如下命令即可运行 Vue 项目。
# 安装项目依赖
npm install
# 运行vue项目
npm run dev
访问终端显示的运行地址,可以看到如下页面,说明项目创建成功。

1.2 安装 ant-design-x-vue
这里直接使用 npm 安装 ant-design-x-vue,命令如下所示:
npm install ant-design-x-vue --save
2.项目开发
直接使用
Ant Design X Vue项目官方提供的代码进行测试,代码地址:https://antd-design-x-vue.netlify.app/playground/independent.html,代码内容如下所示:<script setup lang="ts"> import type { AttachmentsProps, BubbleListProps, ConversationsProps, PromptsProps } from 'ant-design-x-vue' import type { VNode } from 'vue' import { CloudUploadOutlined, CommentOutlined, EllipsisOutlined, FireOutlined, HeartOutlined, PaperClipOutlined, PlusOutlined, ReadOutlined, ShareAltOutlined, SmileOutlined, } from '@ant-design/icons-vue' import { Badge, Button, Flex, Space, Typography, theme } from 'ant-design-vue' import { Attachments, Bubble, Conversations, Prompts, Sender, useXAgent, useXChat, Welcome, } from 'ant-design-x-vue' import { computed, h, ref, watch } from 'vue' const { token } = theme.useToken() const styles = computed(() => { return { 'layout': { 'width': '100%', 'min-width': '1000px', 'height': '722px', 'border-radius': `${token.value.borderRadius}px`, 'display': 'flex', 'background': `${token.value.colorBgContainer}`, 'font-family': `AlibabaPuHuiTi, ${token.value.fontFamily}, sans-serif`, }, 'menu': { 'background': `${token.value.colorBgLayout}80`, 'width': '280px', 'height': '100%', 'display': 'flex', 'flex-direction': 'column', }, 'conversations': { 'padding': '0 12px', 'flex': 1, 'overflow-y': 'auto', }, 'chat': { 'height': '100%', 'width': '100%', 'max-width': '700px', 'margin': '0 auto', 'box-sizing': 'border-box', 'display': 'flex', 'flex-direction': 'column', 'padding': `${token.value.paddingLG}px`, 'gap': '16px', }, 'messages': { flex: 1, }, 'placeholder': { 'padding-top': '32px', 'text-align': 'left', 'flex': 1, }, 'sender': { 'box-shadow': token.value.boxShadow, }, 'logo': { 'display': 'flex', 'height': '72px', 'align-items': 'center', 'justify-content': 'start', 'padding': '0 24px', 'box-sizing': 'border-box', }, 'logo-img': { width: '24px', height: '24px', display: 'inline-block', }, 'logo-span': { 'display': 'inline-block', 'margin': '0 8px', 'font-weight': 'bold', 'color': token.value.colorText, 'font-size': '16px', }, 'addBtn': { background: '#1677ff0f', border: '1px solid #1677ff34', width: 'calc(100% - 24px)', margin: '0 12px 24px 12px', }, } as const }) defineOptions({ name: 'PlaygroundIndependentSetup' }) function renderTitle(icon: VNode, title: string) { return h(Space, { align: 'start' }, [icon, h('span', title)]) } const defaultConversationsItems = [ { key: '0', label: 'What is Ant Design X?', }, ] const placeholderPromptsItems: PromptsProps['items'] = [ { key: '1', label: renderTitle(h(FireOutlined, { style: { color: '#FF4D4F' } }), 'Hot Topics'), description: 'What are you interested in?', children: [ { key: '1-1', description: `What's new in X?`, }, { key: '1-2', description: `What's AGI?`, }, { key: '1-3', description: `Where is the doc?`, }, ], }, { key: '2', label: renderTitle(h(ReadOutlined, { style: { color: '#1890FF' } }), 'Design Guide'), description: 'How to design a good product?', children: [ { key: '2-1', icon: h(HeartOutlined), description: `Know the well`, }, { key: '2-2', icon: h(SmileOutlined), description: `Set the AI role`, }, { key: '2-3', icon: h(CommentOutlined), description: `Express the feeling`, }, ], }, ] const senderPromptsItems: PromptsProps['items'] = [ { key: '1', description: 'Hot Topics', icon: h(FireOutlined, { style: { color: '#FF4D4F' } }), }, { key: '2', description: 'Design Guide', icon: h(ReadOutlined, { style: { color: '#1890FF' } }), }, ] const roles: BubbleListProps['roles'] = { ai: { placement: 'start', typing: { step: 5, interval: 20 }, styles: { content: { borderRadius: '16px', }, }, }, local: { placement: 'end', variant: 'shadow', }, } // ==================== State ==================== const headerOpen = ref(false) const content = ref('') const conversationsItems = ref(defaultConversationsItems) const activeKey = ref(defaultConversationsItems[0].key) const attachedFiles = ref<AttachmentsProps['items']>([]) const agentRequestLoading = ref(false) // ==================== Runtime ==================== const [agent] = useXAgent({ request: async ({ message }, { onSuccess }) => { agentRequestLoading.value = true await sleep() agentRequestLoading.value = false onSuccess(`Mock success return. You said: ${message}`) }, }) const { onRequest, messages, setMessages } = useXChat({ agent: agent.value, }) watch(activeKey, () => { if (activeKey.value !== undefined) { setMessages([]) } }, { immediate: true }) // ==================== Event ==================== function onSubmit(nextContent: string) { if (!nextContent) return onRequest(nextContent) content.value = '' } const onPromptsItemClick: PromptsProps['onItemClick'] = (info) => { onRequest(info.data.description as string) } function onAddConversation() { conversationsItems.value = [ ...conversationsItems.value, { key: `${conversationsItems.value.length}`, label: `New Conversation ${conversationsItems.value.length}`, }, ] activeKey.value = `${conversationsItems.value.length}` } const onConversationClick: ConversationsProps['onActiveChange'] = (key) => { activeKey.value = key } const handleFileChange: AttachmentsProps['onChange'] = info => attachedFiles.value = info.fileList // ==================== Nodes ==================== const placeholderNode = computed(() => h( Space, { direction: "vertical", size: 16, style: styles.value.placeholder }, [ h( Welcome, { variant: "borderless", icon: "https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*s5sNRo5LjfQAAAAAAAAAAAAADgCCAQ/fmt.webp", title: "Hello, I'm Ant Design X", description: "Base on Ant Design, AGI product interface solution, create a better intelligent vision~", extra: h(Space, {}, [h(Button, { icon: h(ShareAltOutlined) }), h(Button, { icon: h(EllipsisOutlined) })]), } ), h( Prompts, { title: "Do you want?", items: placeholderPromptsItems, styles: { list: { width: '100%', }, item: { flex: 1, }, }, onItemClick: onPromptsItemClick, } ) ] )) const items = computed<BubbleListProps['items']>(() => { if (messages.value.length === 0) { return [{ content: placeholderNode, variant: 'borderless' }] } return messages.value.map(({ id, message, status }) => ({ key: id, loading: status === 'loading', role: status === 'local' ? 'local' : 'ai', content: message, })) }) </script> <template> <div :style="styles.layout"> <div :style="styles.menu"> <!-- 🌟 Logo --> <div :style="styles.logo"> <img src="https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*eco6RrQhxbMAAAAAAAAAAAAADgCCAQ/original" draggable="false" alt="logo" :style="styles['logo-img']" > <span :style="styles['logo-span']">Ant Design X Vue</span> </div> <!-- 🌟 添加会话 --> <Button type="link" :style="styles.addBtn" @click="onAddConversation" > <PlusOutlined /> New Conversation </Button> <!-- 🌟 会话管理 --> <Conversations :items="conversationsItems" :style="styles.conversations" :active-key="activeKey" @active-change="onConversationClick" /> </div> <div :style="styles.chat"> <!-- 🌟 消息列表 --> <Bubble.List :items="items" :roles="roles" :style="styles.messages" /> <!-- 🌟 提示词 --> <Prompts :items="senderPromptsItems" @item-click="onPromptsItemClick" /> <!-- 🌟 输入框 --> <Sender :value="content" :style="styles.sender" :loading="agentRequestLoading" @submit="onSubmit" @change="value => content = value" > <template #prefix> <Badge :dot="attachedFiles.length > 0 && !headerOpen"> <Button type="text" @click="() => headerOpen = !headerOpen" > <template #icon> <PaperClipOutlined /> </template> </Button> </Badge> </template> <template #header> <Sender.Header title="Attachments" :open="headerOpen" :styles="{ content: { padding: 0 } }" @open-change="open => headerOpen = open" > <Attachments :before-upload="() => false" :items="attachedFiles" @change="handleFileChange" > <template #placeholder="type"> <Flex v-if="type && type.type === 'inline'" align="center" justify="center" vertical gap="2" > <Typography.Text style="font-size: 30px; line-height: 1;"> <CloudUploadOutlined /> </Typography.Text> <Typography.Title :level="5" style="margin: 0; font-size: 14px; line-height: 1.5;" > Upload files </Typography.Title> <Typography.Text type="secondary"> Click or drag files to this area to upload </Typography.Text> </Flex> <Typography.Text v-if="type && type.type === 'drop'"> Drop file here </Typography.Text> </template> </Attachments> </Sender.Header> </template> </Sender> </div> </div> </template>运行后,页面显示内容如下,这里先不对样式做调整,直接进行大模型的调用。

这里配置了
Dify的api进行调用测试,与常规的前后端接口请求方式不同,大模型的接口支持流式模式,基于SSE(Server-Sent Events)实现类似打字机输出方式的流式响应,这里对接口的响应数据要进行特殊处理。// 解析响应的JSON文本内容 function tryParseJSON(line: string) { try { return JSON.parse(line); } catch { return null; } } // 大模型对话 const [agent] = useXAgent({ request: async ({ message }, { onSuccess, onUpdate, onError }) => { agentRequestLoading.value = true try { // http://127.0.0.1:81/v1/chat-messages 这里是dify发送对话消息的接口 const response = await fetch('http://127.0.0.1:81/v1/chat-messages', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer app-sosbqnUmxQRzvpSmqsDfMAtr', // 秘钥 }, body: JSON.stringify({ "inputs":{}, "query": message, "response_mode": "streaming", "conversation_id": "", "user": "abc-123", "files":[] }), }) if (!response.ok || !response.body) throw new Error('响应异常'); const reader = response.body.getReader(); const decoder = new TextDecoder('utf-8'); let buffer = ''; let fullText = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) { if (!line.startsWith('data:')) continue; const jsonStr = line.replace(/^data:\s*/, '').trim(); const parsed = tryParseJSON(jsonStr); // 这里是Dify工作流的判断逻辑,如其他大模型,需要具体查看 if (parsed && parsed.event == 'workflow_finished') { const content = parsed.data.outputs.answer; fullText += content; onUpdate(fullText); onSuccess(fullText); // 添加这行,在工作流完成时调用 onSuccess } } } } catch (error) { console.error('请求失败:', error) onError(error as Error) } finally { agentRequestLoading.value = false } }, })页面输入内容,能够正确进行响应,如下图所示

3.项目部署
这里对新创建的 Vue 项目进行优化,引入配置文件,使得项目更加完整。
在项目根目录下,创建一下两个文件,分别是开发环境以及生产环境的配置信息:
.env.development.env.production
文件中写入后端大模型的服务地址,如下所示:
VITE_API_BASE_URL=http://127.0.0.1:81修改
Vue文件中请求大模型的地址,修改为如下样式:// 修改前 const response = await fetch('127.0.0.1:81/v1/chat-messages', { // 修改后 const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/v1/chat-messages`, {修改
package.json文件,执行项目执行不同操作的时候,使用的配置文件分别是什么,如下所示:"scripts": { "dev": "vite --mode development", "build": "vue-tsc -b && vite build --mode production", "preview": "vite preview --mode production" },执行
npm run dev,运行项目进行测试,能够正常请求后端大模型服务,如下所示
执行
npm run build,打包当前项目部署到生产环境。
部署到远程主机后,能够正常进行请求,项目部署成功。
