feat: 添加注释

This commit is contained in:
Blizzard
2026-04-01 15:29:35 +08:00
parent aef2e152dc
commit 6162c9110c
28 changed files with 1293 additions and 298 deletions
+22 -7
View File
@@ -1,10 +1,11 @@
import { useEffect, useRef, useState } from 'react';
import { CreateLibrary, DeleteLibrary, ImportCSV, ListLibraries, SwitchLibrary } from 'wailsjs/go/main/App';
import { CreateLibrary, DeleteLibrary, ImportCSV, ImportExcel, ListLibraries, SwitchLibrary } from 'wailsjs/go/main/App';
import type { handler } from 'wailsjs/go/models';
interface LibraryBarProps {
activeName: string;
onSwitch: (name: string) => void;
onManageMode?: () => void;
}
export default function LibraryBar({ activeName, onSwitch }: LibraryBarProps) {
@@ -44,12 +45,21 @@ export default function LibraryBar({ activeName, onSwitch }: LibraryBarProps) {
else setMsg(err);
};
const handleImport = async () => {
const handleImportCSV = 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(); }
else if (result.imported > 0) { setMsg(` CSV 导入了 ${result.imported}`); load(); }
setTimeout(() => setMsg(''), 4000);
};
const handleImportExcel = async () => {
setImporting(true); setMsg('');
const result = await ImportExcel();
setImporting(false);
if (result.error && result.error !== '已取消') setMsg(result.error);
else if (result.imported > 0) { setMsg(`✓ Excel 导入了 ${result.imported}`); load(); }
setTimeout(() => setMsg(''), 4000);
};
@@ -64,10 +74,15 @@ export default function LibraryBar({ activeName, onSwitch }: LibraryBarProps) {
<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 onClick={handleImportCSV} disabled={importing} title="导入 CSV"
className="px-2 py-1.5 rounded-xl bg-accent/15 text-accent-light border border-accent/25
hover:bg-accent/25 transition-all text-[10px] font-medium disabled:opacity-50 whitespace-nowrap">
{importing ? '…' : '📥 CSV'}
</button>
<button onClick={handleImportExcel} disabled={importing} title="导入 Excel"
className="px-2 py-1.5 rounded-xl bg-emerald-500/15 text-emerald-300 border border-emerald-500/25
hover:bg-emerald-500/25 transition-all text-[10px] font-medium disabled:opacity-50 whitespace-nowrap">
{importing ? '…' : '📊 Excel'}
</button>
</div>
+70
View File
@@ -0,0 +1,70 @@
import { ClearDatabase, DeleteItems } from 'wailsjs/go/main/App';
interface ManageModeBarProps {
selected: number[];
totalCount: number;
onSelectAll: () => void;
onDeselectAll: () => void;
onExit: () => void;
onDeleted: (ids: number[]) => void;
showToast: (msg: string, type: 'success' | 'error' | 'info') => void;
}
export default function ManageModeBar({
selected, totalCount, onSelectAll, onDeselectAll, onExit, onDeleted, showToast,
}: ManageModeBarProps) {
const allSelected = selected.length === totalCount && totalCount > 0;
const handleBatchDelete = async () => {
if (selected.length === 0) return;
const err = await DeleteItems(selected as unknown as number[]);
if (err) { showToast(err, 'error'); return; }
showToast(`已删除 ${selected.length}`, 'success');
onDeleted(selected);
};
const handleClearAll = async () => {
if (!window.confirm(`确定要清空当前知识库的所有 ${totalCount} 条记录吗?此操作不可撤销。`)) return;
const err = await ClearDatabase();
if (err) { showToast(err, 'error'); return; }
showToast('已清空知识库', 'success');
onDeleted([]);
};
return (
<div className="border-t border-white/8 bg-[rgba(8,6,20,0.95)] px-4 py-2.5 flex items-center gap-2 flex-shrink-0">
{/* Left: count + select toggle */}
<button onClick={allSelected ? onDeselectAll : onSelectAll}
className="flex items-center gap-1.5 text-[11px] text-white/50 hover:text-white/70 transition-colors">
<div className={`w-4 h-4 rounded border flex items-center justify-center transition-colors
${allSelected ? 'bg-accent border-accent' : 'border-white/30'}`}>
{allSelected && <span className="text-white text-[10px]"></span>}
</div>
{selected.length > 0 ? `已选 ${selected.length}/${totalCount}` : '全选'}
</button>
<div className="flex-1" />
{/* Clear all */}
<button onClick={handleClearAll}
className="text-[11px] px-3 py-1.5 rounded-lg text-red-400/60 hover:text-red-400
hover:bg-red-500/10 border border-transparent hover:border-red-500/20 transition-all">
🗑
</button>
{/* Batch delete */}
<button onClick={handleBatchDelete} disabled={selected.length === 0}
className="text-[11px] px-3 py-1.5 rounded-lg bg-red-500/20 text-red-300 border border-red-500/30
hover:bg-red-500/35 transition-all disabled:opacity-30 disabled:cursor-not-allowed font-medium">
({selected.length})
</button>
{/* Exit */}
<button onClick={onExit}
className="text-[11px] px-3 py-1.5 rounded-lg bg-white/5 text-white/50 border border-white/10
hover:bg-white/10 transition-all">
</button>
</div>
);
}
+91 -55
View File
@@ -4,90 +4,126 @@ import type { SearchResult } from '../hooks/useSearch';
interface ResultCardProps {
result: SearchResult;
onPolish: (result: SearchResult) => void;
onDelete: (id: number) => void;
managementMode: boolean;
selected: boolean;
onToggleSelect: (id: number) => void;
}
const CATEGORY_COLORS: Record<string, string> = {
'通用': 'bg-indigo-500/20 text-indigo-300',
'退款': 'bg-red-500/20 text-red-300',
'物流': 'bg-amber-500/20 text-amber-300',
'产品': 'bg-emerald-500/20 text-emerald-300',
'促销': 'bg-pink-500/20 text-pink-300',
'通用': 'bg-indigo-500/20 text-indigo-300',
'退款': 'bg-red-500/20 text-red-300',
'物流': 'bg-amber-500/20 text-amber-300',
'产品': 'bg-emerald-500/20 text-emerald-300',
'促销': 'bg-pink-500/20 text-pink-300',
'浇水': 'bg-blue-500/20 text-blue-300',
'施肥': 'bg-lime-500/20 text-lime-300',
'病虫害': 'bg-orange-500/20 text-orange-300',
'繁殖': 'bg-purple-500/20 text-purple-300',
'换盆': 'bg-teal-500/20 text-teal-300',
'修剪': 'bg-cyan-500/20 text-cyan-300',
'光照': 'bg-yellow-500/20 text-yellow-300',
'季节养护': 'bg-rose-500/20 text-rose-300',
'土壤': 'bg-stone-500/20 text-stone-300',
};
export default function ResultCard({ result, onPolish }: ResultCardProps) {
export default function ResultCard({ result, onPolish, onDelete, managementMode, selected, onToggleSelect }: ResultCardProps) {
const [copied, setCopied] = useState(false);
const [confirmDelete, setConfirmDelete] = useState(false);
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(result.answer);
} catch {
// Wails fallback — plain execCommand
const el = document.createElement('textarea');
el.value = result.answer;
document.body.appendChild(el);
el.select();
document.execCommand('copy');
const handleCopy = async (e: React.MouseEvent) => {
e.stopPropagation();
try { await navigator.clipboard.writeText(result.answer); }
catch {
const el = document.createElement('textarea'); el.value = result.answer;
document.body.appendChild(el); el.select(); document.execCommand('copy');
document.body.removeChild(el);
}
setCopied(true);
setTimeout(() => setCopied(false), 1000);
};
const handleCardClick = () => {
if (managementMode) { onToggleSelect(result.id); return; }
// default: copy answer
};
const catColor = CATEGORY_COLORS[result.category] ?? 'bg-white/10 text-white/50';
return (
<div className={`result-card group relative ${result.is_fallback ? 'opacity-70' : ''}`} onClick={handleCopy}>
{/* Category badge + match indicator */}
<div className="flex items-center justify-between mb-2">
<div
onClick={handleCardClick}
className={`result-card group relative transition-all
${result.is_fallback ? 'opacity-70' : ''}
${selected ? 'ring-2 ring-accent/60 bg-accent/5' : ''}
${managementMode ? 'cursor-pointer' : ''}`}>
{/* Top row: checkbox (mgmt) | category | match badge | trash */}
<div className="flex items-center gap-2 mb-2">
{managementMode && (
<div className={`w-4 h-4 rounded border flex-shrink-0 flex items-center justify-center transition-colors
${selected ? 'bg-accent border-accent' : 'border-white/25'}`}>
{selected && <span className="text-white text-[10px]"></span>}
</div>
)}
<span className={`text-[10px] font-medium px-2 py-0.5 rounded-full ${catColor}`}>
{result.category}
</span>
{result.is_fallback ? (
<span className="text-[10px] text-white/30 flex items-center gap-1">
🔥
</span>
) : result.score === 2 ? (
<span className="text-[10px] text-accent-light flex items-center gap-1">
</span>
) : null}
<span className="text-[10px] text-white/30 flex-1">
{result.is_fallback ? '🔥 热门推荐' : result.score === 2 ? '⚡ 精准匹配' : null}
</span>
{/* Trash icon — always visible, top-right */}
{!managementMode && (
confirmDelete ? (
<div className="flex items-center gap-1" onClick={e => e.stopPropagation()}>
<button onClick={() => { onDelete(result.id); setConfirmDelete(false); }}
className="text-[10px] px-2 py-0.5 rounded bg-red-500/30 text-red-300 border border-red-500/40 hover:bg-red-500/50 transition-all">
</button>
<button onClick={() => setConfirmDelete(false)}
className="text-[10px] px-2 py-0.5 rounded bg-white/5 text-white/40 border border-white/10 hover:bg-white/10 transition-all">
</button>
</div>
) : (
<button
id={`btn-delete-${result.id}`}
onClick={e => { e.stopPropagation(); setConfirmDelete(true); }}
className="opacity-0 group-hover:opacity-100 text-white/25 hover:text-red-400 transition-all text-sm leading-none p-1 rounded hover:bg-red-500/10"
title="删除此条目">
🗑
</button>
)
)}
</div>
{/* Question */}
<p className="text-[12px] text-white/50 mb-1.5 line-clamp-1">
Q: {result.question}
</p>
<p className="text-[12px] text-white/50 mb-1.5 line-clamp-1">Q: {result.question}</p>
{/* Answer */}
<p className="text-[13px] text-white/90 leading-relaxed line-clamp-3">
{result.answer}
</p>
<p className="text-[13px] text-white/90 leading-relaxed line-clamp-3">{result.answer}</p>
{/* Hover action bar */}
<div className="flex items-center gap-2 mt-3 opacity-0 group-hover:opacity-100 transition-opacity">
<button
id={`btn-copy-${result.id}`}
onClick={e => { e.stopPropagation(); handleCopy(); }}
className="flex-1 text-[11px] py-1.5 rounded-lg bg-accent/20 text-accent-light border border-accent/30
hover:bg-accent/35 transition-all font-medium"
>
{copied ? '✓ 已复制' : '📋 复制'}
</button>
<button
id={`btn-polish-${result.id}`}
onClick={e => { e.stopPropagation(); onPolish(result); }}
className="flex-1 text-[11px] py-1.5 rounded-lg bg-white/5 text-white/60 border border-white/10
hover:bg-accent/15 hover:text-accent-light hover:border-accent/30 transition-all font-medium"
>
AI
</button>
</div>
{/* Hover action bar (only in normal mode) */}
{!managementMode && (
<div className="flex items-center gap-2 mt-3 opacity-0 group-hover:opacity-100 transition-opacity">
<button id={`btn-copy-${result.id}`} onClick={handleCopy}
className="flex-1 text-[11px] py-1.5 rounded-lg bg-accent/20 text-accent-light border border-accent/30
hover:bg-accent/35 transition-all font-medium">
{copied ? '✓ 已复制' : '📋 复制'}
</button>
<button id={`btn-polish-${result.id}`} onClick={e => { e.stopPropagation(); onPolish(result); }}
className="flex-1 text-[11px] py-1.5 rounded-lg bg-white/5 text-white/60 border border-white/10
hover:bg-accent/15 hover:text-accent-light hover:border-accent/30 transition-all font-medium">
AI
</button>
</div>
)}
{/* Copy toast */}
{copied && (
<div className="copy-toast absolute top-2 right-2 text-[10px] bg-green-500/20 text-green-400
border border-green-500/30 px-2 py-1 rounded-md pointer-events-none">
</div>
)}
</div>
+49
View File
@@ -0,0 +1,49 @@
import { useCallback, useEffect, useRef, useState } from 'react';
export interface ToastItem {
id: number;
message: string;
type: 'success' | 'error' | 'info';
}
interface ToastProps { toasts: ToastItem[]; }
export function Toast({ toasts }: ToastProps) {
return (
<div className="fixed bottom-4 left-1/2 -translate-x-1/2 z-[100] flex flex-col gap-2 items-center pointer-events-none">
{toasts.map(t => (
<div key={t.id}
className={`px-4 py-2 rounded-xl text-[12px] font-medium shadow-xl border animate-toast-in
${t.type === 'success' ? 'bg-green-500/20 text-green-300 border-green-500/30' :
t.type === 'error' ? 'bg-red-500/20 text-red-300 border-red-500/30' :
'bg-white/10 text-white/70 border-white/15'}`}>
{t.type === 'success' ? '✓ ' : t.type === 'error' ? '✕ ' : ' '}{t.message}
</div>
))}
</div>
);
}
let _toastId = 0;
export function useToast() {
const [toasts, setToasts] = useState<ToastItem[]>([]);
const timerRef = useRef<Map<number, ReturnType<typeof setTimeout>>>(new Map());
const showToast = useCallback((message: string, type: ToastItem['type'] = 'success', duration = 2500) => {
const id = ++_toastId;
setToasts(prev => [...prev, { id, message, type }]);
const timer = setTimeout(() => {
setToasts(prev => prev.filter(t => t.id !== id));
timerRef.current.delete(id);
}, duration);
timerRef.current.set(id, timer);
}, []);
useEffect(() => {
const timers = timerRef.current;
return () => { timers.forEach(t => clearTimeout(t)); };
}, []);
return { toasts, showToast };
}