Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e2f6646746 | |||
| 316babdea8 |
Regular → Executable
+16
-72
@@ -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<TagSelectorProps> = ({
|
||||
onCreateTag,
|
||||
placeholder = "输入标签后回车添加",
|
||||
}) => {
|
||||
const [inputVal, setInputVal] = useState("")
|
||||
const [showSuggestions, setShowSuggestions] = useState(false)
|
||||
const inputRef = useRef<HTMLInputElement>(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 (
|
||||
<div className="vmat-tag-selector-wrapper">
|
||||
<div className="vmat-tag-selector" onClick={() => inputRef.current?.focus()}>
|
||||
<div className="vmat-tag-selector" onClick={focus}>
|
||||
{value.map((tagId) => (
|
||||
<Tag key={tagId} variant="info" closable onClose={() => removeTagId(tagId)}>
|
||||
{tagMap.get(tagId)?.name ?? tagId}
|
||||
@@ -115,7 +61,6 @@ const TagSelector: React.FC<TagSelectorProps> = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 自动补全下拉 */}
|
||||
{showSuggestions && suggestions.length > 0 && (
|
||||
<div className="vmat-tag-suggestions">
|
||||
{suggestions.slice(0, 6).map((tag) => (
|
||||
@@ -134,7 +79,6 @@ const TagSelector: React.FC<TagSelectorProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 已有标签快捷选择 */}
|
||||
{tags.length > 0 && (
|
||||
<div className="vmat-tag-selector-presets">
|
||||
{tags.map((tag) => {
|
||||
|
||||
@@ -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<TagItem>
|
||||
}
|
||||
|
||||
export function useTagInput({ value, onChange, tags, onCreateTag }: UseTagInputOptions) {
|
||||
const [inputVal, setInputVal] = useState("")
|
||||
const [showSuggestions, setShowSuggestions] = useState(false)
|
||||
const inputRef = useRef<HTMLInputElement>(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,
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { useState } from "react"
|
||||
import { type VoiceMaterial } from "../../../types"
|
||||
import { type VoiceMaterial } from "../../types"
|
||||
import { type AssetLibraryItem } from "@/api/assets"
|
||||
import { useVoiceUpload } from "./actions/useVoiceUpload"
|
||||
import { useVoiceEdit } from "./actions/useVoiceEdit"
|
||||
|
||||
Reference in New Issue
Block a user