mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-06 20:02:49 +08:00
feat: 升级进度弹窗 + 安装源自动检测与切换
- 升级过程改为流式日志推送(Tauri Event),前端展示进度条和实时日志 - 后端自动检测当前安装的是官方版(openclaw)还是汉化版(openclaw-zh) - 服务管理页支持一键切换安装源,切换时先卸载旧包避免 bin 冲突 - 版本号比较改为逐段数值比较,支持 -zh.X 后缀的小版本检测 - 仪表盘、关于页同步显示当前安装源标识
This commit is contained in:
@@ -70,54 +70,130 @@ fn get_local_version() -> Option<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 从 npm registry 获取最新版本号,超时 5 秒
|
/// 从 npm registry 获取最新版本号,超时 5 秒
|
||||||
async fn get_latest_version() -> Option<String> {
|
async fn get_latest_version_for(source: &str) -> Option<String> {
|
||||||
let client = reqwest::Client::builder()
|
let client = reqwest::Client::builder()
|
||||||
.timeout(std::time::Duration::from_secs(5))
|
.timeout(std::time::Duration::from_secs(5))
|
||||||
.build()
|
.build()
|
||||||
.ok()?;
|
.ok()?;
|
||||||
let resp = client
|
let pkg = npm_package_name(source).replace('/', "%2F").replace('@', "%40");
|
||||||
.get("https://registry.npmjs.org/@qingchencloud%2Fopenclaw-zh/latest")
|
let url = format!("https://registry.npmjs.org/{pkg}/latest");
|
||||||
.send()
|
let resp = client.get(&url).send().await.ok()?;
|
||||||
.await
|
|
||||||
.ok()?;
|
|
||||||
let json: Value = resp.json().await.ok()?;
|
let json: Value = resp.json().await.ok()?;
|
||||||
json.get("version")
|
json.get("version")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.map(String::from)
|
.map(String::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 检测当前安装的是官方版还是汉化版
|
||||||
|
fn detect_installed_source() -> String {
|
||||||
|
let output = Command::new("npm")
|
||||||
|
.args(["list", "-g", "@qingchencloud/openclaw-zh", "--depth=0"])
|
||||||
|
.output();
|
||||||
|
if let Ok(o) = output {
|
||||||
|
let text = String::from_utf8_lossy(&o.stdout);
|
||||||
|
if text.contains("openclaw-zh@") {
|
||||||
|
return "chinese".into();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"official".into()
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_version_info() -> Result<VersionInfo, String> {
|
pub async fn get_version_info() -> Result<VersionInfo, String> {
|
||||||
let current = get_local_version();
|
let current = get_local_version();
|
||||||
let latest = get_latest_version().await;
|
let source = detect_installed_source();
|
||||||
|
let latest = get_latest_version_for(&source).await;
|
||||||
|
let parse_ver = |v: &str| -> Vec<u32> {
|
||||||
|
v.split(|c: char| !c.is_ascii_digit())
|
||||||
|
.filter_map(|s| s.parse().ok())
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
let update_available = match (¤t, &latest) {
|
let update_available = match (¤t, &latest) {
|
||||||
(Some(c), Some(l)) => l != c,
|
(Some(c), Some(l)) => parse_ver(l) > parse_ver(c),
|
||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
Ok(VersionInfo {
|
Ok(VersionInfo {
|
||||||
current,
|
current,
|
||||||
latest,
|
latest,
|
||||||
update_available,
|
update_available,
|
||||||
|
source,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 执行 npm 全局升级 openclaw
|
/// npm 包名映射
|
||||||
|
fn npm_package_name(source: &str) -> &'static str {
|
||||||
|
match source {
|
||||||
|
"official" => "openclaw",
|
||||||
|
_ => "@qingchencloud/openclaw-zh",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 执行 npm 全局升级 openclaw(流式推送日志)
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn upgrade_openclaw() -> Result<String, String> {
|
pub async fn upgrade_openclaw(app: tauri::AppHandle, source: String) -> Result<String, String> {
|
||||||
let output = Command::new("npm")
|
use std::process::Stdio;
|
||||||
.args(["install", "-g", "@qingchencloud/openclaw-zh@latest"])
|
use std::io::{BufRead, BufReader};
|
||||||
.output()
|
use tauri::Emitter;
|
||||||
.map_err(|e| format!("执行升级命令失败: {e}"))?;
|
|
||||||
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
let current_source = detect_installed_source();
|
||||||
|
let pkg = format!("{}@latest", npm_package_name(&source));
|
||||||
|
|
||||||
if !output.status.success() {
|
// 切换源时先卸载旧包,避免 bin 冲突
|
||||||
return Err(format!("升级失败: {stderr}"));
|
if current_source != source {
|
||||||
|
let old_pkg = npm_package_name(¤t_source);
|
||||||
|
let _ = app.emit("upgrade-log", format!("正在卸载旧版本 ({old_pkg})..."));
|
||||||
|
let _ = app.emit("upgrade-progress", 5);
|
||||||
|
let _ = Command::new("npm")
|
||||||
|
.args(["uninstall", "-g", old_pkg])
|
||||||
|
.output();
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = app.emit("upgrade-log", format!("$ npm install -g {pkg}"));
|
||||||
|
let _ = app.emit("upgrade-progress", 10);
|
||||||
|
|
||||||
|
let mut child = Command::new("npm")
|
||||||
|
.args(["install", "-g", &pkg])
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.spawn()
|
||||||
|
.map_err(|e| format!("执行升级命令失败: {e}"))?;
|
||||||
|
|
||||||
|
// 读取 stderr(npm 主要输出在 stderr)
|
||||||
|
let stderr = child.stderr.take();
|
||||||
|
let stdout = child.stdout.take();
|
||||||
|
|
||||||
|
let _ = app.emit("upgrade-progress", 30);
|
||||||
|
|
||||||
|
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("upgrade-log", &line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(pipe) = stdout {
|
||||||
|
for line in BufReader::new(pipe).lines().map_while(Result::ok) {
|
||||||
|
let _ = app.emit("upgrade-log", &line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = handle.join();
|
||||||
|
let _ = app.emit("upgrade-progress", 80);
|
||||||
|
|
||||||
|
let status = child.wait().map_err(|e| format!("等待进程失败: {e}"))?;
|
||||||
|
let _ = app.emit("upgrade-progress", 100);
|
||||||
|
|
||||||
|
if !status.success() {
|
||||||
|
let _ = app.emit("upgrade-log", "❌ 升级失败");
|
||||||
|
return Err("升级失败,请查看日志".into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取升级后的版本号
|
|
||||||
let new_ver = get_local_version().unwrap_or_else(|| "未知".into());
|
let new_ver = get_local_version().unwrap_or_else(|| "未知".into());
|
||||||
Ok(format!("升级成功,当前版本: {new_ver}"))
|
let msg = format!("✅ 升级成功,当前版本: {new_ver}");
|
||||||
|
let _ = app.emit("upgrade-log", &msg);
|
||||||
|
Ok(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|||||||
@@ -13,4 +13,5 @@ pub struct VersionInfo {
|
|||||||
pub current: Option<String>,
|
pub current: Option<String>,
|
||||||
pub latest: Option<String>,
|
pub latest: Option<String>,
|
||||||
pub update_available: bool,
|
pub update_available: bool,
|
||||||
|
pub source: String,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,3 +134,63 @@ export function showModal({ title, fields, onConfirm }) {
|
|||||||
const firstInput = overlay.querySelector('input, select')
|
const firstInput = overlay.querySelector('input, select')
|
||||||
if (firstInput) firstInput.focus()
|
if (firstInput) firstInput.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 升级进度弹窗 — 带进度条和实时日志
|
||||||
|
* @returns {{ appendLog, setProgress, setDone, setError, destroy }}
|
||||||
|
*/
|
||||||
|
export function showUpgradeModal() {
|
||||||
|
const overlay = document.createElement('div')
|
||||||
|
overlay.className = 'modal-overlay'
|
||||||
|
overlay.innerHTML = `
|
||||||
|
<div class="modal" style="max-width:520px">
|
||||||
|
<div class="modal-title">升级 OpenClaw</div>
|
||||||
|
<div class="upgrade-progress-wrap">
|
||||||
|
<div class="upgrade-progress-bar"><div class="upgrade-progress-fill" style="width:0%"></div></div>
|
||||||
|
<div class="upgrade-progress-text">准备中...</div>
|
||||||
|
</div>
|
||||||
|
<div class="upgrade-log-box"></div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn btn-secondary btn-sm" data-action="close" disabled>关闭</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
document.body.appendChild(overlay)
|
||||||
|
|
||||||
|
const fill = overlay.querySelector('.upgrade-progress-fill')
|
||||||
|
const text = overlay.querySelector('.upgrade-progress-text')
|
||||||
|
const logBox = overlay.querySelector('.upgrade-log-box')
|
||||||
|
const closeBtn = overlay.querySelector('[data-action="close"]')
|
||||||
|
|
||||||
|
closeBtn.onclick = () => overlay.remove()
|
||||||
|
overlay.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Escape' && !closeBtn.disabled) overlay.remove()
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
appendLog(line) {
|
||||||
|
const div = document.createElement('div')
|
||||||
|
div.textContent = line
|
||||||
|
logBox.appendChild(div)
|
||||||
|
logBox.scrollTop = logBox.scrollHeight
|
||||||
|
},
|
||||||
|
setProgress(pct) {
|
||||||
|
fill.style.width = pct + '%'
|
||||||
|
text.textContent = pct >= 100 ? '完成' : `升级中... ${pct}%`
|
||||||
|
},
|
||||||
|
setDone(msg) {
|
||||||
|
text.textContent = msg || '升级完成'
|
||||||
|
fill.style.width = '100%'
|
||||||
|
fill.classList.add('done')
|
||||||
|
closeBtn.disabled = false
|
||||||
|
closeBtn.focus()
|
||||||
|
},
|
||||||
|
setError(msg) {
|
||||||
|
text.textContent = msg || '升级失败'
|
||||||
|
fill.classList.add('error')
|
||||||
|
closeBtn.disabled = false
|
||||||
|
closeBtn.focus()
|
||||||
|
},
|
||||||
|
destroy() { overlay.remove() },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ export const api = {
|
|||||||
readMcpConfig: () => invoke('read_mcp_config'),
|
readMcpConfig: () => invoke('read_mcp_config'),
|
||||||
writeMcpConfig: (config) => invoke('write_mcp_config', { config }),
|
writeMcpConfig: (config) => invoke('write_mcp_config', { config }),
|
||||||
reloadGateway: () => invoke('reload_gateway'),
|
reloadGateway: () => invoke('reload_gateway'),
|
||||||
upgradeOpenclaw: () => invoke('upgrade_openclaw'),
|
upgradeOpenclaw: (source = 'chinese') => invoke('upgrade_openclaw', { source }),
|
||||||
installGateway: () => invoke('install_gateway'),
|
installGateway: () => invoke('install_gateway'),
|
||||||
uninstallGateway: () => invoke('uninstall_gateway'),
|
uninstallGateway: () => invoke('uninstall_gateway'),
|
||||||
testModel: (baseUrl, apiKey, modelId) => invoke('test_model', { baseUrl, apiKey, modelId }),
|
testModel: (baseUrl, apiKey, modelId) => invoke('test_model', { baseUrl, apiKey, modelId }),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
import { api } from '../lib/tauri-api.js'
|
import { api } from '../lib/tauri-api.js'
|
||||||
import { toast } from '../components/toast.js'
|
import { toast } from '../components/toast.js'
|
||||||
|
import { showUpgradeModal } from '../components/modal.js'
|
||||||
|
|
||||||
export async function render() {
|
export async function render() {
|
||||||
const page = document.createElement('div')
|
const page = document.createElement('div')
|
||||||
@@ -71,7 +72,7 @@ async function loadData(page) {
|
|||||||
<div class="stat-card-meta">Tauri v2 桌面应用</div>
|
<div class="stat-card-meta">Tauri v2 桌面应用</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="stat-card-header"><span class="stat-card-label">OpenClaw</span></div>
|
<div class="stat-card-header"><span class="stat-card-label">OpenClaw · ${version.source === 'official' ? '官方版' : '汉化版'}</span></div>
|
||||||
<div class="stat-card-value">${version.current || '未安装'}</div>
|
<div class="stat-card-value">${version.current || '未安装'}</div>
|
||||||
<div class="stat-card-meta" style="display:flex;align-items:center;gap:8px">
|
<div class="stat-card-meta" style="display:flex;align-items:center;gap:8px">
|
||||||
${version.update_available
|
${version.update_available
|
||||||
@@ -90,17 +91,21 @@ async function loadData(page) {
|
|||||||
const upgradeBtn = cards.querySelector('#btn-upgrade')
|
const upgradeBtn = cards.querySelector('#btn-upgrade')
|
||||||
if (upgradeBtn) {
|
if (upgradeBtn) {
|
||||||
upgradeBtn.onclick = async () => {
|
upgradeBtn.onclick = async () => {
|
||||||
upgradeBtn.disabled = true
|
const modal = showUpgradeModal()
|
||||||
upgradeBtn.textContent = '升级中...'
|
let unlistenLog, unlistenProgress
|
||||||
try {
|
try {
|
||||||
|
const { listen } = await import('@tauri-apps/api/event')
|
||||||
|
unlistenLog = await listen('upgrade-log', (e) => modal.appendLog(e.payload))
|
||||||
|
unlistenProgress = await listen('upgrade-progress', (e) => modal.setProgress(e.payload))
|
||||||
const msg = await api.upgradeOpenclaw()
|
const msg = await api.upgradeOpenclaw()
|
||||||
upgradeBtn.textContent = '完成'
|
modal.setDone(msg)
|
||||||
// 刷新版本信息
|
|
||||||
loadData(page)
|
loadData(page)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
upgradeBtn.disabled = false
|
modal.appendLog(String(e))
|
||||||
upgradeBtn.textContent = '升级失败'
|
modal.setError('升级失败')
|
||||||
toast('升级失败: ' + e, 'error')
|
} finally {
|
||||||
|
unlistenLog?.()
|
||||||
|
unlistenProgress?.()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ function renderStatCards(page, services, version) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="stat-card-header">
|
<div class="stat-card-header">
|
||||||
<span class="stat-card-label">版本</span>
|
<span class="stat-card-label">版本 · ${version.source === 'official' ? '官方' : '汉化'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card-value">${version.current || '未知'}</div>
|
<div class="stat-card-value">${version.current || '未知'}</div>
|
||||||
<div class="stat-card-meta">${version.update_available ? '有新版本: ' + version.latest : '已是最新'}</div>
|
<div class="stat-card-meta">${version.update_available ? '有新版本: ' + version.latest : '已是最新'}</div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
import { api } from '../lib/tauri-api.js'
|
import { api } from '../lib/tauri-api.js'
|
||||||
import { toast } from '../components/toast.js'
|
import { toast } from '../components/toast.js'
|
||||||
import { showConfirm } from '../components/modal.js'
|
import { showConfirm, showUpgradeModal } from '../components/modal.js'
|
||||||
|
|
||||||
// HTML 转义,防止 XSS
|
// HTML 转义,防止 XSS
|
||||||
function escapeHtml(str) {
|
function escapeHtml(str) {
|
||||||
@@ -51,21 +51,32 @@ async function loadAll(page) {
|
|||||||
|
|
||||||
// ===== 版本检测 =====
|
// ===== 版本检测 =====
|
||||||
|
|
||||||
|
// 后端检测到的当前安装源
|
||||||
|
let detectedSource = 'chinese'
|
||||||
|
|
||||||
async function loadVersion(page) {
|
async function loadVersion(page) {
|
||||||
const bar = page.querySelector('#version-bar')
|
const bar = page.querySelector('#version-bar')
|
||||||
try {
|
try {
|
||||||
const info = await api.getVersionInfo()
|
const info = await api.getVersionInfo()
|
||||||
|
detectedSource = info.source || 'chinese'
|
||||||
const ver = info.current || '未知'
|
const ver = info.current || '未知'
|
||||||
const hasUpdate = info.update_available
|
const hasUpdate = info.update_available
|
||||||
|
const isChinese = detectedSource === 'chinese'
|
||||||
|
const sourceTag = isChinese ? '汉化优化版' : '官方原版'
|
||||||
|
const switchLabel = isChinese ? '切换到官方版' : '切换到汉化版'
|
||||||
|
const switchTarget = isChinese ? 'official' : 'chinese'
|
||||||
bar.innerHTML = `
|
bar.innerHTML = `
|
||||||
<div class="stat-cards" style="margin-bottom:var(--space-lg)">
|
<div class="stat-cards" style="margin-bottom:var(--space-lg)">
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="stat-card-header">
|
<div class="stat-card-header">
|
||||||
<span class="stat-card-label">当前版本</span>
|
<span class="stat-card-label">当前版本 · <span style="color:var(--accent)">${sourceTag}</span></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card-value">${ver}</div>
|
<div class="stat-card-value">${ver}</div>
|
||||||
<div class="stat-card-meta">${hasUpdate ? '新版本: ' + info.latest : '已是最新版本'}</div>
|
<div class="stat-card-meta">${hasUpdate ? '新版本: ' + info.latest : '已是最新版本'}</div>
|
||||||
${hasUpdate ? '<button class="btn btn-primary btn-sm" data-action="upgrade" style="margin-top:var(--space-sm)">升级到最新版</button>' : ''}
|
<div style="display:flex;gap:var(--space-sm);margin-top:var(--space-sm);flex-wrap:wrap">
|
||||||
|
${hasUpdate ? '<button class="btn btn-primary btn-sm" data-action="upgrade">升级到最新版</button>' : ''}
|
||||||
|
<button class="btn btn-secondary btn-sm" data-action="switch-source" data-source="${switchTarget}">${switchLabel}</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
@@ -194,6 +205,9 @@ function bindEvents(page) {
|
|||||||
case 'upgrade':
|
case 'upgrade':
|
||||||
await handleUpgrade(btn, page)
|
await handleUpgrade(btn, page)
|
||||||
break
|
break
|
||||||
|
case 'switch-source':
|
||||||
|
await handleSwitchSource(btn.dataset.source, page)
|
||||||
|
break
|
||||||
case 'install-gateway':
|
case 'install-gateway':
|
||||||
await handleInstallGateway(btn, page)
|
await handleInstallGateway(btn, page)
|
||||||
break
|
break
|
||||||
@@ -246,13 +260,37 @@ async function handleDeleteBackup(name, page) {
|
|||||||
|
|
||||||
// ===== 升级操作 =====
|
// ===== 升级操作 =====
|
||||||
|
|
||||||
|
async function doUpgradeWithModal(source, page) {
|
||||||
|
const modal = showUpgradeModal()
|
||||||
|
let unlistenLog, unlistenProgress
|
||||||
|
try {
|
||||||
|
const { listen } = await import('@tauri-apps/api/event')
|
||||||
|
unlistenLog = await listen('upgrade-log', (e) => modal.appendLog(e.payload))
|
||||||
|
unlistenProgress = await listen('upgrade-progress', (e) => modal.setProgress(e.payload))
|
||||||
|
const msg = await api.upgradeOpenclaw(source)
|
||||||
|
modal.setDone(msg)
|
||||||
|
await loadVersion(page)
|
||||||
|
} catch (e) {
|
||||||
|
modal.appendLog(String(e))
|
||||||
|
modal.setError('升级失败')
|
||||||
|
} finally {
|
||||||
|
unlistenLog?.()
|
||||||
|
unlistenProgress?.()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleUpgrade(btn, page) {
|
async function handleUpgrade(btn, page) {
|
||||||
const yes = await showConfirm('确定要升级 OpenClaw 到最新版本吗?\n升级过程中 Gateway 会短暂中断。')
|
const sourceLabel = detectedSource === 'official' ? '官方原版' : '汉化优化版'
|
||||||
|
const yes = await showConfirm(`确定要升级 OpenClaw 到最新${sourceLabel}吗?\n升级过程中 Gateway 会短暂中断。`)
|
||||||
if (!yes) return
|
if (!yes) return
|
||||||
btn.textContent = '升级中...'
|
await doUpgradeWithModal(detectedSource, page)
|
||||||
const msg = await api.upgradeOpenclaw()
|
}
|
||||||
toast(msg, 'success')
|
|
||||||
await loadVersion(page)
|
async function handleSwitchSource(target, page) {
|
||||||
|
const targetLabel = target === 'official' ? '官方原版' : '汉化优化版'
|
||||||
|
const yes = await showConfirm(`确定要切换到${targetLabel}吗?\n这会安装对应的 npm 包,配置数据不受影响。`)
|
||||||
|
if (!yes) return
|
||||||
|
await doUpgradeWithModal(target, page)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Gateway 安装/卸载 =====
|
// ===== Gateway 安装/卸载 =====
|
||||||
|
|||||||
@@ -173,3 +173,45 @@
|
|||||||
padding: var(--space-sm) var(--space-lg);
|
padding: var(--space-sm) var(--space-lg);
|
||||||
border-bottom: 1px solid var(--border-secondary);
|
border-bottom: 1px solid var(--border-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 升级进度弹窗 */
|
||||||
|
.upgrade-progress-wrap {
|
||||||
|
margin-bottom: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upgrade-progress-bar {
|
||||||
|
height: 6px;
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border-radius: 3px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upgrade-progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: var(--accent);
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: width 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upgrade-progress-fill.done { background: var(--success); }
|
||||||
|
.upgrade-progress-fill.error { background: var(--error); }
|
||||||
|
|
||||||
|
.upgrade-progress-text {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upgrade-log-box {
|
||||||
|
max-height: 220px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: var(--space-sm) var(--space-md);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user