diff --git a/src/components/ElementMaskDialog.tsx b/src/components/ElementMaskDialog.tsx index cb30d4f..826e40b 100644 --- a/src/components/ElementMaskDialog.tsx +++ b/src/components/ElementMaskDialog.tsx @@ -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 = ({ onConfirm, }) => { const [draft, setDraft] = useState({ 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 = ({ const canConfirm = maskType !== 'image' || !!draft.image?.trim(); const patchDraft = (patch: Partial) => { - 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 = ({

- 蒙版用于裁剪图层可见区域,相对元素左上角定位;预览、导出与打印均生效。 + 蒙版仅作用于当前图层(背景与内容);先在离屏画布合成,再按蒙版裁剪后绘制到画布。边框不受蒙版影响。

+
+ + + patchDraft({ + mode: e.target.value as ElementMaskConfig['mode'], + }) + } + className="ps-field text-[11px]" + > + + + +
+
= ({ 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 = ({ 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 = ({ 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 = ({ />

- 推荐使用带透明区域的 PNG;不透明区域将保留图层内容,透明区域将被裁剪。 + 按图片亮度控制蒙版:白色区域保留内容,黑色区域裁剪;反向模式下黑白互换。

{draft.image && (
@@ -257,7 +311,7 @@ export const ElementMaskDialog: React.FC = ({ )}
- {element.mask?.enabled && ( + {element.mask && element.mask.enabled !== false && (