fix: refine reference frame previews
This commit is contained in:
2
RULES.md
2
RULES.md
@@ -15,7 +15,7 @@
|
||||
|
||||
## 部署事实
|
||||
- 平台:VPS `76.13.31.179`(Ubuntu 24.04 / Docker Compose / Coolify Traefik)
|
||||
- 发布状态:已部署并验证(2026-05-19,右侧三栏主体管线:竖向参考帧池 + 转换层 + 主体元素,旧主体模板区移出主路径 + 逐句时间轴移到原版视频下方并支持双行文案 + 波形同框时间对齐画面胶片 + 胶片密度按钮上移波形顶部 + 去分隔线 + 胶片上下错落 + body 顶层原位大放大 + 隐藏源视频工作区音频解析摘要卡 + 隐藏工作区顶部状态提示条 + 三字段候选生成工作流 + 折叠紧凑候选区);`https://marketing.skg.com` 已启用应用内登录页,未登录 API 返回 401,认证后首页 200;容器内 `/health` 返回 `ok:true`
|
||||
- 发布状态:已部署并验证(2026-05-19,右侧三栏主体管线:竖向参考帧池 + 转换层 + 主体元素,参考帧缩略图填充裁切 + hover 左侧紧凑预览 + 转换层多参考滚动,旧主体模板区移出主路径 + 逐句时间轴移到原版视频下方并支持双行文案 + 波形同框时间对齐画面胶片 + 胶片密度按钮上移波形顶部 + 去分隔线 + 胶片上下错落 + body 顶层原位大放大 + 隐藏源视频工作区音频解析摘要卡 + 隐藏工作区顶部状态提示条 + 三字段候选生成工作流 + 折叠紧凑候选区);`https://marketing.skg.com` 已启用应用内登录页,未登录 API 返回 401,认证后首页 200;容器内 `/health` 返回 `ok:true`
|
||||
- 主站 / 前端:`https://marketing.skg.com`
|
||||
- API / 后端:`https://marketing.skg.com/api`
|
||||
- 代码仓库 / Gitea:`https://git.kang-kang.com/kangwan/20260512-skg-tk`
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -3226,7 +3226,11 @@ function SourceSubjectPipeline({
|
||||
label={`参考帧 ${String(index + 1).padStart(2, "0")}`}
|
||||
meta={`${frame.timestamp.toFixed(1)}s`}
|
||||
className="h-24"
|
||||
objectFit="contain"
|
||||
objectFit="cover"
|
||||
previewObjectFit="contain"
|
||||
previewPlacement="left"
|
||||
previewMaxWidth={320}
|
||||
previewClassName="p-2"
|
||||
selected={selected || conversionFrameIndices.includes(frame.index)}
|
||||
title={`${selected ? "已选 · 点击取消" : "点击选择"} · 拖到转换层生成主体套图`}
|
||||
onClick={() => onToggleFrame(frame.index)}
|
||||
@@ -3283,7 +3287,7 @@ function SourceSubjectPipeline({
|
||||
<div className="mb-2 rounded-md border border-[#d6b36a]/18 bg-[#d6b36a]/[0.06] px-2.5 py-2 text-[10px] leading-snug text-white/62">
|
||||
拖入 1-2 张参考帧后自动生成主体套图;这里做参考创新,不抠原图。
|
||||
</div>
|
||||
<div className="mb-2 flex flex-col gap-1.5">
|
||||
<div className="mb-2 flex max-h-[224px] flex-col gap-1.5 overflow-y-auto pr-0.5 2xl:max-h-[286px]">
|
||||
{conversionFrames.map((frame, index) => (
|
||||
<div key={frame.index} className="relative">
|
||||
<MediaAssetTile
|
||||
@@ -3292,7 +3296,7 @@ function SourceSubjectPipeline({
|
||||
label={`转换参考 ${index + 1}`}
|
||||
meta={`${frame.timestamp.toFixed(1)}s`}
|
||||
className="h-24"
|
||||
objectFit="contain"
|
||||
objectFit="cover"
|
||||
disablePreview
|
||||
topLeft={<span className="rounded bg-black/72 px-1 font-mono text-[9px] text-white/70">{String(index + 1).padStart(2, "0")}</span>}
|
||||
/>
|
||||
|
||||
@@ -14,6 +14,8 @@ type MediaAssetAction = {
|
||||
tone?: "neutral" | "cyan" | "rose"
|
||||
}
|
||||
|
||||
type MediaAssetPreviewPlacement = "auto" | "left" | "right"
|
||||
|
||||
type MediaAssetTileProps = {
|
||||
kind?: "image" | "video"
|
||||
src?: string
|
||||
@@ -29,6 +31,8 @@ type MediaAssetTileProps = {
|
||||
objectFit?: "contain" | "cover"
|
||||
previewObjectFit?: "contain" | "cover"
|
||||
previewClassName?: string
|
||||
previewPlacement?: MediaAssetPreviewPlacement
|
||||
previewMaxWidth?: number
|
||||
selected?: boolean
|
||||
disabled?: boolean
|
||||
busy?: boolean
|
||||
@@ -55,15 +59,21 @@ function mediaObjectClass(fit: "contain" | "cover") {
|
||||
return fit === "cover" ? "object-cover" : "object-contain"
|
||||
}
|
||||
|
||||
function previewPosition(event: ReactMouseEvent<HTMLElement>) {
|
||||
function previewPosition(event: ReactMouseEvent<HTMLElement>, placement: MediaAssetPreviewPlacement, maxWidth: number) {
|
||||
const margin = 16
|
||||
const previewWidth = Math.min(520, window.innerWidth - margin * 2)
|
||||
const previewWidth = Math.min(maxWidth, window.innerWidth - margin * 2)
|
||||
const previewHeight = Math.min(760, window.innerHeight - margin * 2)
|
||||
let left = event.clientX + 18
|
||||
let left = placement === "left" ? event.clientX - previewWidth - 18 : event.clientX + 18
|
||||
let top = event.clientY + 18
|
||||
if (left + previewWidth > window.innerWidth - margin) left = event.clientX - previewWidth - 18
|
||||
if (placement === "auto" && left + previewWidth > window.innerWidth - margin) left = event.clientX - previewWidth - 18
|
||||
if (placement === "right" && left + previewWidth > window.innerWidth - margin) left = window.innerWidth - previewWidth - margin
|
||||
if (placement === "left" && left < margin) left = margin
|
||||
if (top + previewHeight > window.innerHeight - margin) top = window.innerHeight - previewHeight - margin
|
||||
return { left: Math.max(margin, left), top: Math.max(margin, top) }
|
||||
return {
|
||||
left: Math.max(margin, Math.min(left, window.innerWidth - previewWidth - margin)),
|
||||
top: Math.max(margin, top),
|
||||
width: previewWidth,
|
||||
}
|
||||
}
|
||||
|
||||
export function MediaAssetTile({
|
||||
@@ -81,6 +91,8 @@ export function MediaAssetTile({
|
||||
objectFit = "contain",
|
||||
previewObjectFit,
|
||||
previewClassName = "",
|
||||
previewPlacement = "auto",
|
||||
previewMaxWidth = 520,
|
||||
selected = false,
|
||||
disabled = false,
|
||||
busy = false,
|
||||
@@ -96,7 +108,7 @@ export function MediaAssetTile({
|
||||
actions = [],
|
||||
disablePreview = false,
|
||||
}: MediaAssetTileProps) {
|
||||
const [position, setPosition] = useState<{ left: number; top: number } | null>(null)
|
||||
const [position, setPosition] = useState<{ left: number; top: number; width: number } | null>(null)
|
||||
const mediaSrc = src || poster || ""
|
||||
const canPreview = !!mediaSrc && !disablePreview
|
||||
const fit = mediaObjectClass(objectFit)
|
||||
@@ -104,7 +116,7 @@ export function MediaAssetTile({
|
||||
|
||||
const updatePreview = (event: ReactMouseEvent<HTMLElement>) => {
|
||||
if (!canPreview) return
|
||||
setPosition(previewPosition(event))
|
||||
setPosition(previewPosition(event, previewPlacement, previewMaxWidth))
|
||||
}
|
||||
|
||||
const media = kind === "video" && src ? (
|
||||
@@ -136,7 +148,7 @@ export function MediaAssetTile({
|
||||
? createPortal(
|
||||
<div
|
||||
className={`pointer-events-none fixed z-[10000] w-[min(520px,calc(100vw-32px))] rounded-xl border border-white/15 bg-black/94 p-3 shadow-[0_28px_80px_rgba(0,0,0,0.72)] ${previewClassName}`}
|
||||
style={{ left: position.left, top: position.top }}
|
||||
style={{ left: position.left, top: position.top, width: position.width }}
|
||||
>
|
||||
<div className="flex max-h-[min(76vh,720px)] items-center justify-center overflow-hidden rounded-lg bg-black">
|
||||
{kind === "video" && src ? (
|
||||
|
||||
Reference in New Issue
Block a user