完善合集标题编辑功能和优化切片卡片布局

合集标题编辑功能:
- 创建专门的合集标题生成prompt,参考切片标题的吸引人风格
- 在collections API中添加生成和更新标题的接口
- 创建EditableCollectionTitle组件,支持编辑和AI生成
- 在CollectionCard和CollectionCardMini中集成编辑功能
- 修复合集标题更新后页面不立即刷新的问题

切片卡片布局优化:
- 固定各元素高度,避免按钮位置受内容影响
- 标题区域固定44px高度,内容要点固定58px高度
- 按钮区域固定在底部,与边缘保持16px边距
- 文本超出时显示省略号,hover时通过Tooltip显示完整内容
- 优化布局结构,确保所有卡片元素高度一致
This commit is contained in:
Kris Ka
2025-09-15 20:50:19 +08:00
parent 1a0e93f978
commit 55707d4702
11 changed files with 600 additions and 63 deletions

View File

@@ -125,3 +125,46 @@ NFO - WebSocket网关服务已禁用
2025-09-15 19:33:31,936 - backend.main - INFO - WebSocket网关服务已禁用使用新的简化进度系统
2025-09-15 19:33:48,972 - backend.core.llm_manager - INFO - 已初始化dashscope提供商模型: qwen-plus
2025-09-15 20:19:52,449 - backend.api.v1.projects - ERROR - 生成合集视频失败: clips_dir 参数是必需的,不能使用全局路径
2025-09-15 20:26:48,205 - backend.main - INFO - 正在关闭AutoClip API服务...
2025-09-15 20:26:48,206 - backend.main - INFO - WebSocket网关服务已禁用
2025-09-15 20:26:48,851 - backend.services.processing_orchestrator - INFO - 流水线模块导入成功
2025-09-15 20:26:49,103 - backend.services.simple_progress - INFO - Redis连接成功
2025-09-15 20:26:49,174 - backend.main - INFO - 启动AutoClip API服务...
2025-09-15 20:26:49,175 - backend.main - INFO - 数据库表创建完成
2025-09-15 20:26:49,175 - backend.main - INFO - API密钥已加载到环境变量
2025-09-15 20:26:49,175 - backend.main - INFO - WebSocket网关服务已禁用使用新的简化进度系统
2025-09-15 20:26:55,341 - backend.main - INFO - 正在关闭AutoClip API服务...
2025-09-15 20:26:55,341 - backend.main - INFO - WebSocket网关服务已禁用
2025-09-15 20:26:55,844 - backend.services.processing_orchestrator - INFO - 流水线模块导入成功
2025-09-15 20:26:56,041 - backend.services.simple_progress - INFO - Redis连接成功
2025-09-15 20:26:56,111 - backend.main - INFO - 启动AutoClip API服务...
2025-09-15 20:26:56,112 - backend.main - INFO - 数据库表创建完成
2025-09-15 20:26:56,112 - backend.main - INFO - API密钥已加载到环境变量
2025-09-15 20:26:56,112 - backend.main - INFO - WebSocket网关服务已禁用使用新的简化进度系统
2025-09-15 20:27:05,918 - backend.main - INFO - 正在关闭AutoClip API服务...
2025-09-15 20:27:05,918 - backend.main - INFO - WebSocket网关服务已禁用
2025-09-15 20:27:06,434 - backend.services.processing_orchestrator - INFO - 流水线模块导入成功
2025-09-15 20:27:06,631 - backend.services.simple_progress - INFO - Redis连接成功
2025-09-15 20:27:06,700 - backend.main - INFO - 启动AutoClip API服务...
2025-09-15 20:27:06,701 - backend.main - INFO - 数据库表创建完成
2025-09-15 20:27:06,701 - backend.main - INFO - API密钥已加载到环境变量
2025-09-15 20:27:06,701 - backend.main - INFO - WebSocket网关服务已禁用使用新的简化进度系统
2025-09-15 20:29:47,869 - backend.api.v1.collections - ERROR - 生成合集标题失败: 'ClipRepository' object has no attribute 'get'
2025-09-15 20:30:00,211 - backend.main - INFO - 正在关闭AutoClip API服务...
2025-09-15 20:30:00,211 - backend.main - INFO - WebSocket网关服务已禁用
2025-09-15 20:30:00,840 - backend.services.processing_orchestrator - INFO - 流水线模块导入成功
2025-09-15 20:30:01,091 - backend.services.simple_progress - INFO - Redis连接成功
2025-09-15 20:30:01,164 - backend.main - INFO - 启动AutoClip API服务...
2025-09-15 20:30:01,165 - backend.main - INFO - 数据库表创建完成
2025-09-15 20:30:01,165 - backend.main - INFO - API密钥已加载到环境变量
2025-09-15 20:30:01,166 - backend.main - INFO - WebSocket网关服务已禁用使用新的简化进度系统
2025-09-15 20:30:04,931 - backend.core.llm_manager - INFO - 已初始化dashscope提供商模型: qwen-plus
2025-09-15 20:30:30,747 - backend.main - INFO - 正在关闭AutoClip API服务...
2025-09-15 20:30:30,747 - backend.main - INFO - WebSocket网关服务已禁用
2025-09-15 20:30:31,383 - backend.services.processing_orchestrator - INFO - 流水线模块导入成功
2025-09-15 20:30:31,593 - backend.services.simple_progress - INFO - Redis连接成功
2025-09-15 20:30:31,665 - backend.main - INFO - 启动AutoClip API服务...
2025-09-15 20:30:31,666 - backend.main - INFO - 数据库表创建完成
2025-09-15 20:30:31,666 - backend.main - INFO - API密钥已加载到环境变量
2025-09-15 20:30:31,666 - backend.main - INFO - WebSocket网关服务已禁用使用新的简化进度系统
2025-09-15 20:37:07,166 - backend.core.llm_manager - INFO - 已初始化dashscope提供商模型: qwen-plus

View File

@@ -2,6 +2,7 @@
合集API路由
"""
import logging
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
@@ -10,6 +11,8 @@ from ...services.collection_service import CollectionService
from ...schemas.collection import CollectionCreate, CollectionUpdate, CollectionResponse, CollectionListResponse
from ...schemas.base import PaginationParams
logger = logging.getLogger(__name__)
router = APIRouter()
@@ -238,4 +241,123 @@ async def reorder_collection_clips(
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
raise HTTPException(status_code=400, detail=str(e))
@router.post("/{collection_id}/generate-title", response_model=dict)
async def generate_collection_title(
collection_id: str,
collection_service: CollectionService = Depends(get_collection_service)
):
"""Generate a new title for a collection using LLM."""
try:
collection = collection_service.get(collection_id)
if not collection:
raise HTTPException(status_code=404, detail="合集不存在")
# 获取合集元数据
collection_metadata = getattr(collection, 'collection_metadata', {}) or {}
if not collection_metadata:
raise HTTPException(status_code=404, detail="合集元数据不存在")
# 获取合集中的切片信息
clip_ids = collection_metadata.get('clip_ids', [])
if not clip_ids:
raise HTTPException(status_code=404, detail="合集中没有切片")
# 获取切片详细信息
from ...repositories.clip_repository import ClipRepository
clip_repo = ClipRepository(collection_service.db)
clips_data = []
for clip_id in clip_ids:
clip = clip_repo.get_by_id(clip_id)
if clip:
clip_metadata = getattr(clip, 'clip_metadata', {}) or {}
clips_data.append({
"id": clip_id,
"title": clip_metadata.get('outline', '') or getattr(clip, 'title', ''),
"content": clip_metadata.get('content', []),
"recommend_reason": clip_metadata.get('recommend_reason', '')
})
if not clips_data:
raise HTTPException(status_code=404, detail="无法获取切片内容")
# 构建LLM输入
llm_input = {
"collection_id": collection_id,
"collection_title": collection.name,
"collection_description": collection.description or "",
"clips": clips_data,
"total_duration_minutes": collection_metadata.get('total_duration_minutes', 0),
"target_audience": collection_metadata.get('target_audience', ''),
"key_themes": collection_metadata.get('key_themes', [])
}
# 调用LLM生成标题
from ...utils.llm_client import LLMClient
from ...core.shared_config import PROMPT_FILES
llm_client = LLMClient()
with open(PROMPT_FILES['collection_title'], 'r', encoding='utf-8') as f:
title_prompt = f.read()
raw_response = llm_client.call_with_retry(title_prompt, llm_input)
if not raw_response:
raise HTTPException(status_code=500, detail="LLM调用失败")
title_result = llm_client.parse_json_response(raw_response)
if not isinstance(title_result, dict) or 'generated_title' not in title_result:
raise HTTPException(status_code=500, detail="LLM返回格式错误")
generated_title = title_result['generated_title']
return {
"collection_id": collection_id,
"generated_title": generated_title,
"success": True
}
except HTTPException:
raise
except Exception as e:
logger.error(f"生成合集标题失败: {e}")
raise HTTPException(status_code=500, detail=f"生成合集标题失败: {str(e)}")
@router.put("/{collection_id}/title", response_model=dict)
async def update_collection_title(
collection_id: str,
title_data: dict,
collection_service: CollectionService = Depends(get_collection_service)
):
"""Update collection title."""
try:
new_title = title_data.get('title')
if not new_title:
raise HTTPException(status_code=400, detail="标题不能为空")
collection = collection_service.get(collection_id)
if not collection:
raise HTTPException(status_code=404, detail="合集不存在")
# 更新合集标题
collection.name = new_title
collection_service.db.commit()
return {
"collection_id": collection_id,
"title": new_title,
"success": True
}
except HTTPException:
raise
except Exception as e:
logger.error(f"更新合集标题失败: {e}")
raise HTTPException(status_code=500, detail=f"更新合集标题失败: {str(e)}")

View File

@@ -95,7 +95,8 @@ PROMPT_FILES = {
"timeline": PROMPT_DIR / "时间点.txt",
"recommendation": PROMPT_DIR / "推荐理由.txt",
"title": PROMPT_DIR / "标题生成.txt",
"clustering": PROMPT_DIR / "主题聚类.txt"
"clustering": PROMPT_DIR / "主题聚类.txt",
"collection_title": PROMPT_DIR / "collection_title.txt"
}
# API配置

View File

@@ -0,0 +1,79 @@
#角色设定
你是一位顶级的短视频内容策划,深谙爆款合集标题的创作逻辑。你的任务是为视频合集生成一个最佳的、高质量、高点击率但**绝不脱离内容**的标题。
## 核心原则
1. **忠于内容**: 标题的立意必须直接源自合集内容,严禁无中生有。
2. **拒绝夸张**: 避免使用"震惊"、"惊呆了"等过度营销的词汇。
3. **突出亮点**: 标题需精准捕捉合集最核心的价值、最吸引人的看点或最有用的信息。
4. **精炼有力**: 标题必须简洁、有冲击力,能迅速抓住用户眼球。
5. **合集特色**: 体现合集的完整性和系列性,让用户感受到这是一个完整的内容体系。
## 输入格式
你将收到一个JSON对象包含合集的详细信息
```json
{
"collection_id": "collection_123",
"collection_title": "科技投资全景分析",
"collection_description": "从政策红利到具体标的,全面解析当前科技投资机会与策略,帮助投资者把握科技股投资节奏。",
"clips": [
{
"id": "clip_1",
"title": "科技股投资别追高抓住AI基建这个核心才是关键",
"content": ["算力基建是核心", "AI基建值得关注", "避免追高"],
"recommend_reason": "观点犀利,信息密度极高,精准剖析了当前科技股的核心投资逻辑。"
},
{
"id": "clip_2",
"title": "从迷信大牌到爱上国货,这届年轻人的消费观有多清醒?",
"content": ["不再迷信大牌", "更注重性价比和国货", "理性消费成为主流"],
"recommend_reason": "视角独特,紧贴年轻消费趋势,具有很强的话题性和讨论潜力。"
}
],
"total_duration_minutes": 18,
"target_audience": "科技股投资者",
"key_themes": ["科技股投资", "政策解读", "选股策略"]
}
```
## 任务要求
为输入的合集生成**1个**最佳标题,要求:
- 体现合集的核心价值和看点
- 吸引目标受众点击
- 突出内容的实用性和完整性
- 长度控制在15-25字之间
## 标题风格参考
### 投资理财类:
- "科技股投资全攻略从政策到实操教你抓住AI时代红利"
- "年轻人理财觉醒:从月光到财务自由,这套方法太实用了"
- "A股投资避坑指南散户必知的5个关键策略"
### 职场成长类:
- "职场逆袭秘籍:从社恐到自信,这套沟通技巧改变了我"
- "技能变现指南:如何把爱好变成赚钱的副业"
- "职场新人必看避开这些坑让你少走3年弯路"
### 社会观察类:
- "当代年轻人图鉴:从消费观到价值观,这届年轻人太清醒了"
- "网络时代生存指南:如何在信息爆炸中保持独立思考"
- "社会现象深度解析:那些被忽视的真相和背后的逻辑"
### 生活情感类:
- "恋爱脱单攻略从搭讪到表白这套方法成功率90%"
- "健康生活指南:从熬夜到早睡,我是如何改变生活方式的"
- "情感关系解析:为什么有些人总是遇不到对的人"
## 输出格式
请严格按照下面的JSON格式输出
```json
{
"collection_id": "collection_123",
"generated_title": "科技股投资全攻略从政策到实操教你抓住AI时代红利"
}
```
## 注意事项:
- 输出的`collection_id`必须是输入的`collection_id`。
- `generated_title`必须是一个字符串长度控制在15-25字之间。
- 标题要体现合集的价值和完整性,让用户感受到这是一个完整的内容体系。
- 最终输出必须是**一个完整的JSON对象**,不要添加任何其他解释性文字。

Binary file not shown.

View File

@@ -354,53 +354,84 @@ const ClipCard: React.FC<ClipCardProps> = ({
</div>
}
>
<div style={{ padding: '16px', height: '180px', display: 'flex', flexDirection: 'column' }}>
{/* 标题区域 */}
<div style={{ marginBottom: '8px' }}>
<EditableTitle
title={clip.title || clip.generated_title || '未命名片段'}
clipId={clip.id}
onTitleUpdate={handleTitleUpdate}
style={{
fontSize: '16px',
fontWeight: 600,
lineHeight: '1.4',
color: '#ffffff',
minHeight: '44px'
}}
/>
</div>
{/* 内容要点 */}
<div style={{ flex: 1, marginBottom: '12px', minHeight: '58px' }}>
<Tooltip
title={getDisplayContent()}
placement="top"
overlayStyle={{ maxWidth: '300px' }}
mouseEnterDelay={0.5}
>
<div
ref={textRef}
<div style={{
padding: '16px',
height: '180px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between'
}}>
{/* 内容区域 - 固定高度 */}
<div style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
minHeight: 0 // 允许flex子项收缩
}}>
{/* 标题区域 - 固定高度 */}
<div style={{
height: '44px',
marginBottom: '8px',
display: 'flex',
alignItems: 'flex-start'
}}>
<EditableTitle
title={clip.title || clip.generated_title || '未命名片段'}
clipId={clip.id}
onTitleUpdate={handleTitleUpdate}
style={{
fontSize: '13px',
display: '-webkit-box',
WebkitLineClamp: 3,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
lineHeight: '1.5',
color: '#b0b0b0',
cursor: 'pointer',
wordBreak: 'break-word',
textOverflow: 'ellipsis'
fontSize: '16px',
fontWeight: 600,
lineHeight: '1.4',
color: '#ffffff',
width: '100%'
}}
/>
</div>
{/* 内容要点 - 固定高度 */}
<div style={{
height: '58px',
marginBottom: '12px',
display: 'flex',
alignItems: 'flex-start'
}}>
<Tooltip
title={getDisplayContent()}
placement="top"
overlayStyle={{ maxWidth: '300px' }}
mouseEnterDelay={0.5}
>
{getDisplayContent()}
</div>
</Tooltip>
<div
ref={textRef}
style={{
fontSize: '13px',
display: '-webkit-box',
WebkitLineClamp: 3,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
lineHeight: '1.5',
color: '#b0b0b0',
cursor: 'pointer',
wordBreak: 'break-word',
textOverflow: 'ellipsis',
width: '100%'
}}
>
{getDisplayContent()}
</div>
</Tooltip>
</div>
</div>
{/* 操作按钮 */}
<div style={{ display: 'flex', gap: '8px' }}>
{/* 操作按钮 - 固定在底部 */}
<div style={{
display: 'flex',
gap: '8px',
height: '28px',
alignItems: 'center',
marginTop: 'auto'
}}>
<Button
type="text"
size="small"

View File

@@ -3,6 +3,7 @@ import { Card, Typography, Button, Space, Input, Tag, List, Modal, Tooltip, Divi
import { EditOutlined, DownloadOutlined, SaveOutlined, CloseOutlined, PlayCircleOutlined, DragOutlined, DeleteOutlined } from '@ant-design/icons'
import { Collection, Clip } from '../store/useProjectStore'
import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd'
import EditableCollectionTitle from './EditableCollectionTitle'
const { Text, Title } = Typography
const { TextArea } = Input
@@ -236,9 +237,21 @@ const CollectionCard: React.FC<CollectionCardProps> = ({
</Space>
) : (
<Space direction="vertical" style={{ width: '100%' }} size="small">
<Title level={5} ellipsis={{ rows: 2 }} style={{ margin: 0, minHeight: '44px' }}>
{collection.collection_title}
</Title>
<div style={{ minHeight: '44px' }}>
<EditableCollectionTitle
title={collection.collection_title}
collectionId={collection.id}
onTitleUpdate={(newTitle) => {
// 更新合集标题
onUpdate(collection.id, { collection_title: newTitle })
}}
style={{
fontSize: '16px',
fontWeight: '600',
color: '#ffffff'
}}
/>
</div>
<div>
{collection.collection_type === 'manual' ? (

View File

@@ -2,6 +2,7 @@ import React, { useState } from 'react'
import { Card, Typography, Button, Popconfirm } from 'antd'
import { DeleteOutlined } from '@ant-design/icons'
import { Collection, Clip } from '../store/useProjectStore'
import EditableCollectionTitle from './EditableCollectionTitle'
const { Text, Title } = Typography
@@ -11,6 +12,7 @@ interface CollectionCardMiniProps {
onView: (collection: Collection) => void
onGenerateVideo?: (collectionId: string) => void
onDelete?: (collectionId: string) => void
onUpdate?: (collectionId: string, updates: Partial<Collection>) => void
}
const CollectionCardMini: React.FC<CollectionCardMiniProps> = ({
@@ -18,7 +20,8 @@ const CollectionCardMini: React.FC<CollectionCardMiniProps> = ({
clips,
onView,
onGenerateVideo,
onDelete
onDelete,
onUpdate
}) => {
const [isHovered, setIsHovered] = useState(false)
// 按照collection.clip_ids的顺序排列clips
@@ -75,21 +78,27 @@ const CollectionCardMini: React.FC<CollectionCardMiniProps> = ({
alignItems: 'flex-start',
marginBottom: '12px'
}}>
<Title
level={5}
ellipsis={{ rows: 2 }}
style={{
margin: 0,
fontSize: '15px',
fontWeight: 600,
lineHeight: '1.4',
color: '#ffffff',
flex: 1,
paddingRight: '8px'
}}
>
{collection.collection_title}
</Title>
<div style={{
flex: 1,
paddingRight: '8px'
}}>
<EditableCollectionTitle
title={collection.collection_title}
collectionId={collection.id}
onTitleUpdate={(newTitle) => {
// 更新合集标题
if (onUpdate) {
onUpdate(collection.id, { collection_title: newTitle })
}
}}
style={{
fontSize: '15px',
fontWeight: 600,
lineHeight: '1.4',
color: '#ffffff'
}}
/>
</div>
<span
style={{
fontSize: '10px',

View File

@@ -0,0 +1,226 @@
import React, { useState, useRef, useEffect } from 'react'
import { Input, Button, Space, message, Tooltip, Modal } from 'antd'
import { EditOutlined, CheckOutlined, CloseOutlined } from '@ant-design/icons'
import { projectApi } from '../services/api'
import MagicWandIcon from './icons/MagicWandIcon'
interface EditableCollectionTitleProps {
title: string
collectionId: string
onTitleUpdate?: (newTitle: string) => void
maxLength?: number
style?: React.CSSProperties
className?: string
}
const EditableCollectionTitle: React.FC<EditableCollectionTitleProps> = ({
title,
collectionId,
onTitleUpdate,
maxLength = 50,
style,
className
}) => {
const [isEditing, setIsEditing] = useState(false)
const [editValue, setEditValue] = useState(title)
const [loading, setLoading] = useState(false)
const [generating, setGenerating] = useState(false)
const inputRef = useRef<any>(null)
useEffect(() => {
setEditValue(title)
}, [title])
useEffect(() => {
if (!isEditing) {
setEditValue(title)
}
}, [title, isEditing])
useEffect(() => {
if (isEditing && inputRef.current) {
inputRef.current.focus()
// TextArea组件没有select方法使用setSelectionRange代替
if (inputRef.current.setSelectionRange) {
inputRef.current.setSelectionRange(0, inputRef.current.value.length)
}
}
}, [isEditing])
const handleStartEdit = () => {
setEditValue(title)
setIsEditing(true)
}
const handleCancel = () => {
setEditValue(title)
setIsEditing(false)
}
const handleSave = async () => {
const trimmedValue = editValue.trim()
if (!trimmedValue) {
message.error('标题不能为空')
return
}
if (trimmedValue.length > maxLength) {
message.error(`标题长度不能超过${maxLength}个字符`)
return
}
if (trimmedValue === title) {
setIsEditing(false)
return
}
setLoading(true)
try {
await projectApi.updateCollectionTitle(collectionId, trimmedValue)
message.success('标题更新成功')
setIsEditing(false)
onTitleUpdate?.(trimmedValue)
} catch (error: any) {
console.error('更新标题失败:', error)
message.error(error.userMessage || error.message || '更新标题失败')
} finally {
setLoading(false)
}
}
const handleGenerateTitle = async () => {
console.log('开始生成合集标题collectionId:', collectionId)
setGenerating(true)
try {
const result = await projectApi.generateCollectionTitle(collectionId)
console.log('生成合集标题结果:', result)
if (result.success && result.generated_title) {
setEditValue(result.generated_title)
message.success('标题生成成功,您可以继续编辑或点击保存')
} else {
message.error('标题生成失败')
}
} catch (error: any) {
console.error('生成标题失败:', error)
message.error(error.userMessage || error.message || '生成标题失败')
} finally {
setGenerating(false)
}
}
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
handleSave()
} else if (e.key === 'Escape') {
handleCancel()
}
}
if (isEditing) {
return (
<Modal
title="编辑合集标题"
open={isEditing}
onCancel={handleCancel}
footer={null}
width={600}
destroyOnClose
maskClosable={false}
>
<div style={{ marginBottom: '16px' }}>
<Input.TextArea
ref={inputRef}
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
onKeyDown={handleKeyPress}
maxLength={maxLength}
placeholder="请输入合集标题"
autoSize={{ minRows: 3, maxRows: 8 }}
style={{
resize: 'none',
fontSize: '14px',
lineHeight: '1.5'
}}
/>
<div style={{
textAlign: 'right',
marginTop: '8px',
fontSize: '12px',
color: '#999'
}}>
{editValue.length}/{maxLength}
</div>
</div>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<Button
onClick={handleCancel}
disabled={loading || generating}
>
</Button>
<Space>
<Button
icon={<MagicWandIcon />}
loading={generating}
onClick={handleGenerateTitle}
disabled={loading}
>
AI生成标题
</Button>
<Button
type="primary"
icon={<CheckOutlined />}
loading={loading}
onClick={handleSave}
disabled={generating}
>
</Button>
</Space>
</div>
</Modal>
)
}
return (
<div
style={{
cursor: 'pointer',
padding: '4px 0',
...style
}}
className={className}
onClick={handleStartEdit}
title="点击编辑合集标题"
>
<span style={{
wordBreak: 'break-word',
lineHeight: '1.5',
fontSize: '14px',
minHeight: '20px',
display: 'inline'
}}>
{title}
<EditOutlined
style={{
color: '#1890ff',
fontSize: '12px',
opacity: 0.7,
transition: 'opacity 0.2s',
marginLeft: '6px',
display: 'inline'
}}
/>
</span>
</div>
)
}
export default EditableCollectionTitle

View File

@@ -381,6 +381,9 @@ const ProjectDetailPage: React.FC = () => {
collection={collection}
clips={currentProject.clips || []}
onView={handleViewCollection}
onUpdate={(collectionId, updates) =>
updateCollection(currentProject.id, collectionId, updates)
}
onGenerateVideo={async (collectionId) => {
const collection = currentProject.collections?.find(c => c.id === collectionId)
if (collection) {

View File

@@ -328,6 +328,16 @@ export const projectApi = {
return api.delete(`/collections/${collectionId}`)
},
// 生成合集标题
generateCollectionTitle: (collectionId: string): Promise<{collection_id: string, generated_title: string, success: boolean}> => {
return api.post(`/collections/${collectionId}/generate-title`)
},
// 更新合集标题
updateCollectionTitle: (collectionId: string, title: string): Promise<{collection_id: string, title: string, success: boolean}> => {
return api.put(`/collections/${collectionId}/title`, { title })
},
// 下载切片视频
downloadClip: (_projectId: string, clipId: string): Promise<Blob> => {
return api.get(`/files/projects/${_projectId}/clips/${clipId}`, {