feat: 更新配置导入导出描述,优化设置页面的导入导出功能

This commit is contained in:
VirtualHotBar
2026-03-04 23:02:43 +08:00
parent 90e4f7f750
commit 1923fe9acb
8 changed files with 121 additions and 118 deletions

View File

@@ -694,12 +694,13 @@
"saving": "Saving...",
"data_management": "Data Management",
"export_config": "Export Configuration",
"export_config_description": "Package current configuration into a ZIP file (excluding logs)",
"export_config_description": "Package current configuration into a ZIP file",
"export": "Export",
"config_exported": "Configuration exported",
"import_config": "Import Configuration",
"import_config_description": "Restore configuration from ZIP file (requires restart)",
"import_config_description": "Restore configuration from ZIP file",
"import": "Import",
"confirm_import": "Confirm Import",
"export_import_config_description": "Export current configuration or restore configuration from file",
"confirm_import_description": "Importing configuration will overwrite all current settings and automatically restart the software. Continue?"
}

View File

@@ -701,5 +701,6 @@
"import_config_description": "从 ZIP 文件恢复配置",
"import": "导入",
"confirm_import": "确认导入",
"export_import_config_description": "导出当前配置或者从文件恢复配置",
"confirm_import_description": "导入配置将覆盖当前所有配置并自动重启软件,是否继续?"
}

View File

@@ -694,12 +694,13 @@
"saving": "保存中...",
"data_management": "數據管理",
"export_config": "導出配置",
"export_config_description": "將當前配置打包為 ZIP 文件(排除日誌)",
"export_config_description": "將當前配置打包為 ZIP 文件",
"export": "導出",
"config_exported": "配置已導出",
"import_config": "導入配置",
"import_config_description": "從 ZIP 文件恢復配置(需要重啓)",
"import_config_description": "從 ZIP 文件恢復配置",
"import": "導入",
"confirm_import": "確認導入",
"export_import_config_description": "導出當前配置或者從文件恢復配置",
"confirm_import_description": "導入配置將覆蓋當前所有配置並自動重啓軟件,是否繼續?"
}

View File

@@ -245,6 +245,25 @@ pub fn init() -> anyhow::Result<()> {
};
app.update_app_config()?;
// 主窗口默认从 tauri.conf 设置为 visible=false避免前端再隐藏造成闪屏。
// 在后端读取配置后再决定是否显示窗口。
let start_hide = app.with_app_state::<Config, _>(|config| {
config
.0
.get("settings")
.and_then(|s| s.get("startHide"))
.and_then(|v| v.as_bool())
.unwrap_or(false)
});
if let Some(window) = app.app_main_window() {
if start_hide {
let _ = window.hide();
} else {
let _ = window.show();
let _ = window.set_focus();
}
}
//开发者工具
#[cfg(debug_assertions)]
if let Some(window) = app.app_main_window() {

View File

@@ -68,7 +68,8 @@
"minHeight": 450,
"minWidth": 700,
"transparent": true,
"decorations": false
"decorations": false,
"visible": false
}
],
"trayIcon": {

View File

@@ -5,7 +5,7 @@ import { RcloneVersion } from "../type/rclone/rcloneInfo"
import { startUpdateCont } from "./stats/continue"
import { reupMount } from "./storage/mount/mount"
import { reupStorage } from "./storage/storage"
import { listenWindow, windowsHide } from "./window"
import { listenWindow } from "./window"
import { sleep } from "../utils/utils"
import { t } from "i18next"
import { restartRclone, startRclone, stopRclone } from "../utils/rclone/process"
@@ -44,10 +44,6 @@ async function init(setStartStr: SetStartStrFn) {
setStartStr(t('read_config'))
await readNmConfig()
if (nmConfig.settings.startHide) {
windowsHide()
}
//设置语言
if (nmConfig.settings.language) {
await setLocalized(nmConfig.settings.language);

View File

@@ -139,7 +139,7 @@ function Home_page() {
<div style={{ width: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<Space style={{ height: '100%' }}>
<Card style={{ width: '10rem', height: '6rem' }} hoverable >
<strong ><IconCloud /> {t('storage')}</strong>({isStorageInitPending ? '...' : (isStorageInitFailed ? '--' : storageCount)})<br />
<strong ><IconCloud /> {t('storage')}</strong>({isStorageInitPending ? '_' : (isStorageInitFailed ? '-' : storageCount)})<br />
<div style={{ paddingTop: '1.3rem', width: '100%', textAlign: 'center' }}>
<Space>
<Button type='text' onClick={() => { hooks.navigate('/storage/manage/add') }}> {t('add')} </Button>
@@ -148,7 +148,7 @@ function Home_page() {
</div>
</Card>
<Card style={{ width: '10rem', height: '6rem' }} hoverable>
<strong ><IconStorage /> {t('mount')}</strong>({isStorageInitPending ? '...' : (isStorageInitFailed ? '--' : rcloneInfo.mountList.length)})
<strong ><IconStorage /> {t('mount')}</strong>({isStorageInitPending ? '_' : (isStorageInitFailed ? '-' : rcloneInfo.mountList.length)})
<div style={{ paddingTop: '1.3rem', width: '100%', textAlign: 'center' }}>
<Space>
<Button type='text' onClick={() => { hooks.navigate('/mount/add') }} > {t('add')} </Button>

View File

@@ -1,5 +1,5 @@
import { useEffect, useReducer, useState } from 'react'
import { Button, Card, Form, Grid, Input, Link, Message, Modal, Select, Space, Switch } from '@arco-design/web-react'
import { Button, Card, Form, Grid, Input, Link, Message, Modal, Select, Space, Switch, Tooltip } from '@arco-design/web-react'
import { nmConfig, osInfo, roConfig, saveNmConfig } from '../../services/config';
import { getAutostartState, setAutostartState, setThemeMode } from '../../controller/setting/setting';
import { useTranslation } from 'react-i18next';
@@ -156,113 +156,97 @@ export default function Setting_page() {
</Card>
<Card title={t('data_management')} style={{}} size='small'>
<Space direction="vertical" style={{ width: '100%' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<div style={{ fontWeight: 500, marginBottom: '0.5rem' }}>{t('export_config')}</div>
<div style={{ fontSize: '0.85rem', color: '#86909c' }}>
{t('export_config_description')}
</div>
</div>
<Button type="primary" status="success" onClick={async () => {
try {
const ts = new Date().toISOString().replace(/[:.]/g, '-')
const path = await dialog.save({
title: t('export_config'),
defaultPath: `netmount-config-${ts}.zip`,
filters: [{ name: 'Zip', extensions: ['zip'] }],
})
if (!path) return
const out = await invoke<string>('export_config', { outPath: path })
Message.success(`${t('config_exported')}: ${out}`)
} catch (e) {
const msg = (() => {
if (typeof e === 'string') return e
if (e && typeof e === 'object' && 'message' in e && typeof (e as { message?: unknown }).message === 'string') {
return (e as { message: string }).message
}
try {
return JSON.stringify(e)
} catch {
return String(e)
}
})()
Message.error(msg)
}
}}>{t('export')}</Button>
<Space style={{ width: '100%', justifyContent: 'space-between' }} align='center'>
<div style={{ display: 'flex', alignItems: 'center' }}>
<span style={{ fontWeight: 500 }}>{t('export_import_config_description')}</span>
</div>
<div style={{ height: '1px', backgroundColor: '#e5e6eb', margin: '0.5rem 0' }} />
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<div style={{ fontWeight: 500, marginBottom: '0.5rem' }}>{t('import_config')}</div>
<div style={{ fontSize: '0.85rem', color: '#86909c' }}>
{t('import_config_description')}
</div>
</div>
<Button type="primary" status="warning" onClick={async () => {
try {
const path = await dialog.open({
title: t('import_config'),
multiple: false,
filters: [{ name: 'Zip', extensions: ['zip'] }],
})
if (!path) return
Modal.confirm({
title: t('confirm_import'),
content: t('confirm_import_description'),
okButtonProps: { status: 'warning' },
onOk: async () => {
try {
// 先停止 rclone 和 openlist避免文件占用
await invoke('stop_components')
// 短暂等待,确保进程完全停止
await new Promise(resolve => setTimeout(resolve, 500))
// 导入配置
const result = await invoke<string>('import_config', { zipPath: path })
Message.success(result)
// 延迟重启(不保存当前配置,直接使用导入的配置)
setTimeout(() => {
invoke('restart_self')
}, 1000)
} catch (e) {
const msg = (() => {
if (typeof e === 'string') return e
if (e && typeof e === 'object' && 'message' in e && typeof (e as { message?: unknown }).message === 'string') {
return (e as { message: string }).message
}
try {
return JSON.stringify(e)
} catch {
return String(e)
}
})()
Message.error(msg)
<Space >
<Tooltip content={t('export_config_description')}>
<Button type='text' status='success' onClick={async () => {
try {
const ts = new Date().toISOString().replace(/[:.]/g, '-')
const path = await dialog.save({
title: t('export_config'),
defaultPath: `netmount-config-${ts}.zip`,
filters: [{ name: 'Zip', extensions: ['zip'] }],
})
if (!path) return
const out = await invoke<string>('export_config', { outPath: path })
Message.success(`${t('config_exported')}: ${out}`)
} catch (e) {
const msg = (() => {
if (typeof e === 'string') return e
if (e && typeof e === 'object' && 'message' in e && typeof (e as { message?: unknown }).message === 'string') {
return (e as { message: string }).message
}
},
})
} catch (e) {
const msg = (() => {
if (typeof e === 'string') return e
if (e && typeof e === 'object' && 'message' in e && typeof (e as { message?: unknown }).message === 'string') {
return (e as { message: string }).message
}
try {
return JSON.stringify(e)
} catch {
return String(e)
}
})()
Message.error(msg)
}
}}>{t('import')}</Button>
</div>
try {
return JSON.stringify(e)
} catch {
return String(e)
}
})()
Message.error(msg)
}
}}>{t('export')}</Button>
</Tooltip>
<Tooltip content={t('import_config_description')}>
<Button type='text' status='warning' onClick={async () => {
try {
const path = await dialog.open({
title: t('import_config'),
multiple: false,
filters: [{ name: 'Zip', extensions: ['zip'] }],
})
if (!path) return
Modal.confirm({
title: t('confirm_import'),
content: t('confirm_import_description'),
okButtonProps: { status: 'warning' },
onOk: async () => {
try {
await invoke('stop_components')
await new Promise(resolve => setTimeout(resolve, 500))
const result = await invoke<string>('import_config', { zipPath: path })
Message.success(result)
setTimeout(() => {
invoke('restart_self')
}, 1000)
} catch (e) {
const msg = (() => {
if (typeof e === 'string') return e
if (e && typeof e === 'object' && 'message' in e && typeof (e as { message?: unknown }).message === 'string') {
return (e as { message: string }).message
}
try {
return JSON.stringify(e)
} catch {
return String(e)
}
})()
Message.error(msg)
}
},
})
} catch (e) {
const msg = (() => {
if (typeof e === 'string') return e
if (e && typeof e === 'object' && 'message' in e && typeof (e as { message?: unknown }).message === 'string') {
return (e as { message: string }).message
}
try {
return JSON.stringify(e)
} catch {
return String(e)
}
})()
Message.error(msg)
}
}}>{t('import')}</Button>
</Tooltip>
</Space>
</Space>
</Card>
<Card title={t('components')} style={{}} size='small'>