基于Vue 3与Pinia构建现代化AI智能体展示平台:从技术选型到部署实战
1. 项目概述一个现代化的AI智能体展示平台最近在折腾Dify发现社区里有很多人开发了非常有意思的AI智能体Agent但分享和展示的方式大多比较零散要么是发个链接要么是列个表格体验上总差点意思。作为一个前端开发者我就在想能不能把这些智能体像应用商店里的App一样用精美的卡片形式展示出来让用户能快速浏览、搜索并且一键直达体验。于是就有了“我的AI工坊”这个项目。简单来说这是一个基于Vue 3和Element Plus构建的、专门用于展示Dify智能体的前端应用。它不是一个复杂的后台管理系统核心目标非常明确提供一个美观、响应式、交互友好的“智能体橱窗”。用户打开这个页面就能看到所有已配置的智能体通过卡片了解其功能、评分和热度点击卡片即可跳转到对应的Dify应用页面进行深度使用。对于智能体的提供者或社区运营者而言这个项目可以快速部署通过修改一个JSON配置文件就能搭建起一个专属的智能体展示门户极大地提升了分享效率和用户体验。这个项目适合两类朋友一类是前端开发者尤其是对Vue 3生态感兴趣想学习如何构建一个现代化的、包含主题切换、状态管理等完整功能的前端应用另一类是AI应用爱好者或布道师你可能不写代码但手头有一批好用的Dify智能体想分享给团队或社区那么这个项目提供了近乎“开箱即用”的部署方案你只需要会改JSON文件就能拥有一个专业的展示站。2. 技术选型与架构设计思路为什么选择这套技术栈这背后是基于对项目目标轻量、美观、易维护和现代前端发展趋势的综合考量。下面我拆解一下每个核心技术的选型理由。2.1 为什么是Vue 3 Composition APIVue 3是当前Vue生态的必然选择其带来的性能提升和更好的TypeScript支持是基础。但更关键的是Composition API。在这个项目中我们管理的主要是两类数据智能体列表和主题状态。使用Composition API配合script setup语法糖能让逻辑关注点更集中。例如在AgentCard.vue组件中我们不仅需要展示数据还需要处理卡片的悬停交互逻辑。如果用Options API这些逻辑data,methods,computed会分散在组件的不同选项中。而用Composition API我们可以把与“卡片交互”相关的所有响应式数据、计算属性和函数都组织在一个useCardInteraction的组合式函数里代码的內聚性和可读性会高很多。这对于后续维护和功能扩展比如想给卡片增加收藏功能非常友好。2.2 Element Plus作为UI库的考量选择Element Plus而非其他UI库如Ant Design Vue、Vuetify主要基于以下几点设计语言成熟Element Plus继承了Element UI的设计哲学组件视觉风格统一、稳重非常适合做工具类、展示类应用不会显得过于花哨。对Vue 3支持完善且活跃作为Element UI的Vue 3升级版其社区活跃迭代速度快遇到问题容易找到解决方案。按需引入支持好配合unplugin-vue-components等插件可以做到真正的按需自动导入极大优化了最终打包体积。我们的项目用到的组件如卡片ElCard、按钮ElButton、图标ElIcon等都能被自动识别和引入开发体验流畅。主题定制能力Element Plus内置了暗黑模式并且支持通过CSS变量进行深度主题定制这与我们项目需要实现主题切换的需求完美契合。2.3 状态管理Pinia的必要性可能有人会问一个展示型应用数据源就是一个静态JSON有必要上状态管理吗用provide/inject或者事件总线不行吗我的答案是有必要而且Pinia是最佳选择。首先状态并不只有智能体数据。主题模式深色/浅色是一个典型的全局状态需要在应用顶层如AppHeader修改并在几乎所有组件中消费以应用正确的CSS变量。如果使用provide/inject在组件层级较深时代码会显得繁琐。其次搜索和筛选状态也需要被管理。用户在搜索框输入关键词或者选择了某个分类筛选这个状态需要同步到展示智能体列表的HomeView页面组件。虽然可以通过父子组件通信完成但当状态逻辑变得复杂时比如未来增加排序、标签过滤集中式的状态管理会让数据流更清晰、可预测。Pinia相比Vuex 4API更简洁对TypeScript支持更好并且去掉了mutations这个略显冗余的概念。在我们的stores/目录下你会看到两个清晰的storetheme.js: 管理主题状态提供toggleTheme动作并利用localStorage实现持久化。agents.js: 管理智能体列表并提供filteredAgents这个getter用于根据搜索词和分类返回过滤后的列表。这样HomeView组件只需要引入这个store并渲染filteredAgents即可所有过滤逻辑都被封装在store内部非常干净。2.4 构建工具Vite带来的极致体验Vite取代Webpack成为现代Vue项目的默认选择已经是共识。在这个项目中Vite的优势体现得淋漓尽致闪电般的冷启动运行npm run dev几乎是秒开。这得益于Vite基于ES模块的开发服务器无需打包整个应用。高效的热更新HMR修改组件代码浏览器几乎无感更新保持了开发流程的流畅性。优化的生产构建使用Rollup进行生产构建打包出来的dist目录结构清晰资源经过压缩和代码分割。我们的项目结构简单Vite的“简单”哲学与之匹配。配置文件 (vite.config.js) 非常干净主要就是配置了路径别名和unplugin-vue-components插件用于Element Plus的自动导入。2.5 样式方案CSS变量与Flexbox/Grid布局我们没有选择CSS预处理器如Sass或CSS-in-JS方案而是坚持使用原生CSS3核心是CSS变量Custom Properties和现代布局模型。CSS变量是实现主题切换的核心技术。在src/assets/main.css中我们定义了两套CSS变量分别对应浅色和深色主题:root { /* 浅色主题变量 */ --bg-color-primary: #ffffff; --bg-color-secondary: #f5f7fa; --text-color-primary: #303133; --text-color-secondary: #606266; --border-color: #dcdfe6; --card-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); --primary-color: #409eff; } [data-themedark] { /* 深色主题变量 */ --bg-color-primary: #1a1a1a; --bg-color-secondary: #242424; --text-color-primary: #e5eaf3; --text-color-secondary: #c0c4cc; --border-color: #4c4d4f; --card-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.6); --primary-color: #409eff; /* 主色可以保持一致 */ }然后在整个应用的样式文件中所有颜色、阴影等属性都引用这些变量例如background-color: var(--bg-color-primary);。当用户切换主题时我们只需要通过JavaScript在html标签上设置>.agents-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 24px; padding: 20px; }repeat(auto-fill, minmax(300px, 1fr))这行代码是响应式的精髓它告诉浏览器尽可能多地填充列每列的最小宽度是300px最大是1fr等分剩余空间。这样在宽屏下会显示多列在窄屏如手机下当宽度不足以容纳300px时会自动变为单列完美适配。3. 核心功能模块实现详解3.1 智能体数据管理与展示智能体数据是应用的核心。我们采用静态JSON文件 (src/data/agents.json) 来管理这是一种在数据量不大、变动不频繁的场景下非常高效的方式避免了引入数据库和后端的复杂度。数据结构设计 一个智能体对象包含以下字段设计时考虑了展示和筛选的需求{ id: 1, name: 小红书文案生成器, description: 专注于生成符合小红书平台调性的爆款文案包括标题、正文和话题标签。, icon: Promotion, // 对应Element Plus的图标名 category: 内容创作, difyUrl: https://dify.ai/app/abc123, features: [多种文案风格, 热门话题推荐, 字数控制], rating: 4.7, users: 1500 }icon: 使用Element Plus的图标名便于在组件中直接使用el-icon渲染。category: 用于分类筛选。可以预设几个固定分类如“内容创作”、“编程助手”、“效率工具”等。features: 是一个字符串数组用于在卡片上展示核心卖点通常以标签形式呈现。rating和users: 提供社交证明增加卡片的可信度和吸引力。数据加载与状态管理 在stores/agents.js中我们通过import直接引入JSON文件。在Pinia store的state中定义agents数组。同时我们定义searchQuery和selectedCategory两个响应式状态以及一个filteredAgents的getter。// stores/agents.js 简化示例 import agentsData from /data/agents.json; export const useAgentsStore defineStore(agents, { state: () ({ agents: agentsData, searchQuery: , selectedCategory: 全部 }), getters: { filteredAgents(state) { return state.agents.filter(agent { const matchesSearch agent.name.toLowerCase().includes(state.searchQuery.toLowerCase()) || agent.description.toLowerCase().includes(state.searchQuery.toLowerCase()); const matchesCategory state.selectedCategory 全部 || agent.category state.selectedCategory; return matchesSearch matchesCategory; }); }, categories(state) { // 从所有智能体中提取不重复的分类并加上“全部” const cats [全部, ...new Set(state.agents.map(a a.category))]; return cats; } }, actions: { setSearchQuery(query) { this.searchQuery query; }, setSelectedCategory(category) { this.selectedCategory category; } } });这样在HomeView.vue中我们只需要遍历filteredAgents任何搜索或筛选操作都会自动触发列表更新。3.2 主题切换系统的实现主题切换是一个提升用户体验的亮点功能。我们的目标是支持跟随系统主题、手动切换、状态持久化。1. 状态定义与持久化 在stores/theme.js中我们定义主题状态。初始值优先从localStorage读取如果不存在则使用window.matchMedia检测系统偏好。// stores/theme.js export const useThemeStore defineStore(theme, { state: () ({ isDark: localStorage.getItem(theme) dark || (!(theme in localStorage) window.matchMedia((prefers-color-scheme: dark)).matches) }), actions: { toggleTheme() { this.isDark !this.isDark; localStorage.setItem(theme, this.isDark ? dark : light); this.applyTheme(); }, applyTheme() { const theme this.isDark ? dark : light; document.documentElement.setAttribute(data-theme, theme); } } });在应用入口 (main.js或App.vue) 初始化时需要调用一次applyTheme()来确保主题被正确应用到HTML标签上。2. 切换触发 在AppHeader.vue组件中放置一个切换按钮比如一个太阳/月亮图标按钮点击时触发store的toggleThemeaction。3. CSS变量的应用 如前所述所有样式都基于CSS变量。当>.agent-card { transition: transform 0.3s ease, box-shadow 0.3s ease; cursor: pointer; } .agent-card:hover { transform: translateY(-5px); box-shadow: var(--card-shadow-hover); /* 定义一个更深的阴影变量 */ }点击跳转卡片整体可点击绑定click事件使用window.open(agent.difyUrl, _blank)在新标签页打开Dify应用。为了可访问性最好将整个卡片用a标签包裹并设置rolebutton和相应的ARIA属性但考虑到样式和布局使用click并处理好键盘事件如keyup.enter也是可接受的方案。3.4 搜索与筛选功能实现搜索和筛选功能由SearchFilter.vue组件完成它与agentsstore 紧密交互。搜索框使用ElInput组件绑定v-model到store的searchQuery。为了提升体验可以使用lodash的debounce函数对输入事件进行防抖处理避免频繁触发过滤计算。分类筛选使用ElSelect下拉选择器或一组ElRadio单选按钮其选项列表来源于store的categoriesgetter选中的值绑定到store的selectedCategory。这个组件的关键是将用户输入迅速同步到全局状态。由于filteredAgents是一个计算属性它会立即响应searchQuery和selectedCategory的变化从而实现列表的实时过滤。4. 项目配置与部署实战4.1 如何添加你自己的智能体这是本项目最常用的操作。你不需要懂Vue只需要编辑一个JSON文件。找到项目中的src/data/agents.json文件。按照已有的格式添加一个新的对象。id确保唯一且递增。icon字段需要去 Element Plus图标集合 查找你想要的图标名如ChatLineRound,Setting,Promotion。difyUrl填入你的Dify应用发布后获得的公开访问链接。保存文件重启开发服务器或重新构建新的智能体就会出现在列表里。注意事项agents.json文件最终会被打包到JavaScript中。如果你希望实现动态加载比如从远程API获取列表需要修改stores/agents.js中的加载逻辑使用fetch或axios来异步获取数据并处理好加载和错误状态。对于大多数展示场景静态JSON足够用。4.2 构建与部署到常见平台本地构建 运行npm run build。Vite会在项目根目录生成一个dist文件夹里面包含了所有静态资源HTML, JS, CSS, 图片等。你可以直接把这个文件夹丢到任何静态网站托管服务上。Nginx部署 这是最传统的部署方式。将dist文件夹上传到你的服务器然后配置Nginx。一个最基本的配置如下server { listen 80; server_name your-domain.com; # 你的域名 root /path/to/your/dist; # dist目录的绝对路径 index index.html; # 对于Vue Router的history模式需要此配置以避免404 location / { try_files $uri $uri/ /index.html; } # 可选启用gzip压缩 gzip on; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xmlrss text/javascript; }配置完成后重启Nginx即可。Vercel / Netlify 部署 对于个人项目或快速演示我非常推荐这些Serverless部署平台。它们与GitHub集成可以实现自动部署。将你的代码推送到GitHub仓库。在Vercel/Netlify官网导入你的GitHub仓库。构建命令填写npm run build输出目录填写dist。点击部署。平台会自动分配一个域名如xxx.vercel.app你也可以绑定自己的域名。Docker部署 如果你熟悉Docker可以创建一个Dockerfile来标准化部署环境。# 使用Node.js官方镜像作为构建环境 FROM node:18-alpine as builder WORKDIR /app COPY package*.json ./ RUN npm ci --onlyproduction COPY . . RUN npm run build # 使用Nginx镜像来服务静态文件 FROM nginx:alpine COPY --frombuilder /app/dist /usr/share/nginx/html # 如果需要可以在这里复制自定义的nginx.conf # COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 CMD [nginx, -g, daemon off;]然后构建镜像并运行容器docker build -t my-ai-workshop . docker run -d -p 8080:80 my-ai-workshop4.3 自定义样式与主题如果你想调整颜色、间距或字体主要修改两个地方CSS变量在src/assets/main.css的:root和[data-themedark]中修改对应的颜色值。这是影响全局主题最快的方式。组件样式如果你想覆盖Element Plus组件的默认样式需要使用深度选择器。例如想修改卡片的标题颜色/* 在组件的 style scoped 中 */ :deep(.el-card__header) { background-color: var(--your-custom-color); }注意在Vue单文件组件的style scoped块中需要使用:deep()来穿透作用域影响子组件的样式。5. 开发常见问题与优化技巧在实际开发和部署过程中我遇到并解决了一些典型问题这里分享给大家。5.1 性能优化点图片优化如果智能体卡片需要展示自定义图标或截图请务必对图片进行压缩可以使用 TinyPNG 等工具并使用合适的格式WebP格式通常更优。对于图标优先使用Icon Font或SVG Sprite而不是单独的图片文件。组件懒加载当前项目页面单一无需懒加载。但如果未来扩展为多页面如增加“关于”页可以使用Vue Router的懒加载功能来分割代码。// router/index.js const HomeView () import(/views/HomeView.vue)第三方库按需引入我们已经通过unplugin-vue-components实现了Element Plus的自动按需导入。如果引入了其他工具库如lodash注意只导入用到的方法例如import debounce from lodash/debounce。5.2 遇到的坑与解决方案问题一主题切换后部分Element Plus组件样式未更新。现象切换深色模式后自己写的样式都变了但某些Element Plus组件如下拉菜单、弹窗内部还是浅色。原因Element Plus组件的样式是独立打包的。仅仅切换html上的>// main.js import element-plus/theme-chalk/dark/css-vars.css // 引入暗黑模式CSS变量 import ElementPlus from element-plus import element-plus/dist/index.css app.use(ElementPlus)同时在切换主题的action里除了设置>// stores/theme.js - applyTheme 方法补充 applyTheme() { const theme this.isDark ? dark : light; document.documentElement.setAttribute(data-theme, theme); // 关键为body添加/移除类名以触发Element Plus组件内部样式更新 if (this.isDark) { document.body.classList.add(dark); } else { document.body.classList.remove(dark); } }问题二生产环境构建后页面空白或资源加载404。原因最常见的原因是Vue Router使用了history模式但服务器没有正确配置。或者资源路径在构建后不正确。解决方案路由模式如果使用Vue Router的history模式必须在服务器如Nginx配置中加上try_files $uri $uri/ /index.html;如前文所示。如果使用hash模式 (mode: hash)则没有这个问题但URL中会带#。资源路径在vite.config.js中检查base配置。如果你的应用部署在子路径下如https://example.com/my-app/需要设置base: /my-app/。默认base: /适用于部署在根域名的情况。检查控制台打开浏览器开发者工具查看Console和Network标签页通常会有具体的错误信息提示。问题三搜索框输入时列表过滤卡顿。现象当智能体数据较多比如上百条时每次输入都触发过滤计算可能会感觉输入不跟手。解决方案对搜索输入事件进行防抖debounce。可以使用 lodash 的debounce函数或者自己实现一个简单的。// SearchFilter.vue template el-input v-modelinputValue inputhandleSearchInput placeholder搜索智能体... / /template script setup import { ref } from vue; import { useAgentsStore } from /stores/agents; import { debounce } from lodash-es; // 注意引入es模块版本以支持Tree Shaking const agentsStore useAgentsStore(); const inputValue ref(); // 创建一个防抖函数延迟300毫秒执行 const debouncedSearch debounce((query) { agentsStore.setSearchQuery(query); }, 300); const handleSearchInput (value) { debouncedSearch(value); }; /script5.3 扩展思路这个项目作为一个基础模板有很强的扩展性后端集成将agents.json替换为从后端API动态获取。可以增加智能体的点赞、收藏、评论功能。多语言国际化使用vue-i18n库轻松实现中英文切换。更丰富的筛选除了分类可以增加按评分、使用人数、创建时间排序或筛选。用户系统如果做成一个公共的智能体集市可以接入简单的用户登录让用户提交自己的智能体。SEO优化目前是纯前端应用不利于搜索引擎收录。可以考虑使用SSR框架如Nuxt.js重构或者为每个智能体生成静态页面SSG。最后我想分享一点个人体会。这个项目的价值不在于技术有多高深而在于它精准地解决了一个小痛点并且用现代前端技术栈给出了一个优雅、可复用的解决方案。对于初学者你可以通过它学习Vue 3的组合式API、Pinia状态管理、Vite构建以及CSS变量的应用。对于有经验的开发者你可以把它作为一个快速搭建产品展示页的样板节省大量从零搭建的时间。最重要的是它让分享AI智能体这件事变得简单和美观。