feat(delivery-template): 重构发货模板创建页面并新增打印预览功能

- 将模块库、预览画布和配置面板拆分为独立组件
- 简化页面主组件结构,移除大量内联样式和冗余代码
- 新增 PrintPreview 组件用于模板打印预览
- 更新装箱规格表格数据结构,增加箱类字段
- 扩展车辆信息和合计金额模块的配置选项
- 优化导入语句,仅保留必要的第三方组件引用
- 移除 react-beautiful-dnd 拖拽相关依赖和实现
- 导出 DeliveryTemplate 组件供其他模块使用
This commit is contained in:
shenyifei 2025-11-03 18:18:26 +08:00
parent 4154eb75f5
commit d17eb56a4d
17 changed files with 2323 additions and 1654 deletions

View File

@ -0,0 +1,354 @@
import React from 'react';
import { Card, Checkbox, Empty, Form, Input, Switch } from 'antd';
interface ConfigPanelProps {
selectedModule: any;
onConfigChange: (moduleId: string, config: any) => void;
}
const ConfigPanel: React.FC<ConfigPanelProps> = ({
selectedModule,
onConfigChange,
}) => {
const [form] = Form.useForm();
React.useEffect(() => {
if (selectedModule) {
form.setFieldsValue(selectedModule.config);
}
}, [selectedModule, form]);
const handleValuesChange = (changedValues: any, allValues: any) => {
if (selectedModule) {
onConfigChange(selectedModule.id, {
...selectedModule.config,
...allValues,
});
}
};
const renderTitleConfig = () => (
<div className="mb-4 pb-4 border-b border-gray-200">
<div className="font-bold mb-2 text-blue-500"></div>
<Form.Item name="text" label="标题文本">
<Input placeholder="例如:西瓜发货清单" />
</Form.Item>
</div>
);
const renderDealerInfoConfig = () => (
<div className="mb-4 pb-4 border-b border-gray-200">
<div className="font-bold mb-2 text-blue-500"></div>
<Form.Item
name="showDealerName"
valuePropName="checked"
label="显示经销商名称"
>
<Switch />
</Form.Item>
<Form.Item
name="dealerName"
label="经销商名称"
>
<Input placeholder="请输入经销商名称" />
</Form.Item>
<Form.Item
name="showVehicleNumber"
valuePropName="checked"
label="显示车次信息"
>
<Switch />
</Form.Item>
<Form.Item
name="showDestination"
valuePropName="checked"
label="显示收货地"
>
<Switch />
</Form.Item>
<Form.Item
name="showWatermelonGrade"
valuePropName="checked"
label="填写西瓜品级"
>
<Switch />
</Form.Item>
</div>
);
const renderWeightInfoConfig = () => (
<div className="mb-4 pb-4 border-b border-gray-200">
<div className="font-bold mb-2 text-blue-500"></div>
<Form.Item
name="showGrossWeight"
valuePropName="checked"
label="显示毛重"
>
<Switch />
</Form.Item>
<Form.Item name="showBoxWeight" valuePropName="checked" label="显示箱重">
<Switch />
</Form.Item>
<Form.Item name="showNetWeight" valuePropName="checked" label="显示净重">
<Switch />
</Form.Item>
<Form.Item name="showUnitPrice" valuePropName="checked" label="显示单价">
<Switch />
</Form.Item>
<Form.Item name="showAmount" valuePropName="checked" label="显示金额">
<Switch />
</Form.Item>
<Form.Item name="showGrade" valuePropName="checked" label="填写品级">
<Switch />
</Form.Item>
<Form.Item name="showAccountCompany" valuePropName="checked" label="显示入账公司">
<Switch />
</Form.Item>
</div>
);
const renderPackingSpecConfig = () => (
<div className="mb-4 pb-4 border-b border-gray-200">
<div className="font-bold mb-2 text-blue-500"></div>
<Form.Item name="showBoxCategory" valuePropName="checked" label="显示品牌">
<Switch />
</Form.Item>
<Form.Item name="showBoxType" valuePropName="checked" label="显示箱号">
<Switch />
</Form.Item>
<Form.Item name="showQuantity" valuePropName="checked" label="显示数量">
<Switch />
</Form.Item>
<Form.Item name="showUnitPrice" valuePropName="checked" label="显示单价">
<Switch />
</Form.Item>
<Form.Item name="showAmount" valuePropName="checked" label="显示金额">
<Switch />
</Form.Item>
<Form.Item name="showUnitWeight" valuePropName="checked" label="显示单重">
<Switch />
</Form.Item>
<Form.Item name="showWeight" valuePropName="checked" label="显示重量">
<Switch />
</Form.Item>
</div>
);
const renderOtherFeesConfig = () => (
<div className="mb-4 pb-4 border-b border-gray-200">
<div className="font-bold mb-2 text-blue-500"></div>
<Form.Item name="enabled" valuePropName="checked" label="启用费用模块">
<Switch />
</Form.Item>
<Form.Item name="feeItems" label="显示费用项目">
<Checkbox.Group className="flex flex-col gap-1">
<div className="checkbox-item">
<Checkbox value="trademark"></Checkbox>
</div>
<div className="checkbox-item">
<Checkbox value="labor"></Checkbox>
</div>
<div className="checkbox-item">
<Checkbox value="paperBox"></Checkbox>
</div>
<div className="checkbox-item">
<Checkbox value="fee"></Checkbox>
</div>
<div className="checkbox-item">
<Checkbox value="codingFee"></Checkbox>
</div>
</Checkbox.Group>
</Form.Item>
<Form.Item
name="laborDetails"
valuePropName="checked"
label="人工费用细分"
>
<Switch />
</Form.Item>
</div>
);
const renderModuleConfig = () => {
if (!selectedModule) return null;
const configs = {
title: renderTitleConfig(),
dealerInfo: renderDealerInfoConfig(),
shippingInfo: (
<div className="mb-4 pb-4 border-b border-gray-200">
<div className="font-bold mb-2 text-blue-500"></div>
<Form.Item
name="showShippingFrom"
valuePropName="checked"
label="显示发货地"
>
<Switch />
</Form.Item>
<Form.Item name="showDate" valuePropName="checked" label="显示发货日期">
<Switch />
</Form.Item>
</div>
),
weightInfo: renderWeightInfoConfig(),
packingSpec: renderPackingSpecConfig(),
vehicleInfo: (
<div className="mb-4 pb-4 border-b border-gray-200">
<div className="font-bold mb-2 text-blue-500"></div>
<Form.Item
name="showDriverPhone"
valuePropName="checked"
label="显示司机号码"
>
<Switch />
</Form.Item>
<Form.Item
name="showLicensePlate"
valuePropName="checked"
label="显示车牌"
>
<Switch />
</Form.Item>
<Form.Item
name="showEstimatedArrivalTime"
valuePropName="checked"
label="填写预计到仓时间"
>
<Switch />
</Form.Item>
<Form.Item
name="showFreightDebt"
valuePropName="checked"
label="显示运费欠"
>
<Switch />
</Form.Item>
<Form.Item
name="showSeller"
valuePropName="checked"
label="填写卖货"
>
<Switch />
</Form.Item>
<Form.Item
name="showStrawMatDebt"
valuePropName="checked"
label="显示草帘欠"
>
<Switch />
</Form.Item>
<Form.Item
name="showRemarks"
valuePropName="checked"
label="填写备注"
>
<Switch />
</Form.Item>
<Form.Item
name="freightDebtTitle"
label="运费欠标题"
>
<Input placeholder="请输入运费欠标题" />
</Form.Item>
</div>
),
otherFees: renderOtherFeesConfig(),
totalAmount: (
<div className="mb-4 pb-4 border-b border-gray-200">
<div className="font-bold mb-2 text-blue-500"></div>
<Form.Item
name="enabled"
valuePropName="checked"
label="显示合计金额"
>
<Switch />
</Form.Item>
<Form.Item
name="showFarmer"
valuePropName="checked"
label="信息瓜农"
>
<Switch />
</Form.Item>
<Form.Item
name="sumTitle"
label="合计金额标题"
>
<Input placeholder="请输入合计金额标题" />
</Form.Item>
</div>
),
otherInfo: (
<div className="mb-4 pb-4 border-b border-gray-200">
<div className="font-bold mb-2 text-blue-500"></div>
<Form.Item
name="enabled"
valuePropName="checked"
label="启用其他信息"
>
<Switch />
</Form.Item>
<Form.Item name="showOrigin" valuePropName="checked" label="显示产地">
<Switch />
</Form.Item>
<Form.Item
name="showSupplier"
valuePropName="checked"
label="显示供应商"
>
<Switch />
</Form.Item>
<Form.Item
name="showDepartureTime"
valuePropName="checked"
label="显示发车时间"
>
<Switch />
</Form.Item>
<Form.Item
name="showArrivalTime"
valuePropName="checked"
label="显示预计到达"
>
<Switch />
</Form.Item>
<Form.Item
name="showProductName"
valuePropName="checked"
label="显示品名"
>
<Switch />
</Form.Item>
</div>
),
} as any;
return configs[selectedModule.type] || <div></div>;
};
if (!selectedModule) {
return (
<Card
title="配置面板"
size="small"
className="bg-white rounded-lg p-4 h-full"
>
<Empty description="请选择要配置的模块" />
</Card>
);
}
return (
<Card
title="模块配置"
size="small"
className="bg-white rounded-lg p-4 h-full"
>
<Form form={form} layout="horizontal" onValuesChange={handleValuesChange}>
{renderModuleConfig()}
</Form>
</Card>
);
};
export default ConfigPanel;

View File

@ -0,0 +1,101 @@
import React from 'react';
import { Button, Typography } from 'antd';
import {
MenuOutlined,
ArrowUpOutlined,
ArrowDownOutlined,
DeleteOutlined
} from '@ant-design/icons';
const { Text } = Typography;
interface ModuleProps {
config: any;
isSelected: boolean;
onSelect: () => void;
onDelete: () => void;
onMoveUp?: () => void;
onMoveDown?: () => void;
canMoveUp?: boolean;
canMoveDown?: boolean;
}
const DealerInfoModule: React.FC<ModuleProps> = ({
config,
isSelected,
onSelect,
onDelete,
onMoveUp,
onMoveDown,
canMoveUp,
canMoveDown,
}) => {
return (
<div
className={`border border-gray-300 rounded p-2 mb-2 bg-white transition-all duration-200 ${
isSelected ? 'border-2 border-blue-500 shadow-[0_0_0_2px_rgba(24,144,255,0.2)]' : 'hover:border-blue-300'
}`}
onClick={onSelect}
>
<div className="flex justify-between items-center mb-2 pb-2 border-b border-gray-100">
<Text strong></Text>
<div className="flex gap-1">
<Button
type="text"
icon={<MenuOutlined />}
size="small"
style={{ cursor: 'move' }}
/>
<Button
type="text"
icon={<ArrowUpOutlined />}
onClick={(e) => {
e.stopPropagation();
if (onMoveUp) onMoveUp();
}}
size="small"
disabled={!canMoveUp}
/>
<Button
type="text"
icon={<ArrowDownOutlined />}
onClick={(e) => {
e.stopPropagation();
if (onMoveDown) onMoveDown();
}}
size="small"
disabled={!canMoveDown}
/>
<Button
type="text"
icon={<DeleteOutlined />}
onClick={(e) => {
e.stopPropagation();
onDelete();
}}
danger
size="small"
/>
</div>
</div>
<div className="py-2 text-center flex flex-col gap-2.5">
<div className="flex justify-around w-[70%] font-bold text-lg leading-8 border-b border-black py-0 px-2.5 my-0 mx-auto">
{config.showDealerName && (
<span className="px-2.5 min-w-[80px] text-center inline-block">{config.dealerName}</span>
)}
{config.showWatermelonGrade && (
<span className="px-2.5 min-w-[80px] text-center inline-block">{config.watermelonGrade}</span>
)}
{config.showDestination && (
<span className="px-2.5 min-w-[80px] text-center inline-block">{config.destination}</span>
)}
{config.showVehicleNumber && (
<span className="px-2.5 min-w-[80px] text-center inline-block">{config.vehicleNumber}</span>
)}
</div>
</div>
</div>
);
};
export default DealerInfoModule;

View File

@ -0,0 +1,112 @@
import React from 'react';
import { Card, List, Typography } from 'antd';
import {
FileTextOutlined,
ShopOutlined,
EnvironmentOutlined,
InboxOutlined,
TableOutlined,
CarOutlined,
DollarOutlined,
InfoCircleOutlined,
} from '@ant-design/icons';
const { Text } = Typography;
interface ModuleLibraryItem {
type: string;
name: string;
icon: React.ReactNode;
description: string;
}
interface ModuleLibraryProps {
onModuleAdd: (moduleType: string) => void;
}
const moduleLibrary: ModuleLibraryItem[] = [
{
type: 'title',
name: '发货单标题',
icon: React.createElement(FileTextOutlined),
description: '发货单主标题',
},
{
type: 'dealerInfo',
name: '经销商信息',
icon: React.createElement(ShopOutlined),
description: '经销商名称、车次等信息',
},
{
type: 'shippingInfo',
name: '发货地信息',
icon: React.createElement(EnvironmentOutlined),
description: '发货地、日期等信息',
},
{
type: 'weightInfo',
name: '重量金额信息',
icon: React.createElement(InboxOutlined),
description: '毛重、净重、单价、金额',
},
{
type: 'packingSpec',
name: '装箱规格表格',
icon: React.createElement(TableOutlined),
description: '箱号、数量、单价表格',
},
{
type: 'vehicleInfo',
name: '车辆信息',
icon: React.createElement(CarOutlined),
description: '司机、车牌、运费信息',
},
{
type: 'otherFees',
name: '其他费用明细',
icon: React.createElement(DollarOutlined),
description: '商标、人工、纸箱等费用',
},
{
type: 'totalAmount',
name: '合计金额',
icon: React.createElement(DollarOutlined),
description: '总金额显示',
},
{
type: 'otherInfo',
name: '其他信息',
icon: React.createElement(InfoCircleOutlined),
description: '产地、供应商等信息',
},
];
const ModuleLibrary: React.FC<ModuleLibraryProps> = ({ onModuleAdd }) => {
return (
<Card title="模块库" size="small" className="bg-white rounded-lg p-4 h-full">
<List
dataSource={moduleLibrary}
renderItem={(module) => (
<div
className="flex items-center p-2 cursor-pointer border border-gray-300 rounded hover:border-blue-500 hover:bg-blue-50 transition-all duration-300 mb-2"
onClick={() => onModuleAdd(module.type)}
>
<div className="text-blue-500 text-lg mr-2">{module.icon}</div>
<div>
<div>
<Text strong>{module.name}</Text>
</div>
<div>
<Text type="secondary" style={{ fontSize: '12px' }}>
{module.description}
</Text>
</div>
</div>
</div>
)}
/>
</Card>
);
};
export default ModuleLibrary;

View File

@ -0,0 +1,108 @@
import React from 'react';
import { Button, Typography } from 'antd';
import {
MenuOutlined,
ArrowUpOutlined,
ArrowDownOutlined,
DeleteOutlined
} from '@ant-design/icons';
const { Text } = Typography;
interface ModuleProps {
config: any;
isSelected: boolean;
onSelect: () => void;
onDelete: () => void;
onMoveUp?: () => void;
onMoveDown?: () => void;
canMoveUp?: boolean;
canMoveDown?: boolean;
}
const OtherFeesModule: React.FC<ModuleProps> = ({
config,
isSelected,
onSelect,
onDelete,
onMoveUp,
onMoveDown,
canMoveUp,
canMoveDown,
}) => {
const feeLabels = {
trademark: '商标',
labor: '人工',
paperBox: '纸箱',
fee: '费用',
codingFee: '打码费',
} as any;
return (
<div
className={`border border-gray-300 rounded p-2 mb-2 bg-white transition-all duration-200 ${
isSelected ? 'border-2 border-blue-500 shadow-[0_0_0_2px_rgba(24,144,255,0.2)]' : 'hover:border-blue-300'
}`}
onClick={onSelect}
>
<div className="flex justify-between items-center mb-2 pb-2 border-b border-gray-100">
<Text strong></Text>
<div className="flex gap-1">
<Button
type="text"
icon={<MenuOutlined />}
size="small"
style={{ cursor: 'move' }}
/>
<Button
type="text"
icon={<ArrowUpOutlined />}
onClick={(e) => {
e.stopPropagation();
if (onMoveUp) onMoveUp();
}}
size="small"
disabled={!canMoveUp}
/>
<Button
type="text"
icon={<ArrowDownOutlined />}
onClick={(e) => {
e.stopPropagation();
if (onMoveDown) onMoveDown();
}}
size="small"
disabled={!canMoveDown}
/>
<Button
type="text"
icon={<DeleteOutlined />}
onClick={(e) => {
e.stopPropagation();
onDelete();
}}
danger
size="small"
/>
</div>
</div>
<div className="py-2 text-center flex flex-col gap-2.5">
<div
className="grid grid-cols-4 gap-2.5 text-base leading-4 my-0 mx-auto"
>
{config.feeItems.map((feeType: any) => (
<div key={feeType} className="flex items-center mb-2">
<span className="min-w-[60px] text-center py-0 px-1.5">{feeLabels[feeType]}:</span>
<div className="flex min-w-[90px] py-0 px-2.5 items-center justify-around border-b border-black">
<span className="text-center inline-block">{config[feeType]}</span>
<span className="inline-block"></span>
</div>
</div>
))}
</div>
</div>
</div>
);
};
export default OtherFeesModule;

View File

@ -0,0 +1,145 @@
import React from 'react';
import { Button, Typography } from 'antd';
import {
MenuOutlined,
ArrowUpOutlined,
ArrowDownOutlined,
DeleteOutlined
} from '@ant-design/icons';
const { Text } = Typography;
interface ModuleProps {
config: any;
isSelected: boolean;
onSelect: () => void;
onDelete: () => void;
onMoveUp?: () => void;
onMoveDown?: () => void;
canMoveUp?: boolean;
canMoveDown?: boolean;
}
const OtherInfoModule: React.FC<ModuleProps> = ({
config,
isSelected,
onSelect,
onDelete,
onMoveUp,
onMoveDown,
canMoveUp,
canMoveDown,
}) => {
return (
<div
className={`border border-gray-300 rounded p-2 mb-2 bg-white transition-all duration-200 ${
isSelected ? 'border-2 border-blue-500 shadow-[0_0_0_2px_rgba(24,144,255,0.2)]' : 'hover:border-blue-300'
}`}
onClick={onSelect}
>
<div className="flex justify-between items-center mb-2 pb-2 border-b border-gray-100">
<Text strong></Text>
<div className="flex gap-1">
<Button
type="text"
icon={<MenuOutlined />}
size="small"
style={{ cursor: 'move' }}
/>
<Button
type="text"
icon={<ArrowUpOutlined />}
onClick={(e) => {
e.stopPropagation();
if (onMoveUp) onMoveUp();
}}
size="small"
disabled={!canMoveUp}
/>
<Button
type="text"
icon={<ArrowDownOutlined />}
onClick={(e) => {
e.stopPropagation();
if (onMoveDown) onMoveDown();
}}
size="small"
disabled={!canMoveDown}
/>
<Button
type="text"
icon={<DeleteOutlined />}
onClick={(e) => {
e.stopPropagation();
onDelete();
}}
danger
size="small"
/>
</div>
</div>
<div className="py-2 flex flex-col gap-2.5">
<table className="w-1/2 border-collapse text-sm leading-4">
<tbody>
<tr>
<td className="font-bold p-2 text-left border border-black w-1/3">:</td>
<td colSpan={2} className="p-2 text-left border border-black">175</td>
</tr>
<tr>
<td className="font-bold p-2 text-left border border-black w-1/3">:</td>
<td colSpan={2} className="p-2 text-left border border-black"></td>
</tr>
<tr>
<td className="font-bold p-2 text-left border border-black w-1/3"></td>
<td colSpan={2} className="p-2 text-left border border-black"></td>
</tr>
<tr>
<td className="font-bold p-2 text-left border border-black w-1/3">:</td>
<td colSpan={2} className="p-2 text-left border border-black"></td>
</tr>
<tr>
<td className="font-bold p-2 text-left border border-black w-1/3">:</td>
<td colSpan={2} className="p-2 text-left border border-black">98</td>
</tr>
<tr>
<td className="font-bold p-2 text-left border border-black w-1/3">:</td>
<td colSpan={2} className="p-2 text-left border border-black">99</td>
</tr>
<tr>
<td className="font-bold p-2 text-left border border-black w-1/3">:</td>
<td colSpan={2} className="p-2 text-left border border-black">A级-</td>
</tr>
<tr>
<td className="font-bold p-2 text-left border border-black w-1/3">:</td>
<td colSpan={2} className="p-2 text-left border border-black"></td>
</tr>
<tr>
<td className="font-bold p-2 text-left border border-black w-1/3"></td>
<td colSpan={2} className="p-2 text-left border border-black">3/</td>
</tr>
<tr>
<td className="font-bold p-2 text-left border border-black w-1/3"></td>
<td className="p-2 text-left border border-black">29062</td>
<td className="p-2 text-left border border-black"></td>
</tr>
<tr>
<td className="font-bold p-2 text-left border border-black w-1/3"></td>
<td className="p-2 text-left border border-black">688</td>
<td className="p-2 text-left border border-black"></td>
</tr>
<tr>
<td className="font-bold p-2 text-left border border-black w-1/3">:</td>
<td colSpan={2} className="p-2 text-left border border-black">L80367</td>
</tr>
<tr>
<td className="font-bold p-2 text-left border border-black w-1/3">:</td>
<td colSpan={2} className="p-2 text-left border border-black">15849849656</td>
</tr>
</tbody>
</table>
</div>
</div>
);
};
export default OtherInfoModule;

View File

@ -0,0 +1,118 @@
import React from 'react';
import { Button, Typography } from 'antd';
import {
MenuOutlined,
ArrowUpOutlined,
ArrowDownOutlined,
DeleteOutlined
} from '@ant-design/icons';
const { Text } = Typography;
interface ModuleProps {
config: any;
isSelected: boolean;
onSelect: () => void;
onDelete: () => void;
onMoveUp?: () => void;
onMoveDown?: () => void;
canMoveUp?: boolean;
canMoveDown?: boolean;
}
const PackingSpecModule: React.FC<ModuleProps> = ({
config,
isSelected,
onSelect,
onDelete,
onMoveUp,
onMoveDown,
canMoveUp,
canMoveDown,
}) => {
const columns = [];
if (config.showBoxCategory) columns.push('');
if (config.showBoxType) columns.push('箱号');
if (config.showQuantity) columns.push('数量');
if (config.showUnitPrice) columns.push('单价');
if (config.showAmount) columns.push('金额');
if (config.showUnitWeight) columns.push('单重');
if (config.showWeight) columns.push('重量');
return (
<div
className={`border border-gray-300 rounded p-2 mb-2 bg-white transition-all duration-200 ${
isSelected ? 'border-2 border-blue-500 shadow-[0_0_0_2px_rgba(24,144,255,0.2)]' : 'hover:border-blue-300'
}`}
onClick={onSelect}
>
<div className="flex justify-between items-center mb-2 pb-2 border-b border-gray-100">
<Text strong></Text>
<div className="flex gap-1">
<Button
type="text"
icon={<MenuOutlined />}
size="small"
style={{ cursor: 'move' }}
/>
<Button
type="text"
icon={<ArrowUpOutlined />}
onClick={(e) => {
e.stopPropagation();
if (onMoveUp) onMoveUp();
}}
size="small"
disabled={!canMoveUp}
/>
<Button
type="text"
icon={<ArrowDownOutlined />}
onClick={(e) => {
e.stopPropagation();
if (onMoveDown) onMoveDown();
}}
size="small"
disabled={!canMoveDown}
/>
<Button
type="text"
icon={<DeleteOutlined />}
onClick={(e) => {
e.stopPropagation();
onDelete();
}}
danger
size="small"
/>
</div>
</div>
<div className="py-2 flex flex-col gap-2.5">
<table className="w-full border-collapse text-lg leading-4">
<thead>
<tr>
{columns.map((col) => (
<th key={col} className="font-bold p-2 text-center">{col}</th>
))}
</tr>
</thead>
<tbody>
{config.data.map((row: any, index: any) => (
<tr key={index}>
{config.showBoxCategory && <td className="p-2 text-center border border-black">{row.boxCategory}</td>}
{config.showBoxType && <td className="p-2 text-center border border-black">{row.boxType}</td>}
{config.showQuantity && <td className="p-2 text-center border border-black">{row.quantity}</td>}
{config.showUnitPrice && <td className="p-2 text-center border border-black">{row.unitPrice}</td>}
{config.showAmount && <td className="p-2 text-center border border-black">{row.amount}</td>}
{config.showUnitWeight && <td className="p-2 text-center border border-black">{row.unitWeight}</td>}
{config.showWeight && <td className="p-2 text-center border border-black">{row.weight}</td>}
</tr>
))}
</tbody>
</table>
</div>
</div>
);
};
export default PackingSpecModule;

View File

@ -0,0 +1,164 @@
import React, { Suspense } from 'react';
import { Card, Empty } from 'antd';
import {
DragDropContext,
Droppable,
Draggable,
DropResult,
} from 'react-beautiful-dnd';
// 直接导入组件而不是使用 React.lazy
import TitleModule from './TitleModule';
import DealerInfoModule from './DealerInfoModule';
import ShippingInfoModule from './ShippingInfoModule';
import WeightInfoModule from './WeightInfoModule';
import PackingSpecModule from './PackingSpecModule';
import VehicleInfoModule from './VehicleInfoModule';
import OtherFeesModule from './OtherFeesModule';
import TotalAmountModule from './TotalAmountModule';
import OtherInfoModule from './OtherInfoModule';
interface PreviewCanvasProps {
modules: any[];
selectedModule: any;
onModuleSelect: (module: any) => void;
onModuleDelete: (moduleId: string) => void;
onModuleReorder: (newModules: any[]) => void;
}
const PreviewCanvas: React.FC<PreviewCanvasProps> = ({
modules,
selectedModule,
onModuleSelect,
onModuleDelete,
onModuleReorder,
}) => {
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 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];
return (
<Draggable
key={module.id}
draggableId={module.id}
index={index}
>
{(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',
}}
>
{Component ? <Component {...props} /> : null}
</div>
)}
</Draggable>
);
};
// 为 Droppable 组件创建一个不使用 memo 的包装组件
const DroppableWrapper = ({ children }: { children: React.ReactNode }) => (
<Droppable droppableId="modules">
{(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',
}}
>
{children}
{provided.placeholder}
</div>
)}
</Droppable>
);
return (
<Card
title="模板预览"
size="small"
className="bg-white rounded-lg p-4 min-h-[600px] border border-gray-300"
bodyStyle={{ maxHeight: 'calc(100vh - 200px)', overflowY: 'auto' }}
>
{modules.length === 0 ? (
<div className="empty-preview">
<Empty description="从左侧模块库点击添加模块" />
</div>
) : (
<DragDropContext onDragEnd={handleDragEnd}>
<DroppableWrapper>
{modules.map((module, index) => renderModule(module, index))}
</DroppableWrapper>
</DragDropContext>
)}
</Card>
);
};
export default PreviewCanvas;

View File

@ -0,0 +1,539 @@
import React from 'react';
import { Button, message } from 'antd';
import { PrinterOutlined } from '@ant-design/icons';
interface PrintPreviewProps {
modules: any[];
}
const PrintPreview: React.FC<PrintPreviewProps> = ({ modules }) => {
const handlePrint = () => {
// 创建一个用于打印的隐藏iframe
const printWindow = window.open('', '_blank');
if (printWindow) {
// 构建打印内容
let printContent = `
<html>
<head>
<title></title>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<style>
body {
font-family: Arial, sans-serif;
font-size: 18px;
width: 794px;
margin: 0 auto;
}
@media print {
@page {
size: A4;
margin: 0;
}
body {
width: auto;
margin: 0;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
.print-controls {
display: none !important;
}
}
.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;
}
#print-content {
min-height: 1123px;
padding: 20px;
box-sizing: border-box;
}
</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 id="print-content">
`;
// 添加模块内容到打印内容中
modules.forEach((module) => {
printContent += `
<div class="print-module">
`;
switch (module.type) {
case 'title':
printContent += `
<div class="py-2 text-center flex flex-col gap-2.5">
<div class="font-bold leading-8 text-center text-2xl">
${module.config.text}
</div>
</div>
`;
break;
case 'dealerInfo':
printContent += `
<div class="py-2 text-center flex flex-col gap-2.5">
<div class="flex justify-around w-[70%] font-bold text-lg leading-8 border-b border-black py-0 px-2.5 my-0 mx-auto">
`;
if (module.config.showDealerName) {
printContent += `
<span class="px-2.5 min-w-[80px] text-center inline-block">${module.config.dealerName}</span>
`;
}
if (module.config.showWatermelonGrade) {
printContent += `
<span class="px-2.5 min-w-[80px] text-center inline-block">${module.config.watermelonGrade}</span>
`;
}
if (module.config.showDestination) {
printContent += `
<span class="px-2.5 min-w-[80px] text-center inline-block">${module.config.destination}</span>
`;
}
if (module.config.showVehicleNumber) {
printContent += `
<span class="px-2.5 min-w-[80px] text-center inline-block">${module.config.vehicleNumber}</span>
`;
}
printContent += `
</div>
</div>
`;
break;
case 'shippingInfo':
printContent += `
<div class="py-2 text-center flex flex-col gap-2.5">
<div class="grid grid-cols-2 gap-2.5 text-base leading-4 my-0 mx-auto">
`;
if (module.config.showShippingFrom) {
printContent += `
<div class="flex items-center mb-2">
<span class="min-w-[120px] text-center py-0 px-1.5">:</span>
<div class="flex min-w-[180px] py-0 px-2.5 items-center justify-around border-b border-black">
<span class="text-center inline-block">${module.config.shippingFrom}</span>
</div>
</div>
`;
}
if (module.config.showDate) {
printContent += `
<div class="flex items-center mb-2">
<span class="min-w-[120px] text-center py-0 px-1.5">:</span>
<div class="flex min-w-[180px] py-0 px-2.5 items-center justify-around border-b border-black">
<span class="text-center inline-block">${module.config.date}</span>
</div>
</div>
`;
}
printContent += `
</div>
</div>
`;
break;
case 'weightInfo':
printContent += `
<div class="py-2 text-center flex flex-col gap-2.5">
<div class="grid grid-cols-2 gap-2.5 text-base leading-4 my-0 mx-auto">
`;
if (module.config.showGrossWeight) {
printContent += `
<div class="flex items-center mb-2">
<span class="min-w-[120px] text-center py-0 px-1.5">:</span>
<div class="flex min-w-[180px] py-0 px-2.5 items-center justify-around border-b border-black">
<span class="text-center inline-block">${module.config.grossWeight}</span>
<span class="inline-block"></span>
</div>
</div>
`;
}
if (module.config.showBoxWeight) {
printContent += `
<div class="flex items-center mb-2">
<span class="min-w-[120px] text-center py-0 px-1.5">:</span>
<div class="flex min-w-[180px] py-0 px-2.5 items-center justify-around border-b border-black">
<span class="text-center inline-block">${module.config.boxWeight}</span>
<span class="inline-block"></span>
</div>
</div>
`;
}
if (module.config.showNetWeight) {
printContent += `
<div class="flex items-center mb-2">
<span class="min-w-[120px] text-center py-0 px-1.5">:</span>
<div class="flex min-w-[180px] py-0 px-2.5 items-center justify-around border-b border-black">
<span class="text-center inline-block">${module.config.netWeight}</span>
<span class="inline-block"></span>
</div>
</div>
`;
}
if (module.config.showUnitPrice) {
printContent += `
<div class="flex items-center mb-2">
<span class="min-w-[120px] text-center py-0 px-1.5">:</span>
<div class="flex min-w-[180px] py-0 px-2.5 items-center justify-around border-b border-black">
<span class="text-center inline-block">${module.config.unitPrice}</span>
<span class="inline-block">/</span>
</div>
</div>
`;
}
if (module.config.showAmount) {
printContent += `
<div class="flex items-center mb-2">
<span class="min-w-[120px] text-center py-0 px-1.5">:</span>
<div class="flex min-w-[180px] py-0 px-2.5 items-center justify-around border-b border-black">
<span class="text-center inline-block">${module.config.amount}</span>
<span class="inline-block"></span>
</div>
</div>
`;
}
if (module.config.showGrade) {
printContent += `
<div class="flex items-center mb-2">
<span class="min-w-[120px] text-center py-0 px-1.5">:</span>
<div class="flex min-w-[180px] py-0 px-2.5 items-center justify-around border-b border-black">
<span class="text-center inline-block">${module.config.grade}</span>
</div>
</div>
`;
}
if (module.config.showAccountCompany) {
printContent += `
<div class="flex items-center mb-2">
<span class="min-w-[120px] text-center py-0 px-1.5">:</span>
<div class="flex min-w-[180px] py-0 px-2.5 items-center justify-around border-b border-black">
<span class="text-center inline-block">${module.config.accountCompany}</span>
</div>
</div>
`;
}
printContent += `
</div>
</div>
`;
break;
case 'packingSpec': {
const columns = [];
if (module.config.showBoxCategory) columns.push('');
if (module.config.showBoxType) columns.push('箱号');
if (module.config.showQuantity) columns.push('数量');
if (module.config.showUnitPrice) columns.push('单价');
if (module.config.showAmount) columns.push('金额');
if (module.config.showUnitWeight) columns.push('单重');
if (module.config.showWeight) columns.push('重量');
printContent += `
<div class="py-2 text-center flex flex-col gap-2.5">
<table class="w-full border-collapse text-lg leading-4">
<thead>
<tr>
${columns.map((col) => `<td class="p-2 text-center">${col}</td>`).join('')}
</tr>
</thead>
<tbody class="border-2">
`;
module.config.data.forEach((row: any) => {
printContent += `<tr>`;
if (module.config.showBoxCategory)
printContent += `<td class="p-2 text-center border border-black">${row.boxCategory}</td>`;
if (module.config.showBoxType)
printContent += `<td class="p-2 text-center border border-black">${row.boxType}</td>`;
if (module.config.showQuantity)
printContent += `<td class="p-2 text-center border border-black">${row.quantity}</td>`;
if (module.config.showUnitPrice)
printContent += `<td class="p-2 text-center border border-black">${row.unitPrice}</td>`;
if (module.config.showAmount)
printContent += `<td class="p-2 text-center border border-black">${row.amount}</td>`;
if (module.config.showUnitWeight)
printContent += `<td class="p-2 text-center border border-black">${row.unitWeight}</td>`;
if (module.config.showWeight)
printContent += `<td class="p-2 text-center border border-black">${row.weight}</td>`;
printContent += `</tr>`;
});
printContent += `
</tbody>
</table>
</div>
`;
break;
}
case 'vehicleInfo':
printContent += `
<div class="py-2 text-center flex flex-col gap-2.5">
<div class="grid grid-cols-1 gap-2.5 text-base leading-4 my-0 mx-auto">
`;
if (module.config.showDriverPhone) {
printContent += `
<div class="flex items-center mb-2">
<span class="min-w-[120px] text-center py-0 px-1.5">:</span>
<div class="flex min-w-[480px] py-0 px-2.5 items-center justify-around border-b border-black">
<span class="text-center inline-block">${module.config.driverPhone}</span>
</div>
</div>
`;
}
if (module.config.showLicensePlate) {
printContent += `
<div class="flex items-center mb-2">
<span class="min-w-[120px] text-center py-0 px-1.5">:</span>
<div class="flex min-w-[480px] py-0 px-2.5 items-center justify-around border-b border-black">
<span class="text-center inline-block">${module.config.licensePlate}</span>
</div>
</div>
`;
}
if (module.config.showEstimatedArrivalTime) {
printContent += `
<div class="flex items-center mb-2">
<span class="min-w-[120px] text-center py-0 px-1.5">:</span>
<div class="flex min-w-[480px] py-0 px-2.5 items-center justify-around border-b border-black">
<span class="text-center inline-block">${module.config.estimatedArrivalTime}</span>
</div>
</div>
`;
}
printContent += `
</div>
<div class="grid grid-cols-2 gap-2.5 text-base leading-4 my-0 mx-auto">
`;
if (module.config.showFreightDebt) {
printContent += `
<div class="flex items-center mb-2">
<span class="min-w-[120px] text-center py-0 px-1.5">${module.config.freightDebtTitle || '运费欠'}:</span>
<div class="flex min-w-[180px] py-0 px-2.5 items-center justify-around border-b border-black">
<span class="text-center inline-block">${module.config.freightDebt}</span>
<span class="inline-block"></span>
</div>
</div>
`;
}
if (module.config.showRemarks) {
printContent += `
<div class="flex items-center mb-2">
<span class="min-w-[120px] text-center py-0 px-1.5">:</span>
<div class="flex min-w-[180px] py-0 px-2.5 items-center justify-around border-b border-black">
<span class="text-center inline-block">${module.config.remarks}</span>
</div>
</div>
`;
}
if (module.config.showSeller) {
printContent += `
<div class="flex items-center mb-2">
<span class="min-w-[120px] text-center py-0 px-1.5">:</span>
<div class="flex min-w-[180px] py-0 px-2.5 items-center justify-around border-b border-black">
<span class="text-center inline-block">${module.config.seller}</span>
</div>
</div>
`;
}
if (module.config.showStrawMatDebt) {
printContent += `
<div class="flex items-center mb-2">
<span class="min-w-[120px] text-center py-0 px-1.5">:</span>
<div class="flex min-w-[180px] py-0 px-2.5 items-center justify-around border-b border-black">
<span class="text-center inline-block">${module.config.strawMatDebt}</span>
<span class="inline-block"></span>
</div>
</div>
`;
}
printContent += `
</div>
</div>
`;
break;
case 'otherFees': {
printContent += `
<div class="py-2 text-center flex flex-col gap-2.5">
<div class="grid grid-cols-4 gap-2.5 text-base leading-4 my-0 mx-auto">
`;
const feeLabels = {
trademark: '商标',
labor: '人工',
paperBox: '纸箱',
fee: '费用',
codingFee: '打码费',
};
module.config.feeItems.forEach((feeType: string) => {
printContent += `
<div class="flex items-center mb-2">
<span class="min-w-[60px] text-center py-0 px-1.5">${feeLabels[feeType as keyof typeof feeLabels]}:</span>
<div class="flex min-w-[90px] py-0 px-2.5 items-center justify-around border-b border-black">
<span class="text-center inline-block">${module.config[feeType]}</span>
<span class="inline-block"></span>
</div>
</div>
`;
});
printContent += `
</div>
</div>
`;
break;
}
case 'totalAmount':
printContent += `
<div class="py-2 text-center flex flex-col gap-2.5">
<div class="grid grid-cols-2 gap-2.5 text-base leading-4 my-0 mx-auto">
<div class="flex items-center mb-2">
<span class="min-w-[120px] text-center py-0 px-1.5">${module.config.sumTitle}:</span>
<div class="flex min-w-[180px] py-0 px-2.5 items-center justify-around border-b border-black">
<span class="text-center inline-block">${module.config.amount}</span>
<span class="inline-block"></span>
</div>
</div>
${module.config.showFarmer ? `<div class="flex items-center mb-2">
<span class="min-w-[120px] text-center py-0 px-1.5">:</span>
<div class="flex min-w-[180px] py-0 px-2.5 items-center justify-around border-b border-black">
<span class="text-center inline-block">${module.config.farmer}</span>
</div>
</div>` : ''}
</div>
</div>
`;
break;
case 'otherInfo':
printContent += `
<div class="py-2 text-center flex flex-col gap-2.5">
<table class="w-1/2 border-collapse text-sm leading-4">
<tbody>
<tr>
<td class="font-bold p-2 text-left border border-black w-1/3">:</td>
<td colSpan="2" class="p-2 text-left border border-black">175</td>
</tr>
<tr>
<td class="font-bold p-2 text-left border border-black w-1/3">:</td>
<td colSpan="2" class="p-2 text-left border border-black"></td>
</tr>
<tr>
<td class="font-bold p-2 text-left border border-black w-1/3"></td>
<td colSpan="2" class="p-2 text-left border border-black"></td>
</tr>
<tr>
<td class="font-bold p-2 text-left border border-black w-1/3">:</td>
<td colSpan="2" class="p-2 text-left border border-black"></td>
</tr>
<tr>
<td class="font-bold p-2 text-left border border-black w-1/3">:</td>
<td colSpan="2" class="p-2 text-left border border-black">98</td>
</tr>
<tr>
<td class="font-bold p-2 text-left border border-black w-1/3">:</td>
<td colSpan="2" class="p-2 text-left border border-black">99</td>
</tr>
<tr>
<td class="font-bold p-2 text-left border border-black w-1/3">:</td>
<td colSpan="2" class="p-2 text-left border border-black">A级-</td>
</tr>
<tr>
<td class="font-bold p-2 text-left border border-black w-1/3">:</td>
<td colSpan="2" class="p-2 text-left border border-black"></td>
</tr>
<tr>
<td class="font-bold p-2 text-left border border-black w-1/3"></td>
<td colSpan="2" class="p-2 text-left border border-black">3/</td>
</tr>
<tr>
<td class="font-bold p-2 text-left border border-black w-1/3"></td>
<td class="p-2 text-left border border-black">29062</td>
<td class="p-2 text-left border border-black"></td>
</tr>
<tr>
<td class="font-bold p-2 text-left border border-black w-1/3"></td>
<td class="p-2 text-left border border-black">688</td>
<td class="p-2 text-left border border-black"></td>
</tr>
<tr>
<td class="font-bold p-2 text-left border border-black w-1/3">:</td>
<td colSpan="2" class="p-2 text-left border border-black">L80367</td>
</tr>
<tr>
<td class="font-bold p-2 text-left border border-black w-1/3">:</td>
<td colSpan="2" class="p-2 text-left border border-black">15849849656</td>
</tr>
</tbody>
</table>
</div>
`;
break;
default:
printContent += `
<div>未支持的模块类型: ${module.type}</div>
`;
}
printContent += `
</div>
`;
});
printContent += `
</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';
}
};
</script>
</body>
</html>
`;
// 写入内容到打印窗口
printWindow.document.write(printContent);
printWindow.document.close();
} else {
message.error('无法打开打印预览窗口,请检查浏览器设置');
}
};
return (
<Button icon={<PrinterOutlined />} onClick={handlePrint}>
</Button>
);
};
export default PrintPreview;

View File

@ -0,0 +1,105 @@
import React from 'react';
import { Button, Typography } from 'antd';
import {
MenuOutlined,
ArrowUpOutlined,
ArrowDownOutlined,
DeleteOutlined
} from '@ant-design/icons';
const { Text } = Typography;
interface ModuleProps {
config: any;
isSelected: boolean;
onSelect: () => void;
onDelete: () => void;
onMoveUp?: () => void;
onMoveDown?: () => void;
canMoveUp?: boolean;
canMoveDown?: boolean;
}
const ShippingInfoModule: React.FC<ModuleProps> = ({
config,
isSelected,
onSelect,
onDelete,
onMoveUp,
onMoveDown,
canMoveUp,
canMoveDown,
}) => {
return (
<div
className={`border border-gray-300 rounded p-2 mb-2 bg-white transition-all duration-200 ${
isSelected ? 'border-2 border-blue-500 shadow-[0_0_0_2px_rgba(24,144,255,0.2)]' : 'hover:border-blue-300'
}`}
onClick={onSelect}
>
<div className="flex justify-between items-center mb-2 pb-2 border-b border-gray-100">
<Text strong></Text>
<div className="flex gap-1">
<Button
type="text"
icon={<MenuOutlined />}
size="small"
style={{ cursor: 'move' }}
/>
<Button
type="text"
icon={<ArrowUpOutlined />}
onClick={(e) => {
e.stopPropagation();
if (onMoveUp) onMoveUp();
}}
size="small"
disabled={!canMoveUp}
/>
<Button
type="text"
icon={<ArrowDownOutlined />}
onClick={(e) => {
e.stopPropagation();
if (onMoveDown) onMoveDown();
}}
size="small"
disabled={!canMoveDown}
/>
<Button
type="text"
icon={<DeleteOutlined />}
onClick={(e) => {
e.stopPropagation();
onDelete();
}}
danger
size="small"
/>
</div>
</div>
<div className="py-2 text-center flex flex-col gap-2.5">
<div className="grid grid-cols-2 gap-2.5 text-base leading-4 my-0 mx-auto">
{config.showShippingFrom && (
<div className="flex items-center mb-2">
<span className="min-w-[120px] text-center py-0 px-1.5">:</span>
<div className="flex min-w-[180px] py-0 px-2.5 items-center justify-around border-b border-black">
<span className="text-center inline-block">{config.shippingFrom}</span>
</div>
</div>
)}
{config.showDate && (
<div className="flex items-center mb-2">
<span className="min-w-[120px] text-center py-0 px-1.5">:</span>
<div className="flex min-w-[180px] py-0 px-2.5 items-center justify-around border-b border-black">
<span className="text-center inline-block">{config.date}</span>
</div>
</div>
)}
</div>
</div>
</div>
);
};
export default ShippingInfoModule;

View File

@ -0,0 +1,91 @@
import React from 'react';
import { Button, Typography } from 'antd';
import {
MenuOutlined,
ArrowUpOutlined,
ArrowDownOutlined,
DeleteOutlined
} from '@ant-design/icons';
const { Text } = Typography;
interface ModuleProps {
config: any;
isSelected: boolean;
onSelect: () => void;
onDelete: () => void;
onMoveUp?: () => void;
onMoveDown?: () => void;
canMoveUp?: boolean;
canMoveDown?: boolean;
}
const TitleModule: React.FC<ModuleProps> = ({
config,
isSelected,
onSelect,
onDelete,
onMoveUp,
onMoveDown,
canMoveUp,
canMoveDown,
}) => {
return (
<div
className={`border border-gray-300 rounded p-2 mb-2 bg-white transition-all duration-200 ${
isSelected ? 'border-2 border-blue-500 shadow-[0_0_0_2px_rgba(24,144,255,0.2)]' : 'hover:border-blue-300'
}`}
onClick={onSelect}
>
<div className="flex justify-between items-center mb-2 pb-2 border-b border-gray-100">
<Text strong></Text>
<div className="flex gap-1">
<Button
type="text"
icon={<MenuOutlined />}
size="small"
style={{ cursor: 'move' }}
/>
<Button
type="text"
icon={<ArrowUpOutlined />}
onClick={(e) => {
e.stopPropagation();
if (onMoveUp) onMoveUp();
}}
size="small"
disabled={!canMoveUp}
/>
<Button
type="text"
icon={<ArrowDownOutlined />}
onClick={(e) => {
e.stopPropagation();
if (onMoveDown) onMoveDown();
}}
size="small"
disabled={!canMoveDown}
/>
<Button
type="text"
icon={<DeleteOutlined />}
onClick={(e) => {
e.stopPropagation();
onDelete();
}}
danger
size="small"
/>
</div>
</div>
<div className="py-2">
<div className={`font-bold leading-8 text-center text-2xl`}>
{config.text}
</div>
</div>
</div>
);
};
export default TitleModule;

View File

@ -0,0 +1,104 @@
import React from 'react';
import { Button, Typography } from 'antd';
import {
MenuOutlined,
ArrowUpOutlined,
ArrowDownOutlined,
DeleteOutlined
} from '@ant-design/icons';
const { Text } = Typography;
interface ModuleProps {
config: any;
isSelected: boolean;
onSelect: () => void;
onDelete: () => void;
onMoveUp?: () => void;
onMoveDown?: () => void;
canMoveUp?: boolean;
canMoveDown?: boolean;
}
const TotalAmountModule: React.FC<ModuleProps> = ({
config,
isSelected,
onSelect,
onDelete,
onMoveUp,
onMoveDown,
canMoveUp,
canMoveDown,
}) => {
return (
<div
className={`border border-gray-300 rounded p-2 mb-2 bg-white transition-all duration-200 ${
isSelected ? 'border-2 border-blue-500 shadow-[0_0_0_2px_rgba(24,144,255,0.2)]' : 'hover:border-blue-300'
}`}
onClick={onSelect}
>
<div className="flex justify-between items-center mb-2 pb-2 border-b border-gray-100">
<Text strong></Text>
<div className="flex gap-1">
<Button
type="text"
icon={<MenuOutlined />}
size="small"
style={{ cursor: 'move' }}
/>
<Button
type="text"
icon={<ArrowUpOutlined />}
onClick={(e) => {
e.stopPropagation();
if (onMoveUp) onMoveUp();
}}
size="small"
disabled={!canMoveUp}
/>
<Button
type="text"
icon={<ArrowDownOutlined />}
onClick={(e) => {
e.stopPropagation();
if (onMoveDown) onMoveDown();
}}
size="small"
disabled={!canMoveDown}
/>
<Button
type="text"
icon={<DeleteOutlined />}
onClick={(e) => {
e.stopPropagation();
onDelete();
}}
danger
size="small"
/>
</div>
</div>
<div className="py-2 text-center flex flex-col gap-2.5">
<div className="grid grid-cols-2 gap-2.5 text-base leading-4 my-0 mx-auto">
<div className="flex items-center mb-2">
<span className="min-w-[120px] text-center py-0 px-1.5">{config.sumTitle}:</span>
<div className="flex min-w-[180px] py-0 px-2.5 items-center justify-around border-b border-black">
<span className="text-center inline-block">{config.amount}</span>
<span className="inline-block"></span>
</div>
</div>
{config.showFarmer && (
<div className="flex items-center mb-2">
<span className="min-w-[120px] text-center py-0 px-1.5">:</span>
<div className="flex min-w-[180px] py-0 px-2.5 items-center justify-around border-b border-black">
<span className="text-center inline-block">{config.farmer}</span>
</div>
</div>
)}
</div>
</div>
</div>
);
};
export default TotalAmountModule;

View File

@ -0,0 +1,179 @@
import {
ArrowDownOutlined,
ArrowUpOutlined,
DeleteOutlined,
MenuOutlined,
} from '@ant-design/icons';
import { Button, Typography } from 'antd';
import React from 'react';
const { Text } = Typography;
interface ModuleProps {
config: any;
isSelected: boolean;
onSelect: () => void;
onDelete: () => void;
onMoveUp?: () => void;
onMoveDown?: () => void;
canMoveUp?: boolean;
canMoveDown?: boolean;
}
const VehicleInfoModule: React.FC<ModuleProps> = ({
config,
isSelected,
onSelect,
onDelete,
onMoveUp,
onMoveDown,
canMoveUp,
canMoveDown,
}) => {
return (
<div
className={`border border-gray-300 rounded p-2 mb-2 bg-white transition-all duration-200 ${
isSelected
? 'border-2 border-blue-500 shadow-[0_0_0_2px_rgba(24,144,255,0.2)]'
: 'hover:border-blue-300'
}`}
onClick={onSelect}
>
<div className="flex justify-between items-center mb-2 pb-2 border-b border-gray-100">
<Text strong></Text>
<div className="flex gap-1">
<Button
type="text"
icon={<MenuOutlined />}
size="small"
style={{ cursor: 'move' }}
/>
<Button
type="text"
icon={<ArrowUpOutlined />}
onClick={(e) => {
e.stopPropagation();
if (onMoveUp) onMoveUp();
}}
size="small"
disabled={!canMoveUp}
/>
<Button
type="text"
icon={<ArrowDownOutlined />}
onClick={(e) => {
e.stopPropagation();
if (onMoveDown) onMoveDown();
}}
size="small"
disabled={!canMoveDown}
/>
<Button
type="text"
icon={<DeleteOutlined />}
onClick={(e) => {
e.stopPropagation();
onDelete();
}}
danger
size="small"
/>
</div>
</div>
<div className="py-2 text-center flex flex-col gap-2.5">
<div className="grid grid-cols-1 gap-2.5 text-base leading-4 my-0 mx-auto">
{config.showDriverPhone && (
<div className="flex items-center mb-2">
<span className="min-w-[120px] text-center py-0 px-1.5">
:
</span>
<div className="flex min-w-[480px] py-0 px-2.5 items-center justify-around border-b border-black">
<span className="text-center inline-block">
{config.driverPhone}
</span>
</div>
</div>
)}
{config.showLicensePlate && (
<div className="flex items-center mb-2">
<span className="min-w-[120px] text-center py-0 px-1.5">
:
</span>
<div className="flex min-w-[480px] py-0 px-2.5 items-center justify-around border-b border-black">
<span className="text-center inline-block">
{config.licensePlate}
</span>
</div>
</div>
)}
{config.showEstimatedArrivalTime && (
<div className="flex items-center mb-2">
<span className="min-w-[120px] text-center py-0 px-1.5">
:
</span>
<div className="flex min-w-[480px] py-0 px-2.5 items-center justify-around border-b border-black">
<span className="text-center inline-block">
{config.estimatedArrivalTime}
</span>
</div>
</div>
)}
</div>
<div className="grid grid-cols-2 gap-2.5 text-base leading-4 my-0 mx-auto">
{config.showFreightDebt && (
<div className="flex items-center mb-2">
<span className="min-w-[120px] text-center py-0 px-1.5">
{config.freightDebtTitle || '运费欠'}:
</span>
<div className="flex min-w-[180px] py-0 px-2.5 items-center justify-around border-b border-black">
<span className="text-center inline-block">
{config.freightDebt}
</span>
<span className="inline-block"></span>
</div>
</div>
)}
{config.showRemarks && (
<div className="flex items-center mb-2">
<span className="min-w-[120px] text-center py-0 px-1.5">
:
</span>
<div className="flex min-w-[180px] py-0 px-2.5 items-center justify-around border-b border-black">
<span className="text-center inline-block">
{config.remarks}
</span>
</div>
</div>
)}
{config.showSeller && (
<div className="flex items-center mb-2">
<span className="min-w-[120px] text-center py-0 px-1.5">
:
</span>
<div className="flex min-w-[180px] py-0 px-2.5 items-center justify-around border-b border-black">
<span className="text-center inline-block">
{config.seller}
</span>
</div>
</div>
)}
{config.showStrawMatDebt && (
<div className="flex items-center mb-2">
<span className="min-w-[120px] text-center py-0 px-1.5">
:
</span>
<div className="flex min-w-[180px] py-0 px-2.5 items-center justify-around border-b border-black">
<span className="text-center inline-block">
{config.strawMatDebt}
</span>
<span className="inline-block"></span>
</div>
</div>
)}
</div>
</div>
</div>
);
};
export default VehicleInfoModule;

View File

@ -0,0 +1,150 @@
import React from 'react';
import { Button, Typography } from 'antd';
import {
MenuOutlined,
ArrowUpOutlined,
ArrowDownOutlined,
DeleteOutlined
} from '@ant-design/icons';
const { Text } = Typography;
interface ModuleProps {
config: any;
isSelected: boolean;
onSelect: () => void;
onDelete: () => void;
onMoveUp?: () => void;
onMoveDown?: () => void;
canMoveUp?: boolean;
canMoveDown?: boolean;
}
const WeightInfoModule: React.FC<ModuleProps> = ({
config,
isSelected,
onSelect,
onDelete,
onMoveUp,
onMoveDown,
canMoveUp,
canMoveDown,
}) => {
return (
<div
className={`border border-gray-300 rounded p-2 mb-2 bg-white transition-all duration-200 ${
isSelected ? 'border-2 border-blue-500 shadow-[0_0_0_2px_rgba(24,144,255,0.2)]' : 'hover:border-blue-300'
}`}
onClick={onSelect}
>
<div className="flex justify-between items-center mb-2 pb-2 border-b border-gray-100">
<Text strong></Text>
<div className="flex gap-1">
<Button
type="text"
icon={<MenuOutlined />}
size="small"
style={{ cursor: 'move' }}
/>
<Button
type="text"
icon={<ArrowUpOutlined />}
onClick={(e) => {
e.stopPropagation();
if (onMoveUp) onMoveUp();
}}
size="small"
disabled={!canMoveUp}
/>
<Button
type="text"
icon={<ArrowDownOutlined />}
onClick={(e) => {
e.stopPropagation();
if (onMoveDown) onMoveDown();
}}
size="small"
disabled={!canMoveDown}
/>
<Button
type="text"
icon={<DeleteOutlined />}
onClick={(e) => {
e.stopPropagation();
onDelete();
}}
danger
size="small"
/>
</div>
</div>
<div className="py-2 text-center flex flex-col gap-2.5">
<div className="grid grid-cols-2 gap-2.5 text-base leading-4 my-0 mx-auto">
{config.showGrossWeight && (
<div className="flex items-center mb-2">
<span className="min-w-[120px] text-center py-0 px-1.5">:</span>
<div className="flex min-w-[180px] py-0 px-2.5 items-center justify-around border-b border-black">
<span className="text-center inline-block">{config.grossWeight}</span>
<span className="inline-block"></span>
</div>
</div>
)}
{config.showBoxWeight && (
<div className="flex items-center mb-2">
<span className="min-w-[120px] text-center py-0 px-1.5">:</span>
<div className="flex min-w-[180px] py-0 px-2.5 items-center justify-around border-b border-black">
<span className="text-center inline-block">{config.boxWeight}</span>
<span className="inline-block"></span>
</div>
</div>
)}
{config.showNetWeight && (
<div className="flex items-center mb-2">
<span className="min-w-[120px] text-center py-0 px-1.5">:</span>
<div className="flex min-w-[180px] py-0 px-2.5 items-center justify-around border-b border-black">
<span className="text-center inline-block">{config.netWeight}</span>
<span className="inline-block"></span>
</div>
</div>
)}
{config.showUnitPrice && (
<div className="flex items-center mb-2">
<span className="min-w-[120px] text-center py-0 px-1.5">:</span>
<div className="flex min-w-[180px] py-0 px-2.5 items-center justify-around border-b border-black">
<span className="text-center inline-block">{config.unitPrice}</span>
<span className="inline-block">/</span>
</div>
</div>
)}
{config.showAmount && (
<div className="flex items-center mb-2">
<span className="min-w-[120px] text-center py-0 px-1.5">:</span>
<div className="flex min-w-[180px] py-0 px-2.5 items-center justify-around border-b border-black">
<span className="text-center inline-block">{config.amount}</span>
<span className="inline-block"></span>
</div>
</div>
)}
{config.showGrade && (
<div className="flex items-center mb-2">
<span className="min-w-[120px] text-center py-0 px-1.5">:</span>
<div className="flex min-w-[180px] py-0 px-2.5 items-center justify-around border-b border-black">
<span className="text-center inline-block">{config.grade}</span>
</div>
</div>
)}
{config.showAccountCompany && (
<div className="flex items-center mb-2">
<span className="min-w-[120px] text-center py-0 px-1.5">:</span>
<div className="flex min-w-[180px] py-0 px-2.5 items-center justify-around border-b border-black">
<span className="text-center inline-block">{config.accountCompany}</span>
</div>
</div>
)}
</div>
</div>
</div>
);
};
export default WeightInfoModule;

View File

@ -0,0 +1,15 @@
export { default as ModuleLibrary } from './ModuleLibrary';
export { default as PreviewCanvas } from './PreviewCanvas';
export { default as ConfigPanel } from './ConfigPanel';
export { default as PrintPreview } from './PrintPreview';
// Individual module components
export { default as TitleModule } from './TitleModule';
export { default as DealerInfoModule } from './DealerInfoModule';
export { default as ShippingInfoModule } from './ShippingInfoModule';
export { default as WeightInfoModule } from './WeightInfoModule';
export { default as PackingSpecModule } from './PackingSpecModule';
export { default as VehicleInfoModule } from './VehicleInfoModule';
export { default as OtherFeesModule } from './OtherFeesModule';
export { default as TotalAmountModule } from './TotalAmountModule';
export { default as OtherInfoModule } from './OtherInfoModule';

View File

@ -18,3 +18,4 @@ export * from './Role';
export * from './Setting'; export * from './Setting';
export * from './User'; export * from './User';
export * from './Purchase'; export * from './Purchase';
export * from './DeliveryTemplate';

View File

@ -47,170 +47,3 @@ body::-webkit-scrollbar {
padding: 12px !important; padding: 12px !important;
} }
} }
// 发货单模板配置
.module-library {
background: white;
border-radius: 6px;
padding: 16px;
height: 100%;
}
.module-item {
padding: 12px;
border: 1px solid #d9d9d9;
border-radius: 6px;
margin-bottom: 8px;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
}
.module-item:hover {
border-color: #1890ff;
background-color: #f0f8ff;
}
.module-icon {
margin-right: 12px;
font-size: 16px;
color: #1890ff;
}
.preview-canvas {
background: white;
border-radius: 6px;
padding: 16px;
min-height: 600px;
border: 1px solid #d9d9d9;
}
.config-panel {
background: white;
border-radius: 6px;
padding: 16px;
height: 100%;
}
.preview-module {
margin-bottom: 16px;
border: 1px solid #d9d9d9;
border-radius: 6px;
overflow: hidden;
}
.preview-module.selected {
border: 2px solid #1890ff;
}
.module-header {
background: #fafafa;
padding: 8px 16px;
border-bottom: 1px solid #d9d9d9;
display: flex;
justify-content: space-between;
align-items: center;
}
.module-content {
padding: 16px;
}
.empty-preview {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 400px;
color: #bfbfbf;
}
.template-preview {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
.template-preview th, .template-preview td {
border: 1px solid #d9d9d9;
padding: 8px 12px;
text-align: left;
}
.template-preview th {
background-color: #fafafa;
font-weight: 500;
}
.ant-divider {
margin: 16px 0;
}
.config-section {
margin-bottom: 20px;
}
.config-section-title {
font-weight: 500;
margin-bottom: 12px;
color: #262626;
}
.field-row {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.field-label {
width: 100px;
color: #595959;
}
.field-control {
flex: 1;
}
.checkbox-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.checkbox-item {
display: flex;
align-items: center;
}
.template-title {
text-align: center;
margin: 10px 0;
font-weight: bold;
}
.template-title.large {
font-size: 20px;
}
.template-title.medium {
font-size: 18px;
}
.template-title.small {
font-size: 16px;
}
.template-title.left {
text-align: left;
}
.template-title.center {
text-align: center;
}
.template-title.right {
text-align: right;
}
.info-row {
display: flex;
margin-bottom: 8px;
}
.info-label {
width: 80px;
color: #595959;
}
.info-value {
flex: 1;
}
.weight-info {
display: flex;
flex-wrap: wrap;
gap: 20px;
}
.weight-item {
display: flex;
align-items: center;
}
.fee-list {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.fee-item {
display: flex;
align-items: center;
}

File diff suppressed because it is too large Load Diff