1. 新增合同打印预览功能,支持A4格式打印,包含合同完整信息和产品明细 2. 新增多个产品金额计算工具函数,统一管理产品税额、无税单价等字段计算 3. 重构产品内容组件,新增税率除数、无税单价、税额列,实现字段联动自动计算 4. 新增合同logo静态资源,优化表格布局和样式
436 lines
12 KiB
JavaScript
436 lines
12 KiB
JavaScript
/**
|
|
* productContent 工具类
|
|
* 用于处理合同产品内容的各种操作
|
|
*/
|
|
|
|
// 默认产品内容结构
|
|
const DEFAULT_PRODUCT_CONTENT = {
|
|
products: [],
|
|
productName: '',
|
|
remark: '',
|
|
totalQuantity: 0,
|
|
totalTaxTotal: 0,
|
|
totalAmountInWords: '零元整'
|
|
};
|
|
|
|
/**
|
|
* 解析 productContent JSON 字符串
|
|
* @param {string} jsonStr - JSON 字符串
|
|
* @returns {Object} 解析后的对象
|
|
*/
|
|
export function parseProductContent(jsonStr) {
|
|
if (!jsonStr) {
|
|
return { ...DEFAULT_PRODUCT_CONTENT };
|
|
}
|
|
|
|
try {
|
|
const data = JSON.parse(jsonStr);
|
|
return {
|
|
...DEFAULT_PRODUCT_CONTENT,
|
|
...data,
|
|
products: data.products || []
|
|
};
|
|
} catch (error) {
|
|
console.error('解析 productContent 失败:', error);
|
|
return { ...DEFAULT_PRODUCT_CONTENT };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 将对象转换为 productContent JSON 字符串
|
|
* @param {Object} data - 产品内容对象
|
|
* @returns {string} JSON 字符串
|
|
*/
|
|
export function stringifyProductContent(data) {
|
|
const content = {
|
|
...DEFAULT_PRODUCT_CONTENT,
|
|
...data
|
|
};
|
|
return JSON.stringify(content, null, 2);
|
|
}
|
|
|
|
/**
|
|
* 计算产品列表的总数量
|
|
* @param {Array} products - 产品列表
|
|
* @returns {number} 总数量
|
|
*/
|
|
export function calculateTotalQuantity(products) {
|
|
if (!products || !Array.isArray(products)) {
|
|
return 0;
|
|
}
|
|
return products.reduce((total, item) => {
|
|
return total + (parseFloat(item.quantity) || 0);
|
|
}, 0);
|
|
}
|
|
|
|
/**
|
|
* 计算产品列表的含税总额
|
|
* @param {Array} products - 产品列表
|
|
* @returns {number} 含税总额
|
|
*/
|
|
export function calculateTotalTaxTotal(products) {
|
|
if (!products || !Array.isArray(products)) {
|
|
return 0;
|
|
}
|
|
return products.reduce((total, item) => {
|
|
return total + (parseFloat(item.taxTotal) || 0);
|
|
}, 0);
|
|
}
|
|
|
|
/**
|
|
* 计算单个产品的含税总额
|
|
* @param {Object} product - 产品对象
|
|
* @returns {number} 含税总额
|
|
*/
|
|
export function calculateProductTaxTotal(product) {
|
|
if (!product) return 0;
|
|
const quantity = parseFloat(product.quantity) || 0;
|
|
const taxPrice = parseFloat(product.taxPrice) || 0;
|
|
return quantity * taxPrice;
|
|
}
|
|
|
|
/**
|
|
* 计算无税单价 = 含税单价 / 税率除数
|
|
* @param {Object} product - 产品对象
|
|
* @returns {number} 无税单价
|
|
*/
|
|
export function calculateProductNoTaxPrice(product) {
|
|
if (!product) return 0;
|
|
const taxPrice = parseFloat(product.taxPrice) || 0;
|
|
const taxDivisor = parseFloat(product.taxDivisor) || 1.13;
|
|
return taxPrice / taxDivisor;
|
|
}
|
|
|
|
/**
|
|
* 计算无税总额 = 数量 * 无税单价
|
|
* @param {Object} product - 产品对象
|
|
* @returns {number} 无税总额
|
|
*/
|
|
export function calculateProductNoTaxTotal(product) {
|
|
if (!product) return 0;
|
|
const quantity = parseFloat(product.quantity) || 0;
|
|
const noTaxPrice = parseFloat(product.noTaxPrice) || 0;
|
|
return quantity * noTaxPrice;
|
|
}
|
|
|
|
/**
|
|
* 计算税额 = 含税总额 - 无税总额
|
|
* @param {Object} product - 产品对象
|
|
* @returns {number} 税额
|
|
*/
|
|
export function calculateProductTaxAmount(product) {
|
|
if (!product) return 0;
|
|
const taxTotal = parseFloat(product.taxTotal) || 0;
|
|
const noTaxTotal = parseFloat(product.noTaxTotal) || 0;
|
|
return taxTotal - noTaxTotal;
|
|
}
|
|
|
|
/**
|
|
* 根据变更字段重新计算产品所有金额字段
|
|
* @param {Object} product - 产品对象
|
|
* @param {string} changedField - 变更的字段名
|
|
* @returns {Object} 更新后的产品对象
|
|
*/
|
|
export function calculateProductFields(product, changedField = 'quantity') {
|
|
if (!product) return product;
|
|
|
|
const quantity = parseFloat(product.quantity) || 0;
|
|
let taxPrice = parseFloat(product.taxPrice) || 0;
|
|
let taxDivisor = parseFloat(product.taxDivisor) || 1.13;
|
|
let noTaxPrice, noTaxTotal, taxTotal, taxAmount;
|
|
|
|
switch (changedField) {
|
|
case 'quantity':
|
|
case 'taxPrice':
|
|
taxTotal = quantity * taxPrice;
|
|
noTaxPrice = taxPrice / taxDivisor;
|
|
noTaxTotal = quantity * noTaxPrice;
|
|
taxAmount = taxTotal - noTaxTotal;
|
|
break;
|
|
case 'taxDivisor':
|
|
noTaxPrice = taxPrice / taxDivisor;
|
|
taxTotal = quantity * taxPrice;
|
|
noTaxTotal = quantity * noTaxPrice;
|
|
taxAmount = taxTotal - noTaxTotal;
|
|
break;
|
|
case 'noTaxPrice':
|
|
noTaxPrice = parseFloat(product.noTaxPrice) || 0;
|
|
taxPrice = noTaxPrice * taxDivisor;
|
|
taxTotal = quantity * taxPrice;
|
|
noTaxTotal = quantity * noTaxPrice;
|
|
taxAmount = taxTotal - noTaxTotal;
|
|
break;
|
|
case 'noTaxTotal':
|
|
noTaxTotal = parseFloat(product.noTaxTotal) || 0;
|
|
noTaxPrice = quantity > 0 ? noTaxTotal / quantity : 0;
|
|
taxPrice = noTaxPrice * taxDivisor;
|
|
taxTotal = quantity * taxPrice;
|
|
taxAmount = taxTotal - noTaxTotal;
|
|
break;
|
|
case 'taxTotal':
|
|
taxTotal = parseFloat(product.taxTotal) || 0;
|
|
taxPrice = quantity > 0 ? taxTotal / quantity : 0;
|
|
noTaxPrice = taxPrice / taxDivisor;
|
|
noTaxTotal = quantity * noTaxPrice;
|
|
taxAmount = taxTotal - noTaxTotal;
|
|
break;
|
|
case 'taxAmount':
|
|
taxAmount = parseFloat(product.taxAmount) || 0;
|
|
taxTotal = quantity * taxPrice;
|
|
noTaxTotal = taxTotal - taxAmount;
|
|
noTaxPrice = quantity > 0 ? noTaxTotal / quantity : 0;
|
|
taxPrice = quantity > 0 ? noTaxPrice * taxDivisor : 0;
|
|
taxTotal = quantity * taxPrice;
|
|
noTaxTotal = quantity * noTaxPrice;
|
|
taxAmount = taxTotal - noTaxTotal;
|
|
break;
|
|
default:
|
|
taxTotal = quantity * taxPrice;
|
|
noTaxPrice = taxPrice / taxDivisor;
|
|
noTaxTotal = quantity * noTaxPrice;
|
|
taxAmount = taxTotal - noTaxTotal;
|
|
}
|
|
|
|
const round2 = (v) => Math.round(v * 100) / 100;
|
|
|
|
return {
|
|
...product,
|
|
quantity,
|
|
taxPrice: round2(taxPrice),
|
|
noTaxPrice: round2(noTaxPrice),
|
|
taxTotal: round2(taxTotal),
|
|
noTaxTotal: round2(noTaxTotal),
|
|
taxAmount: round2(taxAmount),
|
|
taxDivisor
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 重新计算所有产品的含税总额和总计
|
|
* @param {Object} content - 产品内容对象
|
|
* @returns {Object} 更新后的产品内容对象
|
|
*/
|
|
export function recalculateTotals(content) {
|
|
if (!content) {
|
|
return { ...DEFAULT_PRODUCT_CONTENT };
|
|
}
|
|
|
|
const products = (content.products || []).map(product =>
|
|
calculateProductFields(product, 'quantity')
|
|
);
|
|
|
|
const totalQuantity = calculateTotalQuantity(products);
|
|
const totalTaxTotal = calculateTotalTaxTotal(products);
|
|
|
|
return {
|
|
...content,
|
|
products,
|
|
totalQuantity,
|
|
totalTaxTotal,
|
|
totalAmountInWords: convertToChinese(totalTaxTotal)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 将订单明细项转换为产品项
|
|
* @param {Object} orderItem - 订单明细对象
|
|
* @returns {Object} 产品项对象
|
|
*/
|
|
export function convertOrderItemToProduct(orderItem) {
|
|
if (!orderItem) return {};
|
|
|
|
const product = {
|
|
spec: orderItem.finishedProductSpec || '',
|
|
material: orderItem.material || '',
|
|
quantity: parseFloat(orderItem.weight) || 0,
|
|
taxPrice: parseFloat(orderItem.contractPrice) || 0,
|
|
taxDivisor: parseFloat(orderItem.taxDivisor) || 1.13,
|
|
remark: orderItem.remark || ''
|
|
};
|
|
|
|
return calculateProductFields(product, 'quantity');
|
|
}
|
|
|
|
/**
|
|
* 将订单明细列表转换为产品内容对象
|
|
* @param {Array} orderItems - 订单明细列表
|
|
* @param {string} productName - 产品名称
|
|
* @param {string} remark - 备注
|
|
* @returns {Object} 产品内容对象
|
|
*/
|
|
export function convertOrderItemsToContent(orderItems, productName = '', remark = '') {
|
|
const products = (orderItems || []).map(convertOrderItemToProduct);
|
|
|
|
const content = {
|
|
products,
|
|
productName,
|
|
remark
|
|
};
|
|
|
|
return recalculateTotals(content);
|
|
}
|
|
|
|
/**
|
|
* 添加产品项到产品内容
|
|
* @param {Object} content - 产品内容对象
|
|
* @param {Object} product - 要添加的产品项
|
|
* @returns {Object} 更新后的产品内容对象
|
|
*/
|
|
export function addProduct(content, product) {
|
|
if (!content) content = { ...DEFAULT_PRODUCT_CONTENT };
|
|
if (!product) return content;
|
|
|
|
const newContent = {
|
|
...content,
|
|
products: [...(content.products || []), { ...product }]
|
|
};
|
|
|
|
return recalculateTotals(newContent);
|
|
}
|
|
|
|
/**
|
|
* 删除产品内容中的产品项
|
|
* @param {Object} content - 产品内容对象
|
|
* @param {number} index - 要删除的索引
|
|
* @returns {Object} 更新后的产品内容对象
|
|
*/
|
|
export function removeProduct(content, index) {
|
|
if (!content || !content.products || index < 0 || index >= content.products.length) {
|
|
return content;
|
|
}
|
|
|
|
const newProducts = [...content.products];
|
|
newProducts.splice(index, 1);
|
|
|
|
const newContent = {
|
|
...content,
|
|
products: newProducts
|
|
};
|
|
|
|
return recalculateTotals(newContent);
|
|
}
|
|
|
|
/**
|
|
* 更新产品内容中的产品项
|
|
* @param {Object} content - 产品内容对象
|
|
* @param {number} index - 要更新的索引
|
|
* @param {Object} product - 新的产品项数据
|
|
* @returns {Object} 更新后的产品内容对象
|
|
*/
|
|
export function updateProduct(content, index, product) {
|
|
if (!content || !content.products || index < 0 || index >= content.products.length || !product) {
|
|
return content;
|
|
}
|
|
|
|
const newProducts = [...content.products];
|
|
newProducts[index] = { ...newProducts[index], ...product };
|
|
|
|
const newContent = {
|
|
...content,
|
|
products: newProducts
|
|
};
|
|
|
|
return recalculateTotals(newContent);
|
|
}
|
|
|
|
/**
|
|
* 数字金额转中文大写
|
|
* @param {number} amount - 数字金额
|
|
* @returns {string} 中文大写金额
|
|
*/
|
|
export function convertToChinese(amount) {
|
|
if (amount === 0) return '零元整';
|
|
|
|
const digits = ['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖'];
|
|
const units = ['', '拾', '佰', '仟'];
|
|
const bigUnits = ['', '万', '亿'];
|
|
|
|
let integerPart = Math.floor(amount);
|
|
let decimalPart = Math.round((amount - integerPart) * 100);
|
|
|
|
let result = '';
|
|
|
|
// 处理整数部分
|
|
if (integerPart > 0) {
|
|
let unitIndex = 0;
|
|
let bigUnitIndex = 0;
|
|
|
|
while (integerPart > 0) {
|
|
let section = integerPart % 10000;
|
|
if (section > 0) {
|
|
let sectionResult = '';
|
|
let temp = section;
|
|
let unitIndexInSection = 0;
|
|
|
|
while (temp > 0) {
|
|
let digit = temp % 10;
|
|
if (digit > 0) {
|
|
sectionResult = digits[digit] + units[unitIndexInSection] + sectionResult;
|
|
} else {
|
|
// 避免连续的零
|
|
if (sectionResult && !sectionResult.startsWith('零')) {
|
|
sectionResult = '零' + sectionResult;
|
|
}
|
|
}
|
|
temp = Math.floor(temp / 10);
|
|
unitIndexInSection++;
|
|
}
|
|
|
|
result = sectionResult + bigUnits[bigUnitIndex] + result;
|
|
}
|
|
|
|
integerPart = Math.floor(integerPart / 10000);
|
|
bigUnitIndex++;
|
|
}
|
|
|
|
result += '元';
|
|
}
|
|
|
|
// 处理小数部分
|
|
if (decimalPart === 0) {
|
|
result += '整';
|
|
} else {
|
|
const jiao = Math.floor(decimalPart / 10);
|
|
const fen = decimalPart % 10;
|
|
if (jiao > 0) {
|
|
result += digits[jiao] + '角';
|
|
}
|
|
if (fen > 0) {
|
|
result += digits[fen] + '分';
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* 获取默认产品内容对象
|
|
* @returns {Object} 默认产品内容
|
|
*/
|
|
export function getDefaultProductContent() {
|
|
return { ...DEFAULT_PRODUCT_CONTENT };
|
|
}
|
|
|
|
/**
|
|
* ProductContent 工具类
|
|
*/
|
|
export default {
|
|
parse: parseProductContent,
|
|
stringify: stringifyProductContent,
|
|
calculateTotalQuantity,
|
|
calculateTotalTaxTotal,
|
|
calculateProductTaxTotal,
|
|
calculateProductNoTaxPrice,
|
|
calculateProductNoTaxTotal,
|
|
calculateProductTaxAmount,
|
|
calculateProductFields,
|
|
recalculateTotals,
|
|
convertOrderItemToProduct,
|
|
convertOrderItemsToContent,
|
|
addProduct,
|
|
removeProduct,
|
|
updateProduct,
|
|
convertToChinese,
|
|
getDefault: getDefaultProductContent
|
|
};
|