chore: 删除前端冗余代码 (-1215行)
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m5s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 1m34s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 1m37s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m13s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 2m14s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m23s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 2m20s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 4m44s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 3m5s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 5s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 23s
AI Code Review / AI Code Review (pull_request) Successful in 6m21s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m5s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 1m34s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 1m37s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m13s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 2m14s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m23s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 2m20s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 4m44s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 3m5s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 5s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 23s
AI Code Review / AI Code Review (pull_request) Successful in 6m21s
Issue #1831 - 前端冗余代码清理 删除 5 处死代码: - editing-planner/components/sticker/ (5文件, 已被 StickerPanel 替代) - editing-planner/components/tts/ (2文件, 已被 TtsPanel 替代) - editing-planner/components/pip-config/ (6文件, 已被 PipConfigPanel 替代) - asset-operations/batch/ (5文件, batch-operations 的完全重复副本) - asset-operations/batchOperations.ts (@deprecated 死代码) 附带修复: - test/pages/assets/smoke.test.tsx: batch/ → batch-operations/ import 路径 - test/pages/editing-planner/smoke.test.tsx: 删除 9 条失效 import 验证: - TypeScript tsc --noEmit 无新增错误 - assets / editing-planner smoke test 全部通过 - 全项目 grep 确认无残留引用
This commit is contained in:
@@ -1,4 +0,0 @@
|
||||
export { useBatchDelete } from "./useBatchDelete"
|
||||
export { useBatchTag } from "./useBatchTag"
|
||||
export { useBatchClassify } from "./useBatchClassify"
|
||||
export { useBatchMark } from "./useBatchMark"
|
||||
@@ -1,58 +0,0 @@
|
||||
import { useState, useCallback } from "react"
|
||||
import { message } from "antd"
|
||||
import { batchClassifyAssets, type BatchOperationResult } from "@/api/assets"
|
||||
|
||||
interface UseBatchClassifyOptions {
|
||||
selectedIds: Set<string>
|
||||
queryClient: ReturnType<typeof import("@tanstack/react-query").useQueryClient>
|
||||
showResult: (result: BatchOperationResult, title: string, clear?: boolean) => void
|
||||
}
|
||||
|
||||
export const useBatchClassify = ({
|
||||
selectedIds,
|
||||
queryClient,
|
||||
showResult,
|
||||
}: UseBatchClassifyOptions) => {
|
||||
const [classifyModalOpen, setClassifyModalOpen] = useState(false)
|
||||
const [batchCategory, setBatchCategory] = useState("")
|
||||
const [batchLoading, setBatchLoading] = useState(false)
|
||||
|
||||
const handleBatchClassify = useCallback(async () => {
|
||||
if (!batchCategory) {
|
||||
message.warning("请选择分类")
|
||||
return
|
||||
}
|
||||
const ids = Array.from(selectedIds)
|
||||
setBatchLoading(true)
|
||||
try {
|
||||
const result = await batchClassifyAssets({
|
||||
asset_ids: ids,
|
||||
category: batchCategory,
|
||||
})
|
||||
queryClient.invalidateQueries({ queryKey: ["assets"] })
|
||||
showResult(result, "批量改分类")
|
||||
setClassifyModalOpen(false)
|
||||
setBatchCategory("")
|
||||
if (result.failure_count === 0) {
|
||||
message.success(`成功将 ${result.success_count} 个素材改为「${batchCategory}」`)
|
||||
} else {
|
||||
message.warning(
|
||||
`改分类完成:成功 ${result.success_count} 个,失败 ${result.failure_count} 个`,
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
message.error("批量改分类失败,请重试")
|
||||
} finally {
|
||||
setBatchLoading(false)
|
||||
}
|
||||
}, [batchCategory, selectedIds, queryClient, showResult])
|
||||
|
||||
return {
|
||||
classifyModalOpen,
|
||||
setClassifyModalOpen,
|
||||
batchCategory,
|
||||
setBatchCategory,
|
||||
batchLoading,
|
||||
handleBatchClassify,
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import { useState, useCallback } from "react"
|
||||
import { message } from "antd"
|
||||
import { batchDeleteAssets, type BatchOperationResult } from "@/api/assets"
|
||||
|
||||
interface UseBatchDeleteOptions {
|
||||
selectedIds: Set<string>
|
||||
invalidateAssets: () => void
|
||||
showResult: (result: BatchOperationResult, title: string, clear?: boolean) => void
|
||||
}
|
||||
|
||||
export const useBatchDelete = ({
|
||||
selectedIds,
|
||||
invalidateAssets,
|
||||
showResult,
|
||||
}: UseBatchDeleteOptions) => {
|
||||
const [batchLoading, setBatchLoading] = useState(false)
|
||||
|
||||
const handleBatchDelete = useCallback(async () => {
|
||||
const ids = Array.from(selectedIds)
|
||||
setBatchLoading(true)
|
||||
try {
|
||||
const result = await batchDeleteAssets(ids)
|
||||
invalidateAssets()
|
||||
showResult(result, "批量删除")
|
||||
if (result.failure_count === 0) {
|
||||
message.success(`成功删除 ${result.success_count} 个素材`)
|
||||
} else {
|
||||
message.warning(
|
||||
`删除完成:成功 ${result.success_count} 个,失败 ${result.failure_count} 个`,
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
message.error("批量删除失败,请重试")
|
||||
} finally {
|
||||
setBatchLoading(false)
|
||||
}
|
||||
}, [selectedIds, invalidateAssets, showResult])
|
||||
|
||||
return { batchLoading, handleBatchDelete }
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { useState, useCallback } from "react"
|
||||
import { message } from "antd"
|
||||
import { batchMarkAssets, type BatchOperationResult } from "@/api/assets"
|
||||
import type { SmartViewType } from "../../../components/BatchMarkModal"
|
||||
import { SMART_VIEW_LABELS } from "../constants"
|
||||
|
||||
interface UseBatchMarkOptions {
|
||||
selectedIds: Set<string>
|
||||
queryClient: ReturnType<typeof import("@tanstack/react-query").useQueryClient>
|
||||
showResult: (result: BatchOperationResult, title: string, clear?: boolean) => void
|
||||
}
|
||||
|
||||
export const useBatchMark = ({ selectedIds, queryClient, showResult }: UseBatchMarkOptions) => {
|
||||
const [markModalOpen, setMarkModalOpen] = useState(false)
|
||||
const [batchSmartView, setBatchSmartView] = useState<SmartViewType>("recommended")
|
||||
const [batchLoading, setBatchLoading] = useState(false)
|
||||
|
||||
const handleBatchMark = useCallback(async () => {
|
||||
const ids = Array.from(selectedIds)
|
||||
setBatchLoading(true)
|
||||
try {
|
||||
const result = await batchMarkAssets({
|
||||
asset_ids: ids,
|
||||
smart_view: batchSmartView,
|
||||
})
|
||||
queryClient.invalidateQueries({ queryKey: ["assets"] })
|
||||
showResult(result, "批量智能标记")
|
||||
setMarkModalOpen(false)
|
||||
if (result.failure_count === 0) {
|
||||
message.success(
|
||||
`成功将 ${result.success_count} 个素材标记为「${SMART_VIEW_LABELS[batchSmartView]}」`,
|
||||
)
|
||||
} else {
|
||||
message.warning(
|
||||
`智能标记完成:成功 ${result.success_count} 个,失败 ${result.failure_count} 个`,
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
message.error("批量智能标记失败,请重试")
|
||||
} finally {
|
||||
setBatchLoading(false)
|
||||
}
|
||||
}, [batchSmartView, selectedIds, queryClient, showResult])
|
||||
|
||||
return {
|
||||
markModalOpen,
|
||||
setMarkModalOpen,
|
||||
batchSmartView,
|
||||
setBatchSmartView,
|
||||
batchLoading,
|
||||
handleBatchMark,
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
import { useState, useCallback } from "react"
|
||||
import { message } from "antd"
|
||||
import { batchTagAssets, type BatchOperationResult } from "@/api/assets"
|
||||
|
||||
interface UseBatchTagOptions {
|
||||
selectedIds: Set<string>
|
||||
queryClient: ReturnType<typeof import("@tanstack/react-query").useQueryClient>
|
||||
showResult: (result: BatchOperationResult, title: string, clear?: boolean) => void
|
||||
}
|
||||
|
||||
export const useBatchTag = ({ selectedIds, queryClient, showResult }: UseBatchTagOptions) => {
|
||||
const [tagModalOpen, setTagModalOpen] = useState(false)
|
||||
const [batchTagInput, setBatchTagInput] = useState("")
|
||||
const [batchTags, setBatchTags] = useState<string[]>([])
|
||||
const [tagMode, setTagMode] = useState<"add" | "replace">("add")
|
||||
const [batchLoading, setBatchLoading] = useState(false)
|
||||
|
||||
const handleBatchTag = useCallback(async () => {
|
||||
if (batchTags.length === 0) {
|
||||
message.warning("请至少输入一个标签")
|
||||
return
|
||||
}
|
||||
const ids = Array.from(selectedIds)
|
||||
setBatchLoading(true)
|
||||
try {
|
||||
const result = await batchTagAssets({
|
||||
asset_ids: ids,
|
||||
tags: batchTags,
|
||||
mode: tagMode,
|
||||
})
|
||||
queryClient.invalidateQueries({ queryKey: ["assets"] })
|
||||
showResult(result, "批量打标签")
|
||||
setTagModalOpen(false)
|
||||
setBatchTags([])
|
||||
setBatchTagInput("")
|
||||
setTagMode("add")
|
||||
if (result.failure_count === 0) {
|
||||
message.success(`成功为 ${result.success_count} 个素材打标签`)
|
||||
} else {
|
||||
message.warning(
|
||||
`打标签完成:成功 ${result.success_count} 个,失败 ${result.failure_count} 个`,
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
message.error("批量打标签失败,请重试")
|
||||
} finally {
|
||||
setBatchLoading(false)
|
||||
}
|
||||
}, [batchTags, selectedIds, tagMode, queryClient, showResult])
|
||||
|
||||
const handleTagInputKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (e.key === "Enter" && batchTagInput.trim()) {
|
||||
e.preventDefault()
|
||||
const tag = batchTagInput.trim()
|
||||
if (!batchTags.includes(tag)) {
|
||||
setBatchTags([...batchTags, tag])
|
||||
}
|
||||
setBatchTagInput("")
|
||||
}
|
||||
},
|
||||
[batchTagInput, batchTags],
|
||||
)
|
||||
|
||||
const removeBatchTag = useCallback(
|
||||
(tag: string) => {
|
||||
setBatchTags(batchTags.filter((t) => t !== tag))
|
||||
},
|
||||
[batchTags],
|
||||
)
|
||||
|
||||
return {
|
||||
tagModalOpen,
|
||||
setTagModalOpen,
|
||||
batchTagInput,
|
||||
setBatchTagInput,
|
||||
batchTags,
|
||||
setBatchTags,
|
||||
tagMode,
|
||||
setTagMode,
|
||||
batchLoading,
|
||||
handleBatchTag,
|
||||
handleTagInputKeyDown,
|
||||
removeBatchTag,
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* @deprecated 请从 ./batch/ 目录导入子模块
|
||||
* 保持向后兼容,re-export 所有批量操作 Hook
|
||||
*/
|
||||
export { useBatchDelete } from "./batch/useBatchDelete"
|
||||
export { useBatchTag } from "./batch/useBatchTag"
|
||||
export { useBatchClassify } from "./batch/useBatchClassify"
|
||||
export { useBatchMark } from "./batch/useBatchMark"
|
||||
@@ -1,5 +0,0 @@
|
||||
/**
|
||||
* LayerConfig 入口(向后兼容)
|
||||
* 实际实现位于 ./layer-config/ 目录
|
||||
*/
|
||||
export { default } from "./layer-config"
|
||||
@@ -1,65 +0,0 @@
|
||||
/**
|
||||
* 混剪图层列表
|
||||
*/
|
||||
import React from "react"
|
||||
import type { PipLayer } from "@/pages/editing-planner/types"
|
||||
import { LAYER_COLORS } from "@/pages/editing-planner/constants/pipConfig"
|
||||
|
||||
interface LayerListProps {
|
||||
layers: PipLayer[]
|
||||
selectedId: string
|
||||
onSelect: (id: string) => void
|
||||
onAdd: () => void
|
||||
onDelete: (id: string) => void
|
||||
}
|
||||
|
||||
const LayerList: React.FC<LayerListProps> = ({ layers, selectedId, onSelect, onAdd, onDelete }) => {
|
||||
return (
|
||||
<div className="pip-layer-list">
|
||||
<div className="pip-toolbar" style={{ marginBottom: 8 }}>
|
||||
<button className="pip-add-btn" onClick={onAdd}>
|
||||
+ 添加图层
|
||||
</button>
|
||||
</div>
|
||||
{layers.length === 0 ? (
|
||||
<div className="pip-layer-empty">暂无图层,点击上方添加</div>
|
||||
) : (
|
||||
layers.map((layer, idx) => (
|
||||
<div
|
||||
key={layer.id}
|
||||
className={`pip-layer-item${selectedId === layer.id ? " active" : ""}`}
|
||||
onClick={() => onSelect(layer.id)}
|
||||
>
|
||||
{layer.thumbnail_url || layer.material_url ? (
|
||||
<img
|
||||
className="pip-layer-thumb"
|
||||
src={layer.thumbnail_url || layer.material_url}
|
||||
alt={layer.name}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className="pip-layer-thumb"
|
||||
style={{
|
||||
background: LAYER_COLORS[idx % LAYER_COLORS.length],
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<span className="pip-layer-name">{layer.name}</span>
|
||||
<button
|
||||
className="pip-layer-delete"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onDelete(layer.id)
|
||||
}}
|
||||
title="删除图层"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LayerList
|
||||
-172
@@ -1,172 +0,0 @@
|
||||
import React from "react"
|
||||
import type { PipLayer, PipGridPosition } from "@/pages/editing-planner/types"
|
||||
import { GRID_POSITIONS } from "@/pages/editing-planner/constants/pipConfig"
|
||||
|
||||
interface LayerPositionSizeProps {
|
||||
layer: PipLayer
|
||||
onUpdate: (id: string, partial: Partial<PipLayer>) => void
|
||||
onGridClick: (pos: PipGridPosition) => void
|
||||
onWidthChange: (val: number) => void
|
||||
onHeightChange: (val: number) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* 图层位置与尺寸配置面板
|
||||
*/
|
||||
export const LayerPositionSize: React.FC<LayerPositionSizeProps> = ({
|
||||
layer,
|
||||
onUpdate,
|
||||
onGridClick,
|
||||
onWidthChange,
|
||||
onHeightChange,
|
||||
}) => (
|
||||
<>
|
||||
{/* 素材类型 */}
|
||||
<div className="pip-field">
|
||||
<label className="pip-field-label">素材类型</label>
|
||||
<div className="pip-type-btns">
|
||||
<button
|
||||
className={`pip-type-btn${layer.material_type === "image" ? " active" : ""}`}
|
||||
onClick={() => onUpdate(layer.id, { material_type: "image" })}
|
||||
>
|
||||
🖼️ 图片
|
||||
</button>
|
||||
<button
|
||||
className={`pip-type-btn${layer.material_type === "video" ? " active" : ""}`}
|
||||
onClick={() => onUpdate(layer.id, { material_type: "video" })}
|
||||
>
|
||||
🎬 视频
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 素材 URL */}
|
||||
<div className="pip-field">
|
||||
<label className="pip-field-label">
|
||||
{layer.material_type === "image" ? "图片" : "视频"} URL
|
||||
</label>
|
||||
<input
|
||||
className="pip-input"
|
||||
type="text"
|
||||
placeholder={
|
||||
layer.material_type === "image"
|
||||
? "https://example.com/image.png"
|
||||
: "https://example.com/video.mp4"
|
||||
}
|
||||
value={layer.material_url}
|
||||
onChange={(e) => onUpdate(layer.id, { material_url: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 位置:九宫格 + 坐标 */}
|
||||
<div className="pip-field">
|
||||
<label className="pip-field-label">位置</label>
|
||||
<div style={{ display: "flex", gap: 16, alignItems: "flex-start" }}>
|
||||
<div className="pip-grid">
|
||||
{GRID_POSITIONS.map((pos) => (
|
||||
<button
|
||||
key={pos}
|
||||
className={`pip-grid-btn${layer.grid_position === pos ? " active" : ""}`}
|
||||
onClick={() => onGridClick(pos)}
|
||||
>
|
||||
<span className="pip-grid-dot" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="pip-field-row" style={{ flex: 1 }}>
|
||||
<div>
|
||||
<label className="pip-field-label">X (%)</label>
|
||||
<input
|
||||
className="pip-number"
|
||||
type="number"
|
||||
min={0}
|
||||
max={100}
|
||||
value={layer.x}
|
||||
onChange={(e) => onUpdate(layer.id, { x: Number(e.target.value) })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="pip-field-label">Y (%)</label>
|
||||
<input
|
||||
className="pip-number"
|
||||
type="number"
|
||||
min={0}
|
||||
max={100}
|
||||
value={layer.y}
|
||||
onChange={(e) => onUpdate(layer.id, { y: Number(e.target.value) })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 尺寸 */}
|
||||
<div className="pip-field">
|
||||
<label className="pip-field-label">尺寸</label>
|
||||
<div className="pip-slider-row">
|
||||
<span style={{ fontSize: 12, color: "#999", width: 20 }}>宽</span>
|
||||
<input
|
||||
className="pip-slider"
|
||||
type="range"
|
||||
min={10}
|
||||
max={80}
|
||||
value={layer.width}
|
||||
onChange={(e) => onWidthChange(Number(e.target.value))}
|
||||
/>
|
||||
<span className="pip-slider-value">{layer.width}%</span>
|
||||
</div>
|
||||
<div className="pip-slider-row" style={{ marginTop: 6 }}>
|
||||
<span style={{ fontSize: 12, color: "#999", width: 20 }}>高</span>
|
||||
<input
|
||||
className="pip-slider"
|
||||
type="range"
|
||||
min={10}
|
||||
max={80}
|
||||
value={layer.height}
|
||||
onChange={(e) => onHeightChange(Number(e.target.value))}
|
||||
/>
|
||||
<span className="pip-slider-value">{layer.height}%</span>
|
||||
</div>
|
||||
<div
|
||||
className="pip-lock-row"
|
||||
style={{ marginTop: 6 }}
|
||||
onClick={() => onUpdate(layer.id, { aspect_lock: !layer.aspect_lock })}
|
||||
>
|
||||
<span className="pip-lock-icon">{layer.aspect_lock ? "🔒" : "🔓"}</span>
|
||||
<span>{layer.aspect_lock ? "已锁定比例" : "锁定宽高比"}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 圆角 */}
|
||||
<div className="pip-field">
|
||||
<label className="pip-field-label">圆角</label>
|
||||
<div className="pip-slider-row">
|
||||
<input
|
||||
className="pip-slider"
|
||||
type="range"
|
||||
min={0}
|
||||
max={50}
|
||||
value={layer.border_radius}
|
||||
onChange={(e) => onUpdate(layer.id, { border_radius: Number(e.target.value) })}
|
||||
/>
|
||||
<span className="pip-slider-value">{layer.border_radius}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 透明度 */}
|
||||
<div className="pip-field">
|
||||
<label className="pip-field-label">透明度</label>
|
||||
<div className="pip-slider-row">
|
||||
<input
|
||||
className="pip-slider"
|
||||
type="range"
|
||||
min={0}
|
||||
max={100}
|
||||
value={layer.opacity}
|
||||
onChange={(e) => onUpdate(layer.id, { opacity: Number(e.target.value) })}
|
||||
/>
|
||||
<span className="pip-slider-value">{layer.opacity}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
-87
@@ -1,87 +0,0 @@
|
||||
import React from "react"
|
||||
import type { PipLayer, PipAnimType, PipSlideDirection } from "@/pages/editing-planner/types"
|
||||
import { ANIM_OPTIONS, SLIDE_DIR_OPTIONS } from "@/pages/editing-planner/constants/pipConfig"
|
||||
|
||||
interface LayerTimingAnimationProps {
|
||||
layer: PipLayer
|
||||
totalDuration: number
|
||||
onUpdate: (id: string, partial: Partial<PipLayer>) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* 图层时间与动画配置面板
|
||||
*/
|
||||
export const LayerTimingAnimation: React.FC<LayerTimingAnimationProps> = ({
|
||||
layer,
|
||||
totalDuration,
|
||||
onUpdate,
|
||||
}) => (
|
||||
<>
|
||||
{/* 时间 */}
|
||||
<div className="pip-field">
|
||||
<label className="pip-field-label">时间</label>
|
||||
<div className="pip-field-row">
|
||||
<div>
|
||||
<label className="pip-field-label">开始 (s)</label>
|
||||
<input
|
||||
className="pip-number"
|
||||
type="number"
|
||||
min={0}
|
||||
max={totalDuration || 999}
|
||||
step={0.1}
|
||||
value={layer.start_time}
|
||||
onChange={(e) => onUpdate(layer.id, { start_time: Number(e.target.value) })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="pip-field-label">持续 (s)</label>
|
||||
<input
|
||||
className="pip-number"
|
||||
type="number"
|
||||
min={0.1}
|
||||
max={totalDuration || 999}
|
||||
step={0.1}
|
||||
value={layer.duration}
|
||||
onChange={(e) => onUpdate(layer.id, { duration: Number(e.target.value) })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 入场动画 */}
|
||||
<div className="pip-field">
|
||||
<label className="pip-field-label">入场动画</label>
|
||||
<select
|
||||
className="pip-select"
|
||||
value={layer.animation}
|
||||
onChange={(e) => onUpdate(layer.id, { animation: e.target.value as PipAnimType })}
|
||||
>
|
||||
{ANIM_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* 滑入方向(仅 slide_in 时显示) */}
|
||||
{layer.animation === "slide_in" && (
|
||||
<div className="pip-field">
|
||||
<label className="pip-field-label">滑入方向</label>
|
||||
<select
|
||||
className="pip-select"
|
||||
value={layer.slide_direction}
|
||||
onChange={(e) =>
|
||||
onUpdate(layer.id, { slide_direction: e.target.value as PipSlideDirection })
|
||||
}
|
||||
>
|
||||
{SLIDE_DIR_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
@@ -1,33 +0,0 @@
|
||||
import React from "react"
|
||||
import type { PipLayer } from "@/pages/editing-planner/types"
|
||||
import { LAYER_COLORS } from "@/pages/editing-planner/constants/pipConfig"
|
||||
|
||||
interface PipPreviewProps {
|
||||
layers: PipLayer[]
|
||||
selectedId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* PIP 图层迷你预览组件
|
||||
*/
|
||||
export const PipPreview: React.FC<PipPreviewProps> = ({ layers, selectedId }) => (
|
||||
<div className="pip-preview-box">
|
||||
{layers.map((l, idx) => (
|
||||
<div
|
||||
key={l.id}
|
||||
className={`pip-preview-layer${selectedId === l.id ? " selected" : ""}`}
|
||||
style={{
|
||||
left: `${l.x}%`,
|
||||
top: `${l.y}%`,
|
||||
width: `${l.width}%`,
|
||||
height: `${l.height}%`,
|
||||
background: LAYER_COLORS[idx % LAYER_COLORS.length],
|
||||
opacity: l.opacity / 100,
|
||||
borderRadius: `${l.border_radius}%`,
|
||||
}}
|
||||
>
|
||||
<span className="pip-preview-label">{l.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
@@ -1,57 +0,0 @@
|
||||
/**
|
||||
* 混剪单图层配置区
|
||||
*/
|
||||
import React from "react"
|
||||
import type { PipLayer, PipGridPosition } from "@/pages/editing-planner/types"
|
||||
import { PipPreview } from "./PipPreview"
|
||||
import { LayerPositionSize } from "./LayerPositionSize"
|
||||
import { LayerTimingAnimation } from "./LayerTimingAnimation"
|
||||
|
||||
interface LayerConfigProps {
|
||||
layer: PipLayer | null
|
||||
layers: PipLayer[]
|
||||
totalDuration: number
|
||||
onUpdate: (id: string, partial: Partial<PipLayer>) => void
|
||||
onGridClick: (pos: PipGridPosition) => void
|
||||
onWidthChange: (val: number) => void
|
||||
onHeightChange: (val: number) => void
|
||||
}
|
||||
|
||||
const LayerConfig: React.FC<LayerConfigProps> = ({
|
||||
layer,
|
||||
layers,
|
||||
totalDuration,
|
||||
onUpdate,
|
||||
onGridClick,
|
||||
onWidthChange,
|
||||
onHeightChange,
|
||||
}) => {
|
||||
if (!layer) {
|
||||
return (
|
||||
<div className="pip-config-area">
|
||||
<div className="pip-config-empty">选择或添加图层以配置</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pip-config-area">
|
||||
{/* 迷你预览 */}
|
||||
<PipPreview layers={layers} selectedId={layer.id} />
|
||||
|
||||
{/* 位置与尺寸 */}
|
||||
<LayerPositionSize
|
||||
layer={layer}
|
||||
onUpdate={onUpdate}
|
||||
onGridClick={onGridClick}
|
||||
onWidthChange={onWidthChange}
|
||||
onHeightChange={onHeightChange}
|
||||
/>
|
||||
|
||||
{/* 时间与动画 */}
|
||||
<LayerTimingAnimation layer={layer} totalDuration={totalDuration} onUpdate={onUpdate} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LayerConfig
|
||||
@@ -1,142 +0,0 @@
|
||||
/**
|
||||
* 贴纸素材库(emoji / 图片 / 文字花字)
|
||||
*/
|
||||
import React, { useState } from "react"
|
||||
import type { StickerType, TextStickerPreset } from "@/pages/editing-planner/types"
|
||||
import {
|
||||
EMOJI_LIST,
|
||||
STICKER_TYPE_TABS,
|
||||
TEXT_PRESET_STYLES,
|
||||
TEXT_STICKER_PRESET_LABELS,
|
||||
} from "@/pages/editing-planner/constants/sticker"
|
||||
|
||||
interface StickerLibraryProps {
|
||||
activeTab: StickerType
|
||||
onTabChange: (tab: StickerType) => void
|
||||
onAddSticker: (type: StickerType, content: string) => void
|
||||
}
|
||||
|
||||
const StickerLibrary: React.FC<StickerLibraryProps> = ({
|
||||
activeTab,
|
||||
onTabChange,
|
||||
onAddSticker,
|
||||
}) => {
|
||||
const [textInput, setTextInput] = useState("")
|
||||
const imageInputRef = React.useRef<HTMLInputElement>(null)
|
||||
|
||||
const handleAddImage = () => {
|
||||
const val = imageInputRef.current?.value.trim()
|
||||
if (val) {
|
||||
onAddSticker("image", val)
|
||||
if (imageInputRef.current) imageInputRef.current.value = ""
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddText = () => {
|
||||
if (textInput.trim()) {
|
||||
onAddSticker("text", textInput.trim())
|
||||
setTextInput("")
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 类型 Tab */}
|
||||
<div className="sticker-tabs">
|
||||
{STICKER_TYPE_TABS.map((t) => (
|
||||
<button
|
||||
key={t.value}
|
||||
className={`sticker-tab${activeTab === t.value ? " active" : ""}`}
|
||||
onClick={() => onTabChange(t.value)}
|
||||
>
|
||||
{t.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Tab 内容区 */}
|
||||
<div className="sticker-tab-content">
|
||||
{/* Emoji 素材库 */}
|
||||
{activeTab === "emoji" && (
|
||||
<div className="sticker-emoji-grid">
|
||||
{EMOJI_LIST.map((emoji) => (
|
||||
<button
|
||||
key={emoji}
|
||||
className="sticker-emoji-btn"
|
||||
onClick={() => onAddSticker("emoji", emoji)}
|
||||
>
|
||||
{emoji}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 图片贴纸 */}
|
||||
{activeTab === "image" && (
|
||||
<div className="sticker-image-input">
|
||||
<input
|
||||
ref={imageInputRef}
|
||||
type="text"
|
||||
className="sticker-url-input"
|
||||
placeholder="输入图片 URL 添加贴纸..."
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && e.currentTarget.value.trim()) {
|
||||
handleAddImage()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button className="sticker-url-add-btn" onClick={handleAddImage}>
|
||||
添加
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 文字花字 */}
|
||||
{activeTab === "text" && (
|
||||
<div className="sticker-text-section">
|
||||
<div className="sticker-text-input-row">
|
||||
<input
|
||||
type="text"
|
||||
className="sticker-text-input"
|
||||
placeholder="输入文字内容..."
|
||||
value={textInput}
|
||||
onChange={(e) => setTextInput(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
className="sticker-text-add-btn"
|
||||
disabled={!textInput.trim()}
|
||||
onClick={handleAddText}
|
||||
>
|
||||
添加
|
||||
</button>
|
||||
</div>
|
||||
<div className="sticker-text-presets">
|
||||
<div className="sticker-preset-title">花字预设预览</div>
|
||||
<div className="sticker-preset-grid">
|
||||
{(Object.keys(TEXT_STICKER_PRESET_LABELS) as TextStickerPreset[]).map((p) => (
|
||||
<div
|
||||
key={p}
|
||||
className="sticker-preset-preview"
|
||||
style={{
|
||||
background:
|
||||
p === "bubble"
|
||||
? "rgba(0,0,0,0.5)"
|
||||
: p === "gradient"
|
||||
? "linear-gradient(90deg,#f093fb,#f5576c)"
|
||||
: "#1a1a2e",
|
||||
}}
|
||||
>
|
||||
<span style={TEXT_PRESET_STYLES[p]}>示例</span>
|
||||
<div className="sticker-preset-name">{TEXT_STICKER_PRESET_LABELS[p]}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default StickerLibrary
|
||||
@@ -1,54 +0,0 @@
|
||||
/**
|
||||
* 已添加贴纸列表
|
||||
*/
|
||||
import React from "react"
|
||||
import type { StickerItem } from "@/pages/editing-planner/types"
|
||||
|
||||
interface StickerListProps {
|
||||
items: StickerItem[]
|
||||
selectedId: string | null
|
||||
onSelect: (id: string) => void
|
||||
onDelete: (id: string) => void
|
||||
}
|
||||
|
||||
const StickerList: React.FC<StickerListProps> = ({ items, selectedId, onSelect, onDelete }) => {
|
||||
if (items.length === 0) return null
|
||||
|
||||
const getDisplayContent = (item: StickerItem) => {
|
||||
if (item.type === "emoji") return { icon: item.content, name: "表情贴纸" }
|
||||
if (item.type === "text") return { icon: "T", name: item.content.slice(0, 10) }
|
||||
return { icon: "🖼", name: "图片贴纸" }
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="sticker-list-section">
|
||||
<div className="sticker-section-title">已添加贴纸 ({items.length})</div>
|
||||
<div className="sticker-list">
|
||||
{items.map((item) => {
|
||||
const { icon, name } = getDisplayContent(item)
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className={`sticker-list-item${selectedId === item.id ? " active" : ""}`}
|
||||
onClick={() => onSelect(item.id)}
|
||||
>
|
||||
<span className="sticker-list-icon">{icon}</span>
|
||||
<span className="sticker-list-name">{name}</span>
|
||||
<button
|
||||
className="sticker-list-delete"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onDelete(item.id)
|
||||
}}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default StickerList
|
||||
@@ -1,39 +0,0 @@
|
||||
import React from "react"
|
||||
import type { StickerItem } from "@/pages/editing-planner/types"
|
||||
import { TEXT_PRESET_STYLES } from "@/pages/editing-planner/constants/sticker"
|
||||
|
||||
interface StickerPreviewProps {
|
||||
sticker: StickerItem
|
||||
}
|
||||
|
||||
export const StickerPreview: React.FC<StickerPreviewProps> = ({ sticker }) => (
|
||||
<div className="sticker-preview-box">
|
||||
<div
|
||||
className="sticker-preview-item"
|
||||
style={{
|
||||
left: `${sticker.x}%`,
|
||||
top: `${sticker.y}%`,
|
||||
width: `${sticker.width}%`,
|
||||
height: `${sticker.height}%`,
|
||||
transform: `translate(-50%, -50%) rotate(${sticker.rotation}deg)`,
|
||||
opacity: sticker.opacity / 100,
|
||||
fontSize: sticker.type === "text" ? `${sticker.font_size}px` : undefined,
|
||||
...TEXT_PRESET_STYLES[sticker.text_preset],
|
||||
}}
|
||||
>
|
||||
{sticker.type === "emoji" && sticker.content}
|
||||
{sticker.type === "text" && sticker.content}
|
||||
{sticker.type === "image" && (
|
||||
<img
|
||||
src={sticker.content}
|
||||
alt="sticker"
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "contain",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -1,130 +0,0 @@
|
||||
/**
|
||||
* 选中贴纸的属性编辑器
|
||||
*/
|
||||
import React from "react"
|
||||
import type { StickerItem } from "@/pages/editing-planner/types"
|
||||
import { StickerPreview } from "./StickerPreview"
|
||||
import { TextStickerPropsEditor } from "./TextStickerPropsEditor"
|
||||
|
||||
interface StickerPropsEditorProps {
|
||||
sticker: StickerItem
|
||||
totalDuration: number
|
||||
onUpdate: (id: string, partial: Partial<StickerItem>) => void
|
||||
}
|
||||
|
||||
const StickerPropsEditor: React.FC<StickerPropsEditorProps> = ({
|
||||
sticker,
|
||||
totalDuration,
|
||||
onUpdate,
|
||||
}) => {
|
||||
return (
|
||||
<div className="sticker-props-section">
|
||||
<div className="sticker-section-title">属性调整</div>
|
||||
|
||||
{/* 位置 */}
|
||||
<div className="sticker-prop-row">
|
||||
<span className="sticker-prop-label">位置 X</span>
|
||||
<input
|
||||
type="range"
|
||||
className="sticker-prop-slider"
|
||||
min={0}
|
||||
max={100}
|
||||
value={sticker.x}
|
||||
onChange={(e) => onUpdate(sticker.id, { x: Number(e.target.value) })}
|
||||
/>
|
||||
<span className="sticker-prop-value">{sticker.x}%</span>
|
||||
</div>
|
||||
<div className="sticker-prop-row">
|
||||
<span className="sticker-prop-label">位置 Y</span>
|
||||
<input
|
||||
type="range"
|
||||
className="sticker-prop-slider"
|
||||
min={0}
|
||||
max={100}
|
||||
value={sticker.y}
|
||||
onChange={(e) => onUpdate(sticker.id, { y: Number(e.target.value) })}
|
||||
/>
|
||||
<span className="sticker-prop-value">{sticker.y}%</span>
|
||||
</div>
|
||||
|
||||
{/* 尺寸 */}
|
||||
<div className="sticker-prop-row">
|
||||
<span className="sticker-prop-label">大小</span>
|
||||
<input
|
||||
type="range"
|
||||
className="sticker-prop-slider"
|
||||
min={5}
|
||||
max={50}
|
||||
value={sticker.width}
|
||||
onChange={(e) =>
|
||||
onUpdate(sticker.id, {
|
||||
width: Number(e.target.value),
|
||||
height: Number(e.target.value),
|
||||
})
|
||||
}
|
||||
/>
|
||||
<span className="sticker-prop-value">{sticker.width}%</span>
|
||||
</div>
|
||||
|
||||
{/* 旋转 */}
|
||||
<div className="sticker-prop-row">
|
||||
<span className="sticker-prop-label">旋转</span>
|
||||
<input
|
||||
type="range"
|
||||
className="sticker-prop-slider"
|
||||
min={-180}
|
||||
max={180}
|
||||
value={sticker.rotation}
|
||||
onChange={(e) => onUpdate(sticker.id, { rotation: Number(e.target.value) })}
|
||||
/>
|
||||
<span className="sticker-prop-value">{sticker.rotation}°</span>
|
||||
</div>
|
||||
|
||||
{/* 透明度 */}
|
||||
<div className="sticker-prop-row">
|
||||
<span className="sticker-prop-label">透明度</span>
|
||||
<input
|
||||
type="range"
|
||||
className="sticker-prop-slider"
|
||||
min={0}
|
||||
max={100}
|
||||
value={sticker.opacity}
|
||||
onChange={(e) => onUpdate(sticker.id, { opacity: Number(e.target.value) })}
|
||||
/>
|
||||
<span className="sticker-prop-value">{sticker.opacity}%</span>
|
||||
</div>
|
||||
|
||||
{/* 时间 */}
|
||||
<div className="sticker-prop-row">
|
||||
<span className="sticker-prop-label">开始</span>
|
||||
<input
|
||||
type="number"
|
||||
className="sticker-prop-number"
|
||||
min={0}
|
||||
max={totalDuration}
|
||||
step={0.1}
|
||||
value={sticker.start_time}
|
||||
onChange={(e) => onUpdate(sticker.id, { start_time: Number(e.target.value) })}
|
||||
/>
|
||||
<span className="sticker-prop-label">时长</span>
|
||||
<input
|
||||
type="number"
|
||||
className="sticker-prop-number"
|
||||
min={0}
|
||||
max={totalDuration}
|
||||
step={0.1}
|
||||
value={sticker.duration}
|
||||
onChange={(e) => onUpdate(sticker.id, { duration: Number(e.target.value) })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 文字贴纸特有属性 */}
|
||||
<TextStickerPropsEditor sticker={sticker} onUpdate={onUpdate} />
|
||||
|
||||
{/* 预览 */}
|
||||
<StickerPreview sticker={sticker} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default StickerPropsEditor
|
||||
@@ -1,57 +0,0 @@
|
||||
import React from "react"
|
||||
import type { StickerItem, TextStickerPreset } from "@/pages/editing-planner/types"
|
||||
import { TEXT_STICKER_PRESET_LABELS } from "@/pages/editing-planner/constants/sticker"
|
||||
|
||||
interface TextStickerPropsEditorProps {
|
||||
sticker: StickerItem
|
||||
onUpdate: (id: string, partial: Partial<StickerItem>) => void
|
||||
}
|
||||
|
||||
export const TextStickerPropsEditor: React.FC<TextStickerPropsEditorProps> = ({
|
||||
sticker,
|
||||
onUpdate,
|
||||
}) => {
|
||||
if (sticker.type !== "text") return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="sticker-prop-row">
|
||||
<span className="sticker-prop-label">花字</span>
|
||||
<select
|
||||
className="sticker-prop-select"
|
||||
value={sticker.text_preset}
|
||||
onChange={(e) =>
|
||||
onUpdate(sticker.id, { text_preset: e.target.value as TextStickerPreset })
|
||||
}
|
||||
>
|
||||
{(Object.keys(TEXT_STICKER_PRESET_LABELS) as TextStickerPreset[]).map((p) => (
|
||||
<option key={p} value={p}>
|
||||
{TEXT_STICKER_PRESET_LABELS[p]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="sticker-prop-row">
|
||||
<span className="sticker-prop-label">字号</span>
|
||||
<input
|
||||
type="range"
|
||||
className="sticker-prop-slider"
|
||||
min={12}
|
||||
max={72}
|
||||
value={sticker.font_size}
|
||||
onChange={(e) => onUpdate(sticker.id, { font_size: Number(e.target.value) })}
|
||||
/>
|
||||
<span className="sticker-prop-value">{sticker.font_size}px</span>
|
||||
</div>
|
||||
<div className="sticker-prop-row">
|
||||
<span className="sticker-prop-label">颜色</span>
|
||||
<input
|
||||
type="color"
|
||||
className="sticker-prop-color"
|
||||
value={sticker.text_color}
|
||||
onChange={(e) => onUpdate(sticker.id, { text_color: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
/**
|
||||
* TTS 滑块组件(语速/语调/音量)
|
||||
*/
|
||||
import React from "react"
|
||||
import { Slider } from "antd"
|
||||
|
||||
interface TtsSliderProps {
|
||||
label: string
|
||||
value: number
|
||||
min: number
|
||||
max: number
|
||||
step: number
|
||||
unit?: string
|
||||
onChange: (val: number) => void
|
||||
marks?: string[]
|
||||
tooltipFormatter?: (v: number) => string
|
||||
}
|
||||
|
||||
const TtsSlider: React.FC<TtsSliderProps> = ({
|
||||
label,
|
||||
value,
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
unit = "",
|
||||
onChange,
|
||||
marks,
|
||||
tooltipFormatter,
|
||||
}) => {
|
||||
const displayValue =
|
||||
unit === "x"
|
||||
? `${value.toFixed(2)}x`
|
||||
: label === "语调"
|
||||
? `${value > 0 ? "+" : ""}${value} 半音`
|
||||
: `${value}${unit}`
|
||||
|
||||
return (
|
||||
<div className="tts-slider-section">
|
||||
<div className="tts-slider-header">
|
||||
<span className="tts-slider-label">{label}</span>
|
||||
<span className="tts-slider-value">{displayValue}</span>
|
||||
</div>
|
||||
<Slider
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
value={value}
|
||||
onChange={(v) => onChange(v as number)}
|
||||
tooltip={tooltipFormatter ? { formatter: (v) => tooltipFormatter(v as number) } : undefined}
|
||||
/>
|
||||
{marks && (
|
||||
<div className="tts-slider-marks">
|
||||
{marks.map((m, i) => (
|
||||
<span key={i}>{m}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TtsSlider
|
||||
@@ -1,50 +0,0 @@
|
||||
/**
|
||||
* TTS 音色选择组件
|
||||
*/
|
||||
import React from "react"
|
||||
import type { TTSVoice } from "@/api/tts"
|
||||
import { VOICE_CATEGORY_MAP } from "../../constants/tts"
|
||||
|
||||
interface VoiceSelectorProps {
|
||||
voices: TTSVoice[]
|
||||
voicesLoading: boolean
|
||||
selectedVoiceId: string
|
||||
onVoiceSelect: (voiceId: string) => void
|
||||
}
|
||||
|
||||
const VoiceSelector: React.FC<VoiceSelectorProps> = ({
|
||||
voices,
|
||||
voicesLoading,
|
||||
selectedVoiceId,
|
||||
onVoiceSelect,
|
||||
}) => {
|
||||
const voiceCategories = Object.entries(VOICE_CATEGORY_MAP)
|
||||
|
||||
return (
|
||||
<div className="tts-voice-section">
|
||||
<div className="tts-voice-label">
|
||||
选择音色
|
||||
{voicesLoading && <span className="tts-voice-loading">加载中...</span>}
|
||||
</div>
|
||||
<div className="tts-voice-grid">
|
||||
{voiceCategories.map(([cat, info]) => {
|
||||
const voice = voices.find((v) => v.category === cat)
|
||||
const isSelected = voice && selectedVoiceId === voice.id
|
||||
return (
|
||||
<button
|
||||
key={cat}
|
||||
className={`tts-voice-card${isSelected ? " active" : ""}`}
|
||||
onClick={() => voice && onVoiceSelect(voice.id)}
|
||||
disabled={!voice || voicesLoading}
|
||||
>
|
||||
<span className="tts-voice-card-icon">{info.icon}</span>
|
||||
<span className="tts-voice-card-name">{voice?.name || info.label}</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default VoiceSelector
|
||||
@@ -36,10 +36,10 @@ import "@/pages/assets/hooks/useLibraryManagement"
|
||||
import "@/pages/assets/hooks/useAssetUpload"
|
||||
import "@/pages/assets/hooks/useAssetSelection"
|
||||
import "@/pages/assets/hooks/useAssetOperations"
|
||||
import "@/pages/assets/hooks/asset-operations/batch/useBatchDelete"
|
||||
import "@/pages/assets/hooks/asset-operations/batch/useBatchTag"
|
||||
import "@/pages/assets/hooks/asset-operations/batch/useBatchClassify"
|
||||
import "@/pages/assets/hooks/asset-operations/batch/useBatchMark"
|
||||
import "@/pages/assets/hooks/asset-operations/batch-operations/useBatchDelete"
|
||||
import "@/pages/assets/hooks/asset-operations/batch-operations/useBatchTag"
|
||||
import "@/pages/assets/hooks/asset-operations/batch-operations/useBatchClassify"
|
||||
import "@/pages/assets/hooks/asset-operations/batch-operations/useBatchMark"
|
||||
|
||||
describe("AssetLibrary module smoke test", () => {
|
||||
it("should load all asset modules", () => {
|
||||
|
||||
@@ -53,13 +53,6 @@ import "@/pages/editing-planner/components/clip-properties/SubtitleSettingsSecti
|
||||
import "@/pages/editing-planner/components/clip-properties/BgmSettingsSection"
|
||||
import "@/pages/editing-planner/components/clip-properties/ClipDetailSection"
|
||||
import "@/pages/editing-planner/components/clip-properties/StatsSection"
|
||||
import "@/pages/editing-planner/components/pip-config/LayerList"
|
||||
import "@/pages/editing-planner/components/pip-config/LayerConfig"
|
||||
import "@/pages/editing-planner/components/sticker/StickerLibrary"
|
||||
import "@/pages/editing-planner/components/sticker/StickerList"
|
||||
import "@/pages/editing-planner/components/sticker/StickerPropsEditor"
|
||||
import "@/pages/editing-planner/components/sticker/StickerPreview"
|
||||
import "@/pages/editing-planner/components/sticker/TextStickerPropsEditor"
|
||||
import "@/pages/editing-planner/components/filter/FilterPresetGrid"
|
||||
import "@/pages/editing-planner/components/filter/FilterManualAdjust"
|
||||
import "@/pages/editing-planner/components/intro-outro/IntroOutroBlock"
|
||||
@@ -67,8 +60,6 @@ import "@/pages/editing-planner/components/subtitle-style/SubtitlePreview"
|
||||
import "@/pages/editing-planner/components/subtitle-style/SubtitleModeSwitch"
|
||||
import "@/pages/editing-planner/components/subtitle-style/SubtitlePositionSelector"
|
||||
import "@/pages/editing-planner/components/subtitle-style/SubtitleEffectButtons"
|
||||
import "@/pages/editing-planner/components/tts/VoiceSelector"
|
||||
import "@/pages/editing-planner/components/tts/TtsSlider"
|
||||
import "@/pages/editing-planner/components/watermark/WatermarkTypeTabs"
|
||||
import "@/pages/editing-planner/components/watermark/ImageWatermarkSection"
|
||||
import "@/pages/editing-planner/components/watermark/TextWatermarkSection"
|
||||
|
||||
Reference in New Issue
Block a user