优化,排列
This commit is contained in:
226
src/App.tsx
226
src/App.tsx
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useMemo, useEffect, useLayoutEffect } from 'react';
|
import React, { useState, useMemo, useEffect, useLayoutEffect, useRef } from 'react';
|
||||||
import { LabelTemplate, PaperConfig, ImportData, RecordRow, ExportRangeConfig, CutLineConfig } from './types';
|
import { LabelTemplate, PaperConfig, ImportData, RecordRow, ExportRangeConfig, CutLineConfig } from './types';
|
||||||
import {
|
import {
|
||||||
stripTemplateForStorage,
|
stripTemplateForStorage,
|
||||||
@@ -28,10 +28,11 @@ import {
|
|||||||
Code,
|
Code,
|
||||||
Loader2,
|
Loader2,
|
||||||
Plus,
|
Plus,
|
||||||
Download,
|
|
||||||
Upload,
|
Upload,
|
||||||
Printer,
|
Printer,
|
||||||
|
Pencil,
|
||||||
Trash2,
|
Trash2,
|
||||||
|
MoreHorizontal,
|
||||||
X,
|
X,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import logoUrl from './assets/logo.svg';
|
import logoUrl from './assets/logo.svg';
|
||||||
@@ -145,6 +146,10 @@ export default function App() {
|
|||||||
const [newTmplName, setNewTmplName] = useState<string>('');
|
const [newTmplName, setNewTmplName] = useState<string>('');
|
||||||
const [newTmplW, setNewTmplW] = useState<number>(75);
|
const [newTmplW, setNewTmplW] = useState<number>(75);
|
||||||
const [newTmplH, setNewTmplH] = useState<number>(45);
|
const [newTmplH, setNewTmplH] = useState<number>(45);
|
||||||
|
const [editingTemplateId, setEditingTemplateId] = useState<string | null>(null);
|
||||||
|
const [editTmplName, setEditTmplName] = useState<string>('');
|
||||||
|
const [templateMenuId, setTemplateMenuId] = useState<string | null>(null);
|
||||||
|
const templateMenuRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
const paper = workingTemplate.paper ?? DEFAULT_PAPER_CONFIG;
|
const paper = workingTemplate.paper ?? DEFAULT_PAPER_CONFIG;
|
||||||
const cutLine = workingTemplate.cutLine ?? DEFAULT_CUT_LINE_CONFIG;
|
const cutLine = workingTemplate.cutLine ?? DEFAULT_CUT_LINE_CONFIG;
|
||||||
@@ -247,7 +252,7 @@ export default function App() {
|
|||||||
const newId = `custom_tmpl_${Date.now()}`;
|
const newId = `custom_tmpl_${Date.now()}`;
|
||||||
const newTmpl: LabelTemplate = {
|
const newTmpl: LabelTemplate = {
|
||||||
id: newId,
|
id: newId,
|
||||||
name: `${newTmplName} (${newTmplW}x${newTmplH}mm)`,
|
name: `${newTmplName}`,
|
||||||
width: Math.max(10, Number(newTmplW) || 70),
|
width: Math.max(10, Number(newTmplW) || 70),
|
||||||
height: Math.max(10, Number(newTmplH) || 40),
|
height: Math.max(10, Number(newTmplH) || 40),
|
||||||
elements: [
|
elements: [
|
||||||
@@ -259,7 +264,7 @@ export default function App() {
|
|||||||
y: 5,
|
y: 5,
|
||||||
width: Math.max(10, newTmplW - 10),
|
width: Math.max(10, newTmplW - 10),
|
||||||
height: 10,
|
height: 10,
|
||||||
content: '请插入绑定的文本内容 {货位编号}',
|
content: '{变量}',
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
@@ -287,11 +292,52 @@ export default function App() {
|
|||||||
setNewTmplH(45);
|
setNewTmplH(45);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openEditTemplateDialog = (tmpl: LabelTemplate) => {
|
||||||
|
setEditingTemplateId(tmpl.id!);
|
||||||
|
setEditTmplName(tmpl.name);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeEditTemplateDialog = () => {
|
||||||
|
setEditingTemplateId(null);
|
||||||
|
setEditTmplName('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeTemplateMenu = () => setTemplateMenuId(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!templateMenuId) return;
|
||||||
|
const handlePointerDown = (e: PointerEvent) => {
|
||||||
|
if (templateMenuRef.current?.contains(e.target as Node)) return;
|
||||||
|
closeTemplateMenu();
|
||||||
|
};
|
||||||
|
document.addEventListener('pointerdown', handlePointerDown);
|
||||||
|
return () => document.removeEventListener('pointerdown', handlePointerDown);
|
||||||
|
}, [templateMenuId]);
|
||||||
|
|
||||||
|
const handleEditTemplateSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!editingTemplateId) return;
|
||||||
|
const trimmed = editTmplName.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
await showAlert('请先输入合法的模板名', { variant: 'warning' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const target = templates.find((t) => t.id === editingTemplateId);
|
||||||
|
if (!target) {
|
||||||
|
closeEditTemplateDialog();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
saveTemplatesToStorage(
|
||||||
|
templates.map((t) => (t.id === editingTemplateId ? { ...t, name: trimmed } : t))
|
||||||
|
);
|
||||||
|
closeEditTemplateDialog();
|
||||||
|
};
|
||||||
|
|
||||||
// Export templates list or single active template as standard json
|
// Export templates list or single active template as standard json
|
||||||
const handleExportTemplate = (tmpl: LabelTemplate) => {
|
const handleExportTemplate = (tmpl: LabelTemplate) => {
|
||||||
const dataStr = JSON.stringify(normalizeTemplate(tmpl), null, 2);
|
const dataStr = JSON.stringify(normalizeTemplate(tmpl), null, 2);
|
||||||
const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr);
|
const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr);
|
||||||
const exportFileDefaultName = `模板_${tmpl.name?.replace(/\s+/g, '_') || tmpl.id}.lad`;
|
const exportFileDefaultName = `${tmpl.name?.replace(/\s+/g, '_') || tmpl.id}_${Date.now()}.lad`;
|
||||||
|
|
||||||
const linkElement = document.createElement('a');
|
const linkElement = document.createElement('a');
|
||||||
linkElement.setAttribute('href', dataUri);
|
linkElement.setAttribute('href', dataUri);
|
||||||
@@ -654,8 +700,8 @@ export default function App() {
|
|||||||
{!(isMobile && workflowStep === 'print-view') && (
|
{!(isMobile && workflowStep === 'print-view') && (
|
||||||
<header
|
<header
|
||||||
className={`no-print bg-indigo-700 border-b border-indigo-900 shrink-0 shadow flex flex-wrap items-center justify-between gap-4 mobile-template-header app-header-sticky ${isMobile && workflowStep === 'template-select' && hasTemplates
|
className={`no-print bg-indigo-700 border-b border-indigo-900 shrink-0 shadow flex flex-wrap items-center justify-between gap-4 mobile-template-header app-header-sticky ${isMobile && workflowStep === 'template-select' && hasTemplates
|
||||||
? 'mobile-template-header-row'
|
? 'mobile-template-header-row'
|
||||||
: ''
|
: ''
|
||||||
} px-4 py-3 md:px-6 md:py-4`}
|
} px-4 py-3 md:px-6 md:py-4`}
|
||||||
>
|
>
|
||||||
{/* Title logo and slogan description */}
|
{/* Title logo and slogan description */}
|
||||||
@@ -691,8 +737,8 @@ export default function App() {
|
|||||||
)}
|
)}
|
||||||
<label
|
<label
|
||||||
className={`inline-flex items-center gap-1.5 bg-indigo-600 hover:bg-indigo-800 text-white rounded-xl font-semibold whitespace-nowrap transition shadow-sm cursor-pointer ${isMobile
|
className={`inline-flex items-center gap-1.5 bg-indigo-600 hover:bg-indigo-800 text-white rounded-xl font-semibold whitespace-nowrap transition shadow-sm cursor-pointer ${isMobile
|
||||||
? 'min-h-[40px] px-3 py-2 text-[11px]'
|
? 'min-h-[40px] px-3 py-2 text-[11px]'
|
||||||
: 'px-4 py-2 text-xs'
|
: 'px-4 py-2 text-xs'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Upload className={`${isMobile ? 'w-3.5 h-3.5' : 'w-4 h-4'} shrink-0`} />
|
<Upload className={`${isMobile ? 'w-3.5 h-3.5' : 'w-4 h-4'} shrink-0`} />
|
||||||
@@ -780,7 +826,7 @@ export default function App() {
|
|||||||
<div
|
<div
|
||||||
key={tmpl.id}
|
key={tmpl.id}
|
||||||
onClick={() => handleSelectTemplate(tmpl.id!)}
|
onClick={() => handleSelectTemplate(tmpl.id!)}
|
||||||
className={`bg-white border rounded-2xl p-5 shadow-xs transition-all flex flex-col cursor-pointer relative overflow-hidden group select-none ${isActive
|
className={`bg-white border rounded-2xl p-5 shadow-xs transition-all flex flex-col cursor-pointer relative overflow-visible group select-none ${isActive
|
||||||
// ? 'ring-2 ring-indigo-500 border-indigo-500 bg-indigo-50/5'
|
// ? 'ring-2 ring-indigo-500 border-indigo-500 bg-indigo-50/5'
|
||||||
? 'border-gray-200 hover:shadow-md hover:border-indigo-200'
|
? 'border-gray-200 hover:shadow-md hover:border-indigo-200'
|
||||||
: 'border-gray-200 hover:shadow-md hover:border-indigo-200'
|
: 'border-gray-200 hover:shadow-md hover:border-indigo-200'
|
||||||
@@ -795,12 +841,15 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
)} */}
|
)} */}
|
||||||
|
|
||||||
<div className="space-y-1 pr-12 mb-3">
|
<div className="space-y-1 mb-3 min-w-0">
|
||||||
<span className="text-[10px] font-mono font-extrabold text-indigo-700 bg-indigo-50 px-2 py-0.5 rounded-lg border border-indigo-100">
|
<h3 className="text-sm font-bold text-gray-900 mt-1 leading-snug truncate">
|
||||||
{tmpl.width} × {tmpl.height} mm
|
{tmpl.name}
|
||||||
</span>
|
</h3>
|
||||||
<h3 className="text-sm font-bold text-gray-900 mt-1 leading-snug">{tmpl.name}</h3>
|
<p className="text-[10px] text-gray-400 flex items-center gap-1">
|
||||||
<p className="text-[10px] text-gray-400">{tmpl.elements.length} 个元素</p>
|
<span className="text-[10px] font-mono font-extrabold text-indigo-700 bg-indigo-50 px-2 py-0.5 rounded-lg border border-indigo-100">
|
||||||
|
{tmpl.width} × {tmpl.height} mm
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Preview */}
|
{/* Preview */}
|
||||||
@@ -850,41 +899,80 @@ export default function App() {
|
|||||||
openPrintView(tmpl.id!);
|
openPrintView(tmpl.id!);
|
||||||
}}
|
}}
|
||||||
className={`inline-flex items-center gap-1.5 bg-indigo-600 hover:bg-indigo-700 active:bg-indigo-800 text-white transition shadow-xs cursor-pointer ${isMobile
|
className={`inline-flex items-center gap-1.5 bg-indigo-600 hover:bg-indigo-700 active:bg-indigo-800 text-white transition shadow-xs cursor-pointer ${isMobile
|
||||||
? 'mobile-primary-btn flex-1 min-w-0 justify-center min-h-[44px] px-4 py-2.5 rounded-xl text-[13px] font-semibold'
|
? 'mobile-primary-btn flex-1 min-w-0 justify-center min-h-[44px] px-4 py-2.5 rounded-xl text-[13px] font-semibold'
|
||||||
: 'shrink-0 whitespace-nowrap px-3 py-1.5 rounded-lg text-[11px] font-bold'
|
: 'shrink-0 whitespace-nowrap px-3 py-1.5 rounded-lg text-[11px] font-bold'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Printer className={`${isMobile ? 'w-4 h-4' : 'w-3.5 h-3.5'} shrink-0`} />
|
<Printer className={`${isMobile ? 'w-4 h-4' : 'w-3.5 h-3.5'} shrink-0`} />
|
||||||
{isMobile ? '排版生成标签' : '排版导出'}
|
{isMobile ? '排版生成标签' : '排版导出'}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<div
|
||||||
onClick={(e) => {
|
ref={templateMenuId === tmpl.id ? templateMenuRef : null}
|
||||||
e.stopPropagation();
|
className="relative shrink-0"
|
||||||
handleExportTemplate(tmpl);
|
|
||||||
}}
|
|
||||||
className={`shrink-0 bg-gray-50 text-gray-500 hover:bg-gray-100 active:bg-gray-200 border border-gray-100 hover:border-gray-300 inline-flex items-center justify-center transition cursor-pointer ${isMobile
|
|
||||||
? 'mobile-icon-btn min-w-[44px] min-h-[44px] rounded-xl'
|
|
||||||
: 'p-1.5 rounded-lg'
|
|
||||||
}`}
|
|
||||||
title="导出"
|
|
||||||
aria-label="导出模板"
|
|
||||||
>
|
>
|
||||||
<Download className={`${isMobile ? 'w-5 h-5' : 'w-3.5 h-3.5'}`} />
|
<button
|
||||||
</button>
|
type="button"
|
||||||
<button
|
onClick={(e) => {
|
||||||
onClick={(e) => {
|
e.stopPropagation();
|
||||||
e.stopPropagation();
|
setTemplateMenuId((id) => (id === tmpl.id ? null : tmpl.id!));
|
||||||
handleDeleteTemplate(tmpl.id!);
|
}}
|
||||||
}}
|
className={`bg-gray-50 text-gray-600 hover:bg-gray-100 active:bg-gray-200 border border-gray-100 hover:border-gray-300 inline-flex items-center justify-center gap-1 transition cursor-pointer ${isMobile
|
||||||
className={`shrink-0 bg-gray-50 text-rose-500 hover:text-white hover:bg-rose-600 active:bg-rose-700 border border-gray-100 inline-flex items-center justify-center transition cursor-pointer ${isMobile
|
? 'mobile-icon-btn min-h-[44px] px-3 rounded-xl text-[13px] font-medium'
|
||||||
? 'mobile-icon-btn min-w-[44px] min-h-[44px] rounded-xl'
|
: 'shrink-0 whitespace-nowrap px-2.5 py-1.5 rounded-lg text-[11px] font-semibold'
|
||||||
: 'p-1.5 rounded-lg'
|
}`}
|
||||||
}`}
|
aria-expanded={templateMenuId === tmpl.id}
|
||||||
title="删除模板"
|
aria-haspopup="menu"
|
||||||
aria-label="删除模板"
|
>
|
||||||
>
|
<MoreHorizontal className={`${isMobile ? 'w-5 h-5' : 'w-4 h-4'} shrink-0`} />
|
||||||
<Trash2 className={`${isMobile ? 'w-5 h-5' : 'w-3.5 h-3.5'}`} />
|
</button>
|
||||||
</button>
|
{templateMenuId === tmpl.id && (
|
||||||
|
<div
|
||||||
|
role="menu"
|
||||||
|
className="absolute right-0 top-full mt-1 z-30 min-w-[90px] py-1 bg-white border border-gray-200 rounded-xl shadow-lg"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="menuitem"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
closeTemplateMenu();
|
||||||
|
openEditTemplateDialog(tmpl);
|
||||||
|
}}
|
||||||
|
className="inline-flex items-center gap-3 w-full px-3 py-2.5 text-left text-sm text-gray-700 hover:bg-gray-50 active:bg-gray-100 cursor-pointer"
|
||||||
|
>
|
||||||
|
<Pencil className="w-4 h-4 shrink-0" />
|
||||||
|
编辑
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="menuitem"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
closeTemplateMenu();
|
||||||
|
handleExportTemplate(tmpl);
|
||||||
|
}}
|
||||||
|
className="inline-flex items-center gap-3 w-full px-3 py-2.5 text-left text-sm text-gray-700 hover:bg-gray-50 active:bg-gray-100 cursor-pointer"
|
||||||
|
>
|
||||||
|
<FileDown className="w-4 h-4 shrink-0" />
|
||||||
|
导出
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="menuitem"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
closeTemplateMenu();
|
||||||
|
void handleDeleteTemplate(tmpl.id!);
|
||||||
|
}}
|
||||||
|
className="inline-flex items-center gap-3 w-full px-3 py-2.5 text-left text-sm text-rose-600 hover:bg-rose-50 active:bg-rose-100 cursor-pointer"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4 shrink-0" />
|
||||||
|
删除
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -1096,6 +1184,54 @@ export default function App() {
|
|||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
{editingTemplateId && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-[9000] flex items-center justify-center bg-slate-950/60 backdrop-blur-sm p-4"
|
||||||
|
onClick={closeEditTemplateDialog}
|
||||||
|
role="presentation"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="bg-white rounded-2xl shadow-2xl w-full max-w-md overflow-hidden"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label="重命名模板"
|
||||||
|
>
|
||||||
|
<form onSubmit={handleEditTemplateSubmit} className="px-6 py-5 space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-[11px] font-semibold text-gray-500 mb-1.5">
|
||||||
|
模板名称
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
autoFocus
|
||||||
|
placeholder="请输入模板名称"
|
||||||
|
value={editTmplName}
|
||||||
|
onChange={(e) => setEditTmplName(e.target.value)}
|
||||||
|
className="w-full px-3 py-2 bg-gray-50 border border-gray-200 rounded-lg text-sm focus:border-indigo-400 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end gap-2 pt-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={closeEditTemplateDialog}
|
||||||
|
className="px-4 py-2 border border-gray-200 text-gray-600 hover:bg-gray-50 rounded-lg text-sm font-medium cursor-pointer"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 text-sm font-semibold cursor-pointer"
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{showAddTmplForm && (
|
{showAddTmplForm && (
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 z-[9000] flex items-center justify-center bg-slate-950/60 backdrop-blur-sm p-4"
|
className="fixed inset-0 z-[9000] flex items-center justify-center bg-slate-950/60 backdrop-blur-sm p-4"
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
alignToSelectionBounds,
|
alignToSelectionBounds,
|
||||||
alignSelectionToCanvas,
|
alignSelectionToCanvas,
|
||||||
tileSelectedEvenly,
|
tileSelectedEvenly,
|
||||||
|
tileSelectedFlush,
|
||||||
type AlignMode,
|
type AlignMode,
|
||||||
} from '../elementAlign';
|
} from '../elementAlign';
|
||||||
import { CollapsiblePanelSection } from './CollapsiblePanelSection';
|
import { CollapsiblePanelSection } from './CollapsiblePanelSection';
|
||||||
@@ -44,6 +45,8 @@ import {
|
|||||||
AlignVerticalJustifyEnd,
|
AlignVerticalJustifyEnd,
|
||||||
AlignHorizontalSpaceBetween,
|
AlignHorizontalSpaceBetween,
|
||||||
AlignVerticalSpaceBetween,
|
AlignVerticalSpaceBetween,
|
||||||
|
Columns2,
|
||||||
|
Rows2,
|
||||||
Link2,
|
Link2,
|
||||||
Plus,
|
Plus,
|
||||||
Ruler,
|
Ruler,
|
||||||
@@ -232,10 +235,12 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const applyTile = (axis: 'horizontal' | 'vertical') => {
|
const applyTile = (axis: 'horizontal' | 'vertical', spacing: 'even' | 'none' = 'even') => {
|
||||||
|
const tile =
|
||||||
|
spacing === 'even' ? tileSelectedEvenly : tileSelectedFlush;
|
||||||
onChangeTemplate({
|
onChangeTemplate({
|
||||||
...template,
|
...template,
|
||||||
elements: tileSelectedEvenly(template.elements, selectedElementIds, axis),
|
elements: tile(template.elements, selectedElementIds, axis),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -760,6 +765,18 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
|
|||||||
title="在选区高度内垂直等距排列"
|
title="在选区高度内垂直等距排列"
|
||||||
onClick={() => applyTile('vertical')}
|
onClick={() => applyTile('vertical')}
|
||||||
/>
|
/>
|
||||||
|
<AlignToolButton
|
||||||
|
icon={<Columns2 className="w-3.5 h-3.5" />}
|
||||||
|
label="水平无间距平铺"
|
||||||
|
title="从最左元素起水平紧密排列"
|
||||||
|
onClick={() => applyTile('horizontal', 'none')}
|
||||||
|
/>
|
||||||
|
<AlignToolButton
|
||||||
|
icon={<Rows2 className="w-3.5 h-3.5" />}
|
||||||
|
label="垂直无间距平铺"
|
||||||
|
title="从最上元素起垂直紧密排列"
|
||||||
|
onClick={() => applyTile('vertical', 'none')}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1348,7 +1365,6 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleElemChange({
|
handleElemChange({
|
||||||
...selectedElem,
|
...selectedElem,
|
||||||
backgroundColor: 'transparent',
|
|
||||||
backgroundOpacity: 0,
|
backgroundOpacity: 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1430,7 +1446,6 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
|
|||||||
if (op <= 0) {
|
if (op <= 0) {
|
||||||
handleElemChange({
|
handleElemChange({
|
||||||
...selectedElem,
|
...selectedElem,
|
||||||
backgroundColor: 'transparent',
|
|
||||||
backgroundOpacity: 0,
|
backgroundOpacity: 0,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -151,3 +151,30 @@ export function tileSelectedEvenly(
|
|||||||
}
|
}
|
||||||
return patchElements(elements, patches);
|
return patchElements(elements, patches);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 按水平/垂直方向无间距紧密平铺(从当前最左/最上元素位置起依次排列) */
|
||||||
|
export function tileSelectedFlush(
|
||||||
|
elements: TemplateElement[],
|
||||||
|
selectedIds: string[],
|
||||||
|
axis: 'horizontal' | 'vertical'
|
||||||
|
): TemplateElement[] {
|
||||||
|
const selected = elements
|
||||||
|
.filter((e) => selectedIds.includes(e.id) && !e.locked)
|
||||||
|
.sort((a, b) => (axis === 'horizontal' ? a.x - b.x : a.y - b.y));
|
||||||
|
if (selected.length < 2) return elements;
|
||||||
|
|
||||||
|
const patches = new Map<string, Partial<TemplateElement>>();
|
||||||
|
const start = axis === 'horizontal' ? selected[0].x : selected[0].y;
|
||||||
|
let cursor = start;
|
||||||
|
|
||||||
|
for (const el of selected) {
|
||||||
|
if (axis === 'horizontal') {
|
||||||
|
patches.set(el.id, { x: round1(cursor) });
|
||||||
|
cursor += el.width;
|
||||||
|
} else {
|
||||||
|
patches.set(el.id, { y: round1(cursor) });
|
||||||
|
cursor += el.height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return patchElements(elements, patches);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user