Files
ds2api/webui/src/components/BatchImport.jsx

187 lines
8.8 KiB
JavaScript

import { useState } from 'react'
import { FileCode, Download, Upload, Copy, Check, AlertTriangle } from 'lucide-react'
import clsx from 'clsx'
import { useI18n } from '../i18n'
import { getBatchImportTemplates } from '../utils/batchImportTemplates'
export default function BatchImport({ onRefresh, onMessage, authFetch }) {
const { t } = useI18n()
const [jsonInput, setJsonInput] = useState('')
const [loading, setLoading] = useState(false)
const [result, setResult] = useState(null)
const [copied, setCopied] = useState(false)
const apiFetch = authFetch || fetch
const templates = getBatchImportTemplates(t)
const handleImport = async () => {
if (!jsonInput.trim()) {
onMessage('error', t('batchImport.enterJson'))
return
}
let config
try {
config = JSON.parse(jsonInput)
} catch (e) {
onMessage('error', t('messages.invalidJson'))
return
}
setLoading(true)
setResult(null)
try {
const res = await apiFetch('/admin/import', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config),
})
const data = await res.json()
if (res.ok) {
setResult(data)
onMessage('success', t('batchImport.importSuccess', { keys: data.imported_keys, accounts: data.imported_accounts }))
onRefresh()
} else {
onMessage('error', data.detail || t('messages.importFailed'))
}
} catch (e) {
onMessage('error', t('messages.networkError'))
} finally {
setLoading(false)
}
}
const loadTemplate = (key) => {
const tpl = templates[key]
if (tpl) {
setJsonInput(JSON.stringify(tpl.config, null, 2))
onMessage('info', t('batchImport.templateLoaded', { name: tpl.name }))
}
}
const handleExport = async () => {
try {
const res = await apiFetch('/admin/export')
if (res.ok) {
const data = await res.json()
setJsonInput(JSON.stringify(JSON.parse(data.json), null, 2))
onMessage('success', t('batchImport.currentConfigLoaded'))
}
} catch (e) {
onMessage('error', t('batchImport.fetchConfigFailed'))
}
}
const copyBase64 = async () => {
try {
const res = await apiFetch('/admin/export')
if (res.ok) {
const data = await res.json()
await navigator.clipboard.writeText(data.base64)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
onMessage('success', t('batchImport.copySuccess'))
}
} catch (e) {
onMessage('error', t('messages.copyFailed'))
}
}
return (
<div className="flex flex-col lg:grid lg:grid-cols-3 gap-6 lg:h-[calc(100vh-140px)]">
{/* Templates Panel */}
<div className="md:col-span-1 space-y-4">
<div className="bg-card border border-border rounded-xl p-5 shadow-sm">
<h3 className="font-semibold flex items-center gap-2 mb-4">
<FileCode className="w-4 h-4 text-primary" />
{t('batchImport.quickTemplates')}
</h3>
<div className="space-y-3">
{Object.entries(templates).map(([key, tpl]) => (
<button
key={key}
onClick={() => loadTemplate(key)}
className="w-full text-left p-3 rounded-lg border border-border bg-secondary/20 hover:bg-secondary/50 hover:border-primary/50 transition-all custom-focus group"
>
<div className="font-medium text-sm group-hover:text-primary transition-colors">{tpl.name}</div>
<div className="text-xs text-muted-foreground mt-0.5">{tpl.desc}</div>
</button>
))}
</div>
</div>
<div className="bg-linear-to-br from-primary/10 to-transparent border border-primary/20 rounded-xl p-5 shadow-sm">
<h3 className="font-semibold flex items-center gap-2 mb-2 text-primary">
<Download className="w-4 h-4" />
{t('batchImport.dataExport')}
</h3>
<p className="text-sm text-muted-foreground mb-4">
{t('batchImport.dataExportDesc')}
</p>
<button
onClick={copyBase64}
className="w-full flex items-center justify-center gap-2 py-2.5 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-all font-medium text-sm shadow-sm"
>
{copied ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
{copied ? t('batchImport.copied') : t('batchImport.copyBase64')}
</button>
<p className="text-[10px] text-muted-foreground mt-2 text-center">
{t('batchImport.variableName')}: <code className="bg-background px-1 py-0.5 rounded border border-border">DS2API_CONFIG_JSON</code>
</p>
</div>
</div>
{/* Editor Panel */}
<div className="lg:col-span-2 flex flex-col bg-card border border-border rounded-xl shadow-sm overflow-hidden min-h-[400px] lg:h-full">
<div className="p-4 border-b border-border flex items-center justify-between bg-muted/20">
<h3 className="font-semibold flex items-center gap-2">
<Upload className="w-4 h-4 text-primary" />
{t('batchImport.jsonEditor')}
</h3>
<div className="flex gap-2">
<button onClick={handleExport} className="px-3 py-1.5 bg-secondary text-secondary-foreground rounded-lg hover:bg-secondary/80 transition-colors text-xs font-medium border border-border">
{t('batchImport.loadCurrentConfig')}
</button>
<button onClick={handleImport} disabled={loading} className="px-3 py-1.5 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors text-xs font-medium disabled:opacity-50">
{loading ? t('batchImport.importing') : t('batchImport.applyConfig')}
</button>
</div>
</div>
<div className="flex-1 relative min-h-[400px]">
<textarea
className="absolute inset-0 w-full h-full p-4 font-mono text-sm bg-[#09090b] text-foreground resize-none focus:outline-none custom-scrollbar"
value={jsonInput}
onChange={e => setJsonInput(e.target.value)}
placeholder={'{\n "keys": ["your-api-key"],\n "accounts": [\n {"email": "...", "password": "...", "token": ""}\n ]\n}'}
spellCheck={false}
/>
</div>
{result && (
<div className={clsx(
"p-4 border-t",
result.imported_keys || result.imported_accounts ? "bg-emerald-500/10 border-emerald-500/20" : "bg-destructive/10 border-destructive/20"
)}>
<div className="flex items-start gap-3">
{result.imported_keys || result.imported_accounts ? (
<Check className="w-5 h-5 text-emerald-500 mt-0.5" />
) : (
<AlertTriangle className="w-5 h-5 text-destructive mt-0.5" />
)}
<div>
<h4 className={clsx("font-medium", result.imported_keys || result.imported_accounts ? "text-emerald-500" : "text-destructive")}>
{t('batchImport.importComplete')}
</h4>
<p className="text-sm opacity-80 mt-1">
{t('batchImport.importSummary', { keys: result.imported_keys, accounts: result.imported_accounts })}
</p>
</div>
</div>
</div>
)}
</div>
</div>
)
}