feat: 添加注释
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
Reference in New Issue
Block a user