This commit is contained in:
iqudoo
2026-06-15 16:59:37 +08:00
parent fcd587bd50
commit 455946c51c
15 changed files with 1003 additions and 54 deletions

View File

@@ -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">
XY
</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}