feat(web): 还车应结款费用明细调整业务服务组与安全组费用项
将违章/保险上浮移至安全组并新增其他违规费用,业务服务组增加支持正负的租金固定项,并纳入待结算汇总计算。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user