修复
This commit is contained in:
@@ -37,6 +37,9 @@ import {
|
||||
Unlock,
|
||||
ArrowUp,
|
||||
ArrowDown,
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
Move,
|
||||
Layers,
|
||||
Trash2,
|
||||
AlignHorizontalJustifyStart,
|
||||
@@ -148,7 +151,9 @@ const PropTextarea: React.FC<PropTextareaProps> = ({ value, onCommit, ...props }
|
||||
);
|
||||
};
|
||||
|
||||
type MobilePropModule = 'content' | 'properties' | 'layers';
|
||||
export type MobilePropModule = 'content' | 'properties' | 'layers' | 'nudge';
|
||||
|
||||
const MOBILE_NUDGE_STEP_MM = 0.5;
|
||||
|
||||
interface PropSidebarProps {
|
||||
template: LabelTemplate;
|
||||
@@ -159,10 +164,13 @@ interface PropSidebarProps {
|
||||
paper: PaperConfig;
|
||||
onSelectElements?: (ids: string[]) => void;
|
||||
variant?: 'desktop' | 'mobile';
|
||||
mobileModule?: MobilePropModule;
|
||||
onMobileModuleChange?: (module: MobilePropModule) => void;
|
||||
}
|
||||
|
||||
const MOBILE_PROP_MODULES: { id: MobilePropModule; label: string; icon: React.ReactNode }[] = [
|
||||
{ id: 'properties', label: '属性', icon: <Sliders className="w-4 h-4" /> },
|
||||
{ id: 'nudge', label: '微调', icon: <Move className="w-4 h-4" /> },
|
||||
{ id: 'content', label: '内容', icon: <Link2 className="w-4 h-4" /> },
|
||||
{ id: 'layers', label: '图层', icon: <Layers className="w-4 h-4" /> },
|
||||
];
|
||||
@@ -176,6 +184,8 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
|
||||
paper,
|
||||
onSelectElements,
|
||||
variant = 'desktop',
|
||||
mobileModule: mobileModuleProp,
|
||||
onMobileModuleChange,
|
||||
}) => {
|
||||
const { showConfirm } = useAppDialog();
|
||||
const isNarrowScreen = useIsNarrowScreen();
|
||||
@@ -186,7 +196,15 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
|
||||
{ mode: 'add' } | { mode: 'edit'; index: number } | null
|
||||
>(null);
|
||||
const [panelCollapsed, setPanelCollapsed] = useState<PanelCollapseState>(loadPanelCollapseState);
|
||||
const [mobileModule, setMobileModule] = useState<MobilePropModule>('properties');
|
||||
const [internalMobileModule, setInternalMobileModule] =
|
||||
useState<MobilePropModule>('properties');
|
||||
const mobileModule = mobileModuleProp ?? internalMobileModule;
|
||||
const setMobileModule = (module: MobilePropModule) => {
|
||||
onMobileModuleChange?.(module);
|
||||
if (mobileModuleProp === undefined) {
|
||||
setInternalMobileModule(module);
|
||||
}
|
||||
};
|
||||
|
||||
const togglePanel = (key: keyof PanelCollapseState) => {
|
||||
setPanelCollapsed((prev) => {
|
||||
@@ -242,6 +260,29 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
|
||||
});
|
||||
};
|
||||
|
||||
const nudgeableElementIds = useMemo(() => {
|
||||
const idSet = new Set(selectedElementIds);
|
||||
return template.elements
|
||||
.filter((el) => idSet.has(el.id) && !el.locked)
|
||||
.map((el) => el.id);
|
||||
}, [selectedElementIds, template.elements]);
|
||||
|
||||
const nudgeSelectedElements = (deltaX: number, deltaY: number) => {
|
||||
if (nudgeableElementIds.length === 0) return;
|
||||
const idSet = new Set(nudgeableElementIds);
|
||||
onChangeTemplate({
|
||||
...template,
|
||||
elements: template.elements.map((el) => {
|
||||
if (!idSet.has(el.id)) return el;
|
||||
return {
|
||||
...el,
|
||||
x: Math.round((el.x + deltaX) * 10) / 10,
|
||||
y: Math.round((el.y + deltaY) * 10) / 10,
|
||||
};
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
const applyVisibilityRules = (next: VisibilityRule[]) => {
|
||||
if (!selectedElem) return;
|
||||
handleElemChange({
|
||||
@@ -1841,6 +1882,82 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
|
||||
</>
|
||||
);
|
||||
|
||||
const renderNudgePanelBody = () => {
|
||||
const canNudge = nudgeableElementIds.length > 0;
|
||||
const primaryElem =
|
||||
selectedElem && nudgeableElementIds.includes(selectedElem.id) ? selectedElem : null;
|
||||
|
||||
return (
|
||||
<div className="mobile-nudge-panel">
|
||||
<div className="mobile-nudge-header space-y-1 mb-4">
|
||||
<h4 className="text-[10px] font-bold uppercase tracking-widest text-[#ccc]">微调位置</h4>
|
||||
<p className="text-[10px] text-[#777]">每次移动 {MOBILE_NUDGE_STEP_MM} mm,与桌面方向键一致</p>
|
||||
</div>
|
||||
|
||||
{!canNudge ? (
|
||||
<div className="mobile-nudge-empty">
|
||||
<Move className="w-8 h-8 text-[#555] mb-2" />
|
||||
<p className="text-[12px] text-[#888]">请先选中未锁定的元素</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="mobile-nudge-pad" role="group" aria-label="位置微调">
|
||||
<button
|
||||
type="button"
|
||||
className="mobile-nudge-btn"
|
||||
onClick={() => nudgeSelectedElements(0, -MOBILE_NUDGE_STEP_MM)}
|
||||
aria-label="向上微调"
|
||||
>
|
||||
<ArrowUp className="w-5 h-5" />
|
||||
</button>
|
||||
<div className="mobile-nudge-pad-row">
|
||||
<button
|
||||
type="button"
|
||||
className="mobile-nudge-btn"
|
||||
onClick={() => nudgeSelectedElements(-MOBILE_NUDGE_STEP_MM, 0)}
|
||||
aria-label="向左微调"
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</button>
|
||||
<div className="mobile-nudge-pad-center" aria-hidden="true" />
|
||||
<button
|
||||
type="button"
|
||||
className="mobile-nudge-btn"
|
||||
onClick={() => nudgeSelectedElements(MOBILE_NUDGE_STEP_MM, 0)}
|
||||
aria-label="向右微调"
|
||||
>
|
||||
<ArrowRight className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="mobile-nudge-btn"
|
||||
onClick={() => nudgeSelectedElements(0, MOBILE_NUDGE_STEP_MM)}
|
||||
aria-label="向下微调"
|
||||
>
|
||||
<ArrowDown className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{primaryElem && (
|
||||
<div className="mobile-nudge-info">
|
||||
<p className="text-[12px] font-medium text-[#ddd] truncate">{primaryElem.name}</p>
|
||||
<p className="text-[11px] font-mono text-[#31a8ff] mt-1">
|
||||
X {primaryElem.x} · Y {primaryElem.y} mm
|
||||
</p>
|
||||
{nudgeableElementIds.length > 1 && (
|
||||
<p className="text-[10px] text-[#888] mt-1">
|
||||
已选 {nudgeableElementIds.length} 个元素,将一起移动
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const visibilityRuleDialogNode = (
|
||||
<VisibilityRuleDialog
|
||||
open={visibilityRuleDialog !== null}
|
||||
@@ -1876,6 +1993,9 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
|
||||
{mobileModule === 'properties' && (
|
||||
<div className="ps-panel-body p-3 space-y-4 min-h-0">{renderPropertiesPanelBody()}</div>
|
||||
)}
|
||||
{mobileModule === 'nudge' && (
|
||||
<div className="ps-panel-body p-3 min-h-0">{renderNudgePanelBody()}</div>
|
||||
)}
|
||||
{mobileModule === 'layers' && (
|
||||
<div className="ps-panel-body min-h-0 mobile-layers-panel">{renderLayersPanelBody()}</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user