feat: ClawPanel v0.1.0 项目骨架

- Tauri v2 + Vanilla JS + Vite 技术栈
- 9 个页面: 仪表盘/服务管理/日志/模型配置/Agent配置/Gateway/MCP工具/记忆文件/部署
- Rust 后端: 配置读写/服务管理(launchd)/日志读取/记忆文件管理
- 暗色主题 + 玻璃拟态 UI
- Mock 数据支持纯浏览器开发调试
This commit is contained in:
晴天
2026-02-26 22:34:55 +08:00
commit e26c4d9307
54 changed files with 13839 additions and 0 deletions

77
src/components/sidebar.js Normal file
View File

@@ -0,0 +1,77 @@
/**
* 侧边导航栏
*/
import { navigate, getCurrentRoute } from '../router.js'
const NAV_ITEMS = [
{
section: '概览',
items: [
{ route: '/dashboard', label: '仪表盘', icon: 'dashboard' },
{ route: '/services', label: '服务管理', icon: 'services' },
{ route: '/logs', label: '日志查看', icon: 'logs' },
]
},
{
section: '配置',
items: [
{ route: '/models', label: '模型配置', icon: 'models' },
{ route: '/agents', label: 'Agent 配置', icon: 'agents' },
{ route: '/gateway', label: 'Gateway', icon: 'gateway' },
{ route: '/mcp', label: 'MCP 工具', icon: 'mcp' },
]
},
{
section: '数据',
items: [
{ route: '/memory', label: '记忆文件', icon: 'memory' },
{ route: '/deploy', label: 'ClawApp 部署', icon: 'deploy' },
]
}
]
const ICONS = {
dashboard: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>',
services: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M12 1v4M12 19v4M4.22 4.22l2.83 2.83M16.95 16.95l2.83 2.83M1 12h4M19 12h4M4.22 19.78l2.83-2.83M16.95 7.05l2.83-2.83"/></svg>',
logs: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><path d="M14 2v6h6"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>',
models: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 003 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0021 16z"/><path d="M3.27 6.96L12 12.01l8.73-5.05M12 22.08V12"/></svg>',
agents: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75"/></svg>',
gateway: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="8" rx="2"/><rect x="2" y="14" width="20" height="8" rx="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg>',
mcp: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/><line x1="4" y1="22" x2="4" y2="15"/></svg>',
memory: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 3h6a4 4 0 014 4v14a3 3 0 00-3-3H2z"/><path d="M22 3h-6a4 4 0 00-4 4v14a3 3 0 013-3h7z"/></svg>',
deploy: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 11-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>',
}
export function renderSidebar(el) {
const current = getCurrentRoute()
let html = `
<div class="sidebar-header">
<div class="sidebar-logo">CP</div>
<span class="sidebar-title">ClawPanel</span>
</div>
<nav class="sidebar-nav">
`
for (const section of NAV_ITEMS) {
html += `<div class="nav-section">
<div class="nav-section-title">${section.section}</div>`
for (const item of section.items) {
const active = current === item.route ? ' active' : ''
html += `<div class="nav-item${active}" data-route="${item.route}">
${ICONS[item.icon] || ''}
<span>${item.label}</span>
</div>`
}
html += '</div>'
}
html += '</nav>'
el.innerHTML = html
// 绑定点击事件
el.querySelectorAll('.nav-item').forEach(item => {
item.onclick = () => navigate(item.dataset.route)
})
}

28
src/components/toast.js Normal file
View File

@@ -0,0 +1,28 @@
/**
* Toast 通知组件
*/
let _container = null
function ensureContainer() {
if (!_container) {
_container = document.createElement('div')
_container.className = 'toast-container'
document.body.appendChild(_container)
}
return _container
}
export function toast(message, type = 'info', duration = 3000) {
const container = ensureContainer()
const el = document.createElement('div')
el.className = `toast ${type}`
el.textContent = message
container.appendChild(el)
setTimeout(() => {
el.style.opacity = '0'
el.style.transform = 'translateX(20px)'
el.style.transition = 'all 250ms ease'
setTimeout(() => el.remove(), 250)
}, duration)
}