- 引入 classNames 库统一管理组件样式类名 - 调整模块选中状态与预览模式下的边框样式 - 优化 PackingSpecModule 模块的数据显示逻辑 - 增加 formatCurrency 工具函数格式化金额显示 - 重构 PreviewCanvas 打印预览页面样式与结构 - 移除无效的模块配置项 showBoxType - 修复全屏事件监听器在不同浏览器的兼容性问题 - 优化打印样式,增加页面尺寸与边距控制 - 调整模块渲染逻辑,提升预览模式下的显示效果
735 lines
18 KiB
TypeScript
735 lines
18 KiB
TypeScript
import {
|
|
CompressOutlined,
|
|
DeleteOutlined,
|
|
ExpandOutlined,
|
|
EyeOutlined,
|
|
PrinterOutlined,
|
|
ZoomInOutlined,
|
|
ZoomOutOutlined,
|
|
} from '@ant-design/icons';
|
|
import { Button, Card, Empty, message, Popconfirm, Slider } from 'antd';
|
|
import React, { useEffect, useRef, useState } from 'react';
|
|
import {
|
|
DragDropContext,
|
|
Draggable,
|
|
Droppable,
|
|
DropResult,
|
|
} from 'react-beautiful-dnd';
|
|
|
|
// 直接导入组件而不是使用 React.lazy
|
|
import DealerInfoModule from './DealerInfoModule';
|
|
import OtherFeesModule from './OtherFeesModule';
|
|
import OtherInfoModule from './OtherInfoModule';
|
|
import PackingSpecModule from './PackingSpecModule';
|
|
import ShippingInfoModule from './ShippingInfoModule';
|
|
import TitleModule from './TitleModule';
|
|
import TotalAmountModule from './TotalAmountModule';
|
|
import VehicleInfoModule from './VehicleInfoModule';
|
|
import WeightInfoModule from './WeightInfoModule';
|
|
|
|
interface PreviewCanvasProps {
|
|
modules: any[];
|
|
selectedModule: any;
|
|
onModuleSelect: (module: any) => void;
|
|
onModuleDelete: (moduleId: string) => void;
|
|
onModuleReorder: (newModules: any[]) => void;
|
|
onClearAll?: () => void; // 添加清空所有模块的回调函数
|
|
}
|
|
|
|
const PreviewCanvas: React.FC<PreviewCanvasProps> = ({
|
|
modules,
|
|
selectedModule,
|
|
onModuleSelect,
|
|
onModuleDelete,
|
|
onModuleReorder,
|
|
onClearAll, // 接收清空所有模块的回调函数
|
|
}) => {
|
|
const [isPreviewMode, setIsPreviewMode] = useState(false);
|
|
const [scale, setScale] = useState(1);
|
|
const [position, setPosition] = useState({ x: 0, y: 0 });
|
|
const [isDragging, setIsDragging] = useState(false);
|
|
const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
|
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
|
const canvasRef = useRef<HTMLDivElement>(null);
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const fullscreenElementRef = useRef<HTMLDivElement>(null);
|
|
|
|
const handleDragEnd = (result: DropResult) => {
|
|
// 如果没有拖拽到有效位置,则直接返回
|
|
if (!result.destination) {
|
|
return;
|
|
}
|
|
|
|
const items = Array.from(modules);
|
|
const [reorderedItem] = items.splice(result.source.index, 1);
|
|
items.splice(result.destination.index, 0, reorderedItem);
|
|
|
|
onModuleReorder(items);
|
|
};
|
|
|
|
// 全屏功能
|
|
const toggleFullscreen = () => {
|
|
if (!fullscreenElementRef.current) return;
|
|
|
|
if (!isFullscreen) {
|
|
// 进入全屏
|
|
const element = fullscreenElementRef.current;
|
|
if (element.requestFullscreen) {
|
|
element.requestFullscreen();
|
|
} else if ((element as any).mozRequestFullScreen) {
|
|
// Firefox
|
|
(element as any).mozRequestFullScreen();
|
|
} else if ((element as any).webkitRequestFullscreen) {
|
|
// Chrome, Safari and Opera
|
|
(element as any).webkitRequestFullscreen();
|
|
} else if ((element as any).msRequestFullscreen) {
|
|
// IE/Edge
|
|
(element as any).msRequestFullscreen();
|
|
}
|
|
setIsFullscreen(true);
|
|
} else {
|
|
// 退出全屏
|
|
if (document.exitFullscreen) {
|
|
document.exitFullscreen();
|
|
} else if ((document as any).mozCancelFullScreen) {
|
|
// Firefox
|
|
(document as any).mozCancelFullScreen();
|
|
} else if ((document as any).webkitExitFullscreen) {
|
|
// Chrome, Safari and Opera
|
|
(document as any).webkitExitFullscreen();
|
|
} else if ((document as any).msExitFullscreen) {
|
|
// IE/Edge
|
|
(document as any).msExitFullscreen();
|
|
}
|
|
setIsFullscreen(false);
|
|
}
|
|
};
|
|
|
|
// 监听全屏状态变化
|
|
useEffect(() => {
|
|
const handleFullscreenChange = () => {
|
|
const isCurrentlyFullscreen =
|
|
document.fullscreenElement ||
|
|
(document as any).webkitFullscreenElement ||
|
|
(document as any).mozFullScreenElement ||
|
|
(document as any).msFullscreenElement;
|
|
|
|
setIsFullscreen(!!isCurrentlyFullscreen);
|
|
};
|
|
|
|
document.addEventListener('fullscreenchange', handleFullscreenChange);
|
|
document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
|
|
document.addEventListener('mozfullscreenchange', handleFullscreenChange);
|
|
document.addEventListener('MSFullscreenChange', handleFullscreenChange);
|
|
|
|
return () => {
|
|
document.removeEventListener('fullscreenchange', handleFullscreenChange);
|
|
document.removeEventListener(
|
|
'webkitfullscreenchange',
|
|
handleFullscreenChange,
|
|
);
|
|
document.removeEventListener(
|
|
'mozfullscreenchange',
|
|
handleFullscreenChange,
|
|
);
|
|
document.removeEventListener(
|
|
'MSFullscreenChange',
|
|
handleFullscreenChange,
|
|
);
|
|
};
|
|
}, []);
|
|
|
|
// 打印预览功能
|
|
const handlePrint = () => {
|
|
// 获取要打印的内容
|
|
const printContentElement = document.getElementById('preview-canvas');
|
|
if (!printContentElement) {
|
|
message.error('未能找到打印内容');
|
|
return;
|
|
}
|
|
|
|
// 创建一个用于打印的隐藏iframe
|
|
const printWindow = window.open('', '_blank');
|
|
if (printWindow) {
|
|
// 获取预览区域的HTML内容
|
|
const contentHTML = printContentElement.innerHTML;
|
|
|
|
// 构建打印内容
|
|
let printContent = `
|
|
<html>
|
|
<head>
|
|
<title>发货单打印预览</title>
|
|
<style>
|
|
@page {
|
|
size: 210mm 297mm;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
* {
|
|
outline: none;
|
|
box-sizing: border-box;
|
|
margin: 0;
|
|
padding: 0;
|
|
border: 0 solid;
|
|
}
|
|
|
|
body {
|
|
background-color: #fff;
|
|
color: #4d4d4d;
|
|
font-size: 14px;
|
|
font-style: normal;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.page-wrap {
|
|
width: 210mm;
|
|
min-height: 297mm;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.page-content {
|
|
position: relative;
|
|
box-sizing: border-box;
|
|
width: 100%;
|
|
height: 100%;
|
|
padding: 20mm 10mm 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2mm;
|
|
}
|
|
|
|
@media print {
|
|
.print-controls {
|
|
display: none !important;
|
|
}
|
|
body {
|
|
padding: 0;
|
|
margin: 0;
|
|
}
|
|
}
|
|
.print-module {
|
|
margin-bottom: 15px;
|
|
text-align: center;
|
|
}
|
|
.print-controls {
|
|
position: fixed;
|
|
top: 10px;
|
|
right: 10px;
|
|
z-index: 9999;
|
|
}
|
|
.print-button, .close-button {
|
|
padding: 8px 16px;
|
|
margin-left: 10px;
|
|
cursor: pointer;
|
|
border: 1px solid #d9d9d9;
|
|
border-radius: 4px;
|
|
}
|
|
.print-button {
|
|
background-color: #1890ff;
|
|
color: white;
|
|
}
|
|
.close-button {
|
|
background-color: #fff;
|
|
color: #000;
|
|
}
|
|
|
|
.preview {
|
|
width: 19cm;
|
|
|
|
div {
|
|
height: 0.69cm;
|
|
}
|
|
}
|
|
|
|
.table-border {
|
|
border: 2px solid #000;
|
|
}
|
|
|
|
.table-border > div {
|
|
border-bottom: 1px solid #000;
|
|
}
|
|
|
|
.table-border > div > div {
|
|
border-right: 1px solid #000;
|
|
}
|
|
|
|
.table-border > div > div:last-child {
|
|
border-right: none;
|
|
}
|
|
|
|
.table-border > div:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.col-span-1 {
|
|
grid-column: span 1 / span 1;
|
|
}
|
|
.col-span-2 {
|
|
grid-column: span 2 / span 2;
|
|
}
|
|
.col-span-3 {
|
|
grid-column: span 3 / span 3;
|
|
}
|
|
.col-span-6 {
|
|
grid-column: span 6 / span 6;
|
|
}
|
|
.col-span-8 {
|
|
grid-column: span 8 / span 8;
|
|
}
|
|
.flex {
|
|
display: flex;
|
|
}
|
|
.grid {
|
|
display: grid;
|
|
}
|
|
.w-full {
|
|
width: 100%;
|
|
}
|
|
.grid-cols-2 {
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
}
|
|
.grid-cols-4 {
|
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
}
|
|
.grid-cols-7 {
|
|
grid-template-columns: repeat(7, minmax(0, 1fr));
|
|
}
|
|
.grid-cols-8 {
|
|
grid-template-columns: repeat(8, minmax(0, 1fr));
|
|
}
|
|
.items-end {
|
|
align-items: flex-end;
|
|
}
|
|
.justify-center {
|
|
justify-content: center;
|
|
}
|
|
.border-t-0 {
|
|
border-top-width: 0px;
|
|
}
|
|
.border-b {
|
|
border-bottom-width: 1px;
|
|
}
|
|
.border-black {
|
|
border-color: #000000;
|
|
}
|
|
.bg-white {
|
|
background-color: #ffffff;
|
|
}
|
|
.text-2xl {
|
|
font-size: 24px;
|
|
line-height: 1;
|
|
}
|
|
.text-base {
|
|
font-size: 16px;
|
|
line-height: 1;
|
|
}
|
|
.text-lg {
|
|
font-size: 18px;
|
|
line-height: 1;
|
|
}
|
|
.font-bold {
|
|
font-weight: bold;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="print-controls">
|
|
<button class="print-button" onclick="window.print()">打印</button>
|
|
<button class="close-button" onclick="window.close()">关闭</button>
|
|
</div>
|
|
<div class="page-wrap">
|
|
<div class="page-content">
|
|
${contentHTML}
|
|
</div>
|
|
</div>
|
|
<script>
|
|
// 打印前隐藏按钮
|
|
window.onbeforeprint = function() {
|
|
var controls = document.querySelector('.print-controls');
|
|
if (controls) {
|
|
controls.style.visibility = 'hidden';
|
|
}
|
|
};
|
|
|
|
// 打印后显示按钮
|
|
window.onafterprint = function() {
|
|
var controls = document.querySelector('.print-controls');
|
|
if (controls) {
|
|
controls.style.visibility = 'visible';
|
|
}
|
|
};
|
|
|
|
// 页面加载完成后自动打印
|
|
window.onload = function() {
|
|
window.print();
|
|
};
|
|
</script>
|
|
</body>
|
|
</html>
|
|
`;
|
|
|
|
// 写入内容到打印窗口
|
|
printWindow.document.write(printContent);
|
|
printWindow.document.close();
|
|
} else {
|
|
message.error('无法打开打印预览窗口,请检查浏览器设置');
|
|
}
|
|
};
|
|
|
|
// 缩放功能
|
|
const handleZoomIn = () => {
|
|
setScale((prev) => Math.min(prev + 0.1, 3));
|
|
};
|
|
|
|
const handleZoomOut = () => {
|
|
setScale((prev) => Math.max(prev - 0.1, 0.5));
|
|
};
|
|
|
|
const handleResetZoom = () => {
|
|
setScale(1);
|
|
setPosition({ x: 0, y: 0 });
|
|
};
|
|
|
|
const handleSliderChange = (value: number) => {
|
|
setScale(value / 100);
|
|
};
|
|
|
|
// 鼠标滚轮缩放
|
|
const handleWheel = (e: React.WheelEvent) => {
|
|
if (!isPreviewMode) return;
|
|
|
|
e.preventDefault();
|
|
const delta = e.deltaY > 0 ? -0.1 : 0.1;
|
|
setScale((prev) => Math.min(Math.max(prev + delta, 0.5), 3));
|
|
};
|
|
|
|
// 拖动功能
|
|
const handleMouseDown = (e: React.MouseEvent) => {
|
|
if (!isPreviewMode) return;
|
|
|
|
setIsDragging(true);
|
|
setDragStart({
|
|
x: e.clientX - position.x,
|
|
y: e.clientY - position.y,
|
|
});
|
|
};
|
|
|
|
const handleMouseMove = (e: React.MouseEvent) => {
|
|
if (!isPreviewMode || !isDragging) return;
|
|
|
|
setPosition({
|
|
x: e.clientX - dragStart.x,
|
|
y: e.clientY - dragStart.y,
|
|
});
|
|
};
|
|
|
|
const handleMouseUp = () => {
|
|
setIsDragging(false);
|
|
};
|
|
|
|
// 点击预览区域外取消选中
|
|
const handleContainerClick = (e: React.MouseEvent) => {
|
|
if (e.target === containerRef.current) {
|
|
onModuleSelect(null);
|
|
}
|
|
};
|
|
|
|
// 清理拖动状态
|
|
useEffect(() => {
|
|
if (!isPreviewMode) {
|
|
setIsDragging(false);
|
|
}
|
|
}, [isPreviewMode]);
|
|
|
|
const renderModule = (module: any, index: number) => {
|
|
const props = {
|
|
config: module.config,
|
|
isSelected: selectedModule?.id === module.id,
|
|
onSelect: () => onModuleSelect(module),
|
|
onDelete: () => onModuleDelete(module.id),
|
|
onMoveUp: () => {
|
|
if (index > 0) {
|
|
const newModules = [...modules];
|
|
const temp = newModules[index];
|
|
newModules[index] = newModules[index - 1];
|
|
newModules[index - 1] = temp;
|
|
onModuleReorder(newModules);
|
|
}
|
|
},
|
|
onMoveDown: () => {
|
|
if (index < modules.length - 1) {
|
|
const newModules = [...modules];
|
|
const temp = newModules[index];
|
|
newModules[index] = newModules[index + 1];
|
|
newModules[index + 1] = temp;
|
|
onModuleReorder(newModules);
|
|
}
|
|
},
|
|
canMoveUp: index > 0,
|
|
canMoveDown: index < modules.length - 1,
|
|
};
|
|
|
|
// 创建组件映射对象
|
|
const componentMap = {
|
|
title: TitleModule,
|
|
dealerInfo: DealerInfoModule,
|
|
shippingInfo: ShippingInfoModule,
|
|
weightInfo: WeightInfoModule,
|
|
packingSpec: PackingSpecModule,
|
|
vehicleInfo: VehicleInfoModule,
|
|
otherFees: OtherFeesModule,
|
|
totalAmount: TotalAmountModule,
|
|
otherInfo: OtherInfoModule,
|
|
};
|
|
|
|
const Component = componentMap[module.type as keyof typeof componentMap];
|
|
if (isPreviewMode) {
|
|
return Component ? (
|
|
<Component {...props} previewMode={isPreviewMode} />
|
|
) : null;
|
|
}
|
|
|
|
return (
|
|
<Draggable
|
|
key={module.id}
|
|
draggableId={module.id}
|
|
index={index}
|
|
isDragDisabled={isPreviewMode}
|
|
>
|
|
{(provided, snapshot) => (
|
|
<div
|
|
ref={provided.innerRef}
|
|
{...provided.draggableProps}
|
|
{...provided.dragHandleProps}
|
|
style={{
|
|
...provided.draggableProps.style,
|
|
marginBottom: '8px',
|
|
borderRadius: '4px',
|
|
border: snapshot.isDragging ? '1px dashed #1890ff' : 'none',
|
|
backgroundColor: snapshot.isDragging ? '#e6f7ff' : 'transparent',
|
|
userSelect: 'none', // 防止拖拽时选中文本
|
|
}}
|
|
>
|
|
{Component ? (
|
|
<Component {...props} previewMode={isPreviewMode} />
|
|
) : null}
|
|
</div>
|
|
)}
|
|
</Draggable>
|
|
);
|
|
};
|
|
|
|
// 为 Droppable 组件创建一个不使用 memo 的包装组件
|
|
const DroppableWrapper = ({ children }: { children: React.ReactNode }) => (
|
|
<Droppable droppableId="modules" isDropDisabled={isPreviewMode}>
|
|
{(provided, snapshot) => (
|
|
<div
|
|
{...provided.droppableProps}
|
|
ref={provided.innerRef}
|
|
style={{
|
|
minHeight: '100px',
|
|
padding: '8px',
|
|
backgroundColor: snapshot.isDraggingOver
|
|
? '#f0f8ff'
|
|
: 'transparent',
|
|
borderRadius: '4px',
|
|
transition: 'background-color 0.2s ease',
|
|
userSelect: 'none', // 防止拖拽时选中文本
|
|
}}
|
|
>
|
|
{children}
|
|
{provided.placeholder}
|
|
</div>
|
|
)}
|
|
</Droppable>
|
|
);
|
|
|
|
return (
|
|
<Card
|
|
title="模板预览"
|
|
size="small"
|
|
className="bg-white rounded-lg border border-gray-300"
|
|
extra={
|
|
modules.length > 0 ? (
|
|
<div className="flex gap-2">
|
|
{onClearAll && (
|
|
<Popconfirm
|
|
title="确定要清空所有模块吗?"
|
|
onConfirm={onClearAll}
|
|
okText="确定"
|
|
cancelText="取消"
|
|
>
|
|
<Button
|
|
type="text"
|
|
icon={<DeleteOutlined />}
|
|
danger
|
|
size="small"
|
|
>
|
|
清空
|
|
</Button>
|
|
</Popconfirm>
|
|
)}
|
|
<Button
|
|
type="text"
|
|
icon={<EyeOutlined />}
|
|
onClick={() => setIsPreviewMode(!isPreviewMode)}
|
|
size="small"
|
|
>
|
|
{isPreviewMode ? '退出预览' : '预览'}
|
|
</Button>
|
|
{isPreviewMode && (
|
|
<>
|
|
<Button
|
|
type="text"
|
|
icon={
|
|
isFullscreen ? <CompressOutlined /> : <ExpandOutlined />
|
|
}
|
|
onClick={toggleFullscreen}
|
|
size="small"
|
|
/>
|
|
<Button
|
|
type="text"
|
|
icon={<ZoomOutOutlined />}
|
|
onClick={handleZoomOut}
|
|
size="small"
|
|
/>
|
|
<span style={{ fontSize: '12px', alignSelf: 'center' }}>
|
|
{Math.round(scale * 100)}%
|
|
</span>
|
|
<Button
|
|
type="text"
|
|
icon={<ZoomInOutlined />}
|
|
onClick={handleZoomIn}
|
|
size="small"
|
|
/>
|
|
<Button type="text" onClick={handleResetZoom} size="small">
|
|
重置
|
|
</Button>
|
|
</>
|
|
)}
|
|
{isPreviewMode && (
|
|
<Button
|
|
type="text"
|
|
icon={<PrinterOutlined />}
|
|
onClick={handlePrint}
|
|
size="small"
|
|
>
|
|
打印预览
|
|
</Button>
|
|
)}
|
|
</div>
|
|
) : null
|
|
}
|
|
bodyStyle={{
|
|
maxHeight: 'calc(100vh - 200px)',
|
|
overflowY: 'auto',
|
|
margin: '0 auto',
|
|
position: 'relative',
|
|
cursor: isPreviewMode ? (isDragging ? 'grabbing' : 'grab') : 'default',
|
|
}}
|
|
>
|
|
<div
|
|
ref={fullscreenElementRef}
|
|
className={isFullscreen ? 'fixed inset-0 z-50 bg-white' : ''}
|
|
>
|
|
{isPreviewMode ? (
|
|
<>
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
padding: '8px',
|
|
backgroundColor: '#f5f5f5',
|
|
borderRadius: '4px',
|
|
marginBottom: '8px',
|
|
}}
|
|
>
|
|
<span style={{ marginRight: '8px', fontSize: '12px' }}>
|
|
缩放:
|
|
</span>
|
|
<Slider
|
|
min={50}
|
|
max={300}
|
|
value={Math.round(scale * 100)}
|
|
onChange={handleSliderChange}
|
|
style={{ width: '120px', marginRight: '8px' }}
|
|
disabled={!isPreviewMode}
|
|
/>
|
|
<span style={{ fontSize: '12px' }}>
|
|
{Math.round(scale * 100)}%
|
|
</span>
|
|
</div>
|
|
|
|
<div
|
|
ref={containerRef}
|
|
style={{
|
|
width: '100%',
|
|
height: isFullscreen
|
|
? 'calc(100vh - 0px)'
|
|
: 'calc(100vh - 280px)',
|
|
overflow: 'hidden',
|
|
position: 'relative',
|
|
backgroundColor: isPreviewMode ? '#f0f0f0' : 'transparent',
|
|
}}
|
|
onClick={handleContainerClick}
|
|
onWheel={handleWheel}
|
|
onMouseDown={handleMouseDown}
|
|
onMouseMove={handleMouseMove}
|
|
onMouseUp={handleMouseUp}
|
|
onMouseLeave={handleMouseUp}
|
|
>
|
|
<div
|
|
ref={canvasRef}
|
|
id={'preview-canvas'}
|
|
style={{
|
|
transform: `scale(${scale}) translate(${position.x}px, ${position.y}px)`,
|
|
transformOrigin: 'center top',
|
|
transition: isDragging ? 'none' : 'transform 0.1s ease',
|
|
width: '21cm',
|
|
padding: '2cm 1cm 0 1cm',
|
|
margin: '0 auto',
|
|
minHeight: '29.7cm',
|
|
backgroundColor: 'white',
|
|
boxShadow: isPreviewMode
|
|
? '0 0 10px rgba(0,0,0,0.1)'
|
|
: 'none',
|
|
position: 'relative',
|
|
userSelect: 'none', // 防止拖拽时选中文本
|
|
}}
|
|
>
|
|
{modules.map((module, index) => renderModule(module, index))}
|
|
</div>
|
|
</div>
|
|
</>
|
|
) : (
|
|
<div
|
|
ref={containerRef}
|
|
style={{
|
|
width: '21cm',
|
|
margin: '0 auto',
|
|
}}
|
|
>
|
|
<div ref={canvasRef} id={'preview-canvas'}>
|
|
{modules.length === 0 ? (
|
|
<div className="empty-preview">
|
|
<Empty description="从左侧模块库点击添加模块" />
|
|
</div>
|
|
) : (
|
|
<DragDropContext onDragEnd={handleDragEnd}>
|
|
<DroppableWrapper>
|
|
{modules.map((module, index) => renderModule(module, index))}
|
|
</DroppableWrapper>
|
|
</DragDropContext>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
);
|
|
};
|
|
|
|
export default PreviewCanvas;
|