Skip to content

Commit 25542c8

Browse files
committed
feat: 添加标签下拉选择功能,优化标签输入体验
1 parent 35e138d commit 25542c8

1 file changed

Lines changed: 135 additions & 20 deletions

File tree

frontend/components/MarkdownEditor.tsx

Lines changed: 135 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ export default function MarkdownEditor() {
2323
const [availableLabels, setAvailableLabels] = useState<any[]>([]);
2424
// 选中的标签ID列表
2525
const [selectedLabelIds, setSelectedLabelIds] = useState<string[]>([]);
26+
// 是否显示标签下拉列表
27+
const [showTagDropdown, setShowTagDropdown] = useState<boolean>(false);
28+
// 过滤后的标签列表
29+
const [filteredLabels, setFilteredLabels] = useState<any[]>([]);
2630
// 文章标题
2731
const [title, setTitle] = useState<string>('');
2832
// 文章摘要
@@ -49,6 +53,10 @@ export default function MarkdownEditor() {
4953
const coverDropzoneRef = useRef<HTMLDivElement>(null);
5054
const tokenInputRef = useRef<HTMLInputElement>(null);
5155
const apiUrlInputRef = useRef<HTMLInputElement>(null);
56+
// 标签输入框引用
57+
const tagInputRef = useRef<HTMLInputElement>(null);
58+
// 标签下拉框引用
59+
const tagDropdownRef = useRef<HTMLDivElement>(null);
5260

5361
// 获取所有标签
5462
const fetchLabels = async () => {
@@ -348,12 +356,22 @@ export default function MarkdownEditor() {
348356
const input = e.target.value;
349357
setTagInput(input);
350358

351-
if (input.endsWith(',')) {
352-
const tagName = input.slice(0, -1).trim();
353-
if (tagName) {
354-
await addTag(tagName);
355-
setTagInput('');
356-
}
359+
// 过滤已有标签列表
360+
if (input.trim()) {
361+
const filtered = availableLabels.filter(
362+
(label) =>
363+
label.name.toLowerCase().includes(input.toLowerCase()) &&
364+
!selectedLabelIds.includes(label.id)
365+
);
366+
setFilteredLabels(filtered);
367+
setShowTagDropdown(true);
368+
} else {
369+
// 显示所有未选中的标签
370+
const filtered = availableLabels.filter(
371+
(label) => !selectedLabelIds.includes(label.id)
372+
);
373+
setFilteredLabels(filtered);
374+
setShowTagDropdown(true);
357375
}
358376
};
359377

@@ -384,6 +402,44 @@ export default function MarkdownEditor() {
384402
}
385403
};
386404

405+
// 选择已有标签
406+
const selectExistingLabel = (label: any) => {
407+
if (!selectedLabelIds.includes(label.id)) {
408+
setSelectedLabelIds([...selectedLabelIds, label.id]);
409+
setTags([...tags, label.name]);
410+
}
411+
setTagInput('');
412+
setShowTagDropdown(false);
413+
};
414+
415+
// 处理标签输入框聚焦
416+
const handleTagInputFocus = () => {
417+
const filtered = availableLabels.filter(
418+
(label) => !selectedLabelIds.includes(label.id)
419+
);
420+
setFilteredLabels(filtered);
421+
setShowTagDropdown(true);
422+
};
423+
424+
// 处理点击外部关闭下拉框
425+
useEffect(() => {
426+
const handleClickOutside = (event: MouseEvent) => {
427+
if (
428+
tagDropdownRef.current &&
429+
!tagDropdownRef.current.contains(event.target as Node) &&
430+
tagInputRef.current &&
431+
!tagInputRef.current.contains(event.target as Node)
432+
) {
433+
setShowTagDropdown(false);
434+
}
435+
};
436+
437+
document.addEventListener('mousedown', handleClickOutside);
438+
return () => {
439+
document.removeEventListener('mousedown', handleClickOutside);
440+
};
441+
}, []);
442+
387443
// 处理标签删除
388444
const removeTag = (index: number) => {
389445
const newTags = [...tags];
@@ -461,13 +517,31 @@ export default function MarkdownEditor() {
461517
};
462518

463519
// 处理标签键盘事件
464-
const handleTagKeyDown = (e: React.KeyboardEvent) => {
520+
const handleTagKeyDown = async (e: React.KeyboardEvent) => {
465521
if (e.key === 'Enter') {
466522
e.preventDefault();
467-
if (tagInput.trim()) {
468-
addTag(tagInput.trim());
469-
setTagInput('');
523+
const trimmedInput = tagInput.trim();
524+
if (trimmedInput) {
525+
// 检查是否有完全匹配的已有标签
526+
const exactMatch = availableLabels.find(
527+
(label) =>
528+
label.name.toLowerCase() === trimmedInput.toLowerCase() &&
529+
!selectedLabelIds.includes(label.id)
530+
);
531+
if (exactMatch) {
532+
selectExistingLabel(exactMatch);
533+
} else {
534+
// 创建新标签
535+
await addTag(trimmedInput);
536+
setTagInput('');
537+
setShowTagDropdown(false);
538+
}
470539
}
540+
} else if (e.key === 'Escape') {
541+
setShowTagDropdown(false);
542+
} else if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
543+
// 可以添加键盘导航支持
544+
e.preventDefault();
471545
}
472546
};
473547

@@ -686,16 +760,57 @@ export default function MarkdownEditor() {
686760
{/* 文章标签和标题 */}
687761
<div className="space-y-4">
688762
<div className="space-y-2">
689-
<div className="flex items-center space-x-2">
690-
<span className="text-sm font-medium">文章标签:</span>
691-
<input
692-
type="text"
693-
placeholder="输入标签,按逗号添加"
694-
className="input input-bordered w-full"
695-
value={tagInput}
696-
onChange={handleTagInput}
697-
onKeyDown={handleTagKeyDown}
698-
/>
763+
<div className="relative">
764+
<div className="flex items-center space-x-2">
765+
<span className="text-sm font-medium">文章标签:</span>
766+
<input
767+
ref={tagInputRef}
768+
type="text"
769+
placeholder="输入标签名搜索或按回车创建新标签"
770+
className="input input-bordered w-full"
771+
value={tagInput}
772+
onChange={handleTagInput}
773+
onKeyDown={handleTagKeyDown}
774+
onFocus={handleTagInputFocus}
775+
/>
776+
</div>
777+
{/* 标签下拉列表 */}
778+
{showTagDropdown && (
779+
<div
780+
ref={tagDropdownRef}
781+
className="absolute left-[72px] right-0 z-50 mt-1 max-h-48 overflow-y-auto rounded-md border border-base-300 bg-base-100 shadow-lg"
782+
>
783+
{filteredLabels.length > 0 ? (
784+
<>
785+
<div className="px-3 py-2 text-xs text-gray-500 border-b border-base-200">
786+
点击选择已有标签
787+
</div>
788+
{filteredLabels.map((label) => (
789+
<div
790+
key={label.id}
791+
className="cursor-pointer px-3 py-2 hover:bg-base-200 transition-colors"
792+
onClick={() => selectExistingLabel(label)}
793+
>
794+
<span className="font-medium">{label.name}</span>
795+
{label.description && (
796+
<span className="ml-2 text-xs text-gray-500">
797+
{label.description}
798+
</span>
799+
)}
800+
</div>
801+
))}
802+
</>
803+
) : tagInput.trim() ? (
804+
<div className="px-3 py-2 text-sm text-gray-500">
805+
没有匹配的标签,按回车创建 &quot;{tagInput.trim()}&quot;
806+
</div>
807+
) : (
808+
<div className="px-3 py-2 text-sm text-gray-500">
809+
暂无可用标签
810+
</div>
811+
)}
812+
</div>
813+
)}
699814
</div>
700815
{tags.length > 0 && (
701816
<div className="mt-2 flex flex-wrap gap-2">

0 commit comments

Comments
 (0)