fix
This commit is contained in:
@@ -1,16 +1,21 @@
|
||||
import React, { useMemo, useState, useEffect } from 'react';
|
||||
import React, { useMemo, useState, useEffect, useRef } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { PaperConfig, LabelTemplate, RecordRow, PaperType, CutLineConfig, ExportRangeConfig } from '../types';
|
||||
import {
|
||||
computeAutoLayoutPaper,
|
||||
DEFAULT_EXPORT_RANGE,
|
||||
DEFAULT_CUT_LINE_CONFIG,
|
||||
buildPrintablePages,
|
||||
collectGridCutLinePositions,
|
||||
getCutLineChannel,
|
||||
loadAutoLayoutMinPageMarginMm,
|
||||
saveAutoLayoutMinPageMarginMm,
|
||||
type PrintableLabel,
|
||||
} from '../utils';
|
||||
import { CanvasLabelImage } from './CanvasLabelImage';
|
||||
import { CommitInput } from './CommitInput';
|
||||
import { FileDown, RefreshCcw } from 'lucide-react';
|
||||
import { useAppDialog } from './AppDialog';
|
||||
import { FileDown, RefreshCcw, X } from 'lucide-react';
|
||||
|
||||
interface PageInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'> {
|
||||
value: string | number;
|
||||
@@ -69,7 +74,16 @@ export const PaperSettingsPanel: React.FC<PaperSettingsPanelProps> = ({
|
||||
theme = 'light',
|
||||
exportRange,
|
||||
}) => {
|
||||
const { showAlert } = useAppDialog();
|
||||
const [draftPaper, setDraftPaper] = useState(paper);
|
||||
const [autoLayoutOpen, setAutoLayoutOpen] = useState(false);
|
||||
const [lastAutoLayoutMinMargin, setLastAutoLayoutMinMargin] = useState(
|
||||
() => loadAutoLayoutMinPageMarginMm()
|
||||
);
|
||||
const [autoLayoutMinMargin, setAutoLayoutMinMargin] = useState(
|
||||
() => String(loadAutoLayoutMinPageMarginMm())
|
||||
);
|
||||
const autoLayoutInputRef = useRef<HTMLInputElement>(null);
|
||||
const panelRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -93,7 +107,7 @@ export const PaperSettingsPanel: React.FC<PaperSettingsPanelProps> = ({
|
||||
const fieldClass = isMobile
|
||||
? 'mobile-field font-mono'
|
||||
: isPs
|
||||
? 'ps-field font-mono text-center'
|
||||
? 'ps-field font-mono'
|
||||
: 'w-full px-2.5 py-1.5 bg-gray-50 border border-gray-100 rounded-lg text-xs text-center font-mono';
|
||||
const selectClass = isMobile
|
||||
? 'mobile-field'
|
||||
@@ -129,32 +143,59 @@ export const PaperSettingsPanel: React.FC<PaperSettingsPanelProps> = ({
|
||||
const selectedCount = layoutStats?.selectedCount ?? draftLayoutStats.selectedCount;
|
||||
const allRowCount = totalRowCount ?? rows.length;
|
||||
|
||||
const autoCenterMargins = () => {
|
||||
const paperW = draftPaper.orientation === 'portrait' ? draftPaper.width : draftPaper.height;
|
||||
const paperH = draftPaper.orientation === 'portrait' ? draftPaper.height : draftPaper.width;
|
||||
const lw = template.width;
|
||||
const lh = template.height;
|
||||
const minMargin = 8;
|
||||
const usableWInit = paperW - minMargin * 2;
|
||||
const usableHInit = paperH - minMargin * 2;
|
||||
const maxCols = Math.floor((usableWInit + draftPaper.columnGap) / (lw + draftPaper.columnGap)) || 1;
|
||||
const maxRows = Math.floor((usableHInit + draftPaper.rowGap) / (lh + draftPaper.rowGap)) || 1;
|
||||
const gridWidth = maxCols * lw + (maxCols - 1) * draftPaper.columnGap;
|
||||
const gridHeight = maxRows * lh + (maxRows - 1) * draftPaper.rowGap;
|
||||
const balancedLR = Math.max(minMargin, Math.round(((paperW - gridWidth) / 2) * 10) / 10);
|
||||
const balancedTB = Math.max(minMargin, Math.round(((paperH - gridHeight) / 2) * 10) / 10);
|
||||
|
||||
const next = {
|
||||
...draftPaper,
|
||||
marginLeft: balancedLR,
|
||||
marginRight: balancedLR,
|
||||
marginTop: balancedTB,
|
||||
marginBottom: balancedTB,
|
||||
};
|
||||
const applyAutoLayoutWithMinMargin = (minMargin: number) => {
|
||||
const next = computeAutoLayoutPaper(draftPaper, template, minMargin);
|
||||
setDraftPaper(next);
|
||||
onChangePaper(next);
|
||||
};
|
||||
|
||||
const closeAutoLayoutDialog = () => setAutoLayoutOpen(false);
|
||||
|
||||
const openAutoLayoutDialog = () => {
|
||||
setAutoLayoutMinMargin(String(lastAutoLayoutMinMargin));
|
||||
setAutoLayoutOpen(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoLayoutOpen) return;
|
||||
const timer = window.setTimeout(() => autoLayoutInputRef.current?.focus(), 0);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [autoLayoutOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoLayoutOpen) return;
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
closeAutoLayoutDialog();
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', onKeyDown);
|
||||
return () => window.removeEventListener('keydown', onKeyDown);
|
||||
}, [autoLayoutOpen]);
|
||||
|
||||
const confirmAutoLayout = async () => {
|
||||
const minMargin = Number(autoLayoutMinMargin);
|
||||
if (!Number.isFinite(minMargin) || minMargin < 0) {
|
||||
await showAlert('请输入不小于 0 的最小页边距(mm)。', { variant: 'warning' });
|
||||
return;
|
||||
}
|
||||
|
||||
const paperW =
|
||||
draftPaper.orientation === 'portrait' ? draftPaper.width : draftPaper.height;
|
||||
const paperH =
|
||||
draftPaper.orientation === 'portrait' ? draftPaper.height : draftPaper.width;
|
||||
if (minMargin * 2 >= paperW || minMargin * 2 >= paperH) {
|
||||
await showAlert('最小页边距过大,当前纸张尺寸无法排版。', { variant: 'warning' });
|
||||
return;
|
||||
}
|
||||
|
||||
applyAutoLayoutWithMinMargin(minMargin);
|
||||
saveAutoLayoutMinPageMarginMm(minMargin);
|
||||
setLastAutoLayoutMinMargin(minMargin);
|
||||
closeAutoLayoutDialog();
|
||||
};
|
||||
|
||||
const applyPaper = (next: PaperConfig) => {
|
||||
setDraftPaper(next);
|
||||
onChangePaper(next);
|
||||
@@ -180,7 +221,78 @@ export const PaperSettingsPanel: React.FC<PaperSettingsPanelProps> = ({
|
||||
applyPaper({ ...draftPaper, ...patch });
|
||||
};
|
||||
|
||||
const autoLayoutDialog =
|
||||
autoLayoutOpen && typeof document !== 'undefined'
|
||||
? createPortal(
|
||||
<div
|
||||
className="ps-modal-overlay"
|
||||
onClick={closeAutoLayoutDialog}
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
className="ps-modal"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="auto-layout-dialog-title"
|
||||
>
|
||||
<div className="ps-modal-header">
|
||||
<h3 id="auto-layout-dialog-title" className="ps-modal-title">
|
||||
自动排版
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeAutoLayoutDialog}
|
||||
className="ps-modal-close"
|
||||
aria-label="关闭"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
void confirmAutoLayout();
|
||||
}}
|
||||
>
|
||||
<div className="ps-modal-body space-y-3">
|
||||
<p className="text-[11px] text-[#888] leading-relaxed">
|
||||
将根据当前纸张与标签尺寸,在不低于最小页边距的前提下,自动计算左右、上下对称边距并居中排版。
|
||||
</p>
|
||||
<div>
|
||||
<label htmlFor="auto-layout-min-margin" className="ps-field-label">
|
||||
最小页边距 (mm)
|
||||
</label>
|
||||
<input
|
||||
id="auto-layout-min-margin"
|
||||
ref={autoLayoutInputRef}
|
||||
type="number"
|
||||
min="0.1"
|
||||
max="100"
|
||||
step="0.1"
|
||||
value={autoLayoutMinMargin}
|
||||
onChange={(e) => setAutoLayoutMinMargin(e.target.value)}
|
||||
className="ps-field font-mono"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ps-modal-footer">
|
||||
<button type="button" onClick={closeAutoLayoutDialog} className="ps-btn text-[11px]">
|
||||
取消
|
||||
</button>
|
||||
<button type="submit" className="ps-btn ps-btn-primary text-[11px]">
|
||||
确定排版
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={panelRef}
|
||||
onBlur={isMobile ? undefined : handlePanelBlur}
|
||||
@@ -326,10 +438,11 @@ export const PaperSettingsPanel: React.FC<PaperSettingsPanelProps> = ({
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<div className="col-span-2 flex justify-end -mt-0.5">
|
||||
<div className="col-span-2 flex flex-col items-end gap-0.5 -mt-0.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={autoCenterMargins}
|
||||
onClick={openAutoLayoutDialog}
|
||||
title="设置最小页边距并自动居中排版"
|
||||
className={
|
||||
isPs
|
||||
? 'text-[10px] text-[#777] hover:text-[#31a8ff] transition cursor-pointer'
|
||||
@@ -338,8 +451,19 @@ export const PaperSettingsPanel: React.FC<PaperSettingsPanelProps> = ({
|
||||
: 'text-[10px] text-gray-500 hover:text-indigo-600 font-normal cursor-pointer'
|
||||
}
|
||||
>
|
||||
自动计算边距
|
||||
自动排版
|
||||
</button>
|
||||
<span
|
||||
className={
|
||||
isPs
|
||||
? 'text-[9px] text-[#666] leading-tight'
|
||||
: isMobile
|
||||
? 'text-[10px] text-slate-400 leading-tight'
|
||||
: 'text-[9px] text-gray-400 leading-tight'
|
||||
}
|
||||
>
|
||||
上次最小页边距 {lastAutoLayoutMinMargin} mm
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -387,6 +511,8 @@ export const PaperSettingsPanel: React.FC<PaperSettingsPanelProps> = ({
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{autoLayoutDialog}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
63
src/utils.ts
63
src/utils.ts
@@ -296,15 +296,70 @@ export function buildPrintablePages(
|
||||
};
|
||||
}
|
||||
|
||||
/** 自动排版时允许的最小页边距 (mm) */
|
||||
export const AUTO_LAYOUT_MIN_PAGE_MARGIN_MM = 10;
|
||||
|
||||
const AUTO_LAYOUT_MIN_MARGIN_STORAGE_KEY = 'label_designer_auto_layout_min_margin_mm';
|
||||
|
||||
/** 读取上次自动排版使用的最小页边距 (mm) */
|
||||
export function loadAutoLayoutMinPageMarginMm(): number {
|
||||
try {
|
||||
const raw = localStorage.getItem(AUTO_LAYOUT_MIN_MARGIN_STORAGE_KEY);
|
||||
if (raw === null) return AUTO_LAYOUT_MIN_PAGE_MARGIN_MM;
|
||||
const n = Number(raw);
|
||||
if (Number.isFinite(n) && n >= 0) return n;
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return AUTO_LAYOUT_MIN_PAGE_MARGIN_MM;
|
||||
}
|
||||
|
||||
/** 保存自动排版使用的最小页边距 (mm) */
|
||||
export function saveAutoLayoutMinPageMarginMm(value: number): void {
|
||||
try {
|
||||
localStorage.setItem(AUTO_LAYOUT_MIN_MARGIN_STORAGE_KEY, String(value));
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
/** 按最小页边距居中计算四边页边距 */
|
||||
export function computeAutoLayoutPaper(
|
||||
paper: PaperConfig,
|
||||
template: Pick<LabelTemplate, 'width' | 'height'>,
|
||||
minMargin: number
|
||||
): PaperConfig {
|
||||
const paperW = paper.orientation === 'portrait' ? paper.width : paper.height;
|
||||
const paperH = paper.orientation === 'portrait' ? paper.height : paper.width;
|
||||
const lw = template.width;
|
||||
const lh = template.height;
|
||||
const usableWInit = paperW - minMargin * 2;
|
||||
const usableHInit = paperH - minMargin * 2;
|
||||
const maxCols = Math.floor((usableWInit + paper.columnGap) / (lw + paper.columnGap)) || 1;
|
||||
const maxRows = Math.floor((usableHInit + paper.rowGap) / (lh + paper.rowGap)) || 1;
|
||||
const gridWidth = maxCols * lw + (maxCols - 1) * paper.columnGap;
|
||||
const gridHeight = maxRows * lh + (maxRows - 1) * paper.rowGap;
|
||||
const balancedLR = Math.max(minMargin, Math.round(((paperW - gridWidth) / 2) * 10) / 10);
|
||||
const balancedTB = Math.max(minMargin, Math.round(((paperH - gridHeight) / 2) * 10) / 10);
|
||||
|
||||
return {
|
||||
...paper,
|
||||
marginLeft: balancedLR,
|
||||
marginRight: balancedLR,
|
||||
marginTop: balancedTB,
|
||||
marginBottom: balancedTB,
|
||||
};
|
||||
}
|
||||
|
||||
export const DEFAULT_PAPER_CONFIG: PaperConfig = {
|
||||
type: 'A4',
|
||||
width: 210,
|
||||
height: 297,
|
||||
orientation: 'portrait',
|
||||
marginTop: 10,
|
||||
marginBottom: 10,
|
||||
marginLeft: 10,
|
||||
marginRight: 10,
|
||||
marginTop: AUTO_LAYOUT_MIN_PAGE_MARGIN_MM,
|
||||
marginBottom: AUTO_LAYOUT_MIN_PAGE_MARGIN_MM,
|
||||
marginLeft: AUTO_LAYOUT_MIN_PAGE_MARGIN_MM,
|
||||
marginRight: AUTO_LAYOUT_MIN_PAGE_MARGIN_MM,
|
||||
columnGap: 2,
|
||||
rowGap: 2,
|
||||
flow: 'left-right-top-bottom',
|
||||
|
||||
Reference in New Issue
Block a user