diff --git a/apps/web/src/pages/generate/hooks/step2-materials/useMaterialLibrary.ts b/apps/web/src/pages/generate/hooks/step2-materials/useMaterialLibrary.ts new file mode 100755 index 000000000..3adbcfd1b --- /dev/null +++ b/apps/web/src/pages/generate/hooks/step2-materials/useMaterialLibrary.ts @@ -0,0 +1,41 @@ +import { useState, useEffect } from "react" +import { useQuery } from "@tanstack/react-query" +import { getAssets, getAssetLibraries } from "@/api/assets" +import type { AssetItem } from "@/api/assets" + +/** + * 素材库加载 Hook + * 管理素材库列表、当前选中库、素材列表加载 + */ +export function useMaterialLibrary() { + /* ── 素材库数据 API ── */ + const { data: libraries = [] } = useQuery({ + queryKey: ["asset-libraries"], + queryFn: getAssetLibraries, + }) + const [selectedLibraryId, setSelectedLibraryId] = useState("") + + // 自动选中第一个视频库 + useEffect(() => { + if (libraries.length > 0 && !selectedLibraryId) { + setSelectedLibraryId(libraries[0].id) + } + }, [libraries, selectedLibraryId]) + + const { data: materials = { items: [], total: 0 }, isLoading: materialsLoading } = useQuery<{ + items: AssetItem[] + total: number + }>({ + queryKey: ["generate-assets", selectedLibraryId], + queryFn: () => getAssets(selectedLibraryId), + enabled: !!selectedLibraryId, + }) + + return { + libraries, + selectedLibraryId, + setSelectedLibraryId, + materials, + materialsLoading, + } +} diff --git a/apps/web/src/pages/generate/hooks/step2-materials/useSmartMatch.ts b/apps/web/src/pages/generate/hooks/step2-materials/useSmartMatch.ts new file mode 100755 index 000000000..98bb0840f --- /dev/null +++ b/apps/web/src/pages/generate/hooks/step2-materials/useSmartMatch.ts @@ -0,0 +1,143 @@ +import { useState, useCallback, useMemo } from "react" +import { message } from "antd" +import type { AssetItem } from "@/api/assets" +import { SMART_MATCH_REASONS } from "../../constants" + +interface SmartMatchedResult { + asset: AssetItem + matchScore: number + matchReason: string +} + +interface UseSmartMatchOptions { + materials: { items: AssetItem[]; total: number } + smartSelectedIds: string[] + onSmartSelectedIdsChange: (ids: string[]) => void +} + +/** + * 智能素材匹配 Hook + * 封装 AI 匹配、换一批、全选/清空等逻辑 + */ +export function useSmartMatch({ + materials, + smartSelectedIds, + onSmartSelectedIdsChange, +}: UseSmartMatchOptions) { + const [smartMatchInput, setSmartMatchInput] = useState("") + const [smartMatching, setSmartMatching] = useState(false) + const [smartMatchedResults, setSmartMatchedResults] = useState([]) + const [hasMatched, setHasMatched] = useState(false) + + /* ── 智能素材匹配 ── */ + const handleSmartMatch = useCallback(async () => { + if (!smartMatchInput.trim()) { + message.warning("请先输入视频内容描述") + return + } + if (materials.items.length === 0) { + message.warning("当前视频库暂无素材") + return + } + + setSmartMatching(true) + setHasMatched(true) + + // 模拟 AI 匹配延迟 + await new Promise((resolve) => setTimeout(resolve, 1500)) + + // 从素材库中随机选取 5-8 个作为推荐结果 + const shuffled = [...materials.items].sort(() => Math.random() - 0.5) + const count = Math.min(shuffled.length, 5 + Math.floor(Math.random() * 4)) + const picked = shuffled.slice(0, count) + + const results = picked.map((asset, idx) => ({ + asset, + matchScore: Math.round(85 + Math.random() * 14), // 85-99 分 + matchReason: + SMART_MATCH_REASONS[idx % SMART_MATCH_REASONS.length] + + (Math.random() > 0.5 ? ",画面质感优秀" : ""), + })) + + // 按匹配度从高到低排序 + results.sort((a, b) => b.matchScore - a.matchScore) + + setSmartMatchedResults(results) + // 默认选中匹配度 >= 90 的素材 + const defaultSelected = results.filter((r) => r.matchScore >= 90).map((r) => r.asset.id) + onSmartSelectedIdsChange( + defaultSelected.length > 0 ? defaultSelected : results.slice(0, 3).map((r) => r.asset.id), + ) + setSmartMatching(false) + }, [smartMatchInput, materials.items, onSmartSelectedIdsChange]) + + const handleToggleSmartSelect = useCallback( + (assetId: string) => { + onSmartSelectedIdsChange( + smartSelectedIds.includes(assetId) + ? smartSelectedIds.filter((id) => id !== assetId) + : [...smartSelectedIds, assetId], + ) + }, + [smartSelectedIds, onSmartSelectedIdsChange], + ) + + const handleRefreshMatch = useCallback(async () => { + if (materials.items.length <= 5) { + message.info("视频库素材较少,无法换一批") + return + } + setSmartMatching(true) + await new Promise((resolve) => setTimeout(resolve, 800)) + + const remaining = materials.items.filter( + (m) => !smartMatchedResults.some((r) => r.asset.id === m.id), + ) + const shuffled = [...remaining].sort(() => Math.random() - 0.5) + const count = Math.min(shuffled.length, 5 + Math.floor(Math.random() * 3)) + const picked = shuffled.slice(0, count) + + const results = picked.map((asset, idx) => ({ + asset, + matchScore: Math.round(80 + Math.random() * 19), + matchReason: + SMART_MATCH_REASONS[(idx + 2) % SMART_MATCH_REASONS.length] + + (Math.random() > 0.5 ? ",节奏明快" : ""), + })) + results.sort((a, b) => b.matchScore - a.matchScore) + + setSmartMatchedResults(results) + onSmartSelectedIdsChange([]) + setSmartMatching(false) + }, [materials.items, smartMatchedResults, onSmartSelectedIdsChange]) + + const handleSelectAllMatched = useCallback(() => { + onSmartSelectedIdsChange(smartMatchedResults.map((r) => r.asset.id)) + }, [smartMatchedResults, onSmartSelectedIdsChange]) + + const handleClearSmartSelect = useCallback(() => { + onSmartSelectedIdsChange([]) + }, [onSmartSelectedIdsChange]) + + /* ── 计算已选智能匹配素材的总时长 ── */ + const smartSelectedTotalDuration = useMemo(() => { + return smartMatchedResults + .filter((r) => smartSelectedIds.includes(r.asset.id)) + .reduce((sum, r) => sum + (r.asset.duration || 0), 0) + }, [smartMatchedResults, smartSelectedIds]) + + return { + smartMatchInput, + setSmartMatchInput, + smartMatching, + smartMatchedResults, + hasMatched, + smartSelectedIds, + handleSmartMatch, + handleToggleSmartSelect, + handleRefreshMatch, + handleSelectAllMatched, + handleClearSmartSelect, + smartSelectedTotalDuration, + } +} diff --git a/apps/web/src/pages/generate/hooks/useStep2Materials.ts b/apps/web/src/pages/generate/hooks/useStep2Materials.ts old mode 100644 new mode 100755 index 127654bfd..3a07d95ef --- a/apps/web/src/pages/generate/hooks/useStep2Materials.ts +++ b/apps/web/src/pages/generate/hooks/useStep2Materials.ts @@ -1,20 +1,11 @@ /** * Step 2 素材选择 Hook - * 封装素材库加载、手动选择、智能匹配等逻辑 + * 组合素材库加载 + 智能匹配两个子 Hook */ -import { useState, useCallback, useEffect, useMemo } from "react" -import { message } from "antd" -import { useQuery } from "@tanstack/react-query" -import { getAssets, getAssetLibraries } from "@/api/assets" -import type { AssetItem } from "@/api/assets" +import { useCallback } from "react" import { formatDuration } from "../utils/formatDuration" -import { SMART_MATCH_REASONS } from "../constants" - -interface SmartMatchedResult { - asset: AssetItem - matchScore: number - matchReason: string -} +import { useMaterialLibrary } from "./step2-materials/useMaterialLibrary" +import { useSmartMatch } from "./step2-materials/useSmartMatch" interface UseStep2MaterialsProps { materialMode: "manual" | "auto" @@ -33,34 +24,14 @@ export function useStep2Materials({ smartSelectedIds, onSmartSelectedIdsChange, }: UseStep2MaterialsProps) { - /* ── 素材库数据 API ── */ - const { data: libraries = [] } = useQuery({ - queryKey: ["asset-libraries"], - queryFn: getAssetLibraries, + const { libraries, selectedLibraryId, setSelectedLibraryId, materials, materialsLoading } = + useMaterialLibrary() + + const smartMatch = useSmartMatch({ + materials, + smartSelectedIds, + onSmartSelectedIdsChange, }) - const [selectedLibraryId, setSelectedLibraryId] = useState("") - - // 自动选中第一个视频库 - useEffect(() => { - if (libraries.length > 0 && !selectedLibraryId) { - setSelectedLibraryId(libraries[0].id) - } - }, [libraries, selectedLibraryId]) - - const { data: materials = { items: [], total: 0 }, isLoading: materialsLoading } = useQuery<{ - items: AssetItem[] - total: number - }>({ - queryKey: ["generate-assets", selectedLibraryId], - queryFn: () => getAssets(selectedLibraryId), - enabled: !!selectedLibraryId, - }) - - /* ── 智能素材匹配状态 ── */ - const [smartMatchInput, setSmartMatchInput] = useState("") - const [smartMatching, setSmartMatching] = useState(false) - const [smartMatchedResults, setSmartMatchedResults] = useState([]) - const [hasMatched, setHasMatched] = useState(false) /* ── 手动选择素材 ── */ const handleToggleMaterial = useCallback( @@ -74,103 +45,6 @@ export function useStep2Materials({ [selectedMaterials, onSelectedMaterialsChange], ) - /* ── 智能素材匹配 ── */ - const handleSmartMatch = useCallback(async () => { - if (!smartMatchInput.trim()) { - message.warning("请先输入视频内容描述") - return - } - if (materials.items.length === 0) { - message.warning("当前视频库暂无素材") - return - } - - setSmartMatching(true) - setHasMatched(true) - - // 模拟 AI 匹配延迟 - await new Promise((resolve) => setTimeout(resolve, 1500)) - - // 从素材库中随机选取 5-8 个作为推荐结果 - const shuffled = [...materials.items].sort(() => Math.random() - 0.5) - const count = Math.min(shuffled.length, 5 + Math.floor(Math.random() * 4)) - const picked = shuffled.slice(0, count) - - const results = picked.map((asset, idx) => ({ - asset, - matchScore: Math.round(85 + Math.random() * 14), // 85-99 分 - matchReason: - SMART_MATCH_REASONS[idx % SMART_MATCH_REASONS.length] + - (Math.random() > 0.5 ? ",画面质感优秀" : ""), - })) - - // 按匹配度从高到低排序 - results.sort((a, b) => b.matchScore - a.matchScore) - - setSmartMatchedResults(results) - // 默认选中匹配度 >= 90 的素材 - const defaultSelected = results.filter((r) => r.matchScore >= 90).map((r) => r.asset.id) - onSmartSelectedIdsChange( - defaultSelected.length > 0 ? defaultSelected : results.slice(0, 3).map((r) => r.asset.id), - ) - setSmartMatching(false) - }, [smartMatchInput, materials.items, onSmartSelectedIdsChange]) - - const handleToggleSmartSelect = useCallback( - (assetId: string) => { - onSmartSelectedIdsChange( - smartSelectedIds.includes(assetId) - ? smartSelectedIds.filter((id) => id !== assetId) - : [...smartSelectedIds, assetId], - ) - }, - [smartSelectedIds, onSmartSelectedIdsChange], - ) - - const handleRefreshMatch = useCallback(async () => { - if (materials.items.length <= 5) { - message.info("视频库素材较少,无法换一批") - return - } - setSmartMatching(true) - await new Promise((resolve) => setTimeout(resolve, 800)) - - const remaining = materials.items.filter( - (m) => !smartMatchedResults.some((r) => r.asset.id === m.id), - ) - const shuffled = [...remaining].sort(() => Math.random() - 0.5) - const count = Math.min(shuffled.length, 5 + Math.floor(Math.random() * 3)) - const picked = shuffled.slice(0, count) - - const results = picked.map((asset, idx) => ({ - asset, - matchScore: Math.round(80 + Math.random() * 19), - matchReason: - SMART_MATCH_REASONS[(idx + 2) % SMART_MATCH_REASONS.length] + - (Math.random() > 0.5 ? ",节奏明快" : ""), - })) - results.sort((a, b) => b.matchScore - a.matchScore) - - setSmartMatchedResults(results) - onSmartSelectedIdsChange([]) - setSmartMatching(false) - }, [materials.items, smartMatchedResults, onSmartSelectedIdsChange]) - - const handleSelectAllMatched = useCallback(() => { - onSmartSelectedIdsChange(smartMatchedResults.map((r) => r.asset.id)) - }, [smartMatchedResults, onSmartSelectedIdsChange]) - - const handleClearSmartSelect = useCallback(() => { - onSmartSelectedIdsChange([]) - }, [onSmartSelectedIdsChange]) - - /* ── 计算已选智能匹配素材的总时长 ── */ - const smartSelectedTotalDuration = useMemo(() => { - return smartMatchedResults - .filter((r) => smartSelectedIds.includes(r.asset.id)) - .reduce((sum, r) => sum + (r.asset.duration || 0), 0) - }, [smartMatchedResults, smartSelectedIds]) - return { // 素材库 libraries, @@ -185,18 +59,18 @@ export function useStep2Materials({ selectedMaterials, handleToggleMaterial, // 智能匹配 - smartMatchInput, - setSmartMatchInput, - smartMatching, - smartMatchedResults, - hasMatched, - smartSelectedIds, - handleSmartMatch, - handleToggleSmartSelect, - handleRefreshMatch, - handleSelectAllMatched, - handleClearSmartSelect, - smartSelectedTotalDuration, + smartMatchInput: smartMatch.smartMatchInput, + setSmartMatchInput: smartMatch.setSmartMatchInput, + smartMatching: smartMatch.smartMatching, + smartMatchedResults: smartMatch.smartMatchedResults, + hasMatched: smartMatch.hasMatched, + smartSelectedIds: smartMatch.smartSelectedIds, + handleSmartMatch: smartMatch.handleSmartMatch, + handleToggleSmartSelect: smartMatch.handleToggleSmartSelect, + handleRefreshMatch: smartMatch.handleRefreshMatch, + handleSelectAllMatched: smartMatch.handleSelectAllMatched, + handleClearSmartSelect: smartMatch.handleClearSmartSelect, + smartSelectedTotalDuration: smartMatch.smartSelectedTotalDuration, // utils formatDuration, }