@@ -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+ 没有匹配的标签,按回车创建 "{ tagInput . trim ( ) } "
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