125 lines
5.3 KiB
TypeScript
125 lines
5.3 KiB
TypeScript
import { useEffect, useRef, useState } from 'react';
|
|
import { CreateLibrary, DeleteLibrary, ImportCSV, ListLibraries, SwitchLibrary } from 'wailsjs/go/main/App';
|
|
import type { handler } from 'wailsjs/go/models';
|
|
|
|
interface LibraryBarProps {
|
|
activeName: string;
|
|
onSwitch: (name: string) => void;
|
|
}
|
|
|
|
export default function LibraryBar({ activeName, onSwitch }: LibraryBarProps) {
|
|
const [open, setOpen] = useState(false);
|
|
const [libs, setLibs] = useState<handler.LibraryInfo[]>([]);
|
|
const [creating, setCreating] = useState(false);
|
|
const [newName, setNewName] = useState('');
|
|
const [importing, setImporting] = useState(false);
|
|
const [msg, setMsg] = useState('');
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
|
|
const load = async () => { setLibs(await ListLibraries()); };
|
|
|
|
useEffect(() => {
|
|
if (open) load();
|
|
}, [open]);
|
|
|
|
// Close on outside click
|
|
useEffect(() => {
|
|
const handler = (e: MouseEvent) => {
|
|
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
|
|
};
|
|
document.addEventListener('mousedown', handler);
|
|
return () => document.removeEventListener('mousedown', handler);
|
|
}, []);
|
|
|
|
const handleSwitch = async (name: string) => {
|
|
await SwitchLibrary(name);
|
|
onSwitch(name);
|
|
setOpen(false);
|
|
};
|
|
|
|
const handleCreate = async () => {
|
|
if (!newName.trim()) return;
|
|
const err = await CreateLibrary(newName.trim(), '');
|
|
if (!err) { onSwitch(newName.trim()); setNewName(''); setCreating(false); load(); }
|
|
else setMsg(err);
|
|
};
|
|
|
|
const handleImport = async () => {
|
|
setImporting(true); setMsg('');
|
|
const result = await ImportCSV();
|
|
setImporting(false);
|
|
if (result.error && result.error !== '已取消') setMsg(result.error);
|
|
else if (result.imported > 0) { setMsg(`✓ 导入了 ${result.imported} 条(跳过 ${result.skipped} 条)`); load(); }
|
|
setTimeout(() => setMsg(''), 4000);
|
|
};
|
|
|
|
return (
|
|
<div ref={ref} className="relative px-4 pb-2">
|
|
{/* Button row */}
|
|
<div className="flex items-center gap-2">
|
|
<button onClick={() => setOpen(o => !o)}
|
|
className="flex-1 flex items-center gap-2 px-3 py-1.5 rounded-xl
|
|
bg-white/5 hover:bg-white/10 border border-white/8 transition-all text-left">
|
|
<span className="text-sm">📚</span>
|
|
<span className="text-[12px] text-white/80 truncate flex-1">{activeName || '选择知识库'}</span>
|
|
<span className="text-[10px] text-white/30">{open ? '▲' : '▾'}</span>
|
|
</button>
|
|
<button onClick={handleImport} disabled={importing} title="导入 CSV"
|
|
className="px-2.5 py-1.5 rounded-xl bg-accent/15 text-accent-light border border-accent/25
|
|
hover:bg-accent/25 transition-all text-[11px] font-medium disabled:opacity-50">
|
|
{importing ? '…' : '📥 导入'}
|
|
</button>
|
|
</div>
|
|
|
|
{msg && <p className={`text-[10px] mt-1 ${msg.startsWith('✓') ? 'text-green-400' : 'text-red-400'}`}>{msg}</p>}
|
|
|
|
{/* Dropdown */}
|
|
{open && (
|
|
<div className="absolute left-4 right-4 top-full mt-1 z-40
|
|
bg-[rgba(12,10,24,0.98)] border border-white/10 rounded-xl
|
|
shadow-2xl overflow-hidden animate-fade-in">
|
|
<div className="max-h-48 overflow-y-auto results-scroll">
|
|
{libs.map(lib => (
|
|
<div key={lib.id}
|
|
className={`flex items-center gap-2 px-3 py-2.5 cursor-pointer transition-colors
|
|
${lib.is_active ? 'bg-accent/15 border-l-2 border-accent' : 'hover:bg-white/5'}`}
|
|
onClick={() => handleSwitch(lib.name)}>
|
|
<span className="text-sm">{lib.is_active ? '✅' : '📁'}</span>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-[12px] text-white/85 truncate">{lib.name}</p>
|
|
<p className="text-[10px] text-white/35">{lib.entry_count} 条记录</p>
|
|
</div>
|
|
{!lib.is_active && (
|
|
<button onClick={e => { e.stopPropagation(); DeleteLibrary(lib.name); load(); }}
|
|
className="text-white/20 hover:text-red-400 text-xs transition-colors">✕</button>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Create new */}
|
|
<div className="border-t border-white/8 p-2">
|
|
{creating ? (
|
|
<div className="flex gap-2">
|
|
<input autoFocus className="flex-1 search-input text-[11px]" placeholder="知识库名称"
|
|
value={newName} onChange={e => setNewName(e.target.value)}
|
|
onKeyDown={e => e.key === 'Enter' && handleCreate()}
|
|
style={{ paddingLeft: '10px', paddingRight: '10px' }} />
|
|
<button onClick={handleCreate}
|
|
className="px-3 py-1.5 rounded-lg bg-accent text-white text-[11px]">✓</button>
|
|
<button onClick={() => setCreating(false)} className="text-white/30 text-xs">✕</button>
|
|
</div>
|
|
) : (
|
|
<button onClick={() => setCreating(true)}
|
|
className="w-full flex items-center gap-2 px-2 py-1.5 rounded-lg
|
|
text-[11px] text-white/50 hover:text-white/70 hover:bg-white/5 transition-all">
|
|
<span>+</span> 新建知识库
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|