一、项目开发背景与初衷
自2024年起,各类AI工具已然成为我日常工作与生活中不可或缺的辅助工具。随着人工智能技术快速迭代,豆包、Kimi、腾讯元宝、文心一言、通义千问等多款AI平台相继成熟,不同工具拥有各自独特的优势,适配文本创作、代码编写、图片生成、视频制作、智能搜索等差异化工作场景。
在长期使用过程中我发现,不同AI工具的功能侧重点各不相同:有的擅长长文本解析,有的专注视频生成,有的深耕AI绘画与智能搜索。日常工作中,往往需要频繁切换多个平台,操作繁琐、效率低下。2025年底,我萌生了一个想法:能否打造一个一站式聚合工具,整合市面上主流的各类AI平台,实现多AI工具对比使用、自由切换,同时根据不同工具的功能特性搭建专属工作流,将文本撰写、图片生成、视频合成、代码开发等全流程功能整合在一个页面中,真正实现一站式AI办公。
我本身没有编程基础,仅能简单阅读代码、理解基础逻辑。为了落地这个想法,我确定了全新的开发模式:由AI自主编写代码,我全程负责需求梳理、功能测试与项目调试。经过对豆包、DeepSeek、Kimi等多款AI编程工具的实测对比,最终选择Kimi作为核心开发辅助工具,开启了「一堆AI」项目的开发之路
二、项目开发过程与迭代历程
项目初期,我一直在思考工具的整体形态与开发思路。一次偶然的机会,我接触到「电子书全网搜」浏览器插件,该插件拥有右键唤起AI搜索的实用功能。我尝试解压插件后台代码,将其导入Kimi中分析复用,以此为基础,开启了项目的初步开发。
最初,我让AI将插件逻辑改写为独立网站程序,初代版本界面简陋、功能单一、实用性较差。但在一次次调试和修改需求的过程中,我逐步理清了前端、后端的核心开发逻辑。结合自身使用需求,我最终确定项目采用纯前端架构,舍弃传统后端服务。
这样的设计核心优势十分明显:全程采用本地存储模式,用户所有账号数据、使用记录仅保存在个人浏览器与用户自有AI账号中,无需上传至服务器。既规避了服务器存储压力不足的问题,也最大程度保护了用户隐私数据,真正做到轻量化、安全化、无门槛使用。
后续我持续迭代优化,不断新增功能、优化界面、修复BUG,对UI样式、交互逻辑、功能模块、适配效果进行全方位升级。从简陋的初代网页,逐步打磨为功能完善、界面美观、操作流畅的一站式AI聚合平台,最终正式命名项目为「一堆AI」。
三、项目成品与核心功能
「一堆AI」是一款轻量化、纯前端、本地化的一站式AI聚合工具,整合国内主流AI平台,支持多工具并行使用、自定义工作流、批量管理、主题切换等实用功能,无需部署、无需注册,打开即可使用,数据全程本地保存。
核心功能亮点:
全品类AI工具聚合:整合豆包、Kimi、腾讯元宝、文心一言、通义千问、讯飞星火、可灵AI、秒画、纳米AI搜索等主流平台,覆盖AI对话、AI视频、AI绘画、AI编程、智能搜索五大核心场景。
自定义工作流:支持自由组合多个AI工具,自定义保存专属工作流,一键批量打开组合工具,适配批量办公、内容创作、程序开发等高频场景。
批量管理操作:内置批量选择、批量添加、批量加入工作流功能,支持工具卡片拖拽排序、单独刷新、全屏、关闭等精细化操作。
个性化设置:支持浅色/深色主题一键切换、自定义添加私有AI工具、快捷键快捷操作、配置导入导出、数据重置等功能。
极致安全轻量化:纯前端运行,无后端服务器,用户数据本地存储,无信息泄露风险,适配电脑端所有浏览器,运行流畅无卡顿。
项目完整网页源代码已开发完成,线上可直接访问使用,全程免费、无广告、无付费功能。
四、二次开发:从网页端到桌面端
2026年5月,为了进一步提升工具的实用性和便捷性,摆脱浏览器标签限制,我接触到aardio桌面端开发工具,决定对「一堆AI」进行二次开发,将网页版程序打包升级为独立桌面端软件。
依托前期网页版成熟的功能架构,结合AI辅助编写桌面端代码,我快速完成了桌面端适配开发。通过WebView2内核嵌套线上网页,完美复刻网页版全部功能,同时拥有独立桌面客户端形态,启动更快、使用更稳定、不受浏览器限制。
桌面端核心开发代码简洁轻量化,依托aardio实现窗口自适应、全屏展示、稳定加载,完整保留网页版所有AI聚合、工作流、个性化设置功能,实现了项目的跨端升级。
目前,「一堆AI」网页端+桌面端已全部开发完成,用户可自由选择网页在线使用或桌面客户端使用,全方位满足日常办公、内容创作、学习研究、程序开发等各类AI使用需求。
五、开发总结
本项目是我零编程基础下,依托AI全流程辅助完成的独立开发作品。从最初的创意构思、参考插件逻辑,到网页版反复迭代优化,再到桌面端跨端升级,全程践行「AI赋能创作、人为核心把控」的模式。
最终打造的「一堆AI」工具,解决了多AI工具频繁切换的痛点,通过工作流组合、批量管理、本地化安全存储等特色功能,大幅提升AI使用效率,同时轻量化、无门槛、高安全的特性,也让普通用户可以零成本体验一站式AI办公创作服务。后续我将持续收录更多优质AI平台、优化交互体验、新增实用工作流模板,持续迭代完善项目功能。
六、源代码
以下就是网站的源代码,网站链接为“一堆AI”(点击就可以进去)。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>一堆AI </title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--primary: #4cd561; --bg-primary: #f6f6f6; --bg-secondary: #ffffff;
--text-primary: #303133; --text-secondary: #606266; --border: #e5e5e5;
--shadow: rgba(0, 0, 0, 0.1); --hover-bg: #f5f5f5;
--danger: #f56c6c; --warning: #f59e0b; --info: #1677ff;
--chat-color: #4b70e2;
--video-color: #ec4899;
--code-color: #ff7a00;
--search-color: #10b981;
--image-color: #8b5cf6;
}
[data-theme="dark"] {
--bg-primary: #1a1a1a; --bg-secondary: #2d2d2d;
--text-primary: #ffffff; --text-secondary: #b0b0b0;
--border: #404040; --hover-bg: #3d3d3d; --shadow: rgba(0, 0, 0, 0.3);
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg-primary); color: var(--text-primary);
height: 100vh; overflow: hidden;
}
#app { height: 100vh; display: flex; flex-direction: column; }
/* 工具栏 */
.toolbar {
height: 50px; background: var(--bg-secondary); border-bottom: 1px solid var(--border);
display: flex; align-items: center; justify-content: space-between;
padding: 0 16px; box-shadow: 0 2px 8px var(--shadow); z-index: 100;
}
.logo { display: flex; align-items: center; gap: 10px; font-size: 18px; font-weight: 600; color: var(--primary); cursor: pointer; }
.logo:hover { opacity: 0.8; }
.logo-icon { width: 32px; height: 32px; position: relative; }
.square { position: absolute; width: 14px; height: 14px; background: var(--primary); border-radius: 3px; box-shadow: 0 1px 3px rgba(0,0,0,0.2); }
.square:nth-child(1) { top: 0; left: 0; opacity: 0.9; }
.square:nth-child(2) { top: 4px; left: 9px; opacity: 0.7; background: #3cb853; }
.square:nth-child(3) { top: 9px; left: 4px; opacity: 0.5; background: #2aa845; }
.toolbar-actions { display: flex; gap: 4px; align-items: center; }
.btn-icon {
width: 32px; height: 32px; border: none; background: transparent;
color: var(--text-secondary); border-radius: 6px; cursor: pointer;
display: flex; align-items: center; justify-content: center;
transition: all 0.2s; font-size: 14px;
}
.btn-icon:hover { background: var(--hover-bg); color: var(--primary); }
.btn-icon.active { color: var(--primary); background: rgba(76, 213, 97, 0.1); }
.btn-icon.workflow { color: var(--warning); }
.btn-icon.workflow:hover { background: rgba(245, 158, 11, 0.1); color: var(--warning); }
.btn-icon.workflow.active { color: var(--warning); background: rgba(245, 158, 11, 0.15); }
/* 主内容区 */
.main-container { flex: 1; display: flex; overflow: hidden; position: relative; }
.engines-container {
flex: 1; overflow-x: auto; overflow-y: hidden;
display: flex; align-items: stretch; gap: 12px; padding: 12px;
scroll-behavior: smooth;
}
.engines-container::-webkit-scrollbar { height: 6px; }
.engines-container::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
/* 引擎卡片 */
.engine-card {
min-width: 400px; max-width: 50%; flex: 1;
background: var(--bg-secondary); border-radius: 12px;
border: 1px solid var(--border); display: flex; flex-direction: column;
overflow: hidden; box-shadow: 0 2px 8px var(--shadow);
}
.engine-card.fullscreen {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
z-index: 1000; max-width: 100%; border-radius: 0; min-width: auto;
}
.engine-card.dragging { opacity: 0.5; transform: scale(0.95); }
.card-header {
height: 44px; padding: 0 12px; display: flex;
align-items: center; justify-content: space-between;
border-bottom: 1px solid var(--border); background: rgba(76, 213, 97, 0.05);
flex-shrink: 0;
}
.engine-info { display: flex; align-items: center; gap: 8px; }
.engine-avatar {
width: 28px; height: 28px; border-radius: 6px;
display: flex; align-items: center; justify-content: center;
font-size: 14px; color: white; font-weight: 600;
}
.engine-name { font-weight: 600; font-size: 14px; }
.tag {
font-size: 10px; padding: 2px 6px; border-radius: 4px; margin-left: 6px; color: white;
}
.tag-custom { background: var(--primary); }
.tag-workflow { background: var(--warning); }
.tag-hot { background: linear-gradient(135deg, #f56c6c, #f59e0b); animation: pulse 2s infinite; }
.tag-category { font-size: 10px; padding: 2px 6px; border-radius: 4px; color: white; margin-left: 4px; }
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
.card-actions { display: flex; gap: 4px; }
.card-btn {
width: 28px; height: 28px; border: none; background: transparent;
color: var(--text-secondary); border-radius: 6px; cursor: pointer;
display: flex; align-items: center; justify-content: center;
font-size: 12px; transition: all 0.2s;
}
.card-btn:hover { background: var(--hover-bg); color: var(--primary); }
.card-btn.delete:hover { color: var(--danger); }
.iframe-container { flex: 1; position: relative; background: var(--bg-primary); }
.engine-iframe { width: 100%; height: 100%; border: none; display: block; }
.loading-mask {
position: absolute; inset: 0; background: var(--bg-secondary);
display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 16px; z-index: 10;
}
.loading-spinner {
width: 40px; height: 40px; border: 3px solid var(--border);
border-top-color: var(--primary); border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.loading-text { color: var(--text-secondary); font-size: 14px; }
/* 空状态 */
.empty-state {
flex: 1; display: flex; flex-direction: column;
align-items: center; justify-content: center;
color: var(--text-secondary); text-align: center; padding: 40px;
}
.empty-state i { font-size: 64px; margin-bottom: 20px; opacity: 0.3; }
.empty-state h3 { font-size: 20px; margin-bottom: 8px; color: var(--text-primary); }
/* 底部备案信息 */
.footer {
height: 32px; background: var(--bg-secondary); border-top: 1px solid var(--border);
display: flex; align-items: center; justify-content: center;
gap: 16px; padding: 0 16px; font-size: 12px; color: var(--text-secondary);
}
.footer a { color: var(--text-secondary); text-decoration: none; }
.footer a:hover { color: var(--primary); }
.footer-divider { color: var(--border); }
/* 备案图标样式 */
.police-icon {
color: #0052d9;
margin-right: 4px;
font-size: 13px;
}
[data-theme="dark"] .police-icon {
color: #4c9aff;
}
/* 侧边栏 */
.sidebar {
position: fixed; top: 50px; right: -400px; width: 380px;
height: calc(100vh - 50px); background: var(--bg-secondary);
border-left: 1px solid var(--border); box-shadow: -4px 0 20px var(--shadow);
transition: right 0.3s ease; z-index: 99; display: flex; flex-direction: column;
}
.sidebar.open { right: 0; }
.sidebar-header { padding: 16px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; }
.sidebar-title { font-size: 16px; font-weight: 600; }
.sidebar-close { width: 28px; height: 28px; border: none; background: transparent; color: var(--text-secondary); border-radius: 6px; cursor: pointer; }
.sidebar-content { flex: 1; overflow-y: auto; padding: 16px; }
.menu-section { margin-bottom: 24px; }
.menu-title {
font-size: 12px; color: var(--text-secondary); margin-bottom: 12px;
font-weight: 600; display: flex; align-items: center; justify-content: space-between;
}
.menu-title button {
font-size: 11px; padding: 4px 10px; background: var(--primary);
color: white; border: none; border-radius: 4px; cursor: pointer;
}
.menu-title button.warning { background: var(--warning); }
.engine-list { display: flex; flex-direction: column; gap: 8px; }
.engine-item {
display: flex; align-items: center; justify-content: space-between;
padding: 10px; border-radius: 8px; cursor: pointer;
transition: all 0.2s; border: 2px solid transparent;
}
.engine-item:hover { background: var(--hover-bg); }
.engine-item.active { border-color: var(--primary); background: rgba(76, 213, 97, 0.05); }
.engine-item.in-workflow { border-color: var(--warning); background: rgba(245, 158, 11, 0.05); }
.engine-item-info { display: flex; align-items: center; gap: 10px; }
.engine-item-avatar {
width: 32px; height: 32px; border-radius: 6px;
display: flex; align-items: center; justify-content: center;
color: white; font-size: 14px; font-weight: 600;
}
.engine-item-details h4 { font-size: 14px; margin-bottom: 2px; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.engine-item-details p { font-size: 12px; color: var(--text-secondary); }
.add-btn {
width: 24px; height: 24px; border: none; background: var(--primary);
color: white; border-radius: 6px; cursor: pointer;
display: flex; align-items: center; justify-content: center; font-size: 12px;
}
.add-btn.added { background: #67c23a; }
.add-btn.in-workflow { background: var(--warning); }
.delete-btn {
width: 24px; height: 24px; border: none; background: transparent;
color: var(--text-secondary); border-radius: 6px; cursor: pointer;
display: flex; align-items: center; justify-content: center; font-size: 12px;
}
.delete-btn:hover { color: var(--danger); background: #fef0f0; }
/* 分类筛选器 */
.category-filter {
display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px;
}
.category-btn {
padding: 6px 12px; border-radius: 6px; border: none;
cursor: pointer; font-size: 12px; display: flex; align-items: center; gap: 6px;
transition: all 0.2s;
}
.category-btn:hover { transform: translateY(-1px); }
.category-btn.active {
background: var(--primary); color: white;
box-shadow: 0 2px 8px rgba(76, 213, 97, 0.3);
}
.category-btn:not(.active) {
background: var(--hover-bg); color: var(--text-secondary);
}
/* 工作流 */
.workflow-section {
background: linear-gradient(135deg, rgba(245, 158, 11, 0.05) 0%, rgba(245, 158, 11, 0.1) 100%);
border-radius: 12px; padding: 16px; margin-bottom: 24px;
border: 1px solid rgba(245, 158, 11, 0.2);
}
.workflow-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
.workflow-title { font-size: 14px; font-weight: 600; color: var(--warning); display: flex; align-items: center; gap: 8px; }
.workflow-btn {
padding: 4px 10px; border: none; border-radius: 4px;
font-size: 11px; cursor: pointer;
}
.workflow-btn-primary { background: var(--warning); color: white; }
.workflow-btn-default { background: var(--hover-bg); color: var(--text-secondary); }
.workflow-list { display: flex; flex-direction: column; gap: 8px; }
.workflow-item {
background: var(--bg-secondary); border-radius: 8px; padding: 12px;
cursor: pointer; transition: all 0.2s; border: 2px solid transparent;
}
.workflow-item:hover { border-color: var(--warning); transform: translateX(4px); }
.workflow-item.active { border-color: var(--warning); background: rgba(245, 158, 11, 0.1); }
.workflow-item-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.workflow-item-name { font-size: 14px; font-weight: 600; display: flex; align-items: center; gap: 6px; }
.workflow-item-count {
font-size: 11px; color: var(--text-secondary); background: var(--bg-primary);
padding: 2px 8px; border-radius: 10px;
}
.workflow-item-engines { display: flex; gap: 6px; flex-wrap: wrap; }
.workflow-engine-tag { font-size: 11px; padding: 3px 8px; border-radius: 4px; color: white; font-weight: 500; }
.workflow-empty { text-align: center; padding: 20px; color: var(--text-secondary); font-size: 13px; }
/* 表单 */
.add-custom-form { background: var(--bg-primary); padding: 16px; border-radius: 8px; margin-bottom: 16px; }
.form-group { margin-bottom: 12px; }
.form-group label { display: block; font-size: 12px; color: var(--text-secondary); margin-bottom: 4px; }
.form-group input, .form-group select {
width: 100%; padding: 8px 12px; border: 1px solid var(--border);
border-radius: 6px; background: var(--bg-secondary);
color: var(--text-primary); font-size: 14px;
}
.form-group input:focus, .form-group select:focus { outline: none; border-color: var(--primary); }
.form-actions { display: flex; gap: 8px; margin-top: 16px; }
.form-actions button { flex: 1; padding: 10px; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; }
.btn-primary { background: var(--primary); color: white; }
.btn-default { background: var(--hover-bg); color: var(--text-secondary); }
.workflow-edit-mode {
background: rgba(245, 158, 11, 0.1); border: 2px dashed var(--warning);
border-radius: 8px; padding: 12px; margin-bottom: 16px; text-align: center;
}
.workflow-edit-mode h4 { color: var(--warning); font-size: 14px; margin-bottom: 8px; }
.workflow-edit-mode p { font-size: 12px; color: var(--text-secondary); margin-bottom: 12px; }
.workflow-selected-count { font-size: 13px; color: var(--warning); font-weight: 600; }
/* 遮罩层 */
.overlay {
position: fixed; top: 50px; left: 0; right: 0; bottom: 0;
background: rgba(0, 0, 0, 0.3); z-index: 98;
opacity: 0; visibility: hidden; transition: all 0.3s;
}
.overlay.show { opacity: 1; visibility: visible; }
/* 颜色选择器 */
.color-picker { display: flex; gap: 8px; flex-wrap: wrap; }
.color-option {
width: 28px; height: 28px; border-radius: 6px;
cursor: pointer; border: 3px solid transparent; transition: all 0.2s;
}
.color-option:hover { transform: scale(1.1); }
.color-option.selected { border-color: var(--text-primary); }
/* Toast */
.toast {
position: fixed; top: 12px; left: 50%; transform: translateX(-50%) translateY(-100px);
background: var(--bg-secondary); padding: 10px 20px; border-radius: 8px;
box-shadow: 0 4px 20px var(--shadow); border: 1px solid var(--border);
z-index: 1000; display: flex; align-items: center; gap: 10px;
font-size: 14px; transition: transform 0.3s ease;
}
.toast.show { transform: translateX(-50%) translateY(0); }
.toast.success { border-color: var(--primary); color: var(--primary); }
.toast.warning { border-color: var(--warning); color: var(--warning); }
/* 设置 */
.settings-section { background: var(--bg-primary); border-radius: 8px; padding: 16px; margin-bottom: 16px; }
.settings-item { display: flex; align-items: center; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid var(--border); }
.settings-item:last-child { border-bottom: none; }
.settings-label { font-size: 13px; color: var(--text-primary); }
.settings-desc { font-size: 11px; color: var(--text-secondary); margin-top: 2px; }
.toggle-switch {
width: 44px; height: 24px; background: var(--border); border-radius: 12px;
position: relative; cursor: pointer; transition: all 0.3s;
}
.toggle-switch.active { background: var(--primary); }
.toggle-switch::after {
content: ''; position: absolute; width: 20px; height: 20px;
background: white; border-radius: 50%; top: 2px; left: 2px;
transition: all 0.3s; box-shadow: 0 1px 3px rgba(0,0,0,0.2);
}
.toggle-switch.active::after { left: 22px; }
.shortcut-list { display: flex; flex-direction: column; gap: 8px; }
.shortcut-item {
display: flex; align-items: center; justify-content: space-between;
padding: 8px 12px; background: var(--bg-secondary); border-radius: 6px; font-size: 12px;
}
.shortcut-key {
background: var(--bg-primary); padding: 2px 8px; border-radius: 4px;
border: 1px solid var(--border); font-family: monospace; color: var(--text-secondary);
}
.data-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 12px; }
.data-btn {
padding: 8px; border: 1px solid var(--border); background: var(--bg-secondary);
border-radius: 6px; cursor: pointer; font-size: 12px;
display: flex; align-items: center; justify-content: center; gap: 6px;
color: var(--text-secondary);
}
.data-btn:hover { border-color: var(--primary); color: var(--primary); }
.data-btn.danger:hover { border-color: var(--danger); color: var(--danger); }
/* 弹窗 */
.modal-content {
position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
background: var(--bg-secondary); padding: 24px; border-radius: 12px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3); max-width: 400px; width: 90%;
}
/* 分类标签颜色 */
.cat-chat { background: var(--chat-color); }
.cat-video { background: var(--video-color); }
.cat-code { background: var(--code-color); }
.cat-search { background: var(--search-color); }
.cat-image { background: var(--image-color); }
/* 批量操作栏 */
.batch-actions {
position: fixed; bottom: 40px; left: 50%; transform: translateX(-50%) translateY(100px);
background: var(--bg-secondary); padding: 12px 24px; border-radius: 24px;
box-shadow: 0 4px 20px var(--shadow); border: 1px solid var(--border);
z-index: 200; display: flex; gap: 12px; transition: transform 0.3s ease;
}
.batch-actions.show { transform: translateX(-50%) translateY(0); }
.batch-btn {
padding: 6px 16px; border: none; border-radius: 4px; cursor: pointer;
font-size: 13px; display: flex; align-items: center; gap: 6px;
}
.batch-btn-primary { background: var(--primary); color: white; }
.batch-btn-default { background: var(--hover-bg); color: var(--text-secondary); }
.batch-btn-danger { background: var(--danger); color: white; }
@media (max-width: 768px) {
.engine-card { min-width: 320px; }
.sidebar { width: 100%; right: -100%; }
.toast { left: 50%; transform: translateX(-50%) translateY(-100px); top: 60px; }
.toast.show { transform: translateX(-50%) translateY(0); }
.footer { flex-direction: column; height: auto; padding: 8px; gap: 4px; font-size: 11px; }
.footer-divider { display: none; }
.batch-actions { width: 90%; justify-content: center; }
}
</style>
<base target="_blank">
</head>
<body>
<div id="app">
<!-- Toast -->
<div class="toast" :class="[toastType, { show: showToast }]">
<i :class="toastIcon"></i>
<span>{{ toastMessage }}</span>
</div>
<!-- 批量操作栏 -->
<div class="batch-actions" :class="{ show: selectedEngines.length > 0 }">
<span style="font-size: 13px; color: var(--text-secondary);">已选择 {{ selectedEngines.length }} 个</span>
<button class="batch-btn batch-btn-primary" @click="addSelectedToWorkflow">
<i class="fas fa-project-diagram"></i> 加入工作流
</button>
<button class="batch-btn batch-btn-default" @click="addSelectedToActive">
<i class="fas fa-plus"></i> 打开
</button>
<button class="batch-btn batch-btn-danger" @click="clearSelection">
<i class="fas fa-times"></i> 取消
</button>
</div>
<!-- 工具栏 -->
<header class="toolbar">
<div class="logo" @click="showShortcutModal = true">
<div class="logo-icon">
<div class="square"></div>
<div class="square"></div>
<div class="square"></div>
</div>
<span>一堆 AI</span>
</div>
<div class="toolbar-actions">
<button class="btn-icon" :class="{ active: isBatchMode }" @click="toggleBatchMode" title="批量选择 (Ctrl+B)">
<i class="fas fa-check-square"></i>
</button>
<button class="btn-icon" :class="{ active: isDarkMode }" @click="toggleTheme" title="切换主题 (Ctrl+T)">
<i :class="isDarkMode ? 'fas fa-sun' : 'fas fa-moon'"></i>
</button>
<button class="btn-icon workflow" :class="{ active: showWorkflowMenu }" @click="toggleWorkflowMenu" title="工作流 (Ctrl+W)">
<i class="fas fa-project-diagram"></i>
</button>
<button class="btn-icon" :class="{ active: showAddMenu }" @click="toggleAddMenu" title="添加引擎 (Ctrl+A)">
<i class="fas fa-plus"></i>
</button>
<button class="btn-icon" :class="{ active: showSettings }" @click="toggleSettings" title="设置 (Ctrl+,)">
<i class="fas fa-cog"></i>
</button>
</div>
</header>
<!-- 主内容区 -->
<div class="main-container">
<div class="engines-container" ref="enginesContainer">
<div v-if="!activeEngines.length" class="empty-state">
<i class="fas fa-robot"></i>
<h3>欢迎使用 一堆AI</h3>
<p>点击右上角 <i class="fas fa-plus"></i> 添加 AI 引擎<br>或使用 <i class="fas fa-project-diagram"></i> 工作流快速启动</p>
<p style="margin-top: 12px; font-size: 13px; opacity: 0.7;">
已收录国内主流 AI 工具,本地存储模式,数据只保存在浏览器及个人ai引擎账号内,你的数据安全至关重要
</p>
</div>
<div v-for="(engine, index) in activeEngines" :key="engine.uniqueId"
class="engine-card"
:class="{ fullscreen: engine.fullscreen, dragging: engine.dragging }"
draggable="true"
@dragstart="dragStart($event, index)"
@dragover.prevent
@drop="drop($event, index)"
@dragend="dragEnd">
<div class="card-header">
<div class="engine-info">
<div class="engine-avatar" :style="{ background: engine.color }">
{{ engine.avatar }}
</div>
<span class="engine-name">{{ engine.name }}</span>
<span v-if="engine.isHot" class="tag tag-hot" title="热门工具">🔥</span>
<span v-if="engine.isCustom" class="tag tag-custom">自定义</span>
<span v-if="engine.fromWorkflow" class="tag tag-workflow">{{ engine.workflowName }}</span>
<span v-if="engine.category" class="tag tag-category" :class="'cat-' + engine.category">
{{ getCategoryLabel(engine.category) }}
</span>
</div>
<div class="card-actions">
<button v-for="action in engineActions" :key="action.key"
class="card-btn"
:class="action.class"
@click="action.handler(engine, index)"
:title="action.title">
<i :class="engine[action.iconKey] || action.icon"></i>
</button>
</div>
</div>
<div class="iframe-container">
<div v-if="engine.loading" class="loading-mask">
<div class="loading-spinner"></div>
<div class="loading-text">正在加载 {{ engine.name }}...</div>
</div>
<iframe
v-show="!engine.loading"
:src="engine.url"
class="engine-iframe"
:id="'iframe-' + engine.id"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox"
@load="engine.loading = false"
></iframe>
</div>
</div>
</div>
</div>
<!-- 底部备案信息 -->
<footer class="footer">
<span>数据更新:2026年4月</span>
<span class="footer-divider">|</span>
<span><i class="fas fa-shield-alt police-icon"></i>鄂ICP备2025096233号-2 闽公网安备35010402351837号</span>
<span class="footer-divider">|</span>
<a href="https://www.yiduiai.com" target="_blank">一堆AI</a>
</footer>
<!-- 遮罩层 -->
<div class="overlay" :class="{ show: sidebarOpen }" @click="closeAllSidebars"></div>
<!-- 设置面板 -->
<div class="sidebar" :class="{ open: showSettings }">
<div class="sidebar-header">
<h3 class="sidebar-title">
<i class="fas fa-cog" style="color: var(--primary); margin-right: 8px;"></i>
设置
</h3>
<button class="sidebar-close" @click="showSettings = false">
<i class="fas fa-times"></i>
</button>
</div>
<div class="sidebar-content">
<div class="menu-section">
<div class="menu-title">外观设置</div>
<div class="settings-section">
<div class="settings-item">
<div>
<div class="settings-label">深色模式</div>
<div class="settings-desc">切换浅色/深色主题</div>
</div>
<div class="toggle-switch" :class="{ active: isDarkMode }" @click="toggleTheme"></div>
</div>
</div>
</div>
<div class="menu-section">
<div class="menu-title">快捷键</div>
<div class="settings-section">
<div class="shortcut-list">
<div v-for="sc in shortcuts" :key="sc.key" class="shortcut-item">
<span>{{ sc.name }}</span>
<span class="shortcut-key">{{ sc.key }}</span>
</div>
</div>
</div>
</div>
<div class="menu-section">
<div class="menu-title">数据管理</div>
<div class="settings-section">
<div class="data-actions">
<button v-for="btn in dataBtns" :key="btn.key"
class="data-btn"
:class="btn.class"
@click="btn.action">
<i :class="btn.icon"></i>
{{ btn.text }}
</button>
</div>
</div>
</div>
<div class="menu-section">
<div class="menu-title">关于</div>
<div class="settings-section" style="text-align: center; padding: 20px;">
<div style="font-size: 24px; margin-bottom: 8px;">🤖</div>
<div style="font-weight: 600; margin-bottom: 4px;">一堆 AI v3.1</div>
<div style="font-size: 12px; color: var(--text-secondary);">
聚合国内主流 AI 平台,提升工作效率<br>
<span style="color: var(--warning);">本地存储模式 - 数据保存在浏览器中</span><br>
<span style="color: var(--primary);">2026年最新数据 | 支持批量操作</span>
</div>
</div>
</div>
</div>
</div>
<!-- 工作流侧边栏 -->
<div class="sidebar" :class="{ open: showWorkflowMenu }">
<div class="sidebar-header">
<h3 class="sidebar-title">
<i class="fas fa-project-diagram" style="color: var(--warning); margin-right: 8px;"></i>
工作流管理
</h3>
<button class="sidebar-close" @click="showWorkflowMenu = false">
<i class="fas fa-times"></i>
</button>
</div>
<div class="sidebar-content">
<div v-if="isEditingWorkflow" class="workflow-edit-mode">
<h4><i class="fas fa-edit"></i> 正在编辑工作流</h4>
<div class="form-group" style="text-align: left; margin-top: 12px;">
<label>工作流名称</label>
<input v-model="editingWorkflowName" placeholder="输入工作流名称" maxlength="20">
</div>
<p style="font-size: 12px; color: var(--text-secondary); margin: 8px 0;">
在下方选择要添加的 AI 引擎
</p>
<div class="workflow-selected-count">已选择 {{ editingWorkflowEngines.length }} 个引擎</div>
<div style="margin-top: 12px; display: flex; gap: 8px; justify-content: center;">
<button class="workflow-btn workflow-btn-default" @click="cancelEditWorkflow">
<i class="fas fa-times"></i> 取消
</button>
<button class="workflow-btn workflow-btn-primary" @click="saveWorkflow">
<i class="fas fa-save"></i> 保存工作流
</button>
</div>
</div>
<div class="workflow-section">
<div class="workflow-header">
<div class="workflow-title">
<i class="fas fa-list"></i>
我的工作流
</div>
<button v-if="!isEditingWorkflow" class="workflow-btn workflow-btn-primary" @click="startCreateWorkflow">
<i class="fas fa-plus"></i> 新建
</button>
</div>
<div v-if="!workflows.length" class="workflow-empty">
<i class="fas fa-folder-open" style="font-size: 32px; margin-bottom: 8px; opacity: 0.3;"></i>
<div>暂无工作流</div>
<div style="font-size: 11px; margin-top: 4px;">点击"新建"创建工作流</div>
</div>
<div class="workflow-list">
<div v-for="workflow in workflows" :key="workflow.id"
class="workflow-item"
:class="{ active: currentWorkflow?.id === workflow.id }"
@click="applyWorkflow(workflow)">
<div class="workflow-item-header">
<div class="workflow-item-name">
<i class="fas fa-sitemap" style="color: var(--warning);"></i>
{{ workflow.name }}
</div>
<div style="display: flex; gap: 4px;">
<span class="workflow-item-count">{{ workflow.engines.length }} 个引擎</span>
<button class="delete-btn" @click.stop="deleteWorkflow(workflow.id)" title="删除">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
<div class="workflow-item-engines">
<span v-for="engine in getWorkflowEnginesInfo(workflow)" :key="engine.id"
class="workflow-engine-tag"
:style="{ background: engine.color }">
{{ engine.avatar }}
</span>
</div>
</div>
</div>
</div>
<div v-if="isEditingWorkflow" class="menu-section">
<div class="menu-title">
<span>选择 AI 引擎</span>
<span style="font-size: 11px; color: var(--warning);">点击添加/移除</span>
</div>
<div class="engine-list">
<div v-for="engine in allEngines" :key="engine.id"
class="engine-item"
:class="{ 'in-workflow': isInEditingWorkflow(engine.id) }"
@click="toggleEngineInWorkflow(engine)">
<div class="engine-item-info">
<div class="engine-item-avatar" :style="{ background: engine.color }">
{{ engine.avatar }}
</div>
<div class="engine-item-details">
<h4>
{{ engine.name }}
<span v-if="engine.isHot" class="tag tag-hot">🔥</span>
<span v-if="engine.isCustom" class="tag tag-custom">自定义</span>
<span v-if="engine.category" class="tag tag-category" :class="'cat-' + engine.category">
{{ getCategoryLabel(engine.category) }}
</span>
</h4>
<p>{{ engine.description }}</p>
</div>
</div>
<button class="add-btn" :class="{ 'in-workflow': isInEditingWorkflow(engine.id) }">
<i :class="isInEditingWorkflow(engine.id) ? 'fas fa-check' : 'fas fa-plus'"></i>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- 添加引擎侧边栏 -->
<div class="sidebar" :class="{ open: showAddMenu }">
<div class="sidebar-header">
<h3 class="sidebar-title">
<i class="fas fa-plus-circle" style="color: var(--primary); margin-right: 8px;"></i>
管理 AI 引擎
</h3>
<button class="sidebar-close" @click="showAddMenu = false">
<i class="fas fa-times"></i>
</button>
</div>
<div class="sidebar-content">
<!-- 分类筛选 -->
<div class="menu-section">
<div class="menu-title">
<span>分类筛选</span>
<span style="font-size: 11px; color: var(--text-secondary);">{{ filteredEngines.length }} 个工具</span>
</div>
<div class="category-filter">
<button v-for="cat in categories" :key="cat.id"
class="category-btn"
:class="{ active: currentCategory === cat.id }"
@click="currentCategory = cat.id">
<i :class="cat.icon"></i>
{{ cat.name }}
</button>
</div>
</div>
<div class="menu-section">
<div class="menu-title">
<span>添加自定义 AI</span>
<button @click="showCustomForm = !showCustomForm">
{{ showCustomForm ? '收起' : '展开' }}
</button>
</div>
<div v-if="showCustomForm" class="add-custom-form">
<div v-for="field in customFormFields" :key="field.key" class="form-group">
<label>{{ field.label }}</label>
<input v-if="field.type !== 'select'"
v-model="customForm[field.key]"
:placeholder="field.placeholder"
:maxlength="field.maxlength"
:type="field.type || 'text'">
<select v-else v-model="customForm[field.key]">
<option v-for="opt in field.options" :key="opt.value" :value="opt.value">
{{ opt.label }}
</option>
</select>
</div>
<div class="form-group">
<label>选择颜色</label>
<div class="color-picker">
<div v-for="color in presetColors" :key="color"
class="color-option"
:style="{ background: color }"
:class="{ selected: customForm.color === color }"
@click="customForm.color = color">
</div>
</div>
</div>
<div class="form-actions">
<button class="btn-default" @click="resetCustomForm">重置</button>
<button class="btn-primary" @click="addCustomEngine">添加</button>
</div>
</div>
</div>
<div class="menu-section">
<div class="menu-title">
<span>{{ currentCategory === 'all' ? '全部 AI 引擎' : categories.find(c => c.id === currentCategory)?.name }}</span>
<button v-if="currentCategory !== 'all'" @click="currentCategory = 'all'" style="background: transparent; color: var(--text-secondary);">
显示全部
</button>
</div>
<div class="engine-list">
<div v-for="engine in filteredEngines" :key="engine.id"
class="engine-item"
:class="{
active: isActive(engine.id),
'in-workflow': isBatchMode && selectedEngines.includes(engine.id)
}"
@click="handleEngineClick(engine)">
<div class="engine-item-info">
<div class="engine-item-avatar" :style="{ background: engine.color }">
{{ engine.avatar }}
</div>
<div class="engine-item-details">
<h4>
{{ engine.name }}
<span v-if="engine.isHot" class="tag tag-hot">🔥</span>
<span v-if="engine.isCustom" class="tag tag-custom">自定义</span>
<span v-if="engine.category && getCategoryLabel(engine.category)" class="tag tag-category" :class="'cat-' + engine.category">
{{ getCategoryLabel(engine.category) }}
</span>
</h4>
<p>{{ engine.description }}</p>
</div>
</div>
<button class="add-btn" :class="{ added: isActive(engine.id), 'in-workflow': isBatchMode && selectedEngines.includes(engine.id) }">
<i v-if="isBatchMode" :class="selectedEngines.includes(engine.id) ? 'fas fa-check-square' : 'far fa-square'"></i>
<i v-else :class="isActive(engine.id) ? 'fas fa-check' : 'fas fa-plus'"></i>
</button>
</div>
</div>
</div>
<div v-if="customEngines.length" class="menu-section">
<div class="menu-title">
<span>自定义 AI</span>
<button class="warning" @click="clearCustomEngines">清空</button>
</div>
<div class="engine-list">
<div v-for="engine in customEngines" :key="engine.id"
class="engine-item"
:class="{ active: isActive(engine.id) }"
@click="toggleEngine(engine)">
<div class="engine-item-info">
<div class="engine-item-avatar" :style="{ background: engine.color }">
{{ engine.avatar }}
</div>
<div class="engine-item-details">
<h4>
{{ engine.name }}
<span class="tag tag-custom">自定义</span>
<span v-if="engine.category" class="tag tag-category" :class="'cat-' + engine.category">
{{ getCategoryLabel(engine.category) }}
</span>
</h4>
<p>{{ engine.description }}</p>
</div>
</div>
<div style="display: flex; gap: 4px;">
<button class="add-btn" :class="{ added: isActive(engine.id) }">
<i :class="isActive(engine.id) ? 'fas fa-check' : 'fas fa-plus'"></i>
</button>
<button class="delete-btn" @click.stop="deleteCustomEngine(engine.id)">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 快捷键帮助弹窗 -->
<div v-if="showShortcutModal" class="overlay show" @click="showShortcutModal = false" style="z-index: 2000; top: 0;">
<div class="modal-content" @click.stop>
<h3 style="margin-bottom: 16px; display: flex; align-items: center; gap: 8px;">
<i class="fas fa-keyboard" style="color: var(--primary);"></i>
快捷键指南
</h3>
<div class="shortcut-list" style="margin-bottom: 16px;">
<div v-for="sc in shortcuts" :key="sc.key" class="shortcut-item">
<span>{{ sc.name }}</span>
<span class="shortcut-key">{{ sc.key }}</span>
</div>
</div>
<div style="background: var(--bg-primary); padding: 12px; border-radius: 8px; margin-bottom: 16px; font-size: 12px; color: var(--text-secondary);">
<div style="font-weight: 600; margin-bottom: 8px; color: var(--text-primary);">💡 使用提示</div>
<div>• 点击 🔥 标签可快速识别热门工具</div>
<div>• 使用分类筛选快速找到所需类型</div>
<div>• 拖拽卡片可调整布局顺序</div>
<div>• 使用批量模式可同时添加多个工具</div>
</div>
<button class="btn-primary" style="width: 100%; padding: 10px;" @click="showShortcutModal = false">知道了</button>
</div>
</div>
</div>
<script>
const { createApp, ref, computed, onMounted, onUnmounted } = Vue;
createApp({
setup() {
// ========== 状态定义 ==========
const showAddMenu = ref(false);
const showWorkflowMenu = ref(false);
const showSettings = ref(false);
const showCustomForm = ref(false);
const showToast = ref(false);
const showShortcutModal = ref(false);
const isEditingWorkflow = ref(false);
const isBatchMode = ref(false);
const editingWorkflowEngines = ref([]);
const editingWorkflowName = ref('');
const dragIndex = ref(-1);
const currentCategory = ref('all');
const selectedEngines = ref([]);
const toastMessage = ref('');
const toastType = ref('success');
const isDarkMode = ref(false);
const currentWorkflow = ref(null);
const presetColors = ['#4cd561', '#1677ff', '#f59e0b', '#f56c6c', '#8b5cf6', '#ec4899', '#06b6d4', '#84cc16', '#4b70e2', '#ff0050'];
const customForm = ref({ name: '', url: '', avatar: '', color: presetColors[0], category: 'chat' });
// ========== 分类定义 ==========
const categories = [
{ id: 'all', name: '全部', icon: 'fas fa-th-large' },
{ id: 'chat', name: 'AI对话', icon: 'fas fa-comments' },
{ id: 'video', name: 'AI视频', icon: 'fas fa-video' },
{ id: 'code', name: 'AI编程', icon: 'fas fa-code' },
{ id: 'search', name: 'AI搜索', icon: 'fas fa-search' },
{ id: 'image', name: 'AI绘画', icon: 'fas fa-image' }
];
const presetEngines = ref([
// ========== AI对话/大模型类 ==========
{
id: 'doubao',
name: '豆包',
avatar: '豆',
color: '#ff6b6b',
description: '字节跳动 · 月活1.16亿 · 多模态AI助手',
url: 'https://www.doubao.com',
category: 'chat',
isHot: true
},
{
id: 'yuanbao',
name: '腾讯元宝',
avatar: '元',
color: '#00a1ff',
description: '腾讯 · 月活4164万 · 接入DeepSeek-R1',
url: 'https://yuanbao.tencent.com',
category: 'chat',
isHot: true
},
{
id: 'kimi',
name: 'Kimi',
avatar: 'K',
color: '#4cd561',
description: '月之暗面 · 月活2649万 · 200万字长文本',
url: 'https://kimi.moonshot.cn',
category: 'chat',
isHot: true
},
{
id: 'wenxin',
name: '文心一言',
avatar: '文',
color: '#1677ff',
description: '百度 · 月活1393万 · 文心大模型4.0',
url: 'https://yiyan.baidu.com',
category: 'chat'
},
{
id: 'qianwen',
name: '通义千问',
avatar: '问',
color: '#ff7a00',
description: '阿里巴巴 · 月活552万 · 开源大模型',
url: 'https://tongyi.aliyun.com/qianwen',
category: 'chat'
},
{
id: 'xinghuo',
name: '讯飞星火',
avatar: '星',
color: '#f59e0b',
description: '科大讯飞 · 语音交互优势',
url: 'https://xinghuo.xfyun.cn',
category: 'chat'
},
{
id: 'hunyuan',
name: '腾讯混元',
avatar: '混',
color: '#0052d9',
description: '腾讯大模型 · 企业级应用',
url: 'https://hunyuan.tencent.com',
category: 'chat'
},
// ========== AI搜索类 ==========
{
id: 'nano',
name: '纳米AI搜索',
avatar: '纳',
color: '#10b981',
description: '360 · 月活1688万 · 集成16家大模型',
url: 'https://www.n.cn',
category: 'search',
isHot: true
},
{
id: 'tiangong',
name: '天工AI',
avatar: '天',
color: '#f97316',
description: '昆仑万维 · 多模态综合搜索',
url: 'https://www.tiangong.cn',
category: 'search'
},
// ========== AI视频生成类 ==========
{
id: 'kling',
name: '可灵AI',
avatar: '灵',
color: '#ff0050',
description: '快手 · 月活1264万 · 商业化领先',
url: 'https://app.klingai.com/cn/',
category: 'video',
isHot: true
},
{
id: 'vidu',
name: 'Vidu',
avatar: 'V',
color: '#84cc16',
description: '清华系 · 生数科技 · 1080p高清',
url: 'https://www.vidu.com',
category: 'video'
},
// ========== AI绘画/图像类 ==========
// 通义万相、文心一格 已移除
{
id: 'miaohua',
name: '秒画',
avatar: '秒',
color: '#ec4899',
description: '商汤科技 · AI绘画 · 产教融合',
url: 'https://miaohua.sensetime.com',
category: 'image'
}
]);
const customEngines = ref([]);
const activeEngines = ref([]);
const workflows = ref([]);
// ========== 计算属性 ==========
const allEngines = computed(() => [...presetEngines.value, ...customEngines.value]);
const filteredEngines = computed(() => {
if (currentCategory.value === 'all') return allEngines.value;
return allEngines.value.filter(e => e.category === currentCategory.value);
});
const sidebarOpen = computed(() => showAddMenu.value || showWorkflowMenu.value || showSettings.value);
const toastIcon = computed(() => toastType.value === 'success' ? 'fas fa-check-circle' : 'fas fa-exclamation-circle');
// ========== 配置数据 ==========
const engineActions = [
{ key: 'refresh', icon: 'fas fa-sync-alt', handler: refreshEngine, title: '刷新' },
{ key: 'fullscreen', icon: 'fas fa-expand', iconKey: 'fullscreenIcon', handler: toggleFullscreen, title: '全屏' },
{ key: 'close', icon: 'fas fa-times', class: 'delete', handler: (e, idx) => removeEngine(idx), title: '关闭' }
];
const shortcuts = [
{ name: '切换主题', key: 'Ctrl + T' },
{ name: '添加引擎', key: 'Ctrl + A' },
{ name: '工作流', key: 'Ctrl + W' },
{ name: '批量模式', key: 'Ctrl + B' },
{ name: '关闭全部', key: 'Ctrl + Shift + C' },
{ name: '刷新全部', key: 'Ctrl + R' }
];
const dataBtns = [
{ key: 'export', icon: 'fas fa-download', text: '导出配置', action: exportData },
{ key: 'import', icon: 'fas fa-upload', text: '导入配置', action: importData },
{ key: 'clear', icon: 'fas fa-trash', text: '清空数据', class: 'danger', action: clearAllData },
{ key: 'reset', icon: 'fas fa-undo', text: '恢复默认', action: resetToDefault }
];
const customFormFields = [
{ key: 'name', label: 'AI 名称', placeholder: '例如:ChatGPT', maxlength: 10 },
{ key: 'url', label: '网站地址', placeholder: 'https://chat.openai.com', type: 'url' },
{ key: 'avatar', label: '缩写(1-2个字符)', placeholder: '例如:GPT', maxlength: 2 },
{ key: 'category', label: '分类', type: 'select', options: [
{ value: 'chat', label: 'AI对话' },
{ value: 'video', label: 'AI视频' },
{ value: 'code', label: 'AI编程' },
{ value: 'search', label: 'AI搜索' },
{ value: 'image', label: 'AI绘画' }
]}
];
// ========== 工具方法 ==========
function showToastMessage(message, type = 'success') {
toastMessage.value = message;
toastType.value = type;
showToast.value = true;
setTimeout(() => showToast.value = false, 2000);
}
function generateId() {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
function getCategoryLabel(category) {
const map = {
'chat': '对话',
'video': '视频',
'code': '编程',
'search': '搜索',
'image': '绘画'
};
return map[category] || category;
}
// ========== LocalStorage 数据管理 ==========
function saveToStorage() {
const data = {
customEngines: customEngines.value,
workflows: workflows.value,
activeEngines: activeEngines.value.map(e => e.id),
currentWorkflow: currentWorkflow.value,
settings: {
isDarkMode: isDarkMode.value,
theme: isDarkMode.value ? 'dark' : 'light'
},
saveTime: new Date().toISOString()
};
localStorage.setItem('ai_engines_data_v3', JSON.stringify(data));
}
function loadFromStorage() {
const data = localStorage.getItem('ai_engines_data_v3');
if (!data) return;
try {
const parsed = JSON.parse(data);
if (parsed.customEngines) {
customEngines.value = parsed.customEngines;
}
if (parsed.workflows) {
workflows.value = parsed.workflows;
}
if (parsed.settings?.isDarkMode !== undefined) {
isDarkMode.value = parsed.settings.isDarkMode;
document.documentElement.setAttribute('data-theme', isDarkMode.value ? 'dark' : 'light');
}
if (parsed.currentWorkflow) {
currentWorkflow.value = parsed.currentWorkflow;
}
if (parsed.activeEngines && Array.isArray(parsed.activeEngines)) {
parsed.activeEngines.forEach(id => {
const engine = allEngines.value.find(e => e.id === id);
if (engine) {
activeEngines.value.push({
...engine,
loading: true,
fullscreen: false,
dragging: false,
uniqueId: Date.now() + Math.random(),
get fullscreenIcon() {
return this.fullscreen ? 'fas fa-compress' : 'fas fa-expand';
}
});
}
});
}
} catch (e) {
console.error('加载本地数据失败:', e);
}
}
// ========== 批量操作 ==========
function toggleBatchMode() {
isBatchMode.value = !isBatchMode.value;
if (!isBatchMode.value) {
selectedEngines.value = [];
} else {
showToastMessage('已进入批量选择模式,点击工具添加选择', 'warning');
}
}
function handleEngineClick(engine) {
if (isBatchMode.value) {
const idx = selectedEngines.value.indexOf(engine.id);
if (idx > -1) {
selectedEngines.value.splice(idx, 1);
} else {
selectedEngines.value.push(engine.id);
}
} else {
toggleEngine(engine);
}
}
function clearSelection() {
selectedEngines.value = [];
isBatchMode.value = false;
}
function addSelectedToActive() {
selectedEngines.value.forEach(id => {
const engine = allEngines.value.find(e => e.id === id);
if (engine && !isActive(engine.id)) {
activeEngines.value.push({
...engine,
loading: true,
fullscreen: false,
dragging: false,
uniqueId: Date.now() + Math.random(),
get fullscreenIcon() {
return this.fullscreen ? 'fas fa-compress' : 'fas fa-expand';
}
});
}
});
showToastMessage(`已添加 ${selectedEngines.value.length} 个工具`);
clearSelection();
saveToStorage();
}
function addSelectedToWorkflow() {
if (selectedEngines.value.length === 0) return;
isEditingWorkflow.value = true;
editingWorkflowEngines.value = [...selectedEngines.value];
editingWorkflowName.value = '新工作流 ' + (workflows.value.length + 1);
showWorkflowMenu.value = true;
showAddMenu.value = false;
isBatchMode.value = false;
selectedEngines.value = [];
}
// ========== 引擎管理 ==========
function toggleEngine(engine) {
const idx = activeEngines.value.findIndex(e => e.id === engine.id);
if (idx > -1) {
activeEngines.value.splice(idx, 1);
showToastMessage(`已移除 ${engine.name}`);
} else {
activeEngines.value.push({
...engine,
loading: true,
fullscreen: false,
dragging: false,
uniqueId: Date.now() + Math.random(),
get fullscreenIcon() {
return this.fullscreen ? 'fas fa-compress' : 'fas fa-expand';
}
});
showToastMessage(`已添加 ${engine.name}`);
}
saveToStorage();
}
function isActive(id) {
return activeEngines.value.some(e => e.id === id);
}
function removeEngine(index) {
const engine = activeEngines.value[index];
activeEngines.value.splice(index, 1);
showToastMessage(`已关闭 ${engine.name}`);
saveToStorage();
}
function refreshEngine(engine) {
engine.loading = true;
const iframe = document.getElementById(`iframe-${engine.id}`);
if (iframe) iframe.src = iframe.src;
}
function toggleFullscreen(engine) {
engine.fullscreen = !engine.fullscreen;
}
function dragStart(e, index) {
dragIndex.value = index;
activeEngines.value[index].dragging = true;
e.dataTransfer.effectAllowed = 'move';
}
function dragEnd() {
if (dragIndex.value > -1) {
activeEngines.value[dragIndex.value].dragging = false;
dragIndex.value = -1;
}
}
function drop(e, index) {
e.preventDefault();
if (dragIndex.value > -1 && dragIndex.value !== index) {
const item = activeEngines.value[dragIndex.value];
activeEngines.value.splice(dragIndex.value, 1);
activeEngines.value.splice(index, 0, item);
saveToStorage();
}
}
function addCustomEngine() {
if (!customForm.value.name || !customForm.value.url) {
showToastMessage('请填写完整信息', 'warning');
return;
}
const newEngine = {
id: 'custom_' + Date.now(),
name: customForm.value.name,
url: customForm.value.url,
avatar: customForm.value.avatar || customForm.value.name.slice(0, 2),
color: customForm.value.color,
isCustom: true,
description: customForm.value.url,
category: customForm.value.category || 'chat'
};
customEngines.value.push(newEngine);
resetCustomForm();
showToastMessage('自定义 AI 添加成功');
saveToStorage();
}
function deleteCustomEngine(id) {
const idx = customEngines.value.findIndex(e => e.id === id);
if (idx > -1) {
customEngines.value.splice(idx, 1);
const activeIdx = activeEngines.value.findIndex(e => e.id === id);
if (activeIdx > -1) activeEngines.value.splice(activeIdx, 1);
showToastMessage('已删除自定义 AI');
saveToStorage();
}
}
function clearCustomEngines() {
if (!confirm('确定要清空所有自定义 AI 吗?')) return;
customEngines.value = [];
activeEngines.value = activeEngines.value.filter(e => !e.isCustom);
showToastMessage('已清空自定义 AI');
saveToStorage();
}
function resetCustomForm() {
customForm.value = { name: '', url: '', avatar: '', color: presetColors[0], category: 'chat' };
}
// ========== 工作流管理 ==========
function startCreateWorkflow() {
isEditingWorkflow.value = true;
editingWorkflowEngines.value = [];
editingWorkflowName.value = '新工作流 ' + (workflows.value.length + 1);
showToastMessage('请选择要添加的 AI 引擎', 'warning');
}
function cancelEditWorkflow() {
isEditingWorkflow.value = false;
editingWorkflowEngines.value = [];
editingWorkflowName.value = '';
}
function toggleEngineInWorkflow(engine) {
const idx = editingWorkflowEngines.value.indexOf(engine.id);
if (idx > -1) {
editingWorkflowEngines.value.splice(idx, 1);
} else {
editingWorkflowEngines.value.push(engine.id);
}
}
function isInEditingWorkflow(id) {
return editingWorkflowEngines.value.includes(id);
}
function saveWorkflow() {
if (!editingWorkflowEngines.value.length) {
showToastMessage('请至少选择一个 AI 引擎', 'warning');
return;
}
if (!editingWorkflowName.value.trim()) {
showToastMessage('请输入工作流名称', 'warning');
return;
}
workflows.value.push({
id: 'workflow_' + Date.now(),
name: editingWorkflowName.value.trim(),
engines: [...editingWorkflowEngines.value]
});
isEditingWorkflow.value = false;
editingWorkflowEngines.value = [];
showToastMessage('工作流创建成功');
saveToStorage();
}
function applyWorkflow(workflow) {
currentWorkflow.value = workflow;
activeEngines.value = workflow.engines.map(id => {
const engine = allEngines.value.find(e => e.id === id);
return engine ? {
...engine,
loading: true,
fullscreen: false,
dragging: false,
fromWorkflow: true,
workflowName: workflow.name,
uniqueId: Date.now() + Math.random()
} : null;
}).filter(Boolean);
showToastMessage(`已应用工作流:${workflow.name}`);
showWorkflowMenu.value = false;
saveToStorage();
}
function deleteWorkflow(id) {
if (!confirm('确定要删除这个工作流吗?')) return;
const idx = workflows.value.findIndex(w => w.id === id);
if (idx > -1) {
workflows.value.splice(idx, 1);
if (currentWorkflow.value?.id === id) currentWorkflow.value = null;
showToastMessage('工作流已删除');
saveToStorage();
}
}
function getWorkflowEnginesInfo(workflow) {
return workflow.engines.map(id =>
allEngines.value.find(e => e.id === id) || { id, avatar: '?', color: '#ccc' }
).slice(0, 5);
}
// ========== 设置与数据 ==========
function toggleTheme() {
isDarkMode.value = !isDarkMode.value;
document.documentElement.setAttribute('data-theme', isDarkMode.value ? 'dark' : 'light');
saveToStorage();
}
function toggleAddMenu() {
showAddMenu.value = !showAddMenu.value;
if (showAddMenu.value) {
showWorkflowMenu.value = false;
showSettings.value = false;
}
}
function toggleWorkflowMenu() {
showWorkflowMenu.value = !showWorkflowMenu.value;
if (showWorkflowMenu.value) {
showAddMenu.value = false;
showSettings.value = false;
}
}
function toggleSettings() {
showSettings.value = !showSettings.value;
if (showSettings.value) {
showAddMenu.value = false;
showWorkflowMenu.value = false;
}
}
function closeAllSidebars() {
showAddMenu.value = false;
showWorkflowMenu.value = false;
showSettings.value = false;
}
function exportData() {
const data = {
customEngines: customEngines.value,
workflows: workflows.value,
settings: { isDarkMode: isDarkMode.value },
exportTime: new Date().toISOString(),
version: '3.1'
};
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `ai-engines-config-${new Date().toISOString().slice(0,10)}.json`;
a.click();
URL.revokeObjectURL(url);
showToastMessage('配置已导出');
}
function importData() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = e => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = event => {
try {
const data = JSON.parse(event.target.result);
if (data.customEngines) customEngines.value = data.customEngines;
if (data.workflows) workflows.value = data.workflows;
if (data.settings?.isDarkMode !== undefined) {
isDarkMode.value = data.settings.isDarkMode;
document.documentElement.setAttribute('data-theme', isDarkMode.value ? 'dark' : 'light');
}
saveToStorage();
showToastMessage('配置已导入');
} catch {
showToastMessage('配置文件格式错误', 'warning');
}
};
reader.readAsText(file);
};
input.click();
}
function clearAllData() {
if (!confirm('确定要清空所有数据吗?此操作不可恢复!')) return;
customEngines.value = [];
workflows.value = [];
activeEngines.value = [];
localStorage.removeItem('ai_engines_data_v3');
showToastMessage('所有数据已清空');
}
function resetToDefault() {
if (!confirm('确定要恢复默认设置吗?')) return;
customEngines.value = [];
workflows.value = [];
activeEngines.value = [];
isDarkMode.value = false;
document.documentElement.setAttribute('data-theme', 'light');
localStorage.removeItem('ai_engines_data_v3');
showToastMessage('已恢复默认设置');
}
// ========== 生命周期 ==========
const keyHandlers = {
'a': toggleAddMenu,
'w': toggleWorkflowMenu,
't': toggleTheme,
'b': toggleBatchMode,
'r': () => {
activeEngines.value.forEach(refreshEngine);
showToastMessage('已刷新所有引擎');
},
',': toggleSettings
};
function handleKeydown(e) {
if (!e.ctrlKey) {
if (e.key === 'Escape') {
closeAllSidebars();
showShortcutModal.value = false;
if (isBatchMode.value) clearSelection();
}
return;
}
const key = e.key.toLowerCase();
if (keyHandlers[key]) {
e.preventDefault();
keyHandlers[key]();
}
if (e.shiftKey && key === 'c') {
e.preventDefault();
if (activeEngines.value.length) {
activeEngines.value = [];
showToastMessage('已关闭所有引擎');
saveToStorage();
}
}
}
onMounted(() => {
loadFromStorage();
document.addEventListener('keydown', handleKeydown);
console.log('🚀 一堆AI v3.1 已启动');
console.log('📊 已收录 12 个国内热门AI工具');
console.log('💬 AI对话: 豆包, 元宝, Kimi, 文心一言, 通义千问, 讯飞星火, 腾讯混元');
console.log('🔍 AI搜索: 纳米AI搜索, 天工AI');
console.log('🎬 AI视频: 可灵AI, Vidu');
console.log('🎨 AI绘画: 秒画');
console.log('⚠️ 精简版:仅保留核心工具');
});
onUnmounted(() => {
document.removeEventListener('keydown', handleKeydown);
});
return {
// 状态
showAddMenu, showWorkflowMenu, showSettings, showCustomForm, showToast,
showShortcutModal, isEditingWorkflow, isBatchMode, editingWorkflowEngines,
toastMessage, toastType, toastIcon, isDarkMode, currentWorkflow,
presetColors, customForm, currentCategory, selectedEngines,
presetEngines, customEngines, activeEngines, workflows, allEngines,
filteredEngines, sidebarOpen, categories,
// 配置
engineActions, shortcuts, dataBtns, customFormFields,
// 方法
toggleEngine, isActive, removeEngine, refreshEngine, toggleFullscreen,
dragStart, dragEnd, drop, addCustomEngine, deleteCustomEngine,
clearCustomEngines, resetCustomForm, toggleTheme, toggleAddMenu,
toggleWorkflowMenu, toggleSettings, closeAllSidebars, startCreateWorkflow,
cancelEditWorkflow, toggleEngineInWorkflow, isInEditingWorkflow,
saveWorkflow, applyWorkflow, deleteWorkflow, getWorkflowEnginesInfo,
exportData, importData, clearAllData, resetToDefault, showToastMessage,
getCategoryLabel, toggleBatchMode, handleEngineClick, clearSelection,
addSelectedToActive, addSelectedToWorkflow
};
}
}).mount('#app');
</script>
</body>
</html>七、桌面端
今年5月份,发现了一个桌面端软件开发工具,叫aardio,于是我就想能不能将网站做成一个桌面端工具,于是在一堆ai的帮助下,形成了一堆AI的桌面端,以下是源代码和软件下载路径。
import win.ui;
import web.view; // WebView2 浏览器控件
/*DSG{{*/
var winform = win.form(text="一堆AI";right=1173;bottom=752)
winform.add()
/*}}*/
// 创建WebView窗口,铺满整个窗体
var wb = web.view(winform);
// 加载线上网址(也可改为 wb.html = "..." 加载本地HTML)
wb.go("https://yiduiai.com/");
winform.show();
win.loopMessage();本项目仅供个人学习与交流使用,版权所有,未经许可禁止篡改、二次分发及商用。