feat(web): 还车应结款费用明细调整业务服务组与安全组费用项

将违章/保险上浮移至安全组并新增其他违规费用,业务服务组增加支持正负的租金固定项,并纳入待结算汇总计算。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
王冕
2026-06-10 13:43:34 +08:00
parent a27e3b8e43
commit 34fd6fcdac

View File

@@ -56,6 +56,35 @@ const Component = function () {
return isNaN(n) ? '' : n.toFixed(2);
}
function filterSignedMoneyInput(raw) {
var s = String(raw == null ? '' : raw);
var neg = s.charAt(0) === '-';
s = s.replace(/[^\d.]/g, '');
var parts = s.split('.');
if (parts.length > 2) {
s = parts[0] + '.' + parts.slice(1).join('');
parts = s.split('.');
}
if (parts[1] && parts[1].length > 2) s = parts[0] + '.' + parts[1].slice(0, 2);
return (neg ? '-' : '') + s;
}
function formatSignedMoney2(raw) {
if (raw === null || raw === undefined) return '';
var v = String(raw).trim();
if (!v || v === '-') return v === '-' ? '-' : '';
var n = parseFloat(v);
return isNaN(n) ? '' : n.toFixed(2);
}
function isValidSignedMoney2(s) {
if (s === null || s === undefined) return false;
var v = String(s).trim();
if (!v || v === '-') return false;
if (!/^-?\d+(\.\d{1,2})?$/.test(v)) return false;
return !isNaN(parseFloat(v));
}
// 页面样式
var layoutStyle = { padding: '16px 24px 88px', background: '#f5f5f5', minHeight: '100vh' };
var cardStyle = { marginBottom: 16 };
@@ -125,7 +154,7 @@ const Component = function () {
};
// 业务服务组
var businessServiceFixedItems = ['违章处理违约金', '保险上浮', 'ETC-客户未缴费用', 'ETC卡缺损费', 'ETC设备缺损费'];
var businessServiceFixedItems = ['租金', 'ETC-客户未缴费用', 'ETC卡缺损费', 'ETC设备缺损费'];
var businessServiceRowsState = useState(
businessServiceFixedItems.map(function (name, i) {
return { key: 'bs-' + i, seq: i + 1, feeItem: name, amount: '', remark: '', lastUpdateTime: '', photos: [], attachments: [], fixed: true };
@@ -474,7 +503,50 @@ const Component = function () {
});
}, []);
// 安全组(示例数据)
// 安全组
var safetyFixedItems = ['违章处理违约金', '保险上浮', '其他违规费用'];
var safetyFeeRowsState = useState(
safetyFixedItems.map(function (name, i) {
return { key: 'sf-' + i, seq: i + 1, feeItem: name, amount: '', remark: '', lastUpdateTime: '', photos: [], attachments: [], fixed: true };
})
);
var safetyFeeRows = safetyFeeRowsState[0];
var setSafetyFeeRows = safetyFeeRowsState[1];
function recalcSafetyTotal(list) {
var sum = 0;
(list || []).forEach(function (r) { sum += (parseFloat(r.amount) || 0); });
return sum.toFixed(2);
}
var safetyTotalComputed = useMemo(function () {
return recalcSafetyTotal(safetyFeeRows || []);
}, [safetyFeeRows]);
var addSafetyFeeRow = useCallback(function () {
setSafetyFeeRows(function (p) {
var next = p.slice();
next.push({ key: 'sf-new-' + Date.now(), seq: next.length + 1, feeItem: '', amount: '', remark: '', lastUpdateTime: '', photos: [], attachments: [], fixed: false });
return next.map(function (r, idx) { var o = {}; for (var k in r) o[k] = r[k]; o.seq = idx + 1; return o; });
});
}, []);
var removeSafetyFeeRow = useCallback(function (key) {
setSafetyFeeRows(function (p) {
var next = p.filter(function (r) { return r.key !== key; });
return next.map(function (r, idx) { var o = {}; for (var k in r) o[k] = r[k]; o.seq = idx + 1; return o; });
});
}, []);
var updateSafetyFeeRow = useCallback(function (key, field, value) {
setSafetyFeeRows(function (p) {
return p.map(function (r) {
if (r.key !== key) return r;
var n = {}; for (var k in r) n[k] = r[k];
n[field] = value;
return n;
});
});
}, []);
var violationList = useMemo(function () {
return [{
key: 'w1',
@@ -525,6 +597,10 @@ const Component = function () {
okText: '确认',
cancelText: '取消',
onOk: function () {
setSafetyFeeRows(function (p) {
var now = formatDateTimeNow();
return p.map(function (r) { var n = {}; for (var k in r) n[k] = r[k]; n.lastUpdateTime = now; return n; });
});
setSafetyMeta(function (m) { var nm = {}; for (var k in m) nm[k] = m[k]; nm.submitBy = nm.submitBy || '安全组-赵六'; nm.status = '已提交'; return nm; });
message.success('安全组已提交');
}
@@ -548,8 +624,9 @@ const Component = function () {
var rentRefund = parseFloat((billInfo && billInfo.shouldRefundRent) || '') || 0;
var op = parseFloat(operationTotalComputed) || 0;
var en = (parseFloat(energy.hydrogenSupplement) || 0) + (parseFloat(energy.hydrogenFee) || 0) + (parseFloat(energy.electricFee) || 0) - (parseFloat(energy.prepayRefund) || 0);
return (bs + rentRefund + en + op).toFixed(2);
}, [businessServiceTotalComputed, billInfo.shouldRefundRent, operationTotalComputed, energy.hydrogenSupplement, energy.hydrogenFee, energy.electricFee, energy.prepayRefund]);
var sf = parseFloat(safetyTotalComputed) || 0;
return (bs + rentRefund + en + op + sf).toFixed(2);
}, [businessServiceTotalComputed, billInfo.shouldRefundRent, operationTotalComputed, energy.hydrogenSupplement, energy.hydrogenFee, energy.electricFee, energy.prepayRefund, safetyTotalComputed]);
var refundTotalComputed = useMemo(function () {
var deposit = parseFloat(stats.depositAmount) || 0;
@@ -597,9 +674,10 @@ const Component = function () {
{ key: 'eFee', item: '能源采购组-电费补缴金额', amount: (toFixed2(energy.electricFee) || '0.00') },
{ key: 'prepay', item: '能源采购组-预付款退费金额(减)', amount: '-' + (toFixed2(energy.prepayRefund) || '0.00') },
{ key: 'op', item: '运维部费用项金额总额', amount: operationTotalComputed || '0.00' },
{ key: 'sf', item: '安全组费用项金额总和', amount: safetyTotalComputed || '0.00' },
{ key: 'total', item: '待结算总额', amount: pendingSettleComputed || '0.00', strong: true }
];
}, [businessServiceTotalComputed, billInfo.shouldRefundRent, energy.hydrogenSupplement, energy.hydrogenFee, energy.electricFee, energy.prepayRefund, operationTotalComputed, pendingSettleComputed]);
}, [businessServiceTotalComputed, billInfo.shouldRefundRent, energy.hydrogenSupplement, energy.hydrogenFee, energy.electricFee, energy.prepayRefund, operationTotalComputed, safetyTotalComputed, pendingSettleComputed]);
var refundBreakdown = useMemo(function () {
return [
@@ -640,7 +718,7 @@ var requirementDocContent = useMemo(function () {
3.还车费用明细:
#上方为还车费用统计数据,包括保证金总额、待结算总额、应退还总额、应补缴总额等相关信息,该部分只有业务管理组能查看;
3.1.保证金总额显示该车辆保证金金额格式为xx.xx元
3.2.待结算总额显示该车辆待结算金额格式为xx.xx元点击以气泡卡片列表显示费用项、金额。计算方式为「业务服务部所有费用项-金额总和」+「业务服务部-车辆应退租金」+「能源采购组-氢量差补缴金额」+「能源采购组-氢费补缴金额」+「能源采购组-电费补缴金额」-「能源采购组-预付款退费金额」+「运维部所有费用项-金额总额」;
3.2.待结算总额显示该车辆待结算金额格式为xx.xx元点击以气泡卡片列表显示费用项、金额。计算方式为「业务服务部所有费用项-金额总和」+「业务服务部-车辆应退租金」+「能源采购组-氢量差补缴金额」+「能源采购组-氢费补缴金额」+「能源采购组-电费补缴金额」-「能源采购组-预付款退费金额」+「运维部所有费用项-金额总额」+「安全组费用项金额总和」
3.3.应退还总额显示该车辆应退还金额格式为xx.xx元点击以气泡卡片列表显示费用项、金额。计算方式为「保证金金额」-「待结算金额」如果是正数,则显示在应退还总额中;
3.4.应补缴总额显示该车辆应补缴金额格式为xx.xx元点击以气泡卡片列表显示费用项、金额。计算方式为「保证金金额」-「待结算金额」如果是负数,则显示在应补缴总额中;
@@ -655,12 +733,11 @@ var requirementDocContent = useMemo(function () {
下方为列表,列表字段为:序号、费用项、金额、备注、照片、附件、操作;
4.7.序号1、2、3....依次类推;
4.8.费用项:固定显示违章处理违约金、保险上浮、ETC-客户未缴费用、ETC卡缺损费、ETC设备缺损费该部分不能删除可通过点击下方新增一行添加新的条目该部分可删除
4.8.1.违章处理违约金:
4.8.2.保险上浮
4.8.3.ETC-客户未缴费用
4.8.4.ETC缺损费:
4.8.5.ETC设备缺损费
4.8.费用项:固定显示租金、ETC-客户未缴费用、ETC卡缺损费、ETC设备缺损费该部分不能删除可通过点击下方新增一行添加新的条目该部分可删除
4.8.1.租金:金额输入框支持正数/负数最多2位小数
4.8.2.ETC-客户未缴费用
4.8.3.ETC卡缺损费
4.8.4.ETC设备缺损费:
4.9.金额输入框支持2位小数后缀为元
4.10.备注:文本域,支持自定义输入;
4.11.最后更新时间显示最后更新时间格式为YYYY-MM-DD HH:MM
@@ -716,14 +793,16 @@ var requirementDocContent = useMemo(function () {
6.13.附件:附件上传按钮,文案为:上传附件;
6.14.操作:除固定费用项外,其余操作中为删除;
7.安全组:仅由安全组人员进行确认提交;标题栏标题为安全组,后方为提交人、状态(待提交、已提交),该部分只有安全组能查看;
7.1.提交人:显示提交人
7.2.状态:分为待提交(点击保存按钮)、已提交(点击提交按钮)
7.3.保存:点击保存已填写内容,如果已提交,则隐藏保存按钮;
7.4.提交:点击进行二次确认,提示信息:请确认业务服务组金额填写无误,点击确认完成提交。如果已提交,隐藏提交按钮;
7.5.撤回:已提交后,显示撤回按钮。点击进行二次确认,提示信息:是否确认撤回,点击确认,重新显示保存、提交按钮;
7.6.违章清单:违章编码、车牌号、违法行为、违法时间、罚款金额、缴费状态、计分值、是否处理、违章客户、违章照片、备注
7.7.事故清单:列表显示:事故编码、车牌号、事故时间、事故地点、事故类型、客户名称、我方定损金额、对方定损金额、责任划分、事故状态、结案时间、其他费用、备注
7.安全组:仅由安全组人员进行确认提交;标题栏标题为安全组,后方为总金额、提交人、状态(待提交、已提交),该部分只有安全组能查看;
7.1.总金额:显示所有费用项金额总和
7.2.提交人:显示提交人
7.3.状态:分为待提交(点击保存按钮)、已提交(点击提交按钮
7.4.保存:点击保存已填写内容,如果已提交,隐藏保存按钮;
7.5.提交:点击进行二次确认,提示信息:请确认安全组信息无误,点击确认完成提交。如果已提交,隐藏提交按钮;
7.6.撤回:已提交后,显示撤回按钮。点击进行二次确认,提示信息:是否确认撤回,点击确认,重新显示保存、提交按钮
7.7.费用项列表(样式同业务服务组):固定显示违章处理违约金、保险上浮、其他违规费用,不可删除;可新增自定义费用项;字段含序号、费用项、金额、备注、最后更新时间、照片、附件、操作
7.8.违章清单:违章编码、车牌号、违法行为、违法时间、罚款金额、缴费状态、计分值、是否处理、违章客户、违章照片、备注;
7.9.事故清单:列表显示:事故编码、车牌号、事故时间、事故地点、事故类型、客户名称、我方定损金额、对方定损金额、责任划分、事故状态、结案时间、其他费用、备注;
8.底部为提交、取消按钮;
8.1.提交审核还车应结款生成后有15天时间倒计时倒计时结束并且业务服务组、能源采购组、运维部、安全部均提交后才启用平时禁用倒计时显示在按钮上格式为x天x小时后可提交审核
@@ -829,6 +908,9 @@ var requirementDocContent = useMemo(function () {
tireTreadList: tireTreadList,
updateBusinessServiceRow: updateBusinessServiceRow,
removeBusinessServiceRow: removeBusinessServiceRow,
safetyMeta: safetyMeta,
updateSafetyFeeRow: updateSafetyFeeRow,
removeSafetyFeeRow: removeSafetyFeeRow,
updateOperationRow: updateOperationRow,
removeOperationRow: removeOperationRow
};
@@ -851,7 +933,21 @@ var requirementDocContent = useMemo(function () {
var ref = latestRefs.current || {};
var meta = ref.businessServiceMeta || {};
var readOnly = meta.status === '已提交';
return React.createElement(Input, { value: v, onChange: function (e) { ref.updateBusinessServiceRow(r.key, 'amount', e.target.value); }, placeholder: '0.00', addonAfter: '元', disabled: readOnly });
var isRent = r.feeItem === '租金';
return React.createElement(Input, {
value: v,
onChange: function (e) {
var nv = isRent ? filterSignedMoneyInput(e.target.value) : e.target.value;
ref.updateBusinessServiceRow(r.key, 'amount', nv);
},
onBlur: isRent ? function (e) {
var formatted = formatSignedMoney2(e.target.value);
if (formatted !== e.target.value) ref.updateBusinessServiceRow(r.key, 'amount', formatted);
} : undefined,
placeholder: '0.00',
addonAfter: '元',
disabled: readOnly
});
}
},
{
@@ -878,6 +974,51 @@ var requirementDocContent = useMemo(function () {
];
}, []);
var safetyFeeColumns = useMemo(function () {
return [
{ title: '序号', dataIndex: 'seq', key: 'seq', width: 60 },
{
title: '费用项', dataIndex: 'feeItem', key: 'feeItem', width: 200,
render: function (v, r) {
var ref = latestRefs.current || {};
var meta = ref.safetyMeta || {};
var readOnly = meta.status === '已提交';
return r.fixed ? (v || '-') : React.createElement(Input, { value: v, onChange: function (e) { ref.updateSafetyFeeRow(r.key, 'feeItem', e.target.value); }, placeholder: '费用项', disabled: readOnly });
}
},
{
title: RequiredLabel('金额'), dataIndex: 'amount', key: 'amount', width: 140,
render: function (v, r) {
var ref = latestRefs.current || {};
var meta = ref.safetyMeta || {};
var readOnly = meta.status === '已提交';
return React.createElement(Input, { value: v, onChange: function (e) { ref.updateSafetyFeeRow(r.key, 'amount', e.target.value); }, placeholder: '0.00', addonAfter: '元', disabled: readOnly });
}
},
{
title: '备注', dataIndex: 'remark', key: 'remark',
render: function (v, r) {
var ref = latestRefs.current || {};
var meta = ref.safetyMeta || {};
var readOnly = meta.status === '已提交';
return React.createElement(TextArea, { value: v, onChange: function (e) { ref.updateSafetyFeeRow(r.key, 'remark', e.target.value); }, placeholder: '备注', rows: 1, disabled: readOnly });
}
},
{ title: '最后更新时间', dataIndex: 'lastUpdateTime', key: 'lastUpdateTime', width: 150, render: function (v) { return React.createElement('span', { style: { fontSize: 12, color: '#666' } }, v || '-'); } },
{ title: '照片', key: 'photo', width: 100, render: function () { var ref = latestRefs.current || {}; var meta = ref.safetyMeta || {}; var readOnly = meta.status === '已提交'; return React.createElement(Button, { size: 'small', disabled: readOnly, onClick: function () { message.info('照片上传(原型)'); } }, '上传'); } },
{ title: '附件', key: 'attachment', width: 110, render: function () { var ref = latestRefs.current || {}; var meta = ref.safetyMeta || {}; var readOnly = meta.status === '已提交'; return React.createElement(Button, { size: 'small', disabled: readOnly, onClick: function () { message.info('上传附件(原型)'); } }, '上传附件'); } },
{
title: '操作', key: 'action', width: 80,
render: function (_, r) {
var ref = latestRefs.current || {};
var meta = ref.safetyMeta || {};
if (r.fixed || meta.status === '已提交') return null;
return React.createElement(Button, { type: 'link', size: 'small', danger: true, onClick: function () { ref.removeSafetyFeeRow(r.key); } }, '删除');
}
}
];
}, []);
var operationColumns = useMemo(function () {
return [
{ title: '序号', dataIndex: 'seq', key: 'seq', width: 60 },
@@ -1042,11 +1183,14 @@ var requirementDocContent = useMemo(function () {
return !isNaN(parseFloat(v));
}
// 业务服务组:每行“金额”必填
// 业务服务组:每行“金额”必填;租金支持正负两位小数
for (var i = 0; i < (businessServiceRows || []).length; i++) {
var r1 = businessServiceRows[i];
if (!isValidMoney(r1.amount)) {
message.error('请填写业务服务组第' + (r1.seq || (i + 1)) + '行金额');
var amountOk = r1.feeItem === '租金' ? isValidSignedMoney2(r1.amount) : isValidMoney(r1.amount);
if (!amountOk) {
message.error(r1.feeItem === '租金'
? '请填写业务服务组租金金额支持正负最多2位小数'
: '请填写业务服务组第' + (r1.seq || (i + 1)) + '行金额');
return;
}
}
@@ -1074,6 +1218,14 @@ var requirementDocContent = useMemo(function () {
}
}
for (var k = 0; k < (safetyFeeRows || []).length; k++) {
var r3 = safetyFeeRows[k];
if (!isValidMoney(r3.amount)) {
message.error('请填写安全组第' + (r3.seq || (k + 1)) + '行金额');
return;
}
}
if (!canSubmitReview()) {
message.error('未满足提交审核条件(倒计时结束且四组均已提交)');
return;
@@ -1290,6 +1442,7 @@ return React.createElement('div', { style: layoutStyle },
setCollapsed: safetyCollapsedState[1],
title: '安全组',
extra: React.createElement(React.Fragment, null,
React.createElement('span', null, '总金额:', safetyTotalComputed, ' 元'),
React.createElement('span', null, '提交人:', safetyMeta.submitBy || '-'),
React.createElement('span', null, '状态:', safetyMeta.status)
),
@@ -1300,7 +1453,13 @@ return React.createElement('div', { style: layoutStyle },
React.createElement(Button, { size: 'small', type: 'primary', onClick: handleSafetySubmit }, '提交')
)
},
React.createElement('div', { style: { marginBottom: 12, fontWeight: 600 } }, '违章清单'),
React.createElement(Table, { rowKey: 'key', columns: safetyFeeColumns, dataSource: safetyFeeRows, pagination: false, bordered: true, size: 'small' }),
safetyMeta.status === '待提交'
? React.createElement('div', { style: { marginTop: 8, width: '100%' } },
React.createElement(Button, { type: 'dashed', size: 'small', onClick: addSafetyFeeRow, block: true, style: { width: '100%' } }, '新增一行')
)
: null,
React.createElement('div', { style: { margin: '16px 0 12px', fontWeight: 600 } }, '违章清单'),
React.createElement(Table, {
rowKey: 'key',
size: 'small',