Merge pull request #136 from CJackHwang/codex/add-file-import-and-export-for-project-config

feat(webui): add config backup download and file-based import in Settings
This commit is contained in:
CJACK.
2026-03-22 00:31:30 +08:00
committed by GitHub
5 changed files with 85 additions and 1 deletions

View File

@@ -6,7 +6,9 @@ export default function BackupSection({
setImportMode,
importing,
onLoadExportData,
onDownloadExportFile,
onImport,
onImportFileChange,
importText,
setImportText,
exportData,
@@ -23,6 +25,27 @@ export default function BackupSection({
<Download className="w-4 h-4" />
{t('settings.loadExport')}
</button>
<button
type="button"
onClick={onDownloadExportFile}
className="px-3 py-2 rounded-lg bg-secondary border border-border hover:bg-secondary/80 text-sm flex items-center gap-2"
>
<Download className="w-4 h-4" />
{t('settings.downloadExport')}
</button>
<label className="px-3 py-2 rounded-lg bg-secondary border border-border hover:bg-secondary/80 text-sm flex items-center gap-2 cursor-pointer">
<Upload className="w-4 h-4" />
{t('settings.chooseImportFile')}
<input
type="file"
accept=".json,application/json"
className="hidden"
onChange={(e) => {
onImportFileChange(e.target.files?.[0] || null)
e.target.value = ''
}}
/>
</label>
<select
value={importMode}
onChange={(e) => setImportMode(e.target.value)}

View File

@@ -36,6 +36,8 @@ export default function SettingsContainer({ onRefresh, onMessage, authFetch, onF
saveSettings,
updatePassword,
loadExportData,
downloadExportFile,
loadImportFile,
doImport,
} = useSettingsForm({
apiFetch,
@@ -102,7 +104,9 @@ export default function SettingsContainer({ onRefresh, onMessage, authFetch, onF
setImportMode={setImportMode}
importing={importing}
onLoadExportData={loadExportData}
onDownloadExportFile={downloadExportFile}
onImport={doImport}
onImportFileChange={loadImportFile}
importText={importText}
setImportText={setImportText}
exportData={exportData}

View File

@@ -222,15 +222,60 @@ export function useSettingsForm({ apiFetch, t, onMessage, onRefresh, onForceLogo
const { res, data } = await getExportData(apiFetch)
if (!res.ok) {
onMessage('error', data.detail || t('settings.exportFailed'))
return
return null
}
setExportData(data)
onMessage('success', t('settings.exportLoaded'))
return data
} catch (_e) {
onMessage('error', t('settings.exportFailed'))
return null
}
}, [apiFetch, onMessage, t])
const downloadExportFile = useCallback(async () => {
let latest = exportData
if (!latest?.json) {
const loaded = await loadExportData()
if (!loaded) {
return
}
latest = loaded
}
const jsonText = String(latest?.json || '').trim()
if (!jsonText) {
onMessage('error', t('settings.exportFailed'))
return
}
const blob = new Blob([jsonText], { type: 'application/json;charset=utf-8' })
const url = URL.createObjectURL(blob)
const now = new Date()
const pad = (n) => String(n).padStart(2, '0')
const filename = `ds2api-config-backup-${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}.json`
const link = document.createElement('a')
link.href = url
link.download = filename
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
onMessage('success', t('settings.exportDownloaded'))
}, [exportData, loadExportData, onMessage, t])
const loadImportFile = useCallback((file) => {
if (!file) return
const reader = new FileReader()
reader.onload = () => {
const text = String(reader.result || '')
setImportText(text)
onMessage('success', t('settings.importFileLoaded'))
}
reader.onerror = () => {
onMessage('error', t('settings.importFileReadFailed'))
}
reader.readAsText(file, 'utf-8')
}, [onMessage, t])
const doImport = useCallback(async () => {
if (!String(importText || '').trim()) {
onMessage('error', t('settings.importEmpty'))
@@ -290,6 +335,8 @@ export function useSettingsForm({ apiFetch, t, onMessage, onRefresh, onForceLogo
saveSettings,
updatePassword,
loadExportData,
downloadExportFile,
loadImportFile,
doImport,
}
}

View File

@@ -240,8 +240,10 @@
"autoDeleteWarning": "Warning: Enabling this will delete all session history after each request. Use with caution.",
"backupTitle": "Backup & Restore",
"loadExport": "Load current export",
"downloadExport": "Download backup file",
"importModeMerge": "Merge import (default)",
"importModeReplace": "Replace all import",
"chooseImportFile": "Choose import file",
"importNow": "Import now",
"importing": "Importing...",
"importPlaceholder": "Paste config JSON to import",
@@ -249,8 +251,11 @@
"importInvalidJson": "Import JSON is invalid.",
"importFailed": "Import failed.",
"importSuccess": "Config imported (mode: {mode}).",
"importFileLoaded": "Import file content loaded.",
"importFileReadFailed": "Failed to read import file.",
"exportFailed": "Export failed.",
"exportLoaded": "Current export loaded.",
"exportDownloaded": "Backup file download started.",
"exportJson": "Export JSON",
"invalidJsonField": "{field} is not a valid JSON object.",
"defaultPasswordWarning": "You are using the default admin password \"admin\". Please change it.",

View File

@@ -240,8 +240,10 @@
"autoDeleteWarning": "开启此功能后,每次请求完成都会删除该账号的所有历史会话,请谨慎使用。",
"backupTitle": "备份与恢复",
"loadExport": "加载当前导出",
"downloadExport": "下载备份文件",
"importModeMerge": "合并导入(默认)",
"importModeReplace": "全量覆盖导入",
"chooseImportFile": "选择导入文件",
"importNow": "立即导入",
"importing": "导入中...",
"importPlaceholder": "粘贴要导入的 JSON 配置",
@@ -249,8 +251,11 @@
"importInvalidJson": "导入 JSON 格式无效",
"importFailed": "导入失败",
"importSuccess": "配置导入成功(模式:{mode}",
"importFileLoaded": "已读取导入文件内容",
"importFileReadFailed": "读取导入文件失败",
"exportFailed": "导出失败",
"exportLoaded": "已加载当前配置导出",
"exportDownloaded": "备份文件下载已开始",
"exportJson": "导出 JSON",
"invalidJsonField": "{field} 不是有效 JSON 对象",
"defaultPasswordWarning": "当前使用默认密码 admin请尽快在此修改。",