feat(web): 同步 web 端目录更新至 Gitea
包含加氢站站点信息、运维交车/故障、台账与数据分析等页面新增与改动。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
515
web端/台账数据/保险分摊明细.jsx
Normal file
515
web端/台账数据/保险分摊明细.jsx
Normal file
@@ -0,0 +1,515 @@
|
||||
// 【重要】必须使用 const Component 作为组件变量名
|
||||
// 台账数据 - 车辆保险台账(保险分摊明细)
|
||||
// 原型:客户名称 + 结算周期筛选、导出;日成本与分摊成本按业务公式计算(联调可替换为接口)
|
||||
|
||||
const Component = function () {
|
||||
var useState = React.useState;
|
||||
var useMemo = React.useMemo;
|
||||
var useCallback = React.useCallback;
|
||||
|
||||
var antd = window.antd;
|
||||
var App = antd.App;
|
||||
var Breadcrumb = antd.Breadcrumb;
|
||||
var Card = antd.Card;
|
||||
var Button = antd.Button;
|
||||
var Table = antd.Table;
|
||||
var Select = antd.Select;
|
||||
var DatePicker = antd.DatePicker;
|
||||
var Row = antd.Row;
|
||||
var Col = antd.Col;
|
||||
var Space = antd.Space;
|
||||
var message = antd.message;
|
||||
|
||||
var CUSTOMER_OPTIONS = [
|
||||
{ value: '浙江羚牛氢能科技有限公司', label: '浙江羚牛氢能科技有限公司' },
|
||||
{ value: '杭州绿运物流有限公司', label: '杭州绿运物流有限公司' },
|
||||
{ value: '宁波港城新能源车队', label: '宁波港城新能源车队' },
|
||||
{ value: '绍兴氢能示范运营中心', label: '绍兴氢能示范运营中心' }
|
||||
];
|
||||
|
||||
var PROJECT_BY_CUSTOMER = {
|
||||
'浙江羚牛氢能科技有限公司': ['氢能重卡租赁一期', '园区通勤包车'],
|
||||
'杭州绿运物流有限公司': ['城配氢能车辆项目', '冷链专线'],
|
||||
'宁波港城新能源车队': ['港口短驳氢能车', '堆场转运'],
|
||||
'绍兴氢能示范运营中心': ['示范线路运营', '加氢站接驳']
|
||||
};
|
||||
|
||||
var PLATE_PREFIX = ['浙A', '浙B', '浙D', '浙G'];
|
||||
|
||||
function filterOption(input, option) {
|
||||
var label = (option && (option.label || option.children)) || '';
|
||||
return String(label).toLowerCase().indexOf(String(input || '').toLowerCase()) >= 0;
|
||||
}
|
||||
|
||||
function numOrZero(v) {
|
||||
if (v === null || v === undefined || v === '') return 0;
|
||||
var n = Number(v);
|
||||
return isNaN(n) ? 0 : n;
|
||||
}
|
||||
|
||||
function fmtMoney(n, digits) {
|
||||
if (n === null || n === undefined || n === '') return '-';
|
||||
var x = Number(n);
|
||||
if (isNaN(x)) return '-';
|
||||
var d = digits === undefined ? 2 : digits;
|
||||
return x.toLocaleString('zh-CN', { minimumFractionDigits: d, maximumFractionDigits: d });
|
||||
}
|
||||
|
||||
function escapeCsv(v) {
|
||||
var s = v == null ? '' : String(v);
|
||||
if (s.indexOf(',') !== -1 || s.indexOf('"') !== -1 || s.indexOf('\n') !== -1 || s.indexOf('\r') !== -1) {
|
||||
return '"' + s.replace(/"/g, '""') + '"';
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
function downloadCsv(filename, lines) {
|
||||
var csv = lines.map(function (row) { return row.map(escapeCsv).join(','); }).join('\n');
|
||||
var blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8' });
|
||||
var url = URL.createObjectURL(blob);
|
||||
var a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function initialSettlementMonth() {
|
||||
try {
|
||||
if (window.dayjs) return window.dayjs('2026-05-01');
|
||||
} catch (e1) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
function settlementYm(d) {
|
||||
if (!d || !window.dayjs) return '';
|
||||
try {
|
||||
return window.dayjs(d).format('YYYY-MM');
|
||||
} catch (e2) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/** 日成本 = 缴费金额 / 生效天数 */
|
||||
function dailyCost(payment, effectiveDays) {
|
||||
var pay = numOrZero(payment);
|
||||
var days = numOrZero(effectiveDays);
|
||||
if (days <= 0) return null;
|
||||
return Math.round((pay / days) * 10000) / 10000;
|
||||
}
|
||||
|
||||
/** 保险分摊成本 = Σ(各险日成本 × 分摊天数) */
|
||||
function calcApportionCost(row) {
|
||||
var d = numOrZero(row.apportionDays);
|
||||
if (d <= 0) return null;
|
||||
var sum = 0;
|
||||
var has = false;
|
||||
['compulsoryDaily', 'commercialDaily', 'excessDaily', 'cargoDaily'].forEach(function (k) {
|
||||
var v = row[k];
|
||||
if (v !== null && v !== undefined && !isNaN(Number(v))) {
|
||||
sum += Number(v) * d;
|
||||
has = true;
|
||||
}
|
||||
});
|
||||
if (!has) return null;
|
||||
return Math.round(sum * 100) / 100;
|
||||
}
|
||||
|
||||
function buildMockRows(ym) {
|
||||
var rows = [];
|
||||
var seed = 0;
|
||||
CUSTOMER_OPTIONS.forEach(function (cust, ci) {
|
||||
var projects = PROJECT_BY_CUSTOMER[cust.value] || ['默认项目'];
|
||||
projects.forEach(function (proj, pi) {
|
||||
seed += 1;
|
||||
var plate = PLATE_PREFIX[ci % PLATE_PREFIX.length] + String(10000 + seed).slice(-5) + 'F';
|
||||
var effDays = 365;
|
||||
var compulsoryPay = 950 + seed * 3;
|
||||
var commercialPay = 4200 + seed * 17;
|
||||
var excessPay = seed % 3 === 0 ? 1800 + seed * 5 : 0;
|
||||
var cargoPay = seed % 2 === 0 ? 600 + seed * 2 : 0;
|
||||
var apportionDays = 18 + ((seed + (ym ? ym.length : 0)) % 13);
|
||||
|
||||
var row = {
|
||||
key: 'r' + seed,
|
||||
seq: seed,
|
||||
settlementCycle: ym || '2026-05',
|
||||
customerName: cust.value,
|
||||
projectName: proj,
|
||||
plateNo: plate,
|
||||
compulsoryDaily: dailyCost(compulsoryPay, effDays),
|
||||
commercialDaily: dailyCost(commercialPay, effDays),
|
||||
excessDaily: excessPay > 0 ? dailyCost(excessPay, effDays) : null,
|
||||
cargoDaily: cargoPay > 0 ? dailyCost(cargoPay, effDays) : null,
|
||||
apportionDays: apportionDays
|
||||
};
|
||||
row.apportionCost = calcApportionCost(row);
|
||||
rows.push(row);
|
||||
});
|
||||
});
|
||||
return rows;
|
||||
}
|
||||
|
||||
|
||||
var layoutStyle = {
|
||||
padding: '16px 24px 24px',
|
||||
minHeight: '100vh',
|
||||
background: 'linear-gradient(165deg, #eef4ff 0%, #f5f7fa 42%, #f0f2f5 100%)'
|
||||
};
|
||||
var filterLabelStyle = { marginBottom: 6, fontSize: 13, color: 'rgba(0,0,0,0.55)', fontWeight: 500 };
|
||||
var filterItemStyle = { marginBottom: 12 };
|
||||
var filterControlStyle = { width: '100%' };
|
||||
var filterActionsColStyle = { flex: '0 0 auto', marginLeft: 'auto' };
|
||||
|
||||
var filterCardStyle = {
|
||||
marginBottom: 20,
|
||||
borderRadius: 16,
|
||||
boxShadow: '0 4px 20px -4px rgba(16,24,40,0.03), 0 0 0 1px rgba(16,24,40,0.06)',
|
||||
border: 'none',
|
||||
background: '#ffffff'
|
||||
};
|
||||
|
||||
var tableCardStyle = {
|
||||
borderRadius: 16,
|
||||
boxShadow: '0 10px 32px -4px rgba(16,24,40,0.06), 0 0 0 1px rgba(16,24,40,0.04)',
|
||||
border: 'none',
|
||||
background: '#ffffff',
|
||||
overflow: 'hidden'
|
||||
};
|
||||
|
||||
var ledgerTableStyle =
|
||||
'.ins-ledger-table-wrap{border-radius:12px;overflow:hidden;box-shadow:0 4px 24px -6px rgba(15,23,42,0.05),0 0 0 1px rgba(22,119,255,0.1)}' +
|
||||
'.ins-ledger-table .ant-table-thead>tr>th{white-space:nowrap;color:#1e293b!important;font-weight:600!important;font-size:13px!important;' +
|
||||
'background:#e8f4fc!important;border-bottom:1px solid #bae6fd!important;border-inline-end:1px solid #dbeafe!important;padding:0 8px!important;height:38px!important}' +
|
||||
'.ins-ledger-table .ant-table-tbody>tr:not(.ant-table-measure-row)>td{white-space:nowrap;font-variant-numeric:tabular-nums;color:#334155;border-bottom:1px solid #f1f5f9!important;border-inline-end:1px solid #f8fafc!important;padding:0 8px!important;height:38px!important}' +
|
||||
'.ins-ledger-table .ant-table-tbody>tr.ins-row-data:hover>td{background:#f0f9ff!important}' +
|
||||
'.ins-ledger-table .ant-table-summary>tr>td{font-weight:700;background:#f8fafc!important;color:#0f172a!important;border-top:2px solid #cbd5e1!important;padding:0 8px!important;height:38px!important}';
|
||||
|
||||
var customerDraftState = useState(undefined);
|
||||
var customerDraft = customerDraftState[0];
|
||||
var setCustomerDraft = customerDraftState[1];
|
||||
|
||||
var monthDraftState = useState(initialSettlementMonth);
|
||||
var monthDraft = monthDraftState[0];
|
||||
var setMonthDraft = monthDraftState[1];
|
||||
|
||||
var customerAppliedState = useState(undefined);
|
||||
var customerApplied = customerAppliedState[0];
|
||||
var setCustomerApplied = customerAppliedState[1];
|
||||
|
||||
var monthAppliedState = useState(initialSettlementMonth);
|
||||
var monthApplied = monthAppliedState[0];
|
||||
var setMonthApplied = monthAppliedState[1];
|
||||
|
||||
var appliedYm = useMemo(function () {
|
||||
return settlementYm(monthApplied) || '2026-05';
|
||||
}, [monthApplied]);
|
||||
|
||||
var allRows = useMemo(function () {
|
||||
return buildMockRows(appliedYm);
|
||||
}, [appliedYm]);
|
||||
|
||||
var dataSource = useMemo(function () {
|
||||
var list = allRows.filter(function (r) {
|
||||
if (customerApplied && r.customerName !== customerApplied) return false;
|
||||
return true;
|
||||
});
|
||||
return list.map(function (r, idx) {
|
||||
return Object.assign({}, r, { seq: idx + 1 });
|
||||
});
|
||||
}, [allRows, customerApplied]);
|
||||
|
||||
var totalApportionCost = useMemo(function () {
|
||||
return dataSource.reduce(function (acc, r) {
|
||||
return acc + numOrZero(r.apportionCost);
|
||||
}, 0);
|
||||
}, [dataSource]);
|
||||
|
||||
var customerDisplayLabel = customerApplied || '默认全量数据';
|
||||
var cycleDisplayLabel = appliedYm || '-';
|
||||
|
||||
var handleQuery = useCallback(function () {
|
||||
setCustomerApplied(customerDraft);
|
||||
setMonthApplied(monthDraft);
|
||||
message.success('查询成功');
|
||||
}, [customerDraft, monthDraft]);
|
||||
|
||||
var handleReset = useCallback(function () {
|
||||
setCustomerDraft(undefined);
|
||||
setMonthDraft(initialSettlementMonth());
|
||||
setCustomerApplied(undefined);
|
||||
setMonthApplied(initialSettlementMonth());
|
||||
}, []);
|
||||
|
||||
var handleExport = useCallback(function () {
|
||||
var headers = [
|
||||
'序号',
|
||||
'结算周期',
|
||||
'客户名称',
|
||||
'项目名称',
|
||||
'车牌号',
|
||||
'交强险日成本',
|
||||
'商业险日成本',
|
||||
'超赔险日成本',
|
||||
'货物险日成本',
|
||||
'分摊天数',
|
||||
'保险分摊成本'
|
||||
];
|
||||
var body = dataSource.map(function (r) {
|
||||
return [
|
||||
r.seq,
|
||||
r.settlementCycle,
|
||||
r.customerName,
|
||||
r.projectName,
|
||||
r.plateNo,
|
||||
r.compulsoryDaily,
|
||||
r.commercialDaily,
|
||||
r.excessDaily,
|
||||
r.cargoDaily,
|
||||
r.apportionDays,
|
||||
r.apportionCost
|
||||
];
|
||||
});
|
||||
body.push(['合计', '', '', '', '', '', '', '', '', '', totalApportionCost]);
|
||||
downloadCsv('车辆保险台账_' + cycleDisplayLabel + '_' + new Date().getTime() + '.csv', [headers].concat(body));
|
||||
message.success('已导出 CSV');
|
||||
}, [dataSource, cycleDisplayLabel, totalApportionCost]);
|
||||
|
||||
var columns = useMemo(function () {
|
||||
return [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'seq',
|
||||
key: 'seq',
|
||||
width: 64,
|
||||
align: 'center',
|
||||
fixed: 'left'
|
||||
},
|
||||
{
|
||||
title: '结算周期',
|
||||
dataIndex: 'settlementCycle',
|
||||
key: 'settlementCycle',
|
||||
width: 100,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '客户名称',
|
||||
dataIndex: 'customerName',
|
||||
key: 'customerName',
|
||||
width: 200,
|
||||
align: 'center',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '项目名称',
|
||||
dataIndex: 'projectName',
|
||||
key: 'projectName',
|
||||
width: 160,
|
||||
align: 'center',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '车牌号',
|
||||
dataIndex: 'plateNo',
|
||||
key: 'plateNo',
|
||||
width: 110,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '交强险日成本',
|
||||
dataIndex: 'compulsoryDaily',
|
||||
key: 'compulsoryDaily',
|
||||
width: 130,
|
||||
align: 'right',
|
||||
render: function (v) { return fmtMoney(v, 4); }
|
||||
},
|
||||
{
|
||||
title: '商业险日成本',
|
||||
dataIndex: 'commercialDaily',
|
||||
key: 'commercialDaily',
|
||||
width: 130,
|
||||
align: 'right',
|
||||
render: function (v) { return fmtMoney(v, 4); }
|
||||
},
|
||||
{
|
||||
title: '超赔险日成本',
|
||||
dataIndex: 'excessDaily',
|
||||
key: 'excessDaily',
|
||||
width: 130,
|
||||
align: 'right',
|
||||
render: function (v) { return fmtMoney(v, 4); }
|
||||
},
|
||||
{
|
||||
title: '货物险日成本',
|
||||
dataIndex: 'cargoDaily',
|
||||
key: 'cargoDaily',
|
||||
width: 130,
|
||||
align: 'right',
|
||||
render: function (v) { return fmtMoney(v, 4); }
|
||||
},
|
||||
{
|
||||
title: '分摊天数',
|
||||
dataIndex: 'apportionDays',
|
||||
key: 'apportionDays',
|
||||
width: 120,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '保险分摊成本',
|
||||
dataIndex: 'apportionCost',
|
||||
key: 'apportionCost',
|
||||
width: 140,
|
||||
align: 'right',
|
||||
fixed: 'right',
|
||||
render: function (v) { return fmtMoney(v, 2); }
|
||||
}
|
||||
];
|
||||
}, []);
|
||||
|
||||
var tableSummary = useCallback(function () {
|
||||
return React.createElement(
|
||||
Table.Summary,
|
||||
null,
|
||||
React.createElement(
|
||||
Table.Summary.Row,
|
||||
null,
|
||||
React.createElement(Table.Summary.Cell, { index: 0, align: 'center', colSpan: 1 }, '合计'),
|
||||
React.createElement(Table.Summary.Cell, { index: 1, colSpan: 9 }),
|
||||
React.createElement(Table.Summary.Cell, { index: 10, align: 'right' }, fmtMoney(totalApportionCost, 2))
|
||||
)
|
||||
);
|
||||
}, [totalApportionCost]);
|
||||
|
||||
return React.createElement(
|
||||
App,
|
||||
null,
|
||||
React.createElement('style', null, ledgerTableStyle),
|
||||
React.createElement(
|
||||
'div',
|
||||
{ style: layoutStyle },
|
||||
React.createElement(Breadcrumb, {
|
||||
style: { marginBottom: 12 },
|
||||
items: [{ title: '台账数据' }, { title: '保险分摊明细' }]
|
||||
}),
|
||||
React.createElement(
|
||||
Card,
|
||||
{ style: filterCardStyle, bodyStyle: { paddingBottom: 4 } },
|
||||
React.createElement(
|
||||
Row,
|
||||
{ gutter: [16, 16], align: 'bottom' },
|
||||
React.createElement(
|
||||
Col,
|
||||
{ xs: 24, sm: 12, md: 8, lg: 6 },
|
||||
React.createElement(
|
||||
'div',
|
||||
{ style: filterItemStyle },
|
||||
React.createElement('div', { style: filterLabelStyle }, '客户名称'),
|
||||
React.createElement(Select, {
|
||||
allowClear: true,
|
||||
showSearch: true,
|
||||
placeholder: '默认全量数据',
|
||||
style: filterControlStyle,
|
||||
value: customerDraft,
|
||||
onChange: function (v) { setCustomerDraft(v); },
|
||||
options: CUSTOMER_OPTIONS,
|
||||
filterOption: filterOption
|
||||
})
|
||||
)
|
||||
),
|
||||
React.createElement(
|
||||
Col,
|
||||
{ xs: 24, sm: 12, md: 8, lg: 6 },
|
||||
React.createElement(
|
||||
'div',
|
||||
{ style: filterItemStyle },
|
||||
React.createElement('div', { style: filterLabelStyle }, '结算周期'),
|
||||
React.createElement(DatePicker, {
|
||||
picker: 'month',
|
||||
style: filterControlStyle,
|
||||
placeholder: '请选择结算周期',
|
||||
format: 'YYYY-MM',
|
||||
value: monthDraft,
|
||||
onChange: function (v) { setMonthDraft(v); }
|
||||
})
|
||||
)
|
||||
),
|
||||
React.createElement(
|
||||
Col,
|
||||
{ xs: 24, sm: 12, md: 8, lg: 6, style: filterActionsColStyle },
|
||||
React.createElement(
|
||||
'div',
|
||||
{ style: filterItemStyle },
|
||||
React.createElement('div', { style: filterLabelStyle }, '\u00a0'),
|
||||
React.createElement(
|
||||
Space,
|
||||
{ wrap: true },
|
||||
React.createElement(Button, { onClick: handleReset }, '重置'),
|
||||
React.createElement(Button, { type: 'primary', onClick: handleQuery }, '查询')
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
React.createElement(
|
||||
Card,
|
||||
{ style: tableCardStyle, bodyStyle: { padding: '20px 20px 24px' } },
|
||||
React.createElement(
|
||||
'div',
|
||||
{ style: { position: 'relative', marginBottom: 8, minHeight: 36 } },
|
||||
React.createElement(
|
||||
'div',
|
||||
{
|
||||
style: {
|
||||
textAlign: 'center',
|
||||
fontSize: 18,
|
||||
fontWeight: 700,
|
||||
color: 'rgba(15,23,42,0.92)',
|
||||
letterSpacing: '0.02em',
|
||||
padding: '0 88px'
|
||||
}
|
||||
},
|
||||
'车辆保险台账'
|
||||
),
|
||||
React.createElement(
|
||||
'div',
|
||||
{ style: { position: 'absolute', right: 0, top: '50%', transform: 'translateY(-50%)' } },
|
||||
React.createElement(Button, { onClick: handleExport }, '导出')
|
||||
)
|
||||
),
|
||||
React.createElement(
|
||||
'div',
|
||||
{
|
||||
style: {
|
||||
textAlign: 'center',
|
||||
marginBottom: 16,
|
||||
fontSize: 13,
|
||||
color: 'rgba(15,23,42,0.55)',
|
||||
fontWeight: 500
|
||||
}
|
||||
},
|
||||
'结算周期:',
|
||||
cycleDisplayLabel,
|
||||
'\u00A0\u00A0\u00A0\u00A0客户:',
|
||||
customerDisplayLabel
|
||||
),
|
||||
React.createElement(
|
||||
'div',
|
||||
{ className: 'ins-ledger-table-wrap' },
|
||||
React.createElement(Table, {
|
||||
className: 'ins-ledger-table',
|
||||
size: 'small',
|
||||
bordered: true,
|
||||
rowKey: 'key',
|
||||
columns: columns,
|
||||
dataSource: dataSource,
|
||||
pagination: false,
|
||||
rowClassName: function () { return 'ins-row-data'; },
|
||||
scroll: { x: 'max-content', y: 'calc(100vh - 320px)' },
|
||||
sticky: true,
|
||||
summary: tableSummary
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user