优化蒙版

This commit is contained in:
iqudoo
2026-06-15 17:43:01 +08:00
parent 455946c51c
commit 25fa186d02
5 changed files with 373 additions and 148 deletions

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { Upload, X } from 'lucide-react';
import type { ElementMaskConfig, TemplateElement } from '../types';
@@ -22,14 +22,16 @@ export const ElementMaskDialog: React.FC<ElementMaskDialogProps> = ({
onConfirm,
}) => {
const [draft, setDraft] = useState<ElementMaskConfig>({ enabled: true, type: 'rect' });
const draftRef = useRef(draft);
draftRef.current = draft;
useEffect(() => {
if (!open || !element) return;
setDraft(
element.mask?.enabled
? { ...createDefaultElementMask(element), ...element.mask, enabled: true }
: createDefaultElementMask(element)
);
const initial = element.mask?.enabled
? { ...createDefaultElementMask(element), ...element.mask, enabled: true }
: createDefaultElementMask(element);
draftRef.current = initial;
setDraft(initial);
}, [open, element]);
useEffect(() => {
@@ -52,14 +54,54 @@ export const ElementMaskDialog: React.FC<ElementMaskDialogProps> = ({
const canConfirm = maskType !== 'image' || !!draft.image?.trim();
const patchDraft = (patch: Partial<ElementMaskConfig>) => {
setDraft((prev) => ({ ...prev, ...patch }));
setDraft((prev) => {
const next = { ...prev, ...patch };
draftRef.current = next;
return next;
});
};
const patchDraftCorner = (key: keyof ElementMaskConfig, val: string) => {
if (val.trim() === '') {
setDraft((prev) => {
const next = { ...prev };
delete next[key];
draftRef.current = next;
return next;
});
return;
}
patchDraft({ [key]: Math.max(0, round1(Number(val) || 0)) });
};
const normalizeDraft = (source: ElementMaskConfig): ElementMaskConfig => ({
...source,
enabled: true,
x: round1(Number(source.x) || 0),
y: round1(Number(source.y) || 0),
width: Math.max(0.5, round1(Number(source.width) || element.width)),
height: Math.max(0.5, round1(Number(source.height) || element.height)),
borderRadius: Math.max(0, round1(Number(source.borderRadius) || 0)),
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!canConfirm) return;
onConfirm({ ...draft, enabled: true });
onClose();
const form = e.currentTarget as HTMLFormElement;
const active = document.activeElement;
const submit = () => {
if (!canConfirm) return;
onConfirm(normalizeDraft(draftRef.current));
onClose();
};
if (active instanceof HTMLElement && form.contains(active)) {
active.blur();
setTimeout(submit, 0);
return;
}
submit();
};
const handleClear = () => {
@@ -87,9 +129,25 @@ export const ElementMaskDialog: React.FC<ElementMaskDialogProps> = ({
<form onSubmit={handleSubmit}>
<div className="ps-modal-body space-y-3">
<p className="text-[11px] text-[#888] leading-relaxed">
</p>
<div>
<label className="ps-field-label"></label>
<FieldSelect
value={draft.mode ?? 'include'}
onChange={(e) =>
patchDraft({
mode: e.target.value as ElementMaskConfig['mode'],
})
}
className="ps-field text-[11px]"
>
<option value="include"></option>
<option value="exclude"></option>
</FieldSelect>
</div>
<div>
<label className="ps-field-label"></label>
<FieldSelect
@@ -135,7 +193,9 @@ export const ElementMaskDialog: React.FC<ElementMaskDialogProps> = ({
min="0.5"
value={regionW}
onCommit={(val) =>
patchDraft({ width: Math.max(0.5, round1(Number(val) || element.width)) })
patchDraft({
width: Math.max(0.5, round1(Number(val) || element.width)),
})
}
className="ps-field font-mono text-[11px]"
/>
@@ -148,7 +208,9 @@ export const ElementMaskDialog: React.FC<ElementMaskDialogProps> = ({
min="0.5"
value={regionH}
onCommit={(val) =>
patchDraft({ height: Math.max(0.5, round1(Number(val) || element.height)) })
patchDraft({
height: Math.max(0.5, round1(Number(val) || element.height)),
})
}
className="ps-field font-mono text-[11px]"
/>
@@ -188,15 +250,7 @@ export const ElementMaskDialog: React.FC<ElementMaskDialogProps> = ({
max="50"
step="0.5"
value={draft[key] ?? ''}
onCommit={(val) => {
if (val === '' || val === undefined) {
const next = { ...draft };
delete next[key];
setDraft(next);
return;
}
patchDraft({ [key]: Math.max(0, round1(Number(val) || 0)) });
}}
onCommit={(val) => patchDraftCorner(key, val)}
className="ps-field font-mono text-[10px] flex-1 min-w-0"
placeholder="—"
/>
@@ -226,7 +280,7 @@ export const ElementMaskDialog: React.FC<ElementMaskDialogProps> = ({
/>
</label>
<p className="text-[9px] text-[#666] leading-relaxed">
使 PNG
</p>
{draft.image && (
<div className="border border-[#3a3a3a] bg-[#1e1e1e] p-2">
@@ -257,7 +311,7 @@ export const ElementMaskDialog: React.FC<ElementMaskDialogProps> = ({
)}
</div>
<div className="ps-modal-footer">
{element.mask?.enabled && (
{element.mask && element.mask.enabled !== false && (
<button
type="button"
onClick={handleClear}

View File

@@ -1636,9 +1636,7 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
>
<div
data-drag-float-placeholder={el.id}
className={`absolute inset-0 bg-white shadow-[0_1px_4px_rgba(0,0,0,0.12)]${
dragMultiSelectionBounds ? '' : ' border border-[#31a8ff]/40'
}`}
className="absolute inset-0 bg-white shadow-[0_1px_4px_rgba(0,0,0,0.12)]"
/>
<img
ref={(node) => {
@@ -1672,6 +1670,12 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
if (placeholder) placeholder.style.display = 'none';
}}
/>
{selectedElementIds.length === 1 && (
<div
className="absolute inset-0 ps-resize-outline pointer-events-none z-[3]"
aria-hidden
/>
)}
</div>
);
})}