mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-05-08 02:47:29 +08:00
webui config: add top-level category sidebar to reduce clutter
This commit is contained in:
@@ -1,26 +1,30 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import { RefreshCw, Save } from 'lucide-react';
|
import { RefreshCw, Save } from 'lucide-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useAppContext } from '../context/AppContext';
|
import { useAppContext } from '../context/AppContext';
|
||||||
import RecursiveConfig from '../components/RecursiveConfig';
|
import RecursiveConfig from '../components/RecursiveConfig';
|
||||||
|
|
||||||
function setPath(obj: any, path: string, value: any) {
|
function setPath(obj: any, path: string, value: any) {
|
||||||
const keys = path.split('.')
|
const keys = path.split('.');
|
||||||
const next = JSON.parse(JSON.stringify(obj || {}))
|
const next = JSON.parse(JSON.stringify(obj || {}));
|
||||||
let cur = next
|
let cur = next;
|
||||||
for (let i = 0; i < keys.length - 1; i++) {
|
for (let i = 0; i < keys.length - 1; i++) {
|
||||||
const k = keys[i]
|
const k = keys[i];
|
||||||
if (typeof cur[k] !== 'object' || cur[k] === null) cur[k] = {}
|
if (typeof cur[k] !== 'object' || cur[k] === null) cur[k] = {};
|
||||||
cur = cur[k]
|
cur = cur[k];
|
||||||
}
|
}
|
||||||
cur[keys[keys.length - 1]] = value
|
cur[keys[keys.length - 1]] = value;
|
||||||
return next
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Config: React.FC = () => {
|
const Config: React.FC = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { cfg, setCfg, cfgRaw, setCfgRaw, loadConfig, hotReloadFieldDetails, q } = useAppContext();
|
const { cfg, setCfg, cfgRaw, setCfgRaw, loadConfig, hotReloadFieldDetails, q } = useAppContext();
|
||||||
const [showRaw, setShowRaw] = useState(false);
|
const [showRaw, setShowRaw] = useState(false);
|
||||||
|
const topKeys = useMemo(() => Object.keys(cfg || {}).filter(k => typeof (cfg as any)?.[k] === 'object' && (cfg as any)?.[k] !== null), [cfg]);
|
||||||
|
const [selectedTop, setSelectedTop] = useState<string>('');
|
||||||
|
|
||||||
|
const activeTop = selectedTop || topKeys[0] || '';
|
||||||
|
|
||||||
async function saveConfig() {
|
async function saveConfig() {
|
||||||
try {
|
try {
|
||||||
@@ -35,8 +39,8 @@ const Config: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-8 max-w-5xl mx-auto space-y-6 flex flex-col min-h-full">
|
<div className="p-4 md:p-8 max-w-7xl mx-auto space-y-6 flex flex-col min-h-full">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between gap-3 flex-wrap">
|
||||||
<h1 className="text-2xl font-semibold tracking-tight">{t('configuration')}</h1>
|
<h1 className="text-2xl font-semibold tracking-tight">{t('configuration')}</h1>
|
||||||
<div className="flex items-center gap-1 bg-zinc-900/80 p-1 rounded-lg border border-zinc-800">
|
<div className="flex items-center gap-1 bg-zinc-900/80 p-1 rounded-lg border border-zinc-800">
|
||||||
<button onClick={() => setShowRaw(false)} className={`px-4 py-1.5 text-sm font-medium rounded-md transition-all ${!showRaw ? 'bg-zinc-800 text-white shadow-sm' : 'text-zinc-400 hover:text-zinc-200'}`}>{t('form')}</button>
|
<button onClick={() => setShowRaw(false)} className={`px-4 py-1.5 text-sm font-medium rounded-md transition-all ${!showRaw ? 'bg-zinc-800 text-white shadow-sm' : 'text-zinc-400 hover:text-zinc-200'}`}>{t('form')}</button>
|
||||||
@@ -65,14 +69,36 @@ const Config: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 bg-zinc-900/40 border border-zinc-800/80 rounded-2xl overflow-hidden flex flex-col shadow-sm">
|
<div className="flex-1 bg-zinc-900/40 border border-zinc-800/80 rounded-2xl overflow-hidden flex flex-col shadow-sm min-h-[420px]">
|
||||||
{!showRaw ? (
|
{!showRaw ? (
|
||||||
<div className="p-8 overflow-y-auto">
|
<div className="flex-1 flex min-h-0">
|
||||||
<RecursiveConfig
|
<aside className="w-44 md:w-56 border-r border-zinc-800 bg-zinc-950/40 p-2 md:p-3 overflow-y-auto shrink-0">
|
||||||
data={cfg}
|
<div className="text-xs text-zinc-500 uppercase tracking-widest mb-2 px-2">Top Level</div>
|
||||||
labels={t('configLabels', { returnObjects: true }) as Record<string, string>}
|
<div className="space-y-1">
|
||||||
onChange={(path, val) => setCfg(v => setPath(v, path, val))}
|
{topKeys.map((k) => (
|
||||||
/>
|
<button
|
||||||
|
key={k}
|
||||||
|
onClick={() => setSelectedTop(k)}
|
||||||
|
className={`w-full text-left px-3 py-2 rounded-lg text-sm transition-colors ${activeTop === k ? 'bg-indigo-500/20 text-indigo-300 border border-indigo-500/30' : 'text-zinc-300 hover:bg-zinc-800/60'}`}
|
||||||
|
>
|
||||||
|
{k}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div className="flex-1 p-4 md:p-6 overflow-y-auto">
|
||||||
|
{activeTop ? (
|
||||||
|
<RecursiveConfig
|
||||||
|
data={(cfg as any)?.[activeTop] || {}}
|
||||||
|
labels={t('configLabels', { returnObjects: true }) as Record<string, string>}
|
||||||
|
path={activeTop}
|
||||||
|
onChange={(path, val) => setCfg(v => setPath(v, path, val))}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="text-zinc-500 text-sm">No config groups found.</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<textarea
|
<textarea
|
||||||
|
|||||||
Reference in New Issue
Block a user