This commit is contained in:
iqudoo
2026-06-13 03:41:20 +08:00
parent 979a565468
commit 3b9724beb3
7 changed files with 634 additions and 129 deletions

View File

@@ -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>
)}