优化预览与导出排版

This commit is contained in:
iqudoo
2026-06-12 20:07:49 +08:00
parent 7ad1f59ae1
commit c9a187bf9b
7 changed files with 291 additions and 116 deletions

View File

@@ -4,9 +4,12 @@ import {
DEFAULT_EXPORT_RANGE,
DEFAULT_CUT_LINE_CONFIG,
buildPrintablePages,
collectGridCutLinePositions,
getCutLineChannel,
type PrintableLabel,
} from '../utils';
import { CanvasLabelImage } from './CanvasLabelImage';
import { CommitInput } from './CommitInput';
import { FileDown, RefreshCcw } from 'lucide-react';
interface PageInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'> {
@@ -15,36 +18,14 @@ interface PageInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement
inputClassName?: string;
}
export const PageInput: React.FC<PageInputProps> = ({ value, onCommit, inputClassName = '', ...props }) => {
const [localVal, setLocalVal] = useState<string>(String(value));
useEffect(() => {
setLocalVal(String(value));
}, [value]);
const handleBlur = () => {
if (localVal !== String(value)) {
onCommit(localVal);
}
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
(e.target as HTMLInputElement).blur();
}
};
return (
<input
{...props}
value={localVal}
onChange={(e) => setLocalVal(e.target.value)}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
className={inputClassName}
/>
);
};
export const PageInput: React.FC<PageInputProps> = ({
value,
onCommit,
inputClassName = '',
...props
}) => (
<CommitInput value={value} onCommit={onCommit} className={inputClassName} {...props} />
);
interface LayoutStats {
gridCols: number;
@@ -176,7 +157,7 @@ export const PaperSettingsPanel: React.FC<PaperSettingsPanelProps> = ({
const applyPaper = (next: PaperConfig) => {
setDraftPaper(next);
if (isMobile) onChangePaper(next);
onChangePaper(next);
};
const handlePaperTypeChange = (type: PaperType) => {
@@ -213,7 +194,7 @@ export const PaperSettingsPanel: React.FC<PaperSettingsPanelProps> = ({
)}
{isPs && (
<p className="text-[10px] text-[#666] leading-relaxed">
</p>
)}
@@ -465,6 +446,31 @@ export const LayoutPreview: React.FC<LayoutPreviewProps> = ({
const currentPaperH = paper.orientation === 'portrait' ? paper.height : paper.width;
const colGapPx = Math.max(0, paper.columnGap * previewScale);
const rowGapPx = Math.max(0, paper.rowGap * previewScale);
const labelWPx = template.width * previewScale;
const labelHPx = template.height * previewScale;
const cutStrokeWidth = Math.max(0.5, cutLine.width * previewScale);
const cutHalfStroke = cutStrokeWidth / 2;
const cutChannel = getCutLineChannel(paper.columnGap, paper.rowGap, cutLine);
const cutChannelXPx = cutChannel.x * previewScale;
const cutChannelYPx = cutChannel.y * previewScale;
const cutDash =
cutLine.style === 'dashed'
? `${Math.max(2, cutStrokeWidth * 4)} ${Math.max(2, cutStrokeWidth * 4)}`
: undefined;
const pageCutLines = useMemo(
() =>
collectGridCutLinePositions(
0,
0,
labelWPx,
labelHPx,
gridInfo.cols,
gridInfo.rows,
colGapPx,
rowGapPx
),
[labelWPx, labelHPx, gridInfo.cols, gridInfo.rows, colGapPx, rowGapPx]
);
const visiblePages = showAllPages ? printablePages : printablePages.slice(0, MAX_PREVIEW_PAGES);
const hasMorePages = printablePages.length > MAX_PREVIEW_PAGES;
@@ -587,15 +593,58 @@ export const LayoutPreview: React.FC<LayoutPreviewProps> = ({
}}
>
<div
className="grid bg-white"
className="grid bg-white relative"
style={{
gridTemplateColumns: `repeat(${gridInfo.cols}, ${template.width * previewScale}px)`,
gridAutoRows: `${template.height * previewScale}px`,
gridTemplateColumns: `repeat(${gridInfo.cols}, ${labelWPx}px)`,
gridAutoRows: `${labelHPx}px`,
columnGap: `${colGapPx}px`,
rowGap: `${rowGapPx}px`,
width: 'max-content',
}}
>
{cutLine.enabled && (
<svg
className="absolute pointer-events-none z-20"
style={{
left: -cutHalfStroke,
top: -cutHalfStroke,
width: pageCutLines.gridW + cutStrokeWidth,
height: pageCutLines.gridH + cutStrokeWidth,
overflow: 'visible',
}}
viewBox={`${-cutHalfStroke} ${-cutHalfStroke} ${pageCutLines.gridW + cutStrokeWidth} ${pageCutLines.gridH + cutStrokeWidth}`}
aria-hidden
>
{pageCutLines.ys.map((y) => (
<line
key={`h-${y}`}
x1={0}
y1={y}
x2={pageCutLines.gridW}
y2={y}
stroke={cutLine.color}
strokeWidth={cutStrokeWidth}
strokeDasharray={cutDash}
strokeLinecap="butt"
shapeRendering="crispEdges"
/>
))}
{pageCutLines.xs.map((x) => (
<line
key={`v-${x}`}
x1={x}
y1={0}
x2={x}
y2={pageCutLines.gridH}
stroke={cutLine.color}
strokeWidth={cutStrokeWidth}
strokeDasharray={cutDash}
strokeLinecap="butt"
shapeRendering="crispEdges"
/>
))}
</svg>
)}
{pageRows.map((item, cellIdx) => {
if (!item) {
return (
@@ -616,30 +665,26 @@ export const LayoutPreview: React.FC<LayoutPreviewProps> = ({
<div
key={cellIdx}
onClick={() => onSelectRowIndex(item.sourceIndex)}
className={`bg-white text-center relative overflow-hidden flex flex-col justify-center cursor-pointer box-border ${isActive ? 'ring-2 ring-inset ring-[#31a8ff] z-10' : ''
className={`bg-white text-center relative flex flex-col justify-center cursor-pointer box-border ${isActive ? 'ring-2 ring-inset ring-[#31a8ff] z-10' : ''
}`}
style={{
width: `${template.width * previewScale}px`,
height: `${template.height * previewScale}px`,
width: `${labelWPx}px`,
height: `${labelHPx}px`,
padding: `${cutChannelYPx}px ${cutChannelXPx}px`,
boxSizing: 'border-box',
}}
>
<CanvasLabelImage
widthMm={template.width}
heightMm={template.height}
elements={template.elements}
rowData={item.row}
rowIndex={item.sourceIndex}
showBorder={false}
mode="output"
/>
{cutLine.enabled && (
<div
className="absolute inset-0 pointer-events-none box-border"
style={{
border: `${Math.max(1, cutLine.width * previewScale)}px ${cutLine.style} ${cutLine.color}`,
}}
<div className="w-full h-full overflow-hidden relative">
<CanvasLabelImage
widthMm={template.width}
heightMm={template.height}
elements={template.elements}
rowData={item.row}
rowIndex={item.sourceIndex}
showBorder={false}
mode="output"
/>
)}
</div>
</div>
);
})}