diff --git a/apps/web/src/pages/voice-materials/components/TagSelector.tsx b/apps/web/src/pages/voice-materials/components/TagSelector.tsx old mode 100644 new mode 100755 index 34ec41207..97b270334 --- a/apps/web/src/pages/voice-materials/components/TagSelector.tsx +++ b/apps/web/src/pages/voice-materials/components/TagSelector.tsx @@ -1,7 +1,8 @@ -import React, { useState, useRef, useCallback, useMemo } from "react" +import React from "react" import { CheckOutlined } from "@ant-design/icons" import { Tag } from "@/components/ui" -import { type TagItem } from "@/api/tags" +import type { TagItem } from "@/api/tags" +import { useTagInput } from "./tag-selector/useTagInput" export interface TagSelectorProps { /** 已选标签 ID 列表 */ @@ -24,77 +25,22 @@ const TagSelector: React.FC = ({ onCreateTag, placeholder = "输入标签后回车添加", }) => { - const [inputVal, setInputVal] = useState("") - const [showSuggestions, setShowSuggestions] = useState(false) - const inputRef = useRef(null) - - /** 按名称查找已有标签(大小写不敏感) */ - const findTagByName = useCallback( - (name: string) => tags.find((t) => t.name.toLowerCase() === name.toLowerCase()), - [tags], - ) - - /** 去重添加标签(按 ID) */ - const addTagId = useCallback( - (tagId: string) => { - if (value.includes(tagId)) return - onChange([...value, tagId]) - setInputVal("") - setShowSuggestions(false) - }, - [value, onChange], - ) - - /** 输入自定义标签名:若已存在则直接选,否则创建新标签 */ - const addTagByName = useCallback( - async (name: string) => { - const trimmed = name.trim() - if (!trimmed) return - const existing = findTagByName(trimmed) - if (existing) { - addTagId(existing.id) - } else { - try { - const created = await onCreateTag(trimmed) - addTagId(created.id) - } catch { - /* 创建失败静默忽略 */ - } - } - }, - [findTagByName, addTagId, onCreateTag], - ) - - const removeTagId = useCallback( - (tagId: string) => { - onChange(value.filter((t) => t !== tagId)) - }, - [value, onChange], - ) - - /** 输入补全建议(排除已选) */ - const suggestions = useMemo(() => { - if (!inputVal.trim()) return [] - const lower = inputVal.toLowerCase() - return tags.filter((t) => t.name.toLowerCase().includes(lower) && !value.includes(t.id)) - }, [inputVal, tags, value]) - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter") { - e.preventDefault() - if (suggestions.length > 0) { - addTagId(suggestions[0].id) - } else { - addTagByName(inputVal) - } - } else if (e.key === "Backspace" && !inputVal && value.length > 0) { - removeTagId(value[value.length - 1]) - } - } + const { + inputVal, + setInputVal, + showSuggestions, + setShowSuggestions, + inputRef, + suggestions, + addTagId, + removeTagId, + handleKeyDown, + focus, + } = useTagInput({ value, onChange, tags, onCreateTag }) return (
-
inputRef.current?.focus()}> +
{value.map((tagId) => ( removeTagId(tagId)}> {tagMap.get(tagId)?.name ?? tagId} @@ -115,7 +61,6 @@ const TagSelector: React.FC = ({ />
- {/* 自动补全下拉 */} {showSuggestions && suggestions.length > 0 && (
{suggestions.slice(0, 6).map((tag) => ( @@ -134,7 +79,6 @@ const TagSelector: React.FC = ({
)} - {/* 已有标签快捷选择 */} {tags.length > 0 && (
{tags.map((tag) => { diff --git a/apps/web/src/pages/voice-materials/components/tag-selector/useTagInput.ts b/apps/web/src/pages/voice-materials/components/tag-selector/useTagInput.ts new file mode 100755 index 000000000..04f5f5e15 --- /dev/null +++ b/apps/web/src/pages/voice-materials/components/tag-selector/useTagInput.ts @@ -0,0 +1,90 @@ +import { useState, useRef, useCallback, useMemo } from "react" +import type { TagItem } from "@/api/tags" + +interface UseTagInputOptions { + value: string[] + onChange: (tagIds: string[]) => void + tags: TagItem[] + onCreateTag: (name: string) => Promise +} + +export function useTagInput({ value, onChange, tags, onCreateTag }: UseTagInputOptions) { + const [inputVal, setInputVal] = useState("") + const [showSuggestions, setShowSuggestions] = useState(false) + const inputRef = useRef(null) + + const findTagByName = useCallback( + (name: string) => tags.find((t) => t.name.toLowerCase() === name.toLowerCase()), + [tags], + ) + + const addTagId = useCallback( + (tagId: string) => { + if (value.includes(tagId)) return + onChange([...value, tagId]) + setInputVal("") + setShowSuggestions(false) + }, + [value, onChange], + ) + + const addTagByName = useCallback( + async (name: string) => { + const trimmed = name.trim() + if (!trimmed) return + const existing = findTagByName(trimmed) + if (existing) { + addTagId(existing.id) + } else { + try { + const created = await onCreateTag(trimmed) + addTagId(created.id) + } catch { + /* 创建失败静默忽略 */ + } + } + }, + [findTagByName, addTagId, onCreateTag], + ) + + const removeTagId = useCallback( + (tagId: string) => { + onChange(value.filter((t) => t !== tagId)) + }, + [value, onChange], + ) + + const suggestions = useMemo(() => { + if (!inputVal.trim()) return [] + const lower = inputVal.toLowerCase() + return tags.filter((t) => t.name.toLowerCase().includes(lower) && !value.includes(t.id)) + }, [inputVal, tags, value]) + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault() + if (suggestions.length > 0) { + addTagId(suggestions[0].id) + } else { + addTagByName(inputVal) + } + } else if (e.key === "Backspace" && !inputVal && value.length > 0) { + removeTagId(value[value.length - 1]) + } + } + + const focus = () => inputRef.current?.focus() + + return { + inputVal, + setInputVal, + showSuggestions, + setShowSuggestions, + inputRef, + suggestions, + addTagId, + removeTagId, + handleKeyDown, + focus, + } +}