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 { PaperConfig, LabelTemplate, RecordRow, PaperType, CutLineConfig, ExportRangeConfig } from '../types';
|
||||||
import {
|
import {
|
||||||
|
computeAutoLayoutPaper,
|
||||||
DEFAULT_EXPORT_RANGE,
|
DEFAULT_EXPORT_RANGE,
|
||||||
DEFAULT_CUT_LINE_CONFIG,
|
DEFAULT_CUT_LINE_CONFIG,
|
||||||
buildPrintablePages,
|
buildPrintablePages,
|
||||||
collectGridCutLinePositions,
|
collectGridCutLinePositions,
|
||||||
getCutLineChannel,
|
getCutLineChannel,
|
||||||
|
loadAutoLayoutMinPageMarginMm,
|
||||||
|
saveAutoLayoutMinPageMarginMm,
|
||||||
type PrintableLabel,
|
type PrintableLabel,
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
import { CanvasLabelImage } from './CanvasLabelImage';
|
import { CanvasLabelImage } from './CanvasLabelImage';
|
||||||
import { CommitInput } from './CommitInput';
|
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'> {
|
interface PageInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'> {
|
||||||
value: string | number;
|
value: string | number;
|
||||||
@@ -69,7 +74,16 @@ export const PaperSettingsPanel: React.FC<PaperSettingsPanelProps> = ({
|
|||||||
theme = 'light',
|
theme = 'light',
|
||||||
exportRange,
|
exportRange,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { showAlert } = useAppDialog();
|
||||||
const [draftPaper, setDraftPaper] = useState(paper);
|
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);
|
const panelRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -93,7 +107,7 @@ export const PaperSettingsPanel: React.FC<PaperSettingsPanelProps> = ({
|
|||||||
const fieldClass = isMobile
|
const fieldClass = isMobile
|
||||||
? 'mobile-field font-mono'
|
? 'mobile-field font-mono'
|
||||||
: isPs
|
: 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';
|
: '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
|
const selectClass = isMobile
|
||||||
? 'mobile-field'
|
? 'mobile-field'
|
||||||
@@ -129,32 +143,59 @@ export const PaperSettingsPanel: React.FC<PaperSettingsPanelProps> = ({
|
|||||||
const selectedCount = layoutStats?.selectedCount ?? draftLayoutStats.selectedCount;
|
const selectedCount = layoutStats?.selectedCount ?? draftLayoutStats.selectedCount;
|
||||||
const allRowCount = totalRowCount ?? rows.length;
|
const allRowCount = totalRowCount ?? rows.length;
|
||||||
|
|
||||||
const autoCenterMargins = () => {
|
const applyAutoLayoutWithMinMargin = (minMargin: number) => {
|
||||||
const paperW = draftPaper.orientation === 'portrait' ? draftPaper.width : draftPaper.height;
|
const next = computeAutoLayoutPaper(draftPaper, template, minMargin);
|
||||||
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,
|
|
||||||
};
|
|
||||||
setDraftPaper(next);
|
setDraftPaper(next);
|
||||||
onChangePaper(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) => {
|
const applyPaper = (next: PaperConfig) => {
|
||||||
setDraftPaper(next);
|
setDraftPaper(next);
|
||||||
onChangePaper(next);
|
onChangePaper(next);
|
||||||
@@ -180,7 +221,78 @@ export const PaperSettingsPanel: React.FC<PaperSettingsPanelProps> = ({
|
|||||||
applyPaper({ ...draftPaper, ...patch });
|
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 (
|
return (
|
||||||
|
<>
|
||||||
<div
|
<div
|
||||||
ref={panelRef}
|
ref={panelRef}
|
||||||
onBlur={isMobile ? undefined : handlePanelBlur}
|
onBlur={isMobile ? undefined : handlePanelBlur}
|
||||||
@@ -326,10 +438,11 @@ export const PaperSettingsPanel: React.FC<PaperSettingsPanelProps> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={autoCenterMargins}
|
onClick={openAutoLayoutDialog}
|
||||||
|
title="设置最小页边距并自动居中排版"
|
||||||
className={
|
className={
|
||||||
isPs
|
isPs
|
||||||
? 'text-[10px] text-[#777] hover:text-[#31a8ff] transition cursor-pointer'
|
? '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'
|
: 'text-[10px] text-gray-500 hover:text-indigo-600 font-normal cursor-pointer'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
自动计算边距
|
自动排版
|
||||||
</button>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -387,6 +511,8 @@ export const PaperSettingsPanel: React.FC<PaperSettingsPanelProps> = ({
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
</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 = {
|
export const DEFAULT_PAPER_CONFIG: PaperConfig = {
|
||||||
type: 'A4',
|
type: 'A4',
|
||||||
width: 210,
|
width: 210,
|
||||||
height: 297,
|
height: 297,
|
||||||
orientation: 'portrait',
|
orientation: 'portrait',
|
||||||
marginTop: 10,
|
marginTop: AUTO_LAYOUT_MIN_PAGE_MARGIN_MM,
|
||||||
marginBottom: 10,
|
marginBottom: AUTO_LAYOUT_MIN_PAGE_MARGIN_MM,
|
||||||
marginLeft: 10,
|
marginLeft: AUTO_LAYOUT_MIN_PAGE_MARGIN_MM,
|
||||||
marginRight: 10,
|
marginRight: AUTO_LAYOUT_MIN_PAGE_MARGIN_MM,
|
||||||
columnGap: 2,
|
columnGap: 2,
|
||||||
rowGap: 2,
|
rowGap: 2,
|
||||||
flow: 'left-right-top-bottom',
|
flow: 'left-right-top-bottom',
|
||||||
|
|||||||
Reference in New Issue
Block a user