feat(dealer): 优化发货单模板模块样式与打印功能

- 引入 classNames 库统一管理组件样式类名
- 调整模块选中状态与预览模式下的边框样式
- 优化 PackingSpecModule 模块的数据显示逻辑
- 增加 formatCurrency 工具函数格式化金额显示
- 重构 PreviewCanvas 打印预览页面样式与结构
- 移除无效的模块配置项 showBoxType
- 修复全屏事件监听器在不同浏览器的兼容性问题
- 优化打印样式,增加页面尺寸与边距控制
- 调整模块渲染逻辑,提升预览模式下的显示效果
This commit is contained in:
shenyifei 2025-11-08 13:29:52 +08:00
parent 0c39c005bd
commit 45dc1de523
12 changed files with 286 additions and 102 deletions

View File

@ -231,11 +231,6 @@ const getModuleFormSchema = (moduleType: string) => {
title: '显示配置',
valueType: 'group',
columns: [
{
dataIndex: 'showBoxType',
title: '显示箱号',
valueType: 'switch',
},
{
dataIndex: 'showQuantity',
title: '显示数量',
@ -565,25 +560,25 @@ const getModuleExampleData = (moduleType: string) => {
boxType: '1号',
quantity: '376',
unitPrice: '8.70',
amount: '3,271',
amount: '3271',
unitWeight: '2.70',
weight: '1,015',
weight: '1015',
},
{
boxCategory: '2粒',
boxType: 'A',
quantity: '700',
unitPrice: '6.30',
amount: '4,410',
amount: '4410',
unitWeight: '1.50',
weight: '1,050',
weight: '1050',
},
{
boxCategory: '2粒',
boxType: 'AA',
quantity: '456',
unitPrice: '6.60',
amount: '3,010',
amount: '3010',
unitWeight: '1.60',
weight: '730',
},

View File

@ -6,6 +6,7 @@ import {
} from '@ant-design/icons';
import { Button, Typography } from 'antd';
import React from 'react';
import classNames from 'classnames';
const { Text } = Typography;
@ -34,11 +35,12 @@ const DealerInfoModule: React.FC<ModuleProps> = ({
}) => {
return (
<div
className={`${previewMode ? '' : 'border border-gray-300'} rounded p-2 mb-2 bg-white transition-all duration-200 ${
isSelected && !previewMode
? 'border-2 border-blue-500 shadow-[0_0_0_2px_rgba(24,144,255,0.2)]'
: 'hover:border-blue-300'
}`}
className={classNames('bg-white transition-all duration-200', {
'border border-black': !previewMode,
'border-2 border-blue-500 shadow-[0_0_0_2px_rgba(24,144,255,0.2)] rounded p-2 mb-2':
isSelected && !previewMode,
'hover:border-blue-300 rounded p-2 mb-2': !isSelected && !previewMode,
})}
onClick={previewMode ? undefined : onSelect}
>
{!previewMode && (

View File

@ -6,6 +6,7 @@ import {
} from '@ant-design/icons';
import { Button, Typography } from 'antd';
import React from 'react';
import classNames from 'classnames';
const { Text } = Typography;
@ -35,11 +36,12 @@ const OtherFeesModule: React.FC<ModuleProps> = ({
return (
<div
className={`${previewMode ? '' : 'border border-gray-300'} rounded p-2 mb-2 bg-white transition-all duration-200 ${
isSelected && !previewMode
? 'border-2 border-blue-500 shadow-[0_0_0_2px_rgba(24,144,255,0.2)]'
: 'hover:border-blue-300'
}`}
className={classNames('bg-white transition-all duration-200', {
'border border-black': !previewMode,
'border-2 border-blue-500 shadow-[0_0_0_2px_rgba(24,144,255,0.2)] rounded p-2 mb-2':
isSelected && !previewMode,
'hover:border-blue-300 rounded p-2 mb-2': !isSelected && !previewMode,
})}
onClick={previewMode ? undefined : onSelect}
>
{!previewMode && (

View File

@ -6,6 +6,7 @@ import {
ArrowDownOutlined,
DeleteOutlined
} from '@ant-design/icons';
import classNames from 'classnames';
const { Text } = Typography;
@ -34,11 +35,12 @@ const OtherInfoModule: React.FC<ModuleProps> = ({
}) => {
return (
<div
className={`${previewMode ? '' : 'border border-gray-300'} rounded p-2 mb-2 bg-white transition-all duration-200 ${
isSelected && !previewMode
? 'border-2 border-blue-500 shadow-[0_0_0_2px_rgba(24,144,255,0.2)]'
: 'hover:border-blue-300'
}`}
className={classNames('bg-white transition-all duration-200', {
'border border-black': !previewMode,
'border-2 border-blue-500 shadow-[0_0_0_2px_rgba(24,144,255,0.2)] rounded p-2 mb-2':
isSelected && !previewMode,
'hover:border-blue-300 rounded p-2 mb-2': !isSelected && !previewMode,
})}
onClick={previewMode ? undefined : onSelect}
>
{!previewMode && (

View File

@ -1,3 +1,4 @@
import { formatCurrency } from '@/utils/format';
import {
ArrowDownOutlined,
ArrowUpOutlined,
@ -47,11 +48,12 @@ const PackingSpecModule: React.FC<ModuleProps> = ({
return (
<div
className={`${previewMode ? '' : 'border border-gray-300'} rounded p-2 mb-2 bg-white transition-all duration-200 ${
isSelected && !previewMode
? 'border-2 border-blue-500 shadow-[0_0_0_2px_rgba(24,144,255,0.2)]'
: 'hover:border-blue-300'
}`}
className={classNames('bg-white transition-all duration-200', {
'border border-black': !previewMode,
'border-2 border-blue-500 shadow-[0_0_0_2px_rgba(24,144,255,0.2)] rounded p-2 mb-2':
isSelected && !previewMode,
'hover:border-blue-300 rounded p-2 mb-2': !isSelected && !previewMode,
})}
onClick={previewMode ? undefined : onSelect}
>
{!previewMode && (
@ -177,11 +179,9 @@ const PackingSpecModule: React.FC<ModuleProps> = ({
{item.boxCategory}
</div>
{config.showBoxType && (
<div className={'flex items-end justify-center'}>
{item.boxType}
</div>
)}
{config.showQuantity && (
<div className={'flex items-end justify-center'}>
@ -230,12 +230,40 @@ const PackingSpecModule: React.FC<ModuleProps> = ({
<div className={`flex items-end justify-center col-span-2`}>
</div>
{config.showQuantity && (
<div className={`flex items-end justify-center col-span-1`}>
{config.data?.reduce(
(acc: any, cur: any) => acc + Number(cur.quantity),
0,
)}
</div>
)}
{config.showUnitPrice && (
<div className={`flex items-end justify-center col-span-1`}></div>
)}
{config.showAmount && (
<div className={`flex items-end justify-center col-span-1`}>
{formatCurrency(
config.data?.reduce(
(acc: any, cur: any) => acc + Number(cur.amount),
0,
),
)}
</div>
)}
{config.showUnitWeight && (
<div className={`flex items-end justify-center col-span-1`}></div>
)}
{config.showWeight && (
<div className={`flex items-end justify-center col-span-1`}>
{formatCurrency(
config.data?.reduce(
(acc: any, cur: any) => acc + Number(cur.weight),
0,
),
)}
</div>
)}
</div>
</div>
</div>

View File

@ -1,9 +1,9 @@
import {
CompressOutlined,
DeleteOutlined,
ExpandOutlined,
EyeOutlined,
PrinterOutlined,
ExpandOutlined,
CompressOutlined,
ZoomInOutlined,
ZoomOutOutlined,
} from '@ant-design/icons';
@ -124,9 +124,18 @@ const PreviewCanvas: React.FC<PreviewCanvasProps> = ({
return () => {
document.removeEventListener('fullscreenchange', handleFullscreenChange);
document.removeEventListener('webkitfullscreenchange', handleFullscreenChange);
document.removeEventListener('mozfullscreenchange', handleFullscreenChange);
document.removeEventListener('MSFullscreenChange', handleFullscreenChange);
document.removeEventListener(
'webkitfullscreenchange',
handleFullscreenChange,
);
document.removeEventListener(
'mozfullscreenchange',
handleFullscreenChange,
);
document.removeEventListener(
'MSFullscreenChange',
handleFullscreenChange,
);
};
}, []);
@ -150,17 +159,46 @@ const PreviewCanvas: React.FC<PreviewCanvasProps> = ({
<html>
<head>
<title></title>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<style>
@page {
size: 210mm 297mm;
margin: 0;
padding: 0;
}
* {
outline: none;
box-sizing: border-box;
margin: 0;
padding: 0;
border: 0 solid;
}
body {
font-family: Arial, sans-serif;
background-color: #fff;
color: #4d4d4d;
font-size: 14px;
width: 21cm;
min-height: 29.7cm;
padding: 2cm 1cm 0 1cm;
margin: 0 auto;
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;
@ -195,9 +233,103 @@ const PreviewCanvas: React.FC<PreviewCanvasProps> = ({
background-color: #fff;
color: #000;
}
#print-content {
min-height: 1123px;
box-sizing: border-box;
.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>
@ -206,9 +338,11 @@ const PreviewCanvas: React.FC<PreviewCanvasProps> = ({
<button class="print-button" onclick="window.print()"></button>
<button class="close-button" onclick="window.close()"></button>
</div>
<div id="print-content">
<div class="page-wrap">
<div class="page-content">
${contentHTML}
</div>
</div>
<script>
// 打印前隐藏按钮
window.onbeforeprint = function() {
@ -350,6 +484,11 @@ const PreviewCanvas: React.FC<PreviewCanvasProps> = ({
};
const Component = componentMap[module.type as keyof typeof componentMap];
if (isPreviewMode) {
return Component ? (
<Component {...props} previewMode={isPreviewMode} />
) : null;
}
return (
<Draggable
@ -443,7 +582,9 @@ const PreviewCanvas: React.FC<PreviewCanvasProps> = ({
<>
<Button
type="text"
icon={isFullscreen ? <CompressOutlined /> : <ExpandOutlined />}
icon={
isFullscreen ? <CompressOutlined /> : <ExpandOutlined />
}
onClick={toggleFullscreen}
size="small"
/>
@ -488,7 +629,10 @@ const PreviewCanvas: React.FC<PreviewCanvasProps> = ({
cursor: isPreviewMode ? (isDragging ? 'grabbing' : 'grab') : 'default',
}}
>
<div ref={fullscreenElementRef} className={isFullscreen ? 'fixed inset-0 z-50 bg-white' : ''}>
<div
ref={fullscreenElementRef}
className={isFullscreen ? 'fixed inset-0 z-50 bg-white' : ''}
>
{isPreviewMode ? (
<>
<div
@ -502,7 +646,9 @@ const PreviewCanvas: React.FC<PreviewCanvasProps> = ({
marginBottom: '8px',
}}
>
<span style={{ marginRight: '8px', fontSize: '12px' }}>:</span>
<span style={{ marginRight: '8px', fontSize: '12px' }}>
:
</span>
<Slider
min={50}
max={300}
@ -511,14 +657,18 @@ const PreviewCanvas: React.FC<PreviewCanvasProps> = ({
style={{ width: '120px', marginRight: '8px' }}
disabled={!isPreviewMode}
/>
<span style={{ fontSize: '12px' }}>{Math.round(scale * 100)}%</span>
<span style={{ fontSize: '12px' }}>
{Math.round(scale * 100)}%
</span>
</div>
<div
ref={containerRef}
style={{
width: '100%',
height: isFullscreen ? 'calc(100vh - 0px)' : 'calc(100vh - 280px)',
height: isFullscreen
? 'calc(100vh - 0px)'
: 'calc(100vh - 280px)',
overflow: 'hidden',
position: 'relative',
backgroundColor: isPreviewMode ? '#f0f0f0' : 'transparent',
@ -542,24 +692,14 @@ const PreviewCanvas: React.FC<PreviewCanvasProps> = ({
margin: '0 auto',
minHeight: '29.7cm',
backgroundColor: 'white',
boxShadow: isPreviewMode ? '0 0 10px rgba(0,0,0,0.1)' : 'none',
boxShadow: isPreviewMode
? '0 0 10px rgba(0,0,0,0.1)'
: 'none',
position: 'relative',
userSelect: 'none', // 防止拖拽时选中文本
}}
>
{modules.length === 0 ? (
<div className="empty-preview">
<Empty description="从左侧模块库点击添加模块" />
</div>
) : (
<DragDropContext onDragEnd={handleDragEnd}>
<DroppableWrapper>
{modules.map((module, index) =>
renderModule(module, index),
)}
</DroppableWrapper>
</DragDropContext>
)}
{modules.map((module, index) => renderModule(module, index))}
</div>
</div>
</>

View File

@ -6,6 +6,7 @@ import {
} from '@ant-design/icons';
import { Button, Typography } from 'antd';
import React from 'react';
import classNames from 'classnames';
const { Text } = Typography;
@ -34,11 +35,12 @@ const ShippingInfoModule: React.FC<ModuleProps> = ({
}) => {
return (
<div
className={`${previewMode ? '' : 'border border-gray-300'} rounded p-2 mb-2 bg-white transition-all duration-200 ${
isSelected && !previewMode
? 'border-2 border-blue-500 shadow-[0_0_0_2px_rgba(24,144,255,0.2)]'
: 'hover:border-blue-300'
}`}
className={classNames('bg-white transition-all duration-200', {
'border border-black': !previewMode,
'border-2 border-blue-500 shadow-[0_0_0_2px_rgba(24,144,255,0.2)] rounded p-2 mb-2':
isSelected && !previewMode,
'hover:border-blue-300 rounded p-2 mb-2': !isSelected && !previewMode,
})}
onClick={previewMode ? undefined : onSelect}
>
{!previewMode && (

View File

@ -5,6 +5,7 @@ import {
MenuOutlined,
} from '@ant-design/icons';
import { Button, Typography } from 'antd';
import classNames from 'classnames';
import React from 'react';
const { Text } = Typography;
@ -34,11 +35,12 @@ const TitleModule: React.FC<ModuleProps> = ({
}) => {
return (
<div
className={`${previewMode ? '' : 'border border-gray-300'} rounded p-2 mb-2 bg-white transition-all duration-200 ${
isSelected && !previewMode
? 'border-2 border-blue-500 shadow-[0_0_0_2px_rgba(24,144,255,0.2)]'
: 'hover:border-blue-300'
}`}
className={classNames('bg-white transition-all duration-200', {
'border border-black': !previewMode,
'border-2 border-blue-500 shadow-[0_0_0_2px_rgba(24,144,255,0.2)] rounded p-2 mb-2':
isSelected && !previewMode,
'hover:border-blue-300 rounded p-2 mb-2': !isSelected && !previewMode,
})}
onClick={previewMode ? undefined : onSelect}
>
{!previewMode && (

View File

@ -6,6 +6,7 @@ import {
} from '@ant-design/icons';
import { Button, Typography } from 'antd';
import React from 'react';
import classNames from 'classnames';
const { Text } = Typography;
@ -34,11 +35,12 @@ const TotalAmountModule: React.FC<ModuleProps> = ({
}) => {
return (
<div
className={`${previewMode ? '' : 'border border-gray-300'} rounded p-2 mb-2 bg-white transition-all duration-200 ${
isSelected && !previewMode
? 'border-2 border-blue-500 shadow-[0_0_0_2px_rgba(24,144,255,0.2)]'
: 'hover:border-blue-300'
}`}
className={classNames('bg-white transition-all duration-200', {
'border border-black': !previewMode,
'border-2 border-blue-500 shadow-[0_0_0_2px_rgba(24,144,255,0.2)] rounded p-2 mb-2':
isSelected && !previewMode,
'hover:border-blue-300 rounded p-2 mb-2': !isSelected && !previewMode,
})}
onClick={previewMode ? undefined : onSelect}
>
{!previewMode && (

View File

@ -6,6 +6,7 @@ import {
} from '@ant-design/icons';
import { Button, Typography } from 'antd';
import React from 'react';
import classNames from 'classnames';
const { Text } = Typography;
@ -34,11 +35,12 @@ const VehicleInfoModule: React.FC<ModuleProps> = ({
}) => {
return (
<div
className={`${previewMode ? '' : 'border border-gray-300'} rounded p-2 mb-2 bg-white transition-all duration-200 ${
isSelected && !previewMode
? 'border-2 border-blue-500 shadow-[0_0_0_2px_rgba(24,144,255,0.2)]'
: 'hover:border-blue-300'
}`}
className={classNames('bg-white transition-all duration-200', {
'border border-black': !previewMode,
'border-2 border-blue-500 shadow-[0_0_0_2px_rgba(24,144,255,0.2)] rounded p-2 mb-2':
isSelected && !previewMode,
'hover:border-blue-300 rounded p-2 mb-2': !isSelected && !previewMode,
})}
onClick={previewMode ? undefined : onSelect}
>
{!previewMode && (

View File

@ -6,6 +6,7 @@ import {
} from '@ant-design/icons';
import { Button, Typography } from 'antd';
import React from 'react';
import classNames from 'classnames';
const { Text } = Typography;
@ -34,11 +35,12 @@ const WeightInfoModule: React.FC<ModuleProps> = ({
}) => {
return (
<div
className={`${previewMode ? '' : 'border border-gray-300'} rounded p-2 mb-2 bg-white transition-all duration-200 ${
isSelected && !previewMode
? 'border-2 border-blue-500 shadow-[0_0_0_2px_rgba(24,144,255,0.2)]'
: 'hover:border-blue-300'
}`}
className={classNames('bg-white transition-all duration-200', {
'border border-black': !previewMode,
'border-2 border-blue-500 shadow-[0_0_0_2px_rgba(24,144,255,0.2)] rounded p-2 mb-2':
isSelected && !previewMode,
'hover:border-blue-300 rounded p-2 mb-2': !isSelected && !previewMode,
})}
onClick={previewMode ? undefined : onSelect}
>
{!previewMode && (

View File

@ -0,0 +1,5 @@
// 格式化金额显示
export const formatCurrency = (value: number) => {
return Number(value || 0)?.toLocaleString();
};