feat: 全新 Logo 设计 + 扩展工具一键安装功能
Logo 设计: - 现代化爪印图标,紫蓝渐变配色 - 生成全套尺寸 PNG (16-1024px) + @2x 版本 - macOS icns + Windows ico + SVG 源文件 - 系统托盘和应用图标全面更新 扩展工具增强: - cftunnel 支持一键安装(自动下载安装脚本) - 实时显示安装进度和日志输出 - 安装完成后自动刷新状态 - 优化未安装状态的 UI 提示
BIN
src-tauri/icons/1024x1024.png
Normal file
|
After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 19 KiB |
BIN
src-tauri/icons/16x16.png
Normal file
|
After Width: | Height: | Size: 704 B |
BIN
src-tauri/icons/16x16@2x.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
src-tauri/icons/256x256.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
src-tauri/icons/256x256@2x.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 448 B After Width: | Height: | Size: 1.6 KiB |
BIN
src-tauri/icons/32x32@2x.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
src-tauri/icons/512x512.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
src-tauri/icons/64x64.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
src-tauri/icons/64x64@2x.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 715 B |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 704 B |
|
Before Width: | Height: | Size: 806 B After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 806 B After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 3.5 KiB |
BIN
src-tauri/icons/icon.iconset/icon_512x512.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
src-tauri/icons/icon.iconset/icon_512x512@2x.png
Normal file
|
After Width: | Height: | Size: 95 KiB |
48
src-tauri/icons/icon.svg
Normal file
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#6366f1;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#4f46e5;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
<filter id="shadow">
|
||||
<feGaussianBlur in="SourceAlpha" stdDeviation="8"/>
|
||||
<feOffset dx="0" dy="4" result="offsetblur"/>
|
||||
<feComponentTransfer>
|
||||
<feFuncA type="linear" slope="0.3"/>
|
||||
</feComponentTransfer>
|
||||
<feMerge>
|
||||
<feMergeNode/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- 背景圆形 -->
|
||||
<circle cx="256" cy="256" r="240" fill="url(#grad1)" filter="url(#shadow)"/>
|
||||
|
||||
<!-- 爪印主体 - 中心大圆 -->
|
||||
<ellipse cx="256" cy="280" rx="70" ry="85" fill="white" opacity="0.95"/>
|
||||
|
||||
<!-- 四个爪尖 -->
|
||||
<!-- 左上 -->
|
||||
<ellipse cx="160" cy="180" rx="35" ry="55" fill="white" opacity="0.95"
|
||||
transform="rotate(-25 160 180)"/>
|
||||
<!-- 左下 -->
|
||||
<ellipse cx="180" cy="360" rx="35" ry="55" fill="white" opacity="0.95"
|
||||
transform="rotate(15 180 360)"/>
|
||||
<!-- 右上 -->
|
||||
<ellipse cx="352" cy="180" rx="35" ry="55" fill="white" opacity="0.95"
|
||||
transform="rotate(25 352 180)"/>
|
||||
<!-- 右下 -->
|
||||
<ellipse cx="332" cy="360" rx="35" ry="55" fill="white" opacity="0.95"
|
||||
transform="rotate(-15 332 360)"/>
|
||||
|
||||
<!-- 面板网格装饰(科技感) -->
|
||||
<g opacity="0.15" stroke="white" stroke-width="2" fill="none">
|
||||
<line x1="256" y1="80" x2="256" y2="432"/>
|
||||
<line x1="80" y1="256" x2="432" y2="256"/>
|
||||
<circle cx="256" cy="256" r="120"/>
|
||||
<circle cx="256" cy="256" r="180"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -194,3 +194,77 @@ pub fn get_clawapp_status() -> Result<Value, String> {
|
||||
result.insert("url".into(), Value::String("http://localhost:3210".into()));
|
||||
Ok(Value::Object(result))
|
||||
}
|
||||
|
||||
/// 一键安装 cftunnel
|
||||
#[tauri::command]
|
||||
pub async fn install_cftunnel(app: tauri::AppHandle) -> Result<String, String> {
|
||||
use std::process::Stdio;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use tauri::Emitter;
|
||||
|
||||
let _ = app.emit("install-log", "开始安装 cftunnel...");
|
||||
let _ = app.emit("install-progress", 10);
|
||||
|
||||
// 下载并安装脚本
|
||||
let install_script = r#"
|
||||
#!/bin/bash
|
||||
set -e
|
||||
cd /tmp
|
||||
echo "下载 cftunnel..."
|
||||
curl -fsSL https://raw.githubusercontent.com/qingchencloud/cftunnel/main/install.sh -o cftunnel-install.sh
|
||||
chmod +x cftunnel-install.sh
|
||||
echo "执行安装..."
|
||||
./cftunnel-install.sh
|
||||
echo "安装完成"
|
||||
"#;
|
||||
|
||||
let _ = app.emit("install-log", "下载安装脚本...");
|
||||
let _ = app.emit("install-progress", 30);
|
||||
|
||||
let mut child = Command::new("bash")
|
||||
.arg("-c")
|
||||
.arg(install_script)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|e| format!("启动安装进程失败: {e}"))?;
|
||||
|
||||
let stderr = child.stderr.take();
|
||||
let stdout = child.stdout.take();
|
||||
|
||||
// 读取 stderr
|
||||
let app2 = app.clone();
|
||||
let handle = std::thread::spawn(move || {
|
||||
if let Some(pipe) = stderr {
|
||||
for line in BufReader::new(pipe).lines().map_while(Result::ok) {
|
||||
let _ = app2.emit("install-log", &line);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 读取 stdout
|
||||
let mut progress = 40;
|
||||
if let Some(pipe) = stdout {
|
||||
for line in BufReader::new(pipe).lines().map_while(Result::ok) {
|
||||
let _ = app.emit("install-log", &line);
|
||||
if progress < 90 {
|
||||
progress += 5;
|
||||
let _ = app.emit("install-progress", progress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = handle.join();
|
||||
let _ = app.emit("install-progress", 95);
|
||||
|
||||
let status = child.wait().map_err(|e| format!("等待安装进程失败: {e}"))?;
|
||||
let _ = app.emit("install-progress", 100);
|
||||
|
||||
if !status.success() {
|
||||
let _ = app.emit("install-log", "❌ 安装失败");
|
||||
return Err("安装失败,请查看日志".into());
|
||||
}
|
||||
|
||||
let _ = app.emit("install-log", "✅ cftunnel 安装成功");
|
||||
Ok("安装成功".into())
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ pub fn run() {
|
||||
extensions::cftunnel_action,
|
||||
extensions::get_cftunnel_logs,
|
||||
extensions::get_clawapp_status,
|
||||
extensions::install_cftunnel,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("启动 ClawPanel 失败");
|
||||
|
||||
@@ -178,4 +178,5 @@ export const api = {
|
||||
cftunnelAction: (action) => invoke('cftunnel_action', { action }),
|
||||
getCftunnelLogs: (lines = 20) => invoke('get_cftunnel_logs', { lines }),
|
||||
getClawappStatus: () => invoke('get_clawapp_status'),
|
||||
installCftunnel: () => invoke('install_cftunnel'),
|
||||
}
|
||||
|
||||
@@ -63,8 +63,12 @@ async function loadCftunnel(page) {
|
||||
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>
|
||||
<div style="color:var(--text-tertiary);margin-bottom:var(--space-md)">cftunnel 未安装</div>
|
||||
<div style="display:flex;gap:var(--space-sm);align-items:center">
|
||||
<button class="btn btn-primary btn-sm" data-action="install-cftunnel">一键安装</button>
|
||||
<a class="btn btn-secondary btn-sm" href="https://github.com/qingchencloud/cftunnel" target="_blank" rel="noopener">查看文档</a>
|
||||
</div>
|
||||
<div id="install-progress-area"></div>
|
||||
`
|
||||
return
|
||||
}
|
||||
@@ -199,6 +203,9 @@ function bindEvents(page) {
|
||||
case 'clawapp-refresh':
|
||||
await loadClawapp(page)
|
||||
break
|
||||
case 'install-cftunnel':
|
||||
await handleInstallCftunnel(page)
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -238,3 +245,58 @@ async function handleCftunnelLogs(page) {
|
||||
area.innerHTML = `<div style="color:var(--error);margin-top:var(--space-sm)">读取日志失败: ${e}</div>`
|
||||
}
|
||||
}
|
||||
|
||||
async function handleInstallCftunnel(page) {
|
||||
const area = page.querySelector('#install-progress-area')
|
||||
if (!area) return
|
||||
|
||||
// 显示进度条
|
||||
area.innerHTML = `
|
||||
<div style="margin-top:var(--space-lg)">
|
||||
<div class="upgrade-progress-wrap">
|
||||
<div class="upgrade-progress-bar">
|
||||
<div class="upgrade-progress-fill" id="install-progress-fill" style="width:0%"></div>
|
||||
</div>
|
||||
<div class="upgrade-progress-text" id="install-progress-text">准备安装...</div>
|
||||
</div>
|
||||
<div class="upgrade-log-box" id="install-log-box"></div>
|
||||
</div>
|
||||
`
|
||||
|
||||
const progressFill = area.querySelector('#install-progress-fill')
|
||||
const progressText = area.querySelector('#install-progress-text')
|
||||
const logBox = area.querySelector('#install-log-box')
|
||||
|
||||
let unlistenLog, unlistenProgress
|
||||
try {
|
||||
const { listen } = await import('@tauri-apps/api/event')
|
||||
|
||||
unlistenLog = await listen('install-log', (e) => {
|
||||
logBox.textContent += e.payload + '\n'
|
||||
logBox.scrollTop = logBox.scrollHeight
|
||||
})
|
||||
|
||||
unlistenProgress = await listen('install-progress', (e) => {
|
||||
const progress = e.payload
|
||||
progressFill.style.width = progress + '%'
|
||||
progressText.textContent = `安装中... ${progress}%`
|
||||
})
|
||||
|
||||
await api.installCftunnel()
|
||||
|
||||
progressFill.classList.add('done')
|
||||
progressText.textContent = '✅ 安装完成'
|
||||
toast('cftunnel 安装成功', 'success')
|
||||
|
||||
// 3 秒后刷新状态
|
||||
setTimeout(() => loadCftunnel(page), 3000)
|
||||
} catch (e) {
|
||||
progressFill.classList.add('error')
|
||||
progressText.textContent = '❌ 安装失败'
|
||||
logBox.textContent += '\n错误: ' + e
|
||||
toast('安装失败: ' + e, 'error')
|
||||
} finally {
|
||||
unlistenLog?.()
|
||||
unlistenProgress?.()
|
||||
}
|
||||
}
|
||||
|
||||