mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-30 21:00:30 +08:00
feat: 新增「关于」和「扩展工具」页面
- 关于页面:版本信息、相关项目链接、快捷链接、开源协议 - 扩展页面:cftunnel 隧道状态/路由/启停/日志 + ClawApp 状态/快捷访问 - Rust 后端:新增 extensions.rs(4 个命令:状态/操作/日志/ClawApp 检测) - 侧边栏新增「扩展」和「关于」导航项,总计 8 个页面
This commit is contained in:
@@ -25,6 +25,18 @@ const NAV_ITEMS = [
|
||||
items: [
|
||||
{ route: '/memory', label: '记忆文件', icon: 'memory' },
|
||||
]
|
||||
},
|
||||
{
|
||||
section: '扩展',
|
||||
items: [
|
||||
{ route: '/extensions', label: '扩展工具', icon: 'extensions' },
|
||||
]
|
||||
},
|
||||
{
|
||||
section: '',
|
||||
items: [
|
||||
{ route: '/about', label: '关于', icon: 'about' },
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -35,6 +47,8 @@ const ICONS = {
|
||||
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>',
|
||||
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>',
|
||||
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>',
|
||||
extensions: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>',
|
||||
about: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>',
|
||||
}
|
||||
|
||||
let _delegated = false
|
||||
|
||||
@@ -107,6 +107,18 @@ function mockInvoke(cmd, args) {
|
||||
create_backup: () => ({ name: 'openclaw-20260226-160000.json', size: 8542 }),
|
||||
restore_backup: () => true,
|
||||
delete_backup: () => true,
|
||||
get_cftunnel_status: () => ({
|
||||
installed: true, version: 'cftunnel 0.7.0', running: true,
|
||||
tunnel_name: 'mac-home', pid: 73325,
|
||||
routes: [
|
||||
{ name: 'clawapp', domain: 'chat.qrj.ai', service: 'http://localhost:3210' },
|
||||
{ name: 'newapi', domain: 'newapi.qrj.ai', service: 'http://localhost:30080' },
|
||||
{ name: 'webhook', domain: 'webhook.qrj.ai', service: 'http://localhost:9801' },
|
||||
],
|
||||
}),
|
||||
cftunnel_action: () => true,
|
||||
get_cftunnel_logs: () => '2026-02-26 13:29:01 [INFO] Tunnel started\n2026-02-26 13:30:00 [INFO] Connection healthy',
|
||||
get_clawapp_status: () => ({ running: true, pid: 7752, port: 3210, url: 'http://localhost:3210' }),
|
||||
}
|
||||
const fn = mocks[cmd]
|
||||
return fn ? Promise.resolve(fn(args)) : Promise.reject(`未知命令: ${cmd}`)
|
||||
@@ -148,4 +160,10 @@ export const api = {
|
||||
createBackup: () => invoke('create_backup'),
|
||||
restoreBackup: (name) => invoke('restore_backup', { name }),
|
||||
deleteBackup: (name) => invoke('delete_backup', { name }),
|
||||
|
||||
// 扩展工具
|
||||
getCftunnelStatus: () => invoke('get_cftunnel_status'),
|
||||
cftunnelAction: (action) => invoke('cftunnel_action', { action }),
|
||||
getCftunnelLogs: (lines = 20) => invoke('get_cftunnel_logs', { lines }),
|
||||
getClawappStatus: () => invoke('get_clawapp_status'),
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ registerRoute('/logs', () => import('./pages/logs.js'))
|
||||
registerRoute('/models', () => import('./pages/models.js'))
|
||||
registerRoute('/gateway', () => import('./pages/gateway.js'))
|
||||
registerRoute('/memory', () => import('./pages/memory.js'))
|
||||
registerRoute('/extensions', () => import('./pages/extensions.js'))
|
||||
registerRoute('/about', () => import('./pages/about.js'))
|
||||
|
||||
// 初始化主题
|
||||
initTheme()
|
||||
|
||||
122
src/pages/about.js
Normal file
122
src/pages/about.js
Normal file
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* 关于页面
|
||||
* 版本信息、项目链接、相关项目、系统环境
|
||||
*/
|
||||
import { api } from '../lib/tauri-api.js'
|
||||
|
||||
export async function render() {
|
||||
const page = document.createElement('div')
|
||||
page.className = 'page'
|
||||
|
||||
page.innerHTML = `
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">关于</h1>
|
||||
<p class="page-desc">ClawPanel — OpenClaw 可视化管理面板</p>
|
||||
</div>
|
||||
<div class="stat-cards" id="version-cards">
|
||||
<div class="stat-card loading-placeholder"></div>
|
||||
<div class="stat-card loading-placeholder"></div>
|
||||
<div class="stat-card loading-placeholder"></div>
|
||||
</div>
|
||||
<div class="config-section">
|
||||
<div class="config-section-title">相关项目</div>
|
||||
<div id="projects-list"></div>
|
||||
</div>
|
||||
<div class="config-section">
|
||||
<div class="config-section-title">快捷链接</div>
|
||||
<div id="links-list"></div>
|
||||
</div>
|
||||
<div class="config-section" style="color:var(--text-tertiary);font-size:var(--font-size-xs)">
|
||||
<p>ClawPanel 基于 Tauri v2 构建,前端 Vanilla JS + Vite,后端 Rust。</p>
|
||||
<p style="margin-top:8px">MIT License © 2026 qingchencloud</p>
|
||||
</div>
|
||||
`
|
||||
|
||||
loadData(page)
|
||||
renderProjects(page)
|
||||
renderLinks(page)
|
||||
return page
|
||||
}
|
||||
|
||||
async function loadData(page) {
|
||||
const cards = page.querySelector('#version-cards')
|
||||
try {
|
||||
const [version, install] = await Promise.all([
|
||||
api.getVersionInfo(),
|
||||
api.checkInstallation(),
|
||||
])
|
||||
cards.innerHTML = `
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-header"><span class="stat-card-label">ClawPanel</span></div>
|
||||
<div class="stat-card-value">0.1.0</div>
|
||||
<div class="stat-card-meta">Tauri v2 桌面应用</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-header"><span class="stat-card-label">OpenClaw</span></div>
|
||||
<div class="stat-card-value">${version.current || '未知'}</div>
|
||||
<div class="stat-card-meta">${version.update_available ? '有新版本可用' : '已是最新'}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-header"><span class="stat-card-label">安装路径</span></div>
|
||||
<div class="stat-card-value" style="font-size:var(--font-size-sm);word-break:break-all">${install.path || '未知'}</div>
|
||||
<div class="stat-card-meta">${install.installed ? '已安装' : '未安装'}</div>
|
||||
</div>
|
||||
`
|
||||
} catch {
|
||||
cards.innerHTML = '<div class="stat-card"><div class="stat-card-label">加载失败</div></div>'
|
||||
}
|
||||
}
|
||||
|
||||
const PROJECTS = [
|
||||
{
|
||||
name: 'OpenClaw',
|
||||
desc: 'AI Agent 框架,支持多模型协作、工具调用、记忆管理',
|
||||
url: 'https://github.com/openclaw-labs/openclaw',
|
||||
},
|
||||
{
|
||||
name: 'ClawApp',
|
||||
desc: '跨平台移动聊天客户端,H5 + 代理服务器架构,支持离线和流式传输',
|
||||
url: 'https://github.com/qingchencloud/clawapp',
|
||||
},
|
||||
{
|
||||
name: 'cftunnel',
|
||||
desc: '全协议内网穿透工具,Cloud 模式免费 HTTP/WS + Relay 模式自建中继',
|
||||
url: 'https://github.com/qingchencloud/cftunnel',
|
||||
},
|
||||
{
|
||||
name: 'ClawPanel',
|
||||
desc: 'OpenClaw 可视化管理面板,Tauri v2 桌面应用',
|
||||
url: 'https://github.com/qingchencloud/clawpanel',
|
||||
},
|
||||
]
|
||||
|
||||
function renderProjects(page) {
|
||||
const el = page.querySelector('#projects-list')
|
||||
el.innerHTML = PROJECTS.map(p => `
|
||||
<div class="service-card">
|
||||
<div class="service-info">
|
||||
<div>
|
||||
<div class="service-name">${p.name}</div>
|
||||
<div class="service-desc">${p.desc}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="service-actions">
|
||||
<a class="btn btn-secondary btn-sm" href="${p.url}" target="_blank" rel="noopener">GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')
|
||||
}
|
||||
|
||||
const LINKS = [
|
||||
{ label: 'cftunnel 官网', url: 'https://cftunnel.qt.cool' },
|
||||
{ label: 'cftunnel 桌面客户端', url: 'https://github.com/qingchencloud/cftunnel-app/releases' },
|
||||
{ label: 'OpenClaw 中文翻译', url: 'https://github.com/1186258278/OpenClawChineseTranslation' },
|
||||
{ label: 'ClawApp 文档', url: 'https://github.com/qingchencloud/clawapp#readme' },
|
||||
]
|
||||
|
||||
function renderLinks(page) {
|
||||
const el = page.querySelector('#links-list')
|
||||
el.innerHTML = `<div style="display:flex;flex-wrap:wrap;gap:var(--space-sm)">
|
||||
${LINKS.map(l => `<a class="btn btn-secondary btn-sm" href="${l.url}" target="_blank" rel="noopener">${l.label}</a>`).join('')}
|
||||
</div>`
|
||||
}
|
||||
215
src/pages/extensions.js
Normal file
215
src/pages/extensions.js
Normal file
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* 扩展工具页面
|
||||
* cftunnel 隧道管理 + ClawApp 状态
|
||||
*/
|
||||
import { api } from '../lib/tauri-api.js'
|
||||
import { toast } from '../components/toast.js'
|
||||
|
||||
let _delegated = false
|
||||
|
||||
export async function render() {
|
||||
const page = document.createElement('div')
|
||||
page.className = 'page'
|
||||
|
||||
page.innerHTML = `
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">扩展工具</h1>
|
||||
<p class="page-desc">管理 cftunnel 内网穿透和 ClawApp 移动客户端</p>
|
||||
</div>
|
||||
<div id="cftunnel-card" class="config-section">
|
||||
<div class="config-section-title">cftunnel 内网穿透</div>
|
||||
<div id="cftunnel-content">加载中...</div>
|
||||
</div>
|
||||
<div id="clawapp-card" class="config-section">
|
||||
<div class="config-section-title">ClawApp 移动客户端</div>
|
||||
<div id="clawapp-content">加载中...</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
bindEvents(page)
|
||||
loadAll(page)
|
||||
return page
|
||||
}
|
||||
|
||||
async function loadAll(page) {
|
||||
await Promise.all([
|
||||
loadCftunnel(page),
|
||||
loadClawapp(page),
|
||||
])
|
||||
}
|
||||
|
||||
// ===== cftunnel =====
|
||||
|
||||
async function loadCftunnel(page) {
|
||||
const el = page.querySelector('#cftunnel-content')
|
||||
try {
|
||||
const status = await api.getCftunnelStatus()
|
||||
renderCftunnel(el, status)
|
||||
} catch (e) {
|
||||
el.innerHTML = `<div style="color:var(--error)">加载失败: ${e}</div>`
|
||||
}
|
||||
}
|
||||
|
||||
function renderCftunnel(el, s) {
|
||||
if (!s.installed) {
|
||||
el.innerHTML = `
|
||||
<div style="color:var(--text-tertiary)">cftunnel 未安装</div>
|
||||
<a class="btn btn-primary btn-sm" href="https://github.com/qingchencloud/cftunnel" target="_blank" rel="noopener" style="margin-top:var(--space-md)">前往安装</a>
|
||||
`
|
||||
return
|
||||
}
|
||||
|
||||
const running = s.running
|
||||
const routes = s.routes || []
|
||||
|
||||
el.innerHTML = `
|
||||
<div class="stat-cards" style="margin-bottom:var(--space-md)">
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-header">
|
||||
<span class="stat-card-label">状态</span>
|
||||
<span class="status-dot ${running ? 'running' : 'stopped'}"></span>
|
||||
</div>
|
||||
<div class="stat-card-value">${running ? '运行中' : '已停止'}</div>
|
||||
<div class="stat-card-meta">${s.tunnel_name || ''}${s.pid ? ' (PID: ' + s.pid + ')' : ''}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-header"><span class="stat-card-label">版本</span></div>
|
||||
<div class="stat-card-value" style="font-size:var(--font-size-md)">${s.version || '未知'}</div>
|
||||
<div class="stat-card-meta">${routes.length} 条路由</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:var(--space-sm);margin-bottom:var(--space-md)">
|
||||
${running
|
||||
? '<button class="btn btn-danger btn-sm" data-action="cftunnel-down">停止隧道</button>'
|
||||
: '<button class="btn btn-primary btn-sm" data-action="cftunnel-up">启动隧道</button>'
|
||||
}
|
||||
<button class="btn btn-secondary btn-sm" data-action="cftunnel-logs">查看日志</button>
|
||||
<button class="btn btn-secondary btn-sm" data-action="cftunnel-refresh">刷新</button>
|
||||
</div>
|
||||
${renderRoutes(routes)}
|
||||
<div id="cftunnel-logs-area"></div>
|
||||
`
|
||||
}
|
||||
|
||||
function renderRoutes(routes) {
|
||||
if (!routes.length) return '<div style="color:var(--text-tertiary)">暂无路由</div>'
|
||||
return `
|
||||
<table class="data-table" style="margin-bottom:0">
|
||||
<thead><tr><th>名称</th><th>域名</th><th>本地服务</th></tr></thead>
|
||||
<tbody>
|
||||
${routes.map(r => `
|
||||
<tr>
|
||||
<td>${r.name}</td>
|
||||
<td><a href="https://${r.domain}" target="_blank" rel="noopener">${r.domain}</a></td>
|
||||
<td><code>${r.service}</code></td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
`
|
||||
}
|
||||
|
||||
// ===== ClawApp =====
|
||||
|
||||
async function loadClawapp(page) {
|
||||
const el = page.querySelector('#clawapp-content')
|
||||
try {
|
||||
const status = await api.getClawappStatus()
|
||||
renderClawapp(el, status)
|
||||
} catch (e) {
|
||||
el.innerHTML = `<div style="color:var(--error)">加载失败: ${e}</div>`
|
||||
}
|
||||
}
|
||||
|
||||
function renderClawapp(el, s) {
|
||||
const running = s.running
|
||||
el.innerHTML = `
|
||||
<div class="stat-cards" style="margin-bottom:var(--space-md)">
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-header">
|
||||
<span class="stat-card-label">状态</span>
|
||||
<span class="status-dot ${running ? 'running' : 'stopped'}"></span>
|
||||
</div>
|
||||
<div class="stat-card-value">${running ? '运行中' : '已停止'}</div>
|
||||
<div class="stat-card-meta">${s.pid ? 'PID: ' + s.pid : ''}${s.port ? ' 端口: ' + s.port : ''}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-header"><span class="stat-card-label">访问地址</span></div>
|
||||
<div class="stat-card-value" style="font-size:var(--font-size-sm)">${s.url || 'http://localhost:3210'}</div>
|
||||
<div class="stat-card-meta">外网: chat.qrj.ai</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:var(--space-sm)">
|
||||
<a class="btn btn-primary btn-sm" href="${s.url || 'http://localhost:3210'}" target="_blank" rel="noopener">打开 ClawApp</a>
|
||||
<a class="btn btn-secondary btn-sm" href="https://chat.qrj.ai" target="_blank" rel="noopener">打开外网地址</a>
|
||||
<button class="btn btn-secondary btn-sm" data-action="clawapp-refresh">刷新</button>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
// ===== 事件绑定 =====
|
||||
|
||||
function bindEvents(page) {
|
||||
if (_delegated) return
|
||||
_delegated = true
|
||||
|
||||
page.addEventListener('click', async (e) => {
|
||||
const btn = e.target.closest('[data-action]')
|
||||
if (!btn) return
|
||||
const action = btn.dataset.action
|
||||
|
||||
switch (action) {
|
||||
case 'cftunnel-up':
|
||||
await handleCftunnelAction(page, 'up')
|
||||
break
|
||||
case 'cftunnel-down':
|
||||
await handleCftunnelAction(page, 'down')
|
||||
break
|
||||
case 'cftunnel-logs':
|
||||
await handleCftunnelLogs(page)
|
||||
break
|
||||
case 'cftunnel-refresh':
|
||||
await loadCftunnel(page)
|
||||
break
|
||||
case 'clawapp-refresh':
|
||||
await loadClawapp(page)
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function handleCftunnelAction(page, action) {
|
||||
const label = action === 'up' ? '启动' : '停止'
|
||||
try {
|
||||
toast(`正在${label}隧道...`, 'info')
|
||||
await api.cftunnelAction(action)
|
||||
toast(`隧道已${label}`, 'success')
|
||||
await loadCftunnel(page)
|
||||
} catch (e) {
|
||||
toast(`${label}失败: ${e}`, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCftunnelLogs(page) {
|
||||
const area = page.querySelector('#cftunnel-logs-area')
|
||||
if (!area) return
|
||||
// 切换显示
|
||||
if (area.innerHTML) {
|
||||
area.innerHTML = ''
|
||||
return
|
||||
}
|
||||
try {
|
||||
const logs = await api.getCftunnelLogs(30)
|
||||
area.innerHTML = `
|
||||
<div style="margin-top:var(--space-md)">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--space-sm)">
|
||||
<span style="font-weight:600;font-size:var(--font-size-sm)">最近日志</span>
|
||||
<button class="btn btn-secondary btn-sm" data-action="cftunnel-logs">收起</button>
|
||||
</div>
|
||||
<pre class="log-viewer">${logs || '暂无日志'}</pre>
|
||||
</div>
|
||||
`
|
||||
} catch (e) {
|
||||
area.innerHTML = `<div style="color:var(--error);margin-top:var(--space-sm)">读取日志失败: ${e}</div>`
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user