9a51cad137
CI/CD Pipeline / Frontend Lint (push) Successful in 48s
CI/CD Pipeline / Validate - Code Quality (push) Failing after 2m24s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m25s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m27s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m8s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m16s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m20s
CI/CD Pipeline / Integration Tests (push) Successful in 3m53s
CI/CD Pipeline / Unit Tests (push) Failing after 7m53s
CI/CD Pipeline / Build Staging API Image (push) Successful in 11m29s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m13s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 26s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m3s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m15s
CI/CD Pipeline / Production Browser E2E (push) Failing after 1294h4m36s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 1294h5m22s
CI/CD Pipeline / Build Production Web Image (push) Failing after 1294h5m22s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 1294h7m45s
CI/CD Pipeline / PR Build Web Image (push) Failing after 1294h7m47s
CI/CD Pipeline / PR Build API Image (push) Failing after 1294h7m47s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 1294h7m49s
CI/CD Pipeline / Deploy Production (push) Failing after 1294h37m5s
CI/CD Pipeline / Build Production API Image (push) Failing after 1294h37m51s
252 lines
7.3 KiB
TypeScript
252 lines
7.3 KiB
TypeScript
/**
|
|
* Step 4 标题设置 Hook
|
|
* 封装 AI 标题生成、标题样式设置等逻辑
|
|
*/
|
|
import { useState, useCallback, useMemo } from "react"
|
|
import { message } from "antd"
|
|
import { useQuery } from "@tanstack/react-query"
|
|
import { getTitles } from "@/api/titles"
|
|
import { TITLE_PRESETS, AI_TITLE_TEMPLATES } from "../constants"
|
|
import type { TitleSettings } from "../types"
|
|
|
|
interface UseStep4TitleProps {
|
|
titleSettings: TitleSettings
|
|
onTitleSettingsChange: (settings: TitleSettings) => void
|
|
}
|
|
|
|
interface AiTitleItem {
|
|
title: string
|
|
highlight: string
|
|
style: "catchy" | "emotional" | "informative"
|
|
}
|
|
|
|
export function useStep4Title({ titleSettings, onTitleSettingsChange }: UseStep4TitleProps) {
|
|
/* ── 标题库 API ── */
|
|
const { data: userTitles = [] } = useQuery({
|
|
queryKey: ["titles"],
|
|
queryFn: () => getTitles(),
|
|
staleTime: 30_000,
|
|
})
|
|
|
|
/* ── AI 标题生成状态 ── */
|
|
const [aiTitleInput, setAiTitleInput] = useState("")
|
|
const [aiTitleGenerating, setAiTitleGenerating] = useState(false)
|
|
const [aiTitleResults, setAiTitleResults] = useState<AiTitleItem[]>([])
|
|
const [hasGeneratedTitles, setHasGeneratedTitles] = useState(false)
|
|
|
|
/* ── 辅助函数 ── */
|
|
const extractTopic = (text: string): string => {
|
|
const keywords = text
|
|
.replace(/[,。!?、,.!?]/g, " ")
|
|
.split(/\s+/)
|
|
.filter(Boolean)
|
|
if (keywords.length === 0) return "这个话题"
|
|
// 取前3个关键词组合
|
|
return keywords.slice(0, 3).join("")
|
|
}
|
|
|
|
const getActivePreset = (settings: TitleSettings): string | null => {
|
|
for (const p of TITLE_PRESETS) {
|
|
if (
|
|
settings.size === p.style.size &&
|
|
settings.color === p.style.color &&
|
|
settings.bold === p.style.bold &&
|
|
settings.italic === p.style.italic &&
|
|
settings.stroke === p.style.stroke &&
|
|
settings.shadow === p.style.shadow
|
|
) {
|
|
return p.key
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
const activePreset = useMemo(() => getActivePreset(titleSettings), [titleSettings])
|
|
|
|
/* ── AI 标题生成 ── */
|
|
const handleGenerateAiTitles = useCallback(async () => {
|
|
if (!aiTitleInput.trim()) {
|
|
message.warning("请先输入视频描述或关键词")
|
|
return
|
|
}
|
|
setAiTitleGenerating(true)
|
|
setHasGeneratedTitles(true)
|
|
|
|
// 模拟 AI 生成延迟
|
|
await new Promise((resolve) => setTimeout(resolve, 1200))
|
|
|
|
const topic = extractTopic(aiTitleInput)
|
|
const results: AiTitleItem[] = []
|
|
|
|
const styles: Array<"catchy" | "emotional" | "informative"> = [
|
|
"catchy",
|
|
"emotional",
|
|
"informative",
|
|
]
|
|
styles.forEach((style) => {
|
|
const templates = AI_TITLE_TEMPLATES[style]
|
|
// 每种风格随机选2个
|
|
const shuffled = [...templates].sort(() => Math.random() - 0.5).slice(0, 2)
|
|
shuffled.forEach((tpl) => {
|
|
const title = tpl.replace(/\{topic\}/g, topic)
|
|
const highlights = {
|
|
catchy: "吸睛标题",
|
|
emotional: "情感共鸣",
|
|
informative: "知识干货",
|
|
}
|
|
results.push({
|
|
title,
|
|
highlight: highlights[style],
|
|
style,
|
|
})
|
|
})
|
|
})
|
|
|
|
// 打乱顺序
|
|
results.sort(() => Math.random() - 0.5)
|
|
setAiTitleResults(results)
|
|
setAiTitleGenerating(false)
|
|
}, [aiTitleInput])
|
|
|
|
const handleSelectAiTitle = useCallback(
|
|
(title: string) => {
|
|
onTitleSettingsChange({ ...titleSettings, title, aiAutoSelect: false })
|
|
message.success("已选用此标题")
|
|
},
|
|
[titleSettings, onTitleSettingsChange],
|
|
)
|
|
|
|
const handleRefreshAiTitles = useCallback(async () => {
|
|
if (!aiTitleInput.trim()) return
|
|
setAiTitleGenerating(true)
|
|
await new Promise((resolve) => setTimeout(resolve, 800))
|
|
// 重新生成一批
|
|
const topic = extractTopic(aiTitleInput)
|
|
const results: AiTitleItem[] = []
|
|
const styles: Array<"catchy" | "emotional" | "informative"> = [
|
|
"catchy",
|
|
"emotional",
|
|
"informative",
|
|
]
|
|
const highlights = { catchy: "吸睛标题", emotional: "情感共鸣", informative: "知识干货" }
|
|
styles.forEach((style) => {
|
|
const templates = AI_TITLE_TEMPLATES[style]
|
|
const shuffled = [...templates].sort(() => Math.random() - 0.5).slice(0, 2)
|
|
shuffled.forEach((tpl) => {
|
|
results.push({
|
|
title: tpl.replace(/\{topic\}/g, topic),
|
|
highlight: highlights[style],
|
|
style,
|
|
})
|
|
})
|
|
})
|
|
results.sort(() => Math.random() - 0.5)
|
|
setAiTitleResults(results)
|
|
setAiTitleGenerating(false)
|
|
}, [aiTitleInput])
|
|
|
|
/* ── 标题设置更新 ── */
|
|
const updateTitle = useCallback(
|
|
(title: string) => {
|
|
onTitleSettingsChange({ ...titleSettings, title })
|
|
},
|
|
[titleSettings, onTitleSettingsChange],
|
|
)
|
|
|
|
const toggleAiAutoSelect = useCallback(() => {
|
|
onTitleSettingsChange({ ...titleSettings, aiAutoSelect: !titleSettings.aiAutoSelect })
|
|
}, [titleSettings, onTitleSettingsChange])
|
|
|
|
const updatePosition = useCallback(
|
|
(position: string) => {
|
|
onTitleSettingsChange({ ...titleSettings, position })
|
|
},
|
|
[titleSettings, onTitleSettingsChange],
|
|
)
|
|
|
|
const updateFont = useCallback(
|
|
(font: string) => {
|
|
onTitleSettingsChange({ ...titleSettings, font })
|
|
},
|
|
[titleSettings, onTitleSettingsChange],
|
|
)
|
|
|
|
const updateSize = useCallback(
|
|
(size: number) => {
|
|
onTitleSettingsChange({ ...titleSettings, size })
|
|
},
|
|
[titleSettings, onTitleSettingsChange],
|
|
)
|
|
|
|
const updateColor = useCallback(
|
|
(color: string) => {
|
|
onTitleSettingsChange({ ...titleSettings, color })
|
|
},
|
|
[titleSettings, onTitleSettingsChange],
|
|
)
|
|
|
|
const toggleBold = useCallback(() => {
|
|
onTitleSettingsChange({ ...titleSettings, bold: !titleSettings.bold })
|
|
}, [titleSettings, onTitleSettingsChange])
|
|
|
|
const toggleItalic = useCallback(() => {
|
|
onTitleSettingsChange({ ...titleSettings, italic: !titleSettings.italic })
|
|
}, [titleSettings, onTitleSettingsChange])
|
|
|
|
const toggleStroke = useCallback(() => {
|
|
onTitleSettingsChange({ ...titleSettings, stroke: !titleSettings.stroke })
|
|
}, [titleSettings, onTitleSettingsChange])
|
|
|
|
const toggleShadow = useCallback(() => {
|
|
onTitleSettingsChange({ ...titleSettings, shadow: !titleSettings.shadow })
|
|
}, [titleSettings, onTitleSettingsChange])
|
|
|
|
const applyPreset = useCallback(
|
|
(presetKey: string) => {
|
|
const preset = TITLE_PRESETS.find((p) => p.key === presetKey)
|
|
if (!preset) return
|
|
onTitleSettingsChange({
|
|
...titleSettings,
|
|
size: preset.style.size,
|
|
color: preset.style.color,
|
|
bold: preset.style.bold,
|
|
italic: preset.style.italic,
|
|
stroke: preset.style.stroke,
|
|
shadow: preset.style.shadow,
|
|
})
|
|
},
|
|
[titleSettings, onTitleSettingsChange],
|
|
)
|
|
|
|
return {
|
|
// 数据
|
|
userTitles,
|
|
titleSettings,
|
|
aiTitleInput,
|
|
setAiTitleInput,
|
|
aiTitleGenerating,
|
|
aiTitleResults,
|
|
hasGeneratedTitles,
|
|
activePreset,
|
|
titlePresets: TITLE_PRESETS,
|
|
// AI 标题操作
|
|
handleGenerateAiTitles,
|
|
handleSelectAiTitle,
|
|
handleRefreshAiTitles,
|
|
// 标题设置操作
|
|
updateTitle,
|
|
toggleAiAutoSelect,
|
|
updatePosition,
|
|
updateFont,
|
|
updateSize,
|
|
updateColor,
|
|
toggleBold,
|
|
toggleItalic,
|
|
toggleStroke,
|
|
toggleShadow,
|
|
applyPreset,
|
|
}
|
|
}
|
|
|
|
export default useStep4Title
|