蒙版
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect, useMemo, useRef } from 'react';
|
||||
import { LabelTemplate, TemplateElement, PaperConfig, VisibilityRule } from '../types';
|
||||
import { LabelTemplate, TemplateElement, PaperConfig, VisibilityRule, ElementMaskConfig } from '../types';
|
||||
import {
|
||||
extractTemplateVariables,
|
||||
extractVariablesFromContent,
|
||||
@@ -11,12 +11,15 @@ import {
|
||||
withContentAndSyncedName,
|
||||
} from '../utils';
|
||||
import { VisibilityRuleDialog } from './VisibilityRuleDialog';
|
||||
import { ElementMaskDialog } from './ElementMaskDialog';
|
||||
import { VisibilityRuleSummary } from './VisibilityRuleSummary';
|
||||
import { ValueExtractFields } from './ValueExtractFields';
|
||||
import { TextDisplayAffixFields } from './TextDisplayAffixFields';
|
||||
import {
|
||||
alignToSelectionBounds,
|
||||
alignSelectionToCanvas,
|
||||
getSelectionBounds,
|
||||
setSelectionOrigin,
|
||||
tileSelectedEvenly,
|
||||
tileSelectedFlush,
|
||||
type AlignMode,
|
||||
@@ -34,6 +37,7 @@ import {
|
||||
} from './TablePropertyFields';
|
||||
import type { TableCellCoord } from '../tableUtils';
|
||||
import { expandTableForPadding } from '../tableUtils';
|
||||
import { isElementMaskActive } from '../elementMask';
|
||||
import {
|
||||
Sliders,
|
||||
LayoutGrid,
|
||||
@@ -77,6 +81,7 @@ import {
|
||||
Image as ImageIcon,
|
||||
Upload,
|
||||
Table2,
|
||||
Blend,
|
||||
} from 'lucide-react';
|
||||
|
||||
const PANEL_COLLAPSE_STORAGE_KEY = 'label_designer_panel_collapse_v2';
|
||||
@@ -220,6 +225,7 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
|
||||
const [visibilityRuleDialog, setVisibilityRuleDialog] = useState<
|
||||
{ mode: 'add' } | { mode: 'edit'; index: number } | null
|
||||
>(null);
|
||||
const [maskDialogElementId, setMaskDialogElementId] = useState<string | null>(null);
|
||||
const [panelCollapsed, setPanelCollapsed] = useState<PanelCollapseState>(loadPanelCollapseState);
|
||||
const [internalMobileModule, setInternalMobileModule] =
|
||||
useState<MobilePropModule>('properties');
|
||||
@@ -288,6 +294,24 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
|
||||
.map((el) => el.id);
|
||||
}, [selectedElementIds, template.elements]);
|
||||
|
||||
const isMultiSelect = selectedElementIds.length >= 2;
|
||||
|
||||
const multiSelectionBounds = useMemo(() => {
|
||||
if (!isMultiSelect) return null;
|
||||
const selected = template.elements.filter(
|
||||
(el) => selectedElementIds.includes(el.id) && !el.locked
|
||||
);
|
||||
if (selected.length < 2) return null;
|
||||
return getSelectionBounds(selected);
|
||||
}, [isMultiSelect, selectedElementIds, template.elements]);
|
||||
|
||||
const moveSelectedGroupTo = (newMinX: number, newMinY: number) => {
|
||||
onChangeTemplate({
|
||||
...template,
|
||||
elements: setSelectionOrigin(template.elements, selectedElementIds, newMinX, newMinY),
|
||||
});
|
||||
};
|
||||
|
||||
const nudgeSelectedElements = (deltaX: number, deltaY: number) => {
|
||||
if (nudgeableElementIds.length === 0) return;
|
||||
const idSet = new Set(nudgeableElementIds);
|
||||
@@ -304,6 +328,19 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
|
||||
});
|
||||
};
|
||||
|
||||
const maskDialogElement = maskDialogElementId
|
||||
? template.elements.find((el) => el.id === maskDialogElementId) ?? null
|
||||
: null;
|
||||
|
||||
const updateElementMask = (elementId: string, mask: ElementMaskConfig | undefined) => {
|
||||
onChangeTemplate({
|
||||
...template,
|
||||
elements: template.elements.map((el) =>
|
||||
el.id === elementId ? { ...el, mask: mask || undefined } : el
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
const applyVisibilityRules = (next: VisibilityRule[]) => {
|
||||
if (!selectedElem) return;
|
||||
handleElemChange({
|
||||
@@ -1710,39 +1747,60 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
|
||||
<div className="space-y-3">
|
||||
<h5 className="ps-section-title">
|
||||
<Ruler className="w-3.5 h-3.5" />
|
||||
位置与尺寸 (mm)
|
||||
{isMultiSelect && multiSelectionBounds ? '选区位置 (mm)' : '位置与尺寸 (mm)'}
|
||||
</h5>
|
||||
{isMultiSelect && multiSelectionBounds && (
|
||||
<p className="text-[10px] text-[#666] leading-snug">
|
||||
X、Y 为选中元素整体包围框左上角,修改后将整体平移
|
||||
</p>
|
||||
)}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="ps-field-label">X 坐标 (左边距)</label>
|
||||
<label className="ps-field-label">
|
||||
{isMultiSelect ? 'X 坐标 (选区左边距)' : 'X 坐标 (左边距)'}
|
||||
</label>
|
||||
<PropInput
|
||||
type="number"
|
||||
step="0.5"
|
||||
value={selectedElem.x}
|
||||
value={isMultiSelect && multiSelectionBounds ? multiSelectionBounds.minX : selectedElem.x}
|
||||
onCommit={(val) => {
|
||||
const xVal = Math.round((Number(val) || 0) * 10) / 10;
|
||||
if (isMultiSelect && multiSelectionBounds) {
|
||||
moveSelectedGroupTo(xVal, multiSelectionBounds.minY);
|
||||
return;
|
||||
}
|
||||
handleElemChange({
|
||||
...selectedElem,
|
||||
x: Math.round((Number(val) || 0) * 10) / 10,
|
||||
x: xVal,
|
||||
});
|
||||
}}
|
||||
className="ps-field font-mono"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="ps-field-label">Y 坐标 (顶边距)</label>
|
||||
<label className="ps-field-label">
|
||||
{isMultiSelect ? 'Y 坐标 (选区顶边距)' : 'Y 坐标 (顶边距)'}
|
||||
</label>
|
||||
<PropInput
|
||||
type="number"
|
||||
step="0.5"
|
||||
value={selectedElem.y}
|
||||
value={isMultiSelect && multiSelectionBounds ? multiSelectionBounds.minY : selectedElem.y}
|
||||
onCommit={(val) => {
|
||||
const yVal = Math.round((Number(val) || 0) * 10) / 10;
|
||||
if (isMultiSelect && multiSelectionBounds) {
|
||||
moveSelectedGroupTo(multiSelectionBounds.minX, yVal);
|
||||
return;
|
||||
}
|
||||
handleElemChange({
|
||||
...selectedElem,
|
||||
y: Math.round((Number(val) || 0) * 10) / 10,
|
||||
y: yVal,
|
||||
});
|
||||
}}
|
||||
className="ps-field font-mono"
|
||||
/>
|
||||
</div>
|
||||
{!isMultiSelect && (
|
||||
<>
|
||||
<div>
|
||||
<label className="ps-field-label">元素宽度</label>
|
||||
<PropInput
|
||||
@@ -1805,6 +1863,8 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2251,6 +2311,23 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{!isLocked && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setMaskDialogElementId(elem.id);
|
||||
}}
|
||||
title={isElementMaskActive(elem.mask) ? '编辑蒙版' : '添加蒙版'}
|
||||
className={
|
||||
isElementMaskActive(elem.mask)
|
||||
? 'text-[#31a8ff]'
|
||||
: 'text-[#666] hover:text-[#ccc]'
|
||||
}
|
||||
>
|
||||
<Blend />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => { e.stopPropagation(); toggleLayerVisibility(actualIdx); }}
|
||||
@@ -2344,14 +2421,16 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
|
||||
|
||||
{primaryElem && (
|
||||
<div className="mobile-nudge-info">
|
||||
<p className="text-[12px] font-medium text-[#ddd] truncate">{primaryElem.name}</p>
|
||||
<p className="text-[12px] font-medium text-[#ddd] truncate">
|
||||
{nudgeableElementIds.length > 1 ? `已选 ${nudgeableElementIds.length} 个元素` : primaryElem.name}
|
||||
</p>
|
||||
<p className="text-[11px] font-mono text-[#31a8ff] mt-1">
|
||||
X {primaryElem.x} · Y {primaryElem.y} mm
|
||||
{nudgeableElementIds.length > 1 && multiSelectionBounds
|
||||
? `X ${multiSelectionBounds.minX} · Y ${multiSelectionBounds.minY} mm`
|
||||
: `X ${primaryElem.x} · Y ${primaryElem.y} mm`}
|
||||
</p>
|
||||
{nudgeableElementIds.length > 1 && (
|
||||
<p className="text-[10px] text-[#888] mt-1">
|
||||
已选 {nudgeableElementIds.length} 个元素,将一起移动
|
||||
</p>
|
||||
<p className="text-[10px] text-[#888] mt-1">将一起移动</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -2372,10 +2451,23 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
|
||||
/>
|
||||
);
|
||||
|
||||
const elementMaskDialogNode = (
|
||||
<ElementMaskDialog
|
||||
open={maskDialogElementId !== null}
|
||||
element={maskDialogElement}
|
||||
onClose={() => setMaskDialogElementId(null)}
|
||||
onConfirm={(mask) => {
|
||||
if (!maskDialogElementId) return;
|
||||
updateElementMask(maskDialogElementId, mask);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
if (variant === 'mobile') {
|
||||
return (
|
||||
<div className="mobile-design-props">
|
||||
{visibilityRuleDialogNode}
|
||||
{elementMaskDialogNode}
|
||||
<nav className="mobile-design-props-nav" aria-label="属性模块">
|
||||
{MOBILE_PROP_MODULES.map((item) => (
|
||||
<button
|
||||
@@ -2434,6 +2526,7 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
|
||||
return (
|
||||
<div className="ps-sidebar-stack">
|
||||
{visibilityRuleDialogNode}
|
||||
{elementMaskDialogNode}
|
||||
<CollapsiblePanelSection
|
||||
sectionClassName="properties-panel flex flex-col"
|
||||
collapsed={panelCollapsed.properties}
|
||||
|
||||
Reference in New Issue
Block a user