优化交互
This commit is contained in:
@@ -35,6 +35,10 @@ export const CanvasLabelImage: React.FC<CanvasLabelImageProps> = ({
|
||||
|
||||
const elementsKey = useMemo(() => JSON.stringify(elements), [elements]);
|
||||
const rowDataKey = useMemo(() => JSON.stringify(rowData), [rowData]);
|
||||
const variableDefaultsKey = useMemo(
|
||||
() => JSON.stringify(variableDefaults),
|
||||
[variableDefaults]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
@@ -52,10 +56,18 @@ export const CanvasLabelImage: React.FC<CanvasLabelImageProps> = ({
|
||||
scale: renderScale,
|
||||
})
|
||||
.then((url) => {
|
||||
if (!active) return;
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
if (active) {
|
||||
setDataUrl(url);
|
||||
setInitialLoading(false);
|
||||
}
|
||||
};
|
||||
img.onerror = () => {
|
||||
if (active) setInitialLoading(false);
|
||||
};
|
||||
img.src = url;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Label graphics rendering error:', err);
|
||||
@@ -65,7 +77,18 @@ export const CanvasLabelImage: React.FC<CanvasLabelImageProps> = ({
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [widthMm, heightMm, elementsKey, rowDataKey, rowIndex, showBorder, transparentBackground, mode, renderScale, elements, variableDefaults]);
|
||||
}, [
|
||||
widthMm,
|
||||
heightMm,
|
||||
elementsKey,
|
||||
rowDataKey,
|
||||
variableDefaultsKey,
|
||||
rowIndex,
|
||||
showBorder,
|
||||
transparentBackground,
|
||||
mode,
|
||||
renderScale,
|
||||
]);
|
||||
|
||||
if (initialLoading && !dataUrl) {
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { TemplateElement } from '../types';
|
||||
import { mmToPx } from '../utils';
|
||||
import { renderLabelToDataUrl } from '../labelRenderer';
|
||||
@@ -42,6 +42,11 @@ export const ElementFloatPreview: React.FC<ElementFloatPreviewProps> = ({
|
||||
}) => {
|
||||
const [dataUrl, setDataUrl] = useState(dataUrlProp ?? '');
|
||||
|
||||
const elementVisualKey = useMemo(() => {
|
||||
const { x: _x, y: _y, ...visual } = element;
|
||||
return JSON.stringify(visual);
|
||||
}, [element]);
|
||||
|
||||
useEffect(() => {
|
||||
if (dataUrlProp) {
|
||||
setDataUrl(dataUrlProp);
|
||||
@@ -60,7 +65,7 @@ export const ElementFloatPreview: React.FC<ElementFloatPreviewProps> = ({
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [element, renderScale, variableDefaults, dataUrlProp]);
|
||||
}, [elementVisualKey, element, renderScale, variableDefaults, dataUrlProp]);
|
||||
|
||||
const left = mmToPx(element.x, displayZoom);
|
||||
const top = mmToPx(element.y, displayZoom);
|
||||
@@ -81,6 +86,12 @@ export const ElementFloatPreview: React.FC<ElementFloatPreviewProps> = ({
|
||||
height: `${height}px`,
|
||||
imageRendering: 'auto',
|
||||
}}
|
||||
onLoad={(e) => {
|
||||
const placeholder = e.currentTarget.previousElementSibling as HTMLElement | null;
|
||||
if (placeholder?.classList.contains('resize-float-placeholder')) {
|
||||
placeholder.style.display = 'none';
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -237,8 +237,12 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
const interactionPortalRef = useRef<HTMLDivElement | null>(null);
|
||||
const dragFloatImgRefs = useRef<Map<string, HTMLImageElement>>(new Map());
|
||||
const dragFloatUrlsRef = useRef<Record<string, string>>({});
|
||||
const dragFloatElementsRef = useRef<TemplateElement[]>([]);
|
||||
const dragVisualActivatedRef = useRef(false);
|
||||
const dragTransformLoopRef = useRef<number | null>(null);
|
||||
const [dragFloatElements, setDragFloatElements] = useState<TemplateElement[]>([]);
|
||||
const [dragInteractionVisualActive, setDragInteractionVisualActive] = useState(false);
|
||||
const [interactionCanvasReady, setInteractionCanvasReady] = useState(false);
|
||||
const interactionFloatPaintRef = useRef(0);
|
||||
const interactionFloatTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
@@ -513,6 +517,10 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
setInteractionSnapshot(null);
|
||||
setInteractionFloatElements(null);
|
||||
setDragFloatElements([]);
|
||||
dragFloatElementsRef.current = [];
|
||||
dragVisualActivatedRef.current = false;
|
||||
setDragInteractionVisualActive(false);
|
||||
setInteractionCanvasReady(false);
|
||||
dragFloatImgRefs.current.clear();
|
||||
dragFloatUrlsRef.current = {};
|
||||
pendingDragDeltaRef.current = { x: 0, y: 0 };
|
||||
@@ -643,6 +651,36 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
const maxZoomRef = useRef(maxZoom);
|
||||
maxZoomRef.current = maxZoom;
|
||||
|
||||
const prefetchDragFloatImages = useCallback(
|
||||
(elements: TemplateElement[]) => {
|
||||
if (elements.length === 0) return;
|
||||
void Promise.all(
|
||||
elements.map(async (el) => {
|
||||
try {
|
||||
const url = await renderElementFloatPreview(
|
||||
el,
|
||||
canvasRenderScale,
|
||||
previewDefaults
|
||||
);
|
||||
dragFloatUrlsRef.current[el.id] = url;
|
||||
const img = dragFloatImgRefs.current.get(el.id);
|
||||
if (img) {
|
||||
img.src = url;
|
||||
if (img.complete) {
|
||||
img.style.opacity = '1';
|
||||
const placeholder = img.previousElementSibling as HTMLElement | null;
|
||||
if (placeholder) placeholder.style.display = 'none';
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Drag float preview error:', err);
|
||||
}
|
||||
})
|
||||
);
|
||||
},
|
||||
[canvasRenderScale, previewDefaults]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setZoom((z) => (z > maxZoom ? clampZoomValue(z, maxZoom) : z));
|
||||
}, [maxZoom]);
|
||||
@@ -788,6 +826,11 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
}
|
||||
if (dragState.type === 'drag' && pastDragThreshold) {
|
||||
dragMovedRef.current = true;
|
||||
if (!dragVisualActivatedRef.current) {
|
||||
dragVisualActivatedRef.current = true;
|
||||
setDragInteractionVisualActive(true);
|
||||
prefetchDragFloatImages(dragFloatElementsRef.current);
|
||||
}
|
||||
}
|
||||
const MM_PER_PX = 1 / (3.78 * displayZoomRef.current);
|
||||
const deltaXMm = deltaX * MM_PER_PX;
|
||||
@@ -933,7 +976,7 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
window.removeEventListener('pointerup', handlePointerUp);
|
||||
window.removeEventListener('pointercancel', handlePointerUp);
|
||||
};
|
||||
}, [dragState, template, displayZoom, onChange, cancelActiveInteraction, clearInteractionCanvasState]);
|
||||
}, [dragState, template, displayZoom, onChange, cancelActiveInteraction, clearInteractionCanvasState, prefetchDragFloatImages]);
|
||||
|
||||
const buildGroupStartPositions = (element: TemplateElement) => {
|
||||
const moveIds =
|
||||
@@ -1048,6 +1091,10 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
dragFloatImgRefs.current.clear();
|
||||
dragCommitHistoryRef.current = null;
|
||||
dragCommitHistoryPrevRef.current = null;
|
||||
dragVisualActivatedRef.current = false;
|
||||
setDragInteractionVisualActive(false);
|
||||
setInteractionCanvasReady(false);
|
||||
dragFloatElementsRef.current = floating;
|
||||
setDragFloatElements(floating);
|
||||
setDragState({
|
||||
elementId: element.id,
|
||||
@@ -1060,32 +1107,6 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
startElemH: element.height,
|
||||
groupStartPositions,
|
||||
});
|
||||
|
||||
if (actionType === 'drag' && floating.length > 0) {
|
||||
void Promise.all(
|
||||
floating.map(async (el) => {
|
||||
try {
|
||||
const url = await renderElementFloatPreview(
|
||||
el,
|
||||
canvasRenderScale,
|
||||
previewDefaults
|
||||
);
|
||||
dragFloatUrlsRef.current[el.id] = url;
|
||||
const img = dragFloatImgRefs.current.get(el.id);
|
||||
if (img) {
|
||||
img.src = url;
|
||||
if (img.complete) {
|
||||
img.style.opacity = '1';
|
||||
const placeholder = img.previousElementSibling as HTMLElement | null;
|
||||
if (placeholder) placeholder.style.display = 'none';
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Drag float preview error:', err);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleResizeStart = (
|
||||
@@ -1103,6 +1124,7 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
setInteractionSnapshot(template.elements);
|
||||
setInteractionFloatElements([element]);
|
||||
interactionFloatPaintRef.current = 0;
|
||||
setInteractionCanvasReady(false);
|
||||
setDragState({
|
||||
elementId: element.id,
|
||||
type: 'resize',
|
||||
@@ -1149,25 +1171,72 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
return new Set(Object.keys(dragState.groupStartPositions));
|
||||
}, [dragState]);
|
||||
|
||||
const liftSelectedAboveDim = activeTool === 'move' && selectedElementIds.length > 0;
|
||||
|
||||
const staticCanvasElements = useMemo(() => {
|
||||
if (dragState?.type === 'drag' && interactionSnapshot && dragMovingIdSet) {
|
||||
return filterVisibleElements(
|
||||
let elements: TemplateElement[];
|
||||
if (
|
||||
dragState?.type === 'drag' &&
|
||||
interactionSnapshot &&
|
||||
dragMovingIdSet &&
|
||||
dragInteractionVisualActive &&
|
||||
interactionCanvasReady
|
||||
) {
|
||||
elements = filterVisibleElements(
|
||||
interactionSnapshot.filter((el) => !dragMovingIdSet.has(el.id))
|
||||
);
|
||||
}
|
||||
if (dragState?.type === 'resize' && interactionSnapshot) {
|
||||
return filterVisibleElements(
|
||||
} else if (dragState?.type === 'resize' && interactionSnapshot && interactionCanvasReady) {
|
||||
elements = filterVisibleElements(
|
||||
interactionSnapshot.filter((el) => el.id !== dragState.elementId)
|
||||
);
|
||||
} else {
|
||||
elements = filterVisibleElements(template.elements);
|
||||
}
|
||||
return filterVisibleElements(template.elements);
|
||||
|
||||
if (liftSelectedAboveDim) {
|
||||
const selectedSet = new Set(selectedElementIds);
|
||||
elements = elements.filter((el) => !selectedSet.has(el.id));
|
||||
}
|
||||
|
||||
return elements;
|
||||
}, [
|
||||
dragState?.type,
|
||||
dragState?.elementId,
|
||||
interactionSnapshot,
|
||||
dragMovingIdSet,
|
||||
dragInteractionVisualActive,
|
||||
interactionCanvasReady,
|
||||
template.elements,
|
||||
filterVisibleElements,
|
||||
liftSelectedAboveDim,
|
||||
selectedElementIds,
|
||||
]);
|
||||
|
||||
const selectedLiftElements = useMemo(() => {
|
||||
if (!liftSelectedAboveDim) return null;
|
||||
|
||||
let elements = filterVisibleElements(
|
||||
template.elements.filter((el) => selectedElementIds.includes(el.id))
|
||||
);
|
||||
|
||||
if (dragState?.type === 'drag' && dragInteractionVisualActive && dragMovingIdSet) {
|
||||
elements = elements.filter((el) => !dragMovingIdSet.has(el.id));
|
||||
}
|
||||
if (dragState?.type === 'resize' && interactionCanvasReady && dragState.elementId) {
|
||||
elements = elements.filter((el) => el.id !== dragState.elementId);
|
||||
}
|
||||
|
||||
return elements.length > 0 ? elements : null;
|
||||
}, [
|
||||
liftSelectedAboveDim,
|
||||
selectedElementIds,
|
||||
template.elements,
|
||||
filterVisibleElements,
|
||||
dragState?.type,
|
||||
dragState?.elementId,
|
||||
dragInteractionVisualActive,
|
||||
dragMovingIdSet,
|
||||
interactionCanvasReady,
|
||||
]);
|
||||
|
||||
const resizeFloatElements = useMemo(() => {
|
||||
@@ -1199,7 +1268,30 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
};
|
||||
}, [allowInteractionOverflow, syncInteractionPortalLayout]);
|
||||
|
||||
const hideSelectionChrome = suppressSelectionChrome || dragState?.type === 'drag';
|
||||
useEffect(() => {
|
||||
const shouldPrepareCanvas =
|
||||
(dragState?.type === 'drag' && dragInteractionVisualActive) ||
|
||||
dragState?.type === 'resize';
|
||||
if (!shouldPrepareCanvas) {
|
||||
setInteractionCanvasReady(false);
|
||||
return;
|
||||
}
|
||||
setInteractionCanvasReady(false);
|
||||
let cancelled = false;
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
if (!cancelled) setInteractionCanvasReady(true);
|
||||
});
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [dragState, dragInteractionVisualActive]);
|
||||
|
||||
const hideSelectionChrome =
|
||||
suppressSelectionChrome || (dragState?.type === 'drag' && dragInteractionVisualActive);
|
||||
const showUnselectedDimLayer = activeTool === 'move';
|
||||
const showUnselectedDim = showUnselectedDimLayer && selectedElementIds.length > 0;
|
||||
const resizingPreviewElement =
|
||||
dragState?.type === 'resize'
|
||||
? interactionFloatElements?.find((el) => el.id === dragState.elementId)
|
||||
@@ -1264,7 +1356,7 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
>
|
||||
<ZoomOut className="w-3 h-3" />
|
||||
</button>
|
||||
<span className="px-1.5 text-[10px] font-mono text-[#ccc] min-w-[42px] text-center select-none">
|
||||
<span className="px-1 text-[10px] font-mono text-[#ccc] min-w-[32px] text-center select-none">
|
||||
{Math.round(displayZoom * 100)}%
|
||||
</span>
|
||||
<button
|
||||
@@ -1289,7 +1381,9 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
|
||||
const showInteractionFloatPortal =
|
||||
typeof document !== 'undefined' &&
|
||||
((dragState?.type === 'drag' && dragFloatElements.length > 0) ||
|
||||
((dragState?.type === 'drag' &&
|
||||
dragInteractionVisualActive &&
|
||||
dragFloatElements.length > 0) ||
|
||||
(resizeFloatElements && resizeFloatElements.length > 0));
|
||||
|
||||
const interactionFloatPortal = showInteractionFloatPortal
|
||||
@@ -1367,13 +1461,25 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
style={{ overflow: 'visible' }}
|
||||
>
|
||||
{resizeFloatElements.map((el) => (
|
||||
<React.Fragment key={el.id}>
|
||||
<div
|
||||
className="absolute pointer-events-none border border-dashed border-[#31a8ff]/40 resize-float-placeholder"
|
||||
style={{
|
||||
left: `${mmToPx(el.x, displayZoom)}px`,
|
||||
top: `${mmToPx(el.y, displayZoom)}px`,
|
||||
width: `${mmToPx(el.width, displayZoom)}px`,
|
||||
height: `${mmToPx(el.height, displayZoom)}px`,
|
||||
transform: el.rotation ? `rotate(${el.rotation}deg)` : undefined,
|
||||
transformOrigin: 'center center',
|
||||
}}
|
||||
/>
|
||||
<ElementFloatPreview
|
||||
key={el.id}
|
||||
element={el}
|
||||
renderScale={canvasRenderScale}
|
||||
displayZoom={displayZoom}
|
||||
variableDefaults={previewDefaults}
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -1456,6 +1562,56 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{showUnselectedDimLayer && (
|
||||
<div
|
||||
className={`ps-unselected-dim-layer${
|
||||
showUnselectedDim ? ' ps-unselected-dim-layer--visible' : ''
|
||||
}`}
|
||||
aria-hidden={!showUnselectedDim}
|
||||
>
|
||||
{overlayElements.map((element) => {
|
||||
if (selectedElementIds.includes(element.id)) return null;
|
||||
if (dragState?.type === 'drag' && dragMovingIdSet?.has(element.id)) return null;
|
||||
const elemX = mmToPx(element.x, displayZoom);
|
||||
const elemY = mmToPx(element.y, displayZoom);
|
||||
const elemW = mmToPx(element.width, displayZoom);
|
||||
const elemH = mmToPx(element.height, displayZoom);
|
||||
return (
|
||||
<div
|
||||
key={`dim-${element.id}`}
|
||||
className="ps-unselected-dim absolute"
|
||||
style={{
|
||||
left: `${elemX}px`,
|
||||
top: `${elemY}px`,
|
||||
width: `${elemW}px`,
|
||||
height: `${elemH}px`,
|
||||
transform: element.rotation
|
||||
? `rotate(${element.rotation}deg)`
|
||||
: undefined,
|
||||
transformOrigin: 'center center',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedLiftElements && (
|
||||
<div className="ps-selected-lift-layer">
|
||||
<CanvasLabelImage
|
||||
widthMm={template.width}
|
||||
heightMm={template.height}
|
||||
elements={selectedLiftElements}
|
||||
rowData={null}
|
||||
rowIndex={0}
|
||||
showBorder={false}
|
||||
transparentBackground
|
||||
variableDefaults={previewDefaults}
|
||||
renderScale={canvasRenderScale}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTool === 'move' && (
|
||||
<div
|
||||
className="absolute inset-0 z-[1] cursor-crosshair touch-none"
|
||||
@@ -1481,7 +1637,9 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
const isSelected = !isLocked && selectedElementIds.includes(element.id);
|
||||
const isPrimary = selectedElementIds.indexOf(element.id) === 0;
|
||||
const isBeingDragged =
|
||||
dragState?.type === 'drag' && !!dragMovingIdSet?.has(element.id);
|
||||
dragInteractionVisualActive &&
|
||||
dragState?.type === 'drag' &&
|
||||
!!dragMovingIdSet?.has(element.id);
|
||||
const isBeingResized =
|
||||
dragState?.type === 'resize' && dragState.elementId === element.id;
|
||||
if (isBeingDragged) {
|
||||
@@ -1491,9 +1649,10 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
const overlayElement =
|
||||
isBeingResized && resizingPreviewElement ? resizingPreviewElement : element;
|
||||
|
||||
const showSelectionRing = isSelected && !hideSelectionChrome;
|
||||
const showResizeHandles =
|
||||
showSelectionRing &&
|
||||
isSelected &&
|
||||
!hideSelectionChrome &&
|
||||
!isLocked &&
|
||||
selectedElementIds.length === 1 &&
|
||||
dragState?.type !== 'resize';
|
||||
const elemX = mmToPx(overlayElement.x, displayZoom);
|
||||
@@ -1508,8 +1667,6 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
className={`absolute group touch-none ${
|
||||
isLocked
|
||||
? 'pointer-events-none'
|
||||
: showSelectionRing
|
||||
? 'ps-selection-ring cursor-move'
|
||||
: isSelected
|
||||
? 'cursor-move'
|
||||
: 'cursor-move hover:outline hover:outline-1 hover:outline-[#31a8ff]/40'
|
||||
@@ -1589,7 +1746,6 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
<div className="mobile-design-toolstrip">
|
||||
<div className="mobile-design-toolstrip-tools">
|
||||
{mainToolButtons}
|
||||
{deleteToolButton}
|
||||
</div>
|
||||
{zoomControls}
|
||||
</div>
|
||||
|
||||
@@ -565,6 +565,36 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const deletableSelectedIds = useMemo(
|
||||
() =>
|
||||
selectedElementIds.filter((id) => {
|
||||
const el = template.elements.find((item) => item.id === id);
|
||||
return el && !el.locked;
|
||||
}),
|
||||
[selectedElementIds, template.elements]
|
||||
);
|
||||
|
||||
const deleteSelectedElements = async () => {
|
||||
if (deletableSelectedIds.length === 0) return;
|
||||
|
||||
const message =
|
||||
deletableSelectedIds.length === 1
|
||||
? `确定删除「${template.elements.find((item) => item.id === deletableSelectedIds[0])?.name ?? '未命名'}」吗?此操作无法撤销。`
|
||||
: `确定删除选中的 ${deletableSelectedIds.length} 个元素吗?此操作无法撤销。`;
|
||||
|
||||
const confirmed = await showConfirm(message, {
|
||||
title: '删除元素',
|
||||
confirmLabel: '删除',
|
||||
variant: 'error',
|
||||
});
|
||||
if (!confirmed) return;
|
||||
|
||||
const idSet = new Set(deletableSelectedIds);
|
||||
const remaining = template.elements.filter((el) => !idSet.has(el.id));
|
||||
onChangeTemplate({ ...template, elements: remaining });
|
||||
onSelectElements?.(selectedElementIds.filter((id) => remaining.some((el) => el.id === id)));
|
||||
};
|
||||
|
||||
const renderContentPanelBody = () => {
|
||||
if (!selectedElem) {
|
||||
return (
|
||||
@@ -1985,6 +2015,18 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
|
||||
<span>{item.label}</span>
|
||||
</button>
|
||||
))}
|
||||
<div className="mobile-design-props-nav-spacer" aria-hidden />
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void deleteSelectedElements()}
|
||||
disabled={deletableSelectedIds.length === 0}
|
||||
className="mobile-design-props-nav-btn mobile-design-props-nav-delete"
|
||||
aria-label="删除选中元素"
|
||||
title="删除选中元素"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
<span>删除</span>
|
||||
</button>
|
||||
</nav>
|
||||
<div className="mobile-design-props-panel scrollbar-hide">
|
||||
{mobileModule === 'content' && (
|
||||
|
||||
@@ -99,16 +99,6 @@ body {
|
||||
color: var(--color-ps-accent);
|
||||
}
|
||||
|
||||
.ps-tool-btn.active::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 2px;
|
||||
background: var(--color-ps-accent);
|
||||
}
|
||||
|
||||
.ps-options-bar {
|
||||
height: 36px;
|
||||
background: var(--color-ps-header);
|
||||
@@ -672,6 +662,33 @@ body {
|
||||
outline-offset: 0;
|
||||
}
|
||||
|
||||
.ps-unselected-dim-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
opacity: 0;
|
||||
transition: opacity 0.12s ease;
|
||||
}
|
||||
|
||||
.ps-unselected-dim-layer--visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.ps-unselected-dim {
|
||||
position: absolute;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.ps-selected-lift-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.ps-handle {
|
||||
position: absolute;
|
||||
width: 8px;
|
||||
@@ -1350,6 +1367,26 @@ body {
|
||||
border-right: 1px solid #1a1a1a;
|
||||
}
|
||||
|
||||
.mobile-design-props-nav-spacer {
|
||||
flex: 1;
|
||||
min-height: 8px;
|
||||
}
|
||||
|
||||
.mobile-design-props-nav-delete:disabled {
|
||||
opacity: 0.35;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.mobile-design-props-nav-delete:not(:disabled) {
|
||||
color: #e57373;
|
||||
}
|
||||
|
||||
.mobile-design-props-nav-delete:not(:disabled).active,
|
||||
.mobile-design-props-nav-delete:not(:disabled):active {
|
||||
background: rgba(239, 68, 68, 0.18);
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
.mobile-design-props-nav-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user