refactor(PipConfigPanel): 混剪配置面板目录化拆分(547→106行, -81%) #1192
+268
@@ -0,0 +1,268 @@
|
||||
import React from "react"
|
||||
import type { PipLayer, PipGridPosition, PipAnimType, PipSlideDirection } from "../../../types"
|
||||
import { GRID_POSITIONS, ANIM_OPTIONS, SLIDE_DIR_OPTIONS } from "../constants"
|
||||
|
||||
export interface LayerConfigProps {
|
||||
layer: PipLayer
|
||||
totalDuration: number
|
||||
onUpdate: (id: string, partial: Partial<PipLayer>) => void
|
||||
onGridClick: (pos: PipGridPosition) => void
|
||||
onWidthChange: (val: number) => void
|
||||
onHeightChange: (val: number) => void
|
||||
}
|
||||
|
||||
export const LayerConfig: React.FC<LayerConfigProps> = ({
|
||||
layer,
|
||||
totalDuration,
|
||||
onUpdate,
|
||||
onGridClick,
|
||||
onWidthChange,
|
||||
onHeightChange,
|
||||
}) => {
|
||||
return (
|
||||
<div className="pip-config-area">
|
||||
{/* ── 素材类型 ── */}
|
||||
<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) => {
|
||||
const val = Number(e.target.value)
|
||||
if (!isNaN(val) && val >= 0 && val <= 100) onUpdate(layer.id, { x: val })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="pip-field-label">Y (%)</label>
|
||||
<input
|
||||
className="pip-number"
|
||||
type="number"
|
||||
min={0}
|
||||
max={100}
|
||||
value={layer.y}
|
||||
onChange={(e) => {
|
||||
const val = Number(e.target.value)
|
||||
if (!isNaN(val) && val >= 0 && val <= 100) onUpdate(layer.id, { y: val })
|
||||
}}
|
||||
/>
|
||||
</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) => {
|
||||
const val = Number(e.target.value)
|
||||
if (!isNaN(val)) onWidthChange(val)
|
||||
}}
|
||||
/>
|
||||
<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) => {
|
||||
const val = Number(e.target.value)
|
||||
if (!isNaN(val)) onHeightChange(val)
|
||||
}}
|
||||
/>
|
||||
<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) => {
|
||||
const val = Number(e.target.value)
|
||||
if (!isNaN(val)) onUpdate(layer.id, { border_radius: val })
|
||||
}}
|
||||
/>
|
||||
<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) => {
|
||||
const val = Number(e.target.value)
|
||||
if (!isNaN(val)) onUpdate(layer.id, { opacity: val })
|
||||
}}
|
||||
/>
|
||||
<span className="pip-slider-value">{layer.opacity}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── 时间 ── */}
|
||||
<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) => {
|
||||
const val = Number(e.target.value)
|
||||
if (!isNaN(val) && val >= 0 && val <= totalDuration)
|
||||
onUpdate(layer.id, { start_time: val })
|
||||
}}
|
||||
/>
|
||||
</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) => {
|
||||
const val = Number(e.target.value)
|
||||
if (!isNaN(val) && val > 0 && val <= totalDuration)
|
||||
onUpdate(layer.id, { duration: val })
|
||||
}}
|
||||
/>
|
||||
</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>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import React from "react"
|
||||
import type { PipLayer } from "../../../types"
|
||||
import { LAYER_COLORS } from "../constants"
|
||||
|
||||
export interface LayerListProps {
|
||||
layers: PipLayer[]
|
||||
selectedId: string
|
||||
onSelect: (id: string) => void
|
||||
onDelete: (id: string) => void
|
||||
}
|
||||
|
||||
export const LayerList: React.FC<LayerListProps> = ({ layers, selectedId, onSelect, onDelete }) => {
|
||||
if (layers.length === 0) {
|
||||
return (
|
||||
<div className="pip-layer-list">
|
||||
<div className="pip-layer-empty">暂无图层,点击上方添加</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pip-layer-list">
|
||||
{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 || undefined}
|
||||
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>
|
||||
)
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import React from "react"
|
||||
import type { PipLayer } from "../../../types"
|
||||
import { LAYER_COLORS } from "../constants"
|
||||
|
||||
export interface PipPreviewProps {
|
||||
layers: PipLayer[]
|
||||
selectedId: string
|
||||
}
|
||||
|
||||
export const PipPreview: React.FC<PipPreviewProps> = ({ layers, selectedId }) => {
|
||||
return (
|
||||
<div className="pip-preview-box">
|
||||
{layers.map((layer, idx) => (
|
||||
<div
|
||||
key={layer.id}
|
||||
className={`pip-preview-layer${selectedId === layer.id ? " selected" : ""}`}
|
||||
style={{
|
||||
left: `${layer.x}%`,
|
||||
top: `${layer.y}%`,
|
||||
width: `${layer.width}%`,
|
||||
height: `${layer.height}%`,
|
||||
background: LAYER_COLORS[idx % LAYER_COLORS.length],
|
||||
opacity: layer.opacity / 100,
|
||||
borderRadius: `${layer.border_radius}%`,
|
||||
zIndex: layer.z_index,
|
||||
}}
|
||||
>
|
||||
<span className="pip-preview-label">{layer.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { PipGridPosition, PipAnimType, PipSlideDirection } from "../../types"
|
||||
|
||||
/** 九宫格位置 → 百分比坐标映射 */
|
||||
export const GRID_POSITION_MAP: Record<PipGridPosition, { x: number; y: number }> = {
|
||||
top_left: { x: 5, y: 5 },
|
||||
top_center: { x: 37.5, y: 5 },
|
||||
top_right: { x: 70, y: 5 },
|
||||
center_left: { x: 5, y: 37.5 },
|
||||
center: { x: 37.5, y: 37.5 },
|
||||
center_right: { x: 70, y: 37.5 },
|
||||
bottom_left: { x: 5, y: 70 },
|
||||
bottom_center: { x: 37.5, y: 70 },
|
||||
bottom_right: { x: 70, y: 70 },
|
||||
}
|
||||
|
||||
/** 九宫格位置选项 */
|
||||
export const GRID_POSITIONS: PipGridPosition[] = [
|
||||
"top_left",
|
||||
"top_center",
|
||||
"top_right",
|
||||
"center_left",
|
||||
"center",
|
||||
"center_right",
|
||||
"bottom_left",
|
||||
"bottom_center",
|
||||
"bottom_right",
|
||||
]
|
||||
|
||||
/** 入场动画选项 */
|
||||
export const ANIM_OPTIONS: { value: PipAnimType; label: string }[] = [
|
||||
{ value: "none", label: "无" },
|
||||
{ value: "fade_in", label: "淡入" },
|
||||
{ value: "slide_in", label: "滑入" },
|
||||
]
|
||||
|
||||
/** 滑入方向选项 */
|
||||
export const SLIDE_DIR_OPTIONS: { value: PipSlideDirection; label: string }[] = [
|
||||
{ value: "left", label: "← 左" },
|
||||
{ value: "right", label: "→ 右" },
|
||||
{ value: "up", label: "↑ 上" },
|
||||
{ value: "down", label: "↓ 下" },
|
||||
]
|
||||
|
||||
/** 预览图层颜色池 */
|
||||
export const LAYER_COLORS = [
|
||||
"rgba(22,119,255,0.5)",
|
||||
"rgba(82,196,26,0.5)",
|
||||
"rgba(250,173,20,0.5)",
|
||||
"rgba(255,77,79,0.5)",
|
||||
"rgba(114,46,209,0.5)",
|
||||
"rgba(19,194,194,0.5)",
|
||||
]
|
||||
|
||||
let layerIdCounter = 0
|
||||
export const genLayerId = () => `pip_layer_${Date.now()}_${++layerIdCounter}`
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
import { useState, useCallback, useMemo } from "react"
|
||||
import type { PipConfig, PipLayer, PipGridPosition } from "../../../types"
|
||||
import { DEFAULT_PIP_LAYER, DEFAULT_PIP_CONFIG } from "../../../types"
|
||||
import { GRID_POSITION_MAP, genLayerId } from "../constants"
|
||||
|
||||
const MIN_DIMENSION = 10
|
||||
const MAX_DIMENSION = 80
|
||||
|
||||
interface UsePipConfigPanelOptions {
|
||||
config: PipConfig
|
||||
onChange: (config: PipConfig) => void
|
||||
}
|
||||
|
||||
export const usePipConfigPanel = ({ config, onChange }: UsePipConfigPanelOptions) => {
|
||||
const [selectedId, setSelectedId] = useState<string>("")
|
||||
|
||||
const selectedLayer = useMemo(
|
||||
() => config.layers.find((l) => l.id === selectedId) ?? null,
|
||||
[config.layers, selectedId],
|
||||
)
|
||||
|
||||
/* ── 添加图层 ── */
|
||||
const handleAddLayer = useCallback(() => {
|
||||
const newLayer: PipLayer = {
|
||||
...DEFAULT_PIP_LAYER,
|
||||
id: genLayerId(),
|
||||
name: `图层 ${config.layers.length + 1}`,
|
||||
z_index: config.layers.length + 1,
|
||||
}
|
||||
onChange({
|
||||
...config,
|
||||
layers: [...config.layers, newLayer],
|
||||
})
|
||||
setSelectedId(newLayer.id)
|
||||
}, [config, onChange])
|
||||
|
||||
/* ── 删除图层 ── */
|
||||
const handleDeleteLayer = useCallback(
|
||||
(id: string) => {
|
||||
const newLayers = config.layers.filter((l) => l.id !== id)
|
||||
onChange({ ...config, layers: newLayers })
|
||||
if (selectedId === id) {
|
||||
setSelectedId(newLayers.length > 0 ? newLayers[0].id : "")
|
||||
}
|
||||
},
|
||||
[config, onChange, selectedId],
|
||||
)
|
||||
|
||||
/* ── 更新图层 ── */
|
||||
const updateLayer = useCallback(
|
||||
(id: string, partial: Partial<PipLayer>) => {
|
||||
onChange({
|
||||
...config,
|
||||
layers: config.layers.map((l) => (l.id === id ? { ...l, ...partial } : l)),
|
||||
})
|
||||
},
|
||||
[config, onChange],
|
||||
)
|
||||
|
||||
/* ── 切换启用 ── */
|
||||
const handleEnableToggle = useCallback(
|
||||
(checked: boolean) => {
|
||||
onChange({ ...config, enabled: checked })
|
||||
},
|
||||
[config, onChange],
|
||||
)
|
||||
|
||||
/* ── 重置 ── */
|
||||
const handleReset = useCallback(() => {
|
||||
onChange({ ...DEFAULT_PIP_CONFIG })
|
||||
setSelectedId("")
|
||||
}, [onChange])
|
||||
|
||||
/* ── 九宫格点击 ── */
|
||||
const handleGridClick = useCallback(
|
||||
(pos: PipGridPosition) => {
|
||||
if (!selectedLayer) return
|
||||
const coords = GRID_POSITION_MAP[pos]
|
||||
updateLayer(selectedLayer.id, {
|
||||
grid_position: pos,
|
||||
x: coords.x,
|
||||
y: coords.y,
|
||||
})
|
||||
},
|
||||
[selectedLayer, updateLayer],
|
||||
)
|
||||
|
||||
/* ── 宽高变化 ── */
|
||||
const handleWidthChange = useCallback(
|
||||
(val: number) => {
|
||||
if (!selectedLayer) return
|
||||
const partial: Partial<PipLayer> = { width: val }
|
||||
if (selectedLayer.aspect_lock && selectedLayer.height > 0) {
|
||||
const ratio = selectedLayer.width / selectedLayer.height
|
||||
const newHeight = Math.round(val / ratio)
|
||||
partial.height = Math.max(MIN_DIMENSION, Math.min(MAX_DIMENSION, newHeight))
|
||||
}
|
||||
updateLayer(selectedLayer.id, partial)
|
||||
},
|
||||
[selectedLayer, updateLayer],
|
||||
)
|
||||
|
||||
const handleHeightChange = useCallback(
|
||||
(val: number) => {
|
||||
if (!selectedLayer) return
|
||||
const partial: Partial<PipLayer> = { height: val }
|
||||
if (selectedLayer.aspect_lock && selectedLayer.width > 0 && selectedLayer.height > 0) {
|
||||
const ratio = selectedLayer.width / selectedLayer.height
|
||||
const newWidth = Math.round(val * ratio)
|
||||
partial.width = Math.max(MIN_DIMENSION, Math.min(MAX_DIMENSION, newWidth))
|
||||
}
|
||||
updateLayer(selectedLayer.id, partial)
|
||||
},
|
||||
[selectedLayer, updateLayer],
|
||||
)
|
||||
|
||||
return {
|
||||
selectedId,
|
||||
setSelectedId,
|
||||
selectedLayer,
|
||||
handleAddLayer,
|
||||
handleDeleteLayer,
|
||||
updateLayer,
|
||||
handleEnableToggle,
|
||||
handleReset,
|
||||
handleGridClick,
|
||||
handleWidthChange,
|
||||
handleHeightChange,
|
||||
}
|
||||
}
|
||||
+31
-17
@@ -4,12 +4,14 @@
|
||||
*/
|
||||
import React from "react"
|
||||
import { Drawer, Switch } from "antd"
|
||||
import type { PipConfig } from "@/pages/editing-planner/types"
|
||||
import LayerList from "./pip-config/LayerList"
|
||||
import LayerConfig from "./pip-config/LayerConfig"
|
||||
import { usePipLayers } from "@/pages/editing-planner/hooks/usePipLayers"
|
||||
import type { PipConfig } from "../../types"
|
||||
|
||||
interface PipConfigPanelProps {
|
||||
import { usePipConfigPanel } from "./hooks/usePipConfigPanel"
|
||||
import { LayerList } from "./components/LayerList"
|
||||
import { PipPreview } from "./components/PipPreview"
|
||||
import { LayerConfig } from "./components/LayerConfig"
|
||||
|
||||
export interface PipConfigPanelProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
config: PipConfig
|
||||
@@ -36,7 +38,7 @@ const PipConfigPanel: React.FC<PipConfigPanelProps> = ({
|
||||
handleGridClick,
|
||||
handleWidthChange,
|
||||
handleHeightChange,
|
||||
} = usePipLayers({ config, onChange })
|
||||
} = usePipConfigPanel({ config, onChange })
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
@@ -50,7 +52,9 @@ const PipConfigPanel: React.FC<PipConfigPanelProps> = ({
|
||||
{/* ═══ 顶部工具栏 ═══ */}
|
||||
<div className="pip-toolbar">
|
||||
<div className="pip-toolbar-left">
|
||||
<span style={{ fontSize: 13, color: "#666" }}>共 {config.layers.length} 个图层</span>
|
||||
<button className="pip-add-btn" onClick={handleAddLayer}>
|
||||
+ 添加图层
|
||||
</button>
|
||||
</div>
|
||||
<div className="pip-enable-switch">
|
||||
<span>启用</span>
|
||||
@@ -60,22 +64,32 @@ const PipConfigPanel: React.FC<PipConfigPanelProps> = ({
|
||||
|
||||
{/* ═══ 主体:图层列表 + 配置区 ═══ */}
|
||||
<div className="pip-body">
|
||||
{/* 左侧图层列表 */}
|
||||
<LayerList
|
||||
layers={config.layers}
|
||||
selectedId={selectedId}
|
||||
onSelect={setSelectedId}
|
||||
onAdd={handleAddLayer}
|
||||
onDelete={handleDeleteLayer}
|
||||
/>
|
||||
<LayerConfig
|
||||
layer={selectedLayer}
|
||||
layers={config.layers}
|
||||
totalDuration={totalDuration}
|
||||
onUpdate={updateLayer}
|
||||
onGridClick={handleGridClick}
|
||||
onWidthChange={handleWidthChange}
|
||||
onHeightChange={handleHeightChange}
|
||||
/>
|
||||
|
||||
{/* 右侧配置区 */}
|
||||
{!selectedLayer ? (
|
||||
<div className="pip-config-area">
|
||||
<div className="pip-config-empty">选择或添加图层以配置</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<PipPreview layers={config.layers} selectedId={selectedId} />
|
||||
<LayerConfig
|
||||
layer={selectedLayer}
|
||||
totalDuration={totalDuration}
|
||||
onUpdate={updateLayer}
|
||||
onGridClick={handleGridClick}
|
||||
onWidthChange={handleWidthChange}
|
||||
onHeightChange={handleHeightChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── 底部操作 ── */}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Smoke test for LayerConfig.test
|
||||
*/
|
||||
import { describe, it, expect } from "vitest"
|
||||
import "@/pages/editing-planner/components/PipConfigPanel/components/LayerConfig"
|
||||
|
||||
describe("LayerConfig.test smoke", () => {
|
||||
it("should load module successfully", () => {
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
})
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Smoke test for LayerList.test
|
||||
*/
|
||||
import { describe, it, expect } from "vitest"
|
||||
import "@/pages/editing-planner/components/PipConfigPanel/components/LayerList"
|
||||
|
||||
describe("LayerList.test smoke", () => {
|
||||
it("should load module successfully", () => {
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
})
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Smoke test for PipPreview.test
|
||||
*/
|
||||
import { describe, it, expect } from "vitest"
|
||||
import "@/pages/editing-planner/components/PipConfigPanel/components/PipPreview"
|
||||
|
||||
describe("PipPreview.test smoke", () => {
|
||||
it("should load module successfully", () => {
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Smoke test for constants.test
|
||||
*/
|
||||
import { describe, it, expect } from "vitest"
|
||||
import "@/pages/editing-planner/components/PipConfigPanel/constants"
|
||||
|
||||
describe("constants.test smoke", () => {
|
||||
it("should load module successfully", () => {
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
})
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Smoke test for usePipConfigPanel.test
|
||||
*/
|
||||
import { describe, it, expect } from "vitest"
|
||||
import "@/pages/editing-planner/components/PipConfigPanel/hooks/usePipConfigPanel"
|
||||
|
||||
describe("usePipConfigPanel.test smoke", () => {
|
||||
it("should load module successfully", () => {
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user