64387c00bb
CI/CD Pipeline / ACR Image Cleanup (push) Failing after 1247h21m43s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 1247h21m43s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1247h21m44s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Failing after 1247h27m46s
CI/CD Pipeline / Production Browser E2E (push) Failing after 1247h33m44s
CI/CD Pipeline / Deploy Production (push) Failing after 1247h40m40s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 1247h46m28s
CI/CD Pipeline / Build Production API Image (push) Failing after 1247h46m32s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 1247h48m13s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 1247h48m17s
CI/CD Pipeline / PR Build Web Image (push) Failing after 1247h48m15s
CI/CD Pipeline / Validate - Code Quality (push) Failing after 2m17s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m54s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m43s
CI/CD Pipeline / Unit Tests (push) Failing after 1h6m19s
CI/CD Pipeline / Integration Tests (push) Successful in 2m26s
CI/CD Pipeline / Frontend Lint (push) Failing after 36s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m10s
CI/CD Pipeline / Build Production Web Image (push) Failing after 1248h19m9s
CI/CD Pipeline / PR Build API Image (push) Failing after 1248h20m54s
CI/CD Pipeline / Build Staging API Image (push) Successful in 15m48s
CI/CD Pipeline / Build Staging Web Image (push) Failing after 3m10s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 6m8s
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
201 lines
6.2 KiB
TypeScript
201 lines
6.2 KiB
TypeScript
/**
|
|
* 选中贴纸的属性编辑器
|
|
*/
|
|
import React from "react"
|
|
import type { StickerItem, TextStickerPreset } from "@/pages/editing-planner/types"
|
|
import {
|
|
TEXT_PRESET_STYLES,
|
|
TEXT_STICKER_PRESET_LABELS,
|
|
} from "@/pages/editing-planner/constants/sticker"
|
|
|
|
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>
|
|
|
|
{/* 文字贴纸特有属性 */}
|
|
{sticker.type === "text" && (
|
|
<>
|
|
<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>
|
|
</>
|
|
)}
|
|
|
|
{/* 预览 */}
|
|
<div className="sticker-preview-box">
|
|
<div
|
|
className="sticker-preview-item"
|
|
style={{
|
|
left: `${sticker.x}%`,
|
|
top: `${sticker.y}%`,
|
|
width: `${sticker.width}%`,
|
|
height: `${sticker.width}%`,
|
|
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>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default StickerPropsEditor
|