/** * 叙事剪辑 — 文案选择弹窗(#1970) * - 搜索框:防抖 300ms,命中文字黄色高亮 * - 标签筛选行:全部/带货/工厂/测评/教程/口播/种草 * - 数量统计 + 卡片列表(可滚动,max-height 420px) * - 调用 GET /api/v1/scripts?keyword=&tag=&page_size=200 */ import React, { useState, useEffect, useMemo, useRef, useCallback } from "react" import { Modal, Input, Tag, Spin } from "antd" import { SearchOutlined, CheckCircleFilled } from "@ant-design/icons" import { useQuery } from "@tanstack/react-query" import { getScripts } from "@/api/scripts" import type { ScriptItem } from "@/api/scripts" interface ScriptSelectModalProps { open: boolean selectedScriptId: string | null onCancel: () => void onConfirm: (script: ScriptItem) => void } const SCRIPT_TABS = [ { key: "all", label: "全部" }, { key: "带货", label: "带货" }, { key: "工厂", label: "工厂" }, { key: "测评", label: "测评" }, { key: "教程", label: "教程" }, { key: "口播", label: "口播" }, { key: "种草", label: "种草" }, ] /** 在文本中用 高亮关键词(黄色背景) */ function highlight(text: string, keyword: string): React.ReactNode { if (!keyword) return text const idx = text.toLowerCase().indexOf(keyword.toLowerCase()) if (idx < 0) return text return ( <> {text.slice(0, idx)} {text.slice(idx, idx + keyword.length)} {text.slice(idx + keyword.length)} ) } const ScriptSelectModal: React.FC = ({ open, selectedScriptId, onCancel, onConfirm, }) => { const [innerSelected, setInnerSelected] = useState(selectedScriptId) const [activeTag, setActiveTag] = useState("all") const [searchInput, setSearchInput] = useState("") const [debouncedKw, setDebouncedKw] = useState("") const debounceRef = useRef | null>(null) useEffect(() => { if (open) { setInnerSelected(selectedScriptId) setActiveTag("all") setSearchInput("") setDebouncedKw("") } }, [open, selectedScriptId]) // 300ms 防抖 useEffect(() => { if (debounceRef.current) clearTimeout(debounceRef.current) debounceRef.current = setTimeout(() => setDebouncedKw(searchInput.trim()), 300) return () => { if (debounceRef.current) clearTimeout(debounceRef.current) } }, [searchInput]) const { data, isLoading } = useQuery({ queryKey: ["scripts", "select-modal", debouncedKw, activeTag], queryFn: () => getScripts({ page: 1, page_size: 200, keyword: debouncedKw || undefined, tag: activeTag === "all" ? undefined : activeTag, }), enabled: open, }) const scripts: ScriptItem[] = useMemo(() => data?.items ?? [], [data]) const selected = useMemo( () => scripts.find((s) => s.id === innerSelected) ?? null, [scripts, innerSelected], ) const handleConfirm = useCallback(() => { if (selected) onConfirm(selected) }, [selected, onConfirm]) return ( {/* 搜索 */} } placeholder="搜索标题、内容或标签" value={searchInput} onChange={(e) => setSearchInput(e.target.value)} style={{ marginBottom: 12 }} /> {/* 标签筛选 */}
{SCRIPT_TABS.map((t) => { const active = activeTag === t.key return ( setActiveTag(t.key)} style={{ cursor: "pointer", padding: "4px 14px", borderRadius: 16, border: active ? "1px solid #7c3aed" : "1px solid #e5e7eb", background: active ? "#ede9fe" : "#fff", color: active ? "#7c3aed" : "#4b5563", margin: 0, fontSize: 13, }} > {t.label} ) })}
{/* 数量统计 */}
共 {data?.total ?? scripts.length} 条文案
{/* 卡片列表 */}
{isLoading ? (
) : scripts.length === 0 ? (
暂无匹配文案
) : (
{scripts.map((s) => { const isSel = innerSelected === s.id const preview = (s.content || "").replace(/\s+/g, " ").slice(0, 80) return (
setInnerSelected(s.id)} style={{ padding: 14, borderRadius: 8, border: isSel ? "2px solid #7c3aed" : "1px solid #e5e7eb", background: isSel ? "#faf5ff" : "#fff", cursor: "pointer", transition: "all 0.2s", position: "relative", }} > {isSel && ( )}
{highlight(s.title || "未命名", debouncedKw)}
{highlight(preview + ((s.content || "").length > 80 ? "..." : ""), debouncedKw)}
{s.tags && s.tags.length > 0 && (
{s.tags.slice(0, 5).map((tg) => ( {tg} ))}
)}
) })}
)}
) } export default ScriptSelectModal