mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-07-13 00:11:52 +08:00
feat(settings): 便携迁移界面重设计——方向示意、状态可见、操作忙碌态
便携模式区块从裸输入框+按钮升级为完整迁移卡片: - 状态行:模式指示点(本机灰点 / 便携绿点)+ 当前模式与便携根路径 - 方向示意图:本机 ⇄ U 盘 节点 + 高亮箭头,当前可执行的迁移方向 一眼可读(本机模式显示 本机→U 盘,便携模式显示 U 盘→本机) - 操作区:按钮带图标与说明文字并排;迁移期间按钮文字切换为 「正在迁移...」忙碌态,结束后恢复(成功/失败都不遗留状态) - 双向共用同一卡片结构与结果卡片样式,交互一致 i18n 补充 本机/U 盘/当前为本机模式 三键(简/繁/英)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,9 @@ export default {
|
||||
portableDesktopOnly: _('便携模式迁移仅支持桌面客户端', 'Portable migration is only available in the desktop client', '便攜模式遷移僅支援桌面客戶端', 'ポータブル移行はデスクトップクライアントでのみ利用できます', '휴대용 마이그레이션은 데스크톱 클라이언트에서만 사용할 수 있습니다'),
|
||||
portableAlreadyEnabled: _('已启用便携模式', 'Portable mode enabled', '已啟用便攜模式', 'ポータブルモード有効', '휴대용 모드 활성화됨'),
|
||||
portableEnabledHint: _('当前会话已从便携目录启动,配置和 OpenClaw 数据会优先写入该目录。', 'This session was started from a portable directory. Config and OpenClaw data are written there first.', '目前工作階段已從便攜目錄啟動,設定與 OpenClaw 資料會優先寫入該目錄。'),
|
||||
portableThisPc: _('本机', 'This PC', '本機'),
|
||||
portableUsb: _('U 盘', 'USB Drive', 'USB'),
|
||||
portableStatusLocal: _('当前为本机模式', 'Currently in local mode', '目前為本機模式'),
|
||||
portableRestoreBtn: _('迁移回本机', 'Migrate to This Computer', '遷移回本機'),
|
||||
portableRestoreHint: _('把 U 盘上的配置、Agent、记忆等用户数据迁回本机默认位置;本机已有数据会先整体备份。', 'Copies your config, agents, and memory from the portable drive back to this computer; existing local data is backed up first.', '把 USB 上的設定、Agent、記憶等使用者資料遷回本機預設位置;本機已有資料會先整體備份。'),
|
||||
portableRestoreConfirm: _('确定把便携数据迁移回本机?', 'Migrate portable data back to this computer?', '確定把便攜資料遷移回本機?'),
|
||||
|
||||
@@ -9,6 +9,7 @@ import { t, getLang, setLang, getAvailableLangs, onLangChange } from '../lib/i18
|
||||
import { isMacPlatform } from '../lib/app-state.js'
|
||||
import { renderSidebar } from '../components/sidebar.js'
|
||||
import { getActiveEngineId } from '../lib/engine-manager.js'
|
||||
import { icon } from '../lib/icons.js'
|
||||
|
||||
const isTauri = !!window.__TAURI_INTERNALS__
|
||||
|
||||
@@ -812,6 +813,22 @@ function portableTargetPlaceholder() {
|
||||
return isWin ? 'E:\\ClawPanelPortable' : '/Volumes/USB/ClawPanelPortable'
|
||||
}
|
||||
|
||||
// 迁移方向示意:高亮当前迁移方向(本机 ⇄ U 盘),一眼看懂数据往哪走
|
||||
function renderPortableDirection(toPortable) {
|
||||
const node = (label, iconName, active) => `
|
||||
<span style="display:inline-flex;align-items:center;gap:6px;padding:6px 12px;border-radius:var(--radius-md,8px);border:1px solid ${active ? 'color-mix(in srgb,var(--primary) 45%,var(--border-primary))' : 'var(--border-primary)'};background:${active ? 'color-mix(in srgb,var(--primary) 8%,var(--bg-primary))' : 'var(--bg-primary)'};font-size:var(--font-size-sm);font-weight:600;color:var(--text-primary)">
|
||||
${icon(iconName, 14)} ${label}
|
||||
</span>`
|
||||
const arrow = `<span style="display:inline-flex;align-items:center;color:var(--primary)">
|
||||
<svg width="34" height="14" viewBox="0 0 34 14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="2" y1="7" x2="28" y2="7"/><polyline points="23 2 28 7 23 12"/></svg>
|
||||
</span>`
|
||||
const host = node(t('settings.portableThisPc'), 'monitor', !toPortable)
|
||||
const usb = node(t('settings.portableUsb'), 'inbox', toPortable)
|
||||
return `<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:10px 0 14px">
|
||||
${toPortable ? host + arrow + usb : usb + arrow + host}
|
||||
</div>`
|
||||
}
|
||||
|
||||
async function loadPortableMigration(page) {
|
||||
const bar = page.querySelector('#portable-bar')
|
||||
if (!bar) return
|
||||
@@ -821,29 +838,42 @@ async function loadPortableMigration(page) {
|
||||
}
|
||||
try {
|
||||
const status = await api.getPortableStatus()
|
||||
const card = (inner) => `
|
||||
<div style="border:1px solid var(--border-primary);border-radius:var(--radius-md,10px);background:var(--bg-secondary);padding:14px 16px">${inner}</div>`
|
||||
|
||||
if (status?.enabled) {
|
||||
bar.innerHTML = `
|
||||
// 便携模式:U 盘 → 本机
|
||||
bar.innerHTML = card(`
|
||||
<div style="display:flex;align-items:center;gap:var(--space-sm);flex-wrap:wrap">
|
||||
<span class="clawhub-badge" style="background:rgba(16,185,129,0.14);color:#059669">${t('settings.portableAlreadyEnabled')}</span>
|
||||
<code style="font-size:var(--font-size-xs)">${escapeHtml(status.root || '')}</code>
|
||||
<span style="width:8px;height:8px;border-radius:999px;background:var(--success);flex:0 0 auto"></span>
|
||||
<span style="font-weight:600;color:var(--text-primary)">${t('settings.portableAlreadyEnabled')}</span>
|
||||
<code style="font-size:var(--font-size-xs);color:var(--text-secondary)">${escapeHtml(status.root || '')}</code>
|
||||
</div>
|
||||
<div class="form-hint" style="margin-top:var(--space-xs)">${t('settings.portableEnabledHint')}</div>
|
||||
<div style="display:flex;align-items:center;gap:var(--space-sm);margin-top:var(--space-sm)">
|
||||
<button class="btn btn-secondary btn-sm" data-action="migrate-local">${t('settings.portableRestoreBtn')}</button>
|
||||
${renderPortableDirection(false)}
|
||||
<div style="display:flex;align-items:center;gap:var(--space-sm);flex-wrap:wrap">
|
||||
<button class="btn btn-primary btn-sm" data-action="migrate-local">${icon('download', 13)} ${t('settings.portableRestoreBtn')}</button>
|
||||
<span class="form-hint" style="margin:0">${t('settings.portableRestoreHint')}</span>
|
||||
</div>
|
||||
<div class="form-hint" style="margin-top:var(--space-xs)">${t('settings.portableRestoreHint')}</div>
|
||||
<div id="portable-restore-result" style="margin-top:var(--space-sm);display:none"></div>
|
||||
`
|
||||
`)
|
||||
return
|
||||
}
|
||||
bar.innerHTML = `
|
||||
|
||||
// 本机模式:本机 → U 盘
|
||||
bar.innerHTML = card(`
|
||||
<div style="display:flex;align-items:center;gap:var(--space-sm);flex-wrap:wrap">
|
||||
<span style="width:8px;height:8px;border-radius:999px;background:var(--text-tertiary);flex:0 0 auto"></span>
|
||||
<span style="font-weight:600;color:var(--text-primary)">${t('settings.portableStatusLocal')}</span>
|
||||
</div>
|
||||
${renderPortableDirection(true)}
|
||||
<div style="display:flex;align-items:center;gap:var(--space-sm);flex-wrap:wrap">
|
||||
<input class="form-input" data-name="portable-target-root" placeholder="${escapeHtml(portableTargetPlaceholder())}" style="max-width:420px;min-width:260px">
|
||||
<button class="btn btn-primary btn-sm" data-action="migrate-portable">${t('settings.portableMigrateBtn')}</button>
|
||||
<button class="btn btn-primary btn-sm" data-action="migrate-portable">${icon('upload', 13)} ${t('settings.portableMigrateBtn')}</button>
|
||||
</div>
|
||||
<div class="form-hint" style="margin-top:var(--space-xs)">${t('settings.portableModeHint')}</div>
|
||||
<div id="portable-migrate-result" style="margin-top:var(--space-sm);display:none"></div>
|
||||
`
|
||||
`)
|
||||
} catch (e) {
|
||||
bar.innerHTML = `<div style="color:var(--error)">${t('common.loadFailed')}: ${escapeHtml(String(e))}</div>`
|
||||
}
|
||||
@@ -890,6 +920,9 @@ async function handleMigrateToLocal(page) {
|
||||
],
|
||||
})
|
||||
if (!ok) return
|
||||
const btn = page.querySelector('[data-action="migrate-local"]')
|
||||
const originalLabel = btn?.innerHTML
|
||||
if (btn) btn.innerHTML = t('settings.portableRestoring')
|
||||
if (resultEl) {
|
||||
resultEl.style.display = 'block'
|
||||
resultEl.innerHTML = `<div style="color:var(--text-tertiary);font-size:var(--font-size-sm)">${t('settings.portableRestoring')}</div>`
|
||||
@@ -903,6 +936,8 @@ async function handleMigrateToLocal(page) {
|
||||
resultEl.innerHTML = `<div style="color:var(--error);font-size:var(--font-size-sm)">${escapeHtml(e?.message || String(e))}</div>`
|
||||
}
|
||||
throw e
|
||||
} finally {
|
||||
if (btn && originalLabel) btn.innerHTML = originalLabel
|
||||
}
|
||||
if (resultEl) {
|
||||
const backups = Array.isArray(report?.backups) ? report.backups : []
|
||||
@@ -929,6 +964,9 @@ async function handleMigrateToPortable(page) {
|
||||
}
|
||||
const ok = await showConfirm(t('settings.portableMigrateConfirm'))
|
||||
if (!ok) return
|
||||
const btn = page.querySelector('[data-action="migrate-portable"]')
|
||||
const originalLabel = btn?.innerHTML
|
||||
if (btn) btn.innerHTML = t('settings.portableMigrating')
|
||||
if (resultEl) {
|
||||
resultEl.style.display = 'block'
|
||||
resultEl.innerHTML = `<div style="color:var(--text-tertiary);font-size:var(--font-size-sm)">${t('settings.portableMigrating')}</div>`
|
||||
@@ -942,6 +980,8 @@ async function handleMigrateToPortable(page) {
|
||||
resultEl.innerHTML = `<div style="color:var(--error);font-size:var(--font-size-sm)">${escapeHtml(e?.message || String(e))}</div>`
|
||||
}
|
||||
throw e
|
||||
} finally {
|
||||
if (btn && originalLabel) btn.innerHTML = originalLabel
|
||||
}
|
||||
if (resultEl) {
|
||||
resultEl.innerHTML = renderPortableMigrationResult(report)
|
||||
|
||||
Reference in New Issue
Block a user