feat(web): 同步 web 端目录更新至 Gitea
包含加氢站站点信息、运维交车/故障、台账与数据分析等页面新增与改动。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
839
web端/数据分析/业务台账.jsx
Normal file
839
web端/数据分析/业务台账.jsx
Normal file
@@ -0,0 +1,839 @@
|
||||
// 【重要】必须使用 const Component 作为组件变量名
|
||||
// 数据分析 - 业务台账(多层级表头:自营/租赁/销售/氢费/电费/ETC 各业绩·成本·利润;其他单列)
|
||||
// 原型:年份 + 业务部筛选、导出;业绩列可钻取业务员 → 项目明细(联调可替换接口)
|
||||
|
||||
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 Modal = antd.Modal;
|
||||
var message = antd.message;
|
||||
|
||||
/** 与示意图一致:前 6 类为三列;其他为单列 */
|
||||
var TRIPLE_DEFS = [
|
||||
{ key: 'self', groupTitle: '自营业务', short: '自营' },
|
||||
{ key: 'lease', groupTitle: '租赁业务', short: '租赁' },
|
||||
{ key: 'resale', groupTitle: '销售', short: '销售' },
|
||||
{ key: 'h2', groupTitle: '氢费', short: '氢费' },
|
||||
{ key: 'apply', groupTitle: '电费', short: '电费' },
|
||||
{ key: 'etc', groupTitle: 'ETC', short: 'ETC' }
|
||||
];
|
||||
|
||||
var TRIPLE_KEYS = TRIPLE_DEFS.map(function (c) { return c.key; });
|
||||
|
||||
var ALL_CAT_FOR_DRILL = TRIPLE_DEFS.map(function (c) {
|
||||
return { key: c.key, label: c.groupTitle };
|
||||
}).concat([{ key: 'other', label: '其他' }]);
|
||||
|
||||
function filterOption(input, option) {
|
||||
var label = (option && (option.label || option.children)) || '';
|
||||
return String(label).toLowerCase().indexOf(String(input || '').toLowerCase()) >= 0;
|
||||
}
|
||||
|
||||
function fmtMoney(n) {
|
||||
if (n === null || n === undefined || n === '') return '-';
|
||||
var x = Number(n);
|
||||
if (isNaN(x)) return '-';
|
||||
if (x === 0) return '-';
|
||||
return x.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
}
|
||||
|
||||
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 initialYear() {
|
||||
try {
|
||||
if (window.dayjs) return window.dayjs('2026-01-01');
|
||||
} catch (e1) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
function numOrZero(v) {
|
||||
if (v === null || v === undefined || v === '') return 0;
|
||||
var n = Number(v);
|
||||
return isNaN(n) ? 0 : n;
|
||||
}
|
||||
|
||||
function pad2(m) {
|
||||
var n = Number(m);
|
||||
if (n < 10) return '0' + n;
|
||||
return String(n);
|
||||
}
|
||||
|
||||
/** 将金额(元)均分到 n 行,保证合计精确 */
|
||||
function splitAcrossN(total, n) {
|
||||
if (n <= 0) return [];
|
||||
var cents = Math.round(numOrZero(total) * 100);
|
||||
if (cents === 0) {
|
||||
var z = [];
|
||||
var j;
|
||||
for (j = 0; j < n; j++) z.push(0);
|
||||
return z;
|
||||
}
|
||||
var each = Math.floor(cents / n);
|
||||
var rem = cents % n;
|
||||
var out = [];
|
||||
var i;
|
||||
for (i = 0; i < n; i++) {
|
||||
out.push((each + (i < rem ? 1 : 0)) / 100);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function sumLedgerRows(monthRows) {
|
||||
var acc = { month: 13, monthLabel: '合计', rowType: 'total', key: 'total' };
|
||||
TRIPLE_KEYS.forEach(function (k) {
|
||||
acc[k + 'Perf'] = 0;
|
||||
acc[k + 'Cost'] = 0;
|
||||
acc[k + 'Profit'] = 0;
|
||||
});
|
||||
acc.otherAmount = 0;
|
||||
(monthRows || []).forEach(function (r) {
|
||||
TRIPLE_KEYS.forEach(function (k) {
|
||||
acc[k + 'Perf'] += numOrZero(r[k + 'Perf']);
|
||||
acc[k + 'Cost'] += numOrZero(r[k + 'Cost']);
|
||||
acc[k + 'Profit'] += numOrZero(r[k + 'Profit']);
|
||||
});
|
||||
acc.otherAmount += numOrZero(r.otherAmount);
|
||||
});
|
||||
TRIPLE_KEYS.forEach(function (k) {
|
||||
if (acc[k + 'Perf'] === 0) acc[k + 'Perf'] = null;
|
||||
if (acc[k + 'Cost'] === 0) acc[k + 'Cost'] = null;
|
||||
if (acc[k + 'Profit'] === 0) acc[k + 'Profit'] = null;
|
||||
});
|
||||
if (acc.otherAmount === 0) acc.otherAmount = null;
|
||||
return acc;
|
||||
}
|
||||
|
||||
/** 演示数据:2026;1 月氢气/申办/ETC 等与业务部汇总台账样例同源占位 */
|
||||
function buildMockYear2026() {
|
||||
var rows = [];
|
||||
var i;
|
||||
for (i = 1; i <= 12; i++) {
|
||||
var selfPerf = 250000 + Math.random() * 100000;
|
||||
var selfCost = selfPerf * (0.8 + Math.random() * 0.1);
|
||||
|
||||
var leasePerf = 180000 + Math.random() * 50000;
|
||||
var leaseCost = leasePerf * (0.6 + Math.random() * 0.1);
|
||||
|
||||
var resalePerf = 300000 + Math.random() * 200000;
|
||||
var resaleCost = resalePerf * (0.7 + Math.random() * 0.15);
|
||||
|
||||
var h2Perf = 80000 + Math.random() * 60000;
|
||||
var h2Cost = h2Perf * (0.6 + Math.random() * 0.2);
|
||||
|
||||
var applyPerf = 3000 + Math.random() * 2000;
|
||||
var applyCost = applyPerf * (0.3 + Math.random() * 0.1);
|
||||
|
||||
var etcPerf = 70000 + Math.random() * 20000;
|
||||
var etcCost = etcPerf * (0.5 + Math.random() * 0.1);
|
||||
|
||||
var otherAmount = 8000 + Math.random() * 5000;
|
||||
|
||||
var src = {
|
||||
selfPerf: Math.round(selfPerf * 100) / 100,
|
||||
selfCost: Math.round(selfCost * 100) / 100,
|
||||
selfProfit: Math.round((selfPerf - selfCost) * 100) / 100,
|
||||
|
||||
leasePerf: Math.round(leasePerf * 100) / 100,
|
||||
leaseCost: Math.round(leaseCost * 100) / 100,
|
||||
leaseProfit: Math.round((leasePerf - leaseCost) * 100) / 100,
|
||||
|
||||
resalePerf: Math.round(resalePerf * 100) / 100,
|
||||
resaleCost: Math.round(resaleCost * 100) / 100,
|
||||
resaleProfit: Math.round((resalePerf - resaleCost) * 100) / 100,
|
||||
|
||||
h2Perf: Math.round(h2Perf * 100) / 100,
|
||||
h2Cost: Math.round(h2Cost * 100) / 100,
|
||||
h2Profit: Math.round((h2Perf - h2Cost) * 100) / 100,
|
||||
|
||||
applyPerf: Math.round(applyPerf * 100) / 100,
|
||||
applyCost: Math.round(applyCost * 100) / 100,
|
||||
applyProfit: Math.round((applyPerf - applyCost) * 100) / 100,
|
||||
|
||||
etcPerf: Math.round(etcPerf * 100) / 100,
|
||||
etcCost: Math.round(etcCost * 100) / 100,
|
||||
etcProfit: Math.round((etcPerf - etcCost) * 100) / 100,
|
||||
|
||||
otherAmount: Math.round(otherAmount * 100) / 100
|
||||
};
|
||||
|
||||
rows.push({
|
||||
key: 'm' + i,
|
||||
month: i,
|
||||
monthLabel: i + '月',
|
||||
rowType: 'month',
|
||||
selfPerf: src.selfPerf, selfCost: src.selfCost, selfProfit: src.selfProfit,
|
||||
leasePerf: src.leasePerf, leaseCost: src.leaseCost, leaseProfit: src.leaseProfit,
|
||||
resalePerf: src.resalePerf, resaleCost: src.resaleCost, resaleProfit: src.resaleProfit,
|
||||
h2Perf: src.h2Perf, h2Cost: src.h2Cost, h2Profit: src.h2Profit,
|
||||
applyPerf: src.applyPerf, applyCost: src.applyCost, applyProfit: src.applyProfit,
|
||||
etcPerf: src.etcPerf, etcCost: src.etcCost, etcProfit: src.etcProfit,
|
||||
otherAmount: src.otherAmount
|
||||
});
|
||||
}
|
||||
rows.push(sumLedgerRows(rows));
|
||||
return rows;
|
||||
}
|
||||
|
||||
function mockSalesmenDrill(month, catKey, cellPerf) {
|
||||
var base = [
|
||||
{ key: 's1', name: '尚建华', ratio: 0.42 },
|
||||
{ key: 's2', name: '刘念念', ratio: 0.35 },
|
||||
{ key: 's3', name: '谯云', ratio: 0.15 },
|
||||
{ key: 's4', name: '董剑煜', ratio: 0.08 }
|
||||
];
|
||||
var total = numOrZero(cellPerf);
|
||||
if (total <= 0) total = 100000;
|
||||
var assigned = 0;
|
||||
return base.map(function (b, idx) {
|
||||
if (idx === base.length - 1) {
|
||||
return { key: b.key, salesperson: b.name, amount: Math.round((total - assigned) * 100) / 100 };
|
||||
}
|
||||
var amt = Math.round(total * b.ratio * 100) / 100;
|
||||
assigned += amt;
|
||||
return { key: b.key, salesperson: b.name, amount: amt };
|
||||
});
|
||||
}
|
||||
|
||||
function mockProjectRows(salesperson, catKey) {
|
||||
var catLabel = (ALL_CAT_FOR_DRILL.find(function (c) { return c.key === catKey; }) || {}).label || catKey;
|
||||
return [
|
||||
{ key: 'p1', projectCode: 'PRJ-2026-001', projectName: catLabel + ' · 嘉兴冷链城配项目', plateNo: '沪A62261F', amount: null, bizDate: '2026-01-08', remark: '演示' },
|
||||
{ key: 'p2', projectCode: 'PRJ-2026-018', projectName: catLabel + ' · 沪浙干线运输', plateNo: '粤AGP3649', amount: null, bizDate: '2026-01-15', remark: '-' },
|
||||
{ key: 'p3', projectCode: 'PRJ-2026-033', projectName: catLabel + ' · 园区短驳', plateNo: '苏E·D32891', amount: null, bizDate: '2026-01-22', remark: '-' }
|
||||
].map(function (r, i, arr) {
|
||||
var share = i === arr.length - 1
|
||||
? 1 - arr.slice(0, -1).reduce(function (acc) { return acc + 0.31; }, 0)
|
||||
: 0.31;
|
||||
return Object.assign({}, r, { amount: Math.round(88000 * share * 100) / 100 });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 自营/租赁业绩钻取:按已选业务部列出业务员行;金额列由主表当月汇总均分(氢费=氢气业绩,电费=申办业绩,与演示口径一致)
|
||||
*/
|
||||
function buildSelfLeaseDrillRows(salesModal, deptApplied, deptOptions, yearApplied) {
|
||||
var src = salesModal.sourceRow;
|
||||
if (!src || salesModal.month == null) {
|
||||
return { rows: [], totals: { main: 0, h2: 0, elec: 0, etc: 0, other: 0 } };
|
||||
}
|
||||
var year = yearApplied && yearApplied.format ? yearApplied.format('YYYY') : '2026';
|
||||
var month = salesModal.month;
|
||||
var ym = year + '-' + pad2(month);
|
||||
var deptValues = (deptApplied && deptApplied.length)
|
||||
? deptApplied.slice()
|
||||
: deptOptions.map(function (o) { return o.value; });
|
||||
var namesPool = ['尚建华', '刘念念', '谯云', '董剑煜', '陈思', '周宁'];
|
||||
var perDept = 2;
|
||||
var n = deptValues.length * perDept;
|
||||
if (n === 0) {
|
||||
return { rows: [], totals: { main: 0, h2: 0, elec: 0, etc: 0, other: 0 } };
|
||||
}
|
||||
var totalMain = numOrZero(salesModal.perf);
|
||||
var totalH2 = numOrZero(src.h2Perf);
|
||||
var totalElec = numOrZero(src.applyPerf);
|
||||
var totalEtc = numOrZero(src.etcPerf);
|
||||
var totalOther = numOrZero(src.otherAmount);
|
||||
var mains = splitAcrossN(totalMain, n);
|
||||
var h2s = splitAcrossN(totalH2, n);
|
||||
var elecs = splitAcrossN(totalElec, n);
|
||||
var etss = splitAcrossN(totalEtc, n);
|
||||
var others = splitAcrossN(totalOther, n);
|
||||
var rows = [];
|
||||
var idx = 0;
|
||||
var d;
|
||||
for (d = 0; d < deptValues.length; d++) {
|
||||
var dv = deptValues[d];
|
||||
var deptLabel = (deptOptions.find(function (o) { return o.value === dv; }) || {}).label || dv;
|
||||
var j;
|
||||
for (j = 0; j < perDept; j++) {
|
||||
rows.push({
|
||||
key: 'drill-' + ym + '-' + String(dv) + '-' + j,
|
||||
ym: ym,
|
||||
monthRowSpan: idx === 0 ? n : 0,
|
||||
deptName: deptLabel,
|
||||
salesperson: namesPool[idx % namesPool.length],
|
||||
mainPerf: mains[idx] || 0,
|
||||
h2PerfCol: h2s[idx] || 0,
|
||||
elecPerf: elecs[idx] || 0,
|
||||
etcPerfCol: etss[idx] || 0,
|
||||
otherAmt: others[idx] || 0
|
||||
});
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
return {
|
||||
rows: rows,
|
||||
totals: {
|
||||
main: totalMain,
|
||||
h2: totalH2,
|
||||
elec: totalElec,
|
||||
etc: totalEtc,
|
||||
other: totalOther
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
var layoutStyle = {
|
||||
padding: '20px 24px 32px',
|
||||
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 =
|
||||
'.biz-standbook-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)}' +
|
||||
'.biz-standbook-table .ant-table-thead>tr>th{white-space:nowrap;color:#1e293b!important;font-weight:600!important;font-size:13px!important;' +
|
||||
'background:#f8fafc!important;border-bottom:1px solid #e2e8f0!important;border-inline-end:1px solid #f1f5f9!important;padding:12px 16px!important;transition:background 0.2s}' +
|
||||
'.biz-standbook-table .ant-table-thead>tr:first-child>th{text-align:center;background:#f1f5f9!important;color:#0f172a!important;font-size:14px!important;border-bottom:2px solid #e2e8f0!important}' +
|
||||
'.biz-standbook-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:12px 16px!important}' +
|
||||
'.biz-standbook-table .ant-table-tbody>tr.biz-row-month:hover>td{background:#f0f9ff!important;color:#0f172a}' +
|
||||
'.biz-standbook-table .ant-table-tbody>tr[data-row-key=\"total\"]>td{font-weight:700;background:#f8fafc!important;color:#0f172a!important;border-top:2px solid #cbd5e1!important;border-bottom:none!important}' +
|
||||
'.biz-standbook-perf-link{cursor:pointer;color:#0ea5e9;padding:4px 8px;margin:-4px -8px;border:none;background:transparent;font:inherit;border-radius:6px;transition:all 0.2s}' +
|
||||
'.biz-standbook-perf-link:hover{color:#0284c7;background:#e0f2fe}' +
|
||||
'.biz-standbook-perf-link:focus{outline:2px solid #38bdf8;outline-offset:2px}' +
|
||||
'@media (prefers-reduced-motion:reduce){.biz-standbook-table .ant-table-tbody>tr,.biz-standbook-perf-link{transition:none}}';
|
||||
|
||||
var deptOptions = useMemo(function () {
|
||||
return [
|
||||
{ value: '业务二部', label: '业务二部' },
|
||||
{ value: '华东业务部', label: '华东业务部' },
|
||||
{ value: '华南业务部', label: '华南业务部' },
|
||||
{ value: '华北业务部', label: '华北业务部' },
|
||||
{ value: '西南业务部', label: '西南业务部' }
|
||||
];
|
||||
}, []);
|
||||
|
||||
var yearDraftState = useState(initialYear);
|
||||
var yearDraft = yearDraftState[0];
|
||||
var setYearDraft = yearDraftState[1];
|
||||
|
||||
var yearAppliedState = useState(initialYear);
|
||||
var yearApplied = yearAppliedState[0];
|
||||
var setYearApplied = yearAppliedState[1];
|
||||
|
||||
/** 业务部多选:空数组表示「全部」 */
|
||||
var deptDraftState = useState([]);
|
||||
var deptDraft = deptDraftState[0];
|
||||
var setDeptDraft = deptDraftState[1];
|
||||
|
||||
var deptAppliedState = useState([]);
|
||||
var deptApplied = deptAppliedState[0];
|
||||
var setDeptApplied = deptAppliedState[1];
|
||||
|
||||
var dataSource = useMemo(function () {
|
||||
var y = yearApplied && yearApplied.format ? yearApplied.format('YYYY') : '';
|
||||
if (y === '2026') return buildMockYear2026();
|
||||
return [];
|
||||
}, [yearApplied]);
|
||||
|
||||
var tableTitle = useMemo(function () {
|
||||
var y = yearApplied && yearApplied.format ? yearApplied.format('YYYY') : '—';
|
||||
return '浙江羚牛氢能业务部汇总台账(' + y + '年度)';
|
||||
}, [yearApplied]);
|
||||
|
||||
/** 与已应用筛选一致:空为全部,多选为「、」连接 */
|
||||
var deptDisplayLabel = useMemo(function () {
|
||||
if (!deptApplied || deptApplied.length === 0) return '全部';
|
||||
return deptApplied.map(function (v) {
|
||||
var o = deptOptions.find(function (x) { return x.value === v; });
|
||||
return o ? o.label : v;
|
||||
}).join('、');
|
||||
}, [deptApplied, deptOptions]);
|
||||
|
||||
var handleQuery = useCallback(function () {
|
||||
setYearApplied(yearDraft);
|
||||
setDeptApplied(deptDraft);
|
||||
}, [yearDraft, deptDraft]);
|
||||
|
||||
var handleReset = useCallback(function () {
|
||||
var y0 = initialYear();
|
||||
setYearDraft(y0);
|
||||
setYearApplied(y0);
|
||||
setDeptDraft([]);
|
||||
setDeptApplied([]);
|
||||
}, []);
|
||||
|
||||
var viewState = useState('main');
|
||||
var view = viewState[0];
|
||||
var setView = viewState[1];
|
||||
|
||||
var salesModalState = useState({ month: null, monthLabel: '', catKey: '', catLabel: '', perf: null, sourceRow: null });
|
||||
var salesModal = salesModalState[0];
|
||||
var setSalesModal = salesModalState[1];
|
||||
|
||||
var projectModalState = useState({ salesperson: '', catKey: '', amount: null });
|
||||
var projectModal = projectModalState[0];
|
||||
var setProjectModal = projectModalState[1];
|
||||
|
||||
var showSelfLeaseDrill = !!((view === 'sales' || view === 'project') && (salesModal.catKey === 'self' || salesModal.catKey === 'lease') && salesModal.sourceRow);
|
||||
|
||||
var salesModalRows = useMemo(function () {
|
||||
if (view === 'main' || salesModal.month == null || !salesModal.catKey) return [];
|
||||
if (salesModal.catKey === 'self' || salesModal.catKey === 'lease') return [];
|
||||
return mockSalesmenDrill(salesModal.month, salesModal.catKey, salesModal.perf);
|
||||
}, [view, salesModal.month, salesModal.catKey, salesModal.perf]);
|
||||
|
||||
var selfLeaseDrillPayload = useMemo(function () {
|
||||
if (view === 'main' || (salesModal.catKey !== 'self' && salesModal.catKey !== 'lease') || !salesModal.sourceRow) {
|
||||
return { rows: [], totals: { main: 0, h2: 0, elec: 0, etc: 0, other: 0 } };
|
||||
}
|
||||
return buildSelfLeaseDrillRows(salesModal, deptApplied, deptOptions, yearApplied);
|
||||
}, [view, salesModal.catKey, salesModal.sourceRow, salesModal.perf, salesModal.month, deptApplied, deptOptions, yearApplied]);
|
||||
|
||||
var selfLeaseDrillTitle = useMemo(function () {
|
||||
if (view === 'main' || salesModal.month == null) return '';
|
||||
var y = yearApplied && yearApplied.format ? yearApplied.format('YYYY') : 'YYYY';
|
||||
var ym = y + '-' + pad2(salesModal.month);
|
||||
var kind = salesModal.catKey === 'lease' ? '租赁' : '自营';
|
||||
return '浙江羚牛氢能业务员' + kind + '业务汇总(' + ym + ')';
|
||||
}, [view, salesModal.month, salesModal.catKey, yearApplied]);
|
||||
|
||||
var projectModalRows = useMemo(function () {
|
||||
if (view !== 'project' || !projectModal.salesperson) return [];
|
||||
return mockProjectRows(projectModal.salesperson, projectModal.catKey || 'self');
|
||||
}, [view, projectModal.salesperson, projectModal.catKey]);
|
||||
|
||||
var openPerfDrill = useCallback(function (row, catKey) {
|
||||
if (row.rowType === 'total') {
|
||||
message.info('合计行不支持钻取,请从各月份对应的「业绩」进入');
|
||||
return;
|
||||
}
|
||||
var v = row[catKey + 'Perf'];
|
||||
if (v === null || v === undefined || v === '' || numOrZero(v) === 0) {
|
||||
message.warning('该单元格无业绩数据');
|
||||
return;
|
||||
}
|
||||
var catLabel = (TRIPLE_DEFS.find(function (c) { return c.key === catKey; }) || {}).groupTitle || catKey;
|
||||
setSalesModal({
|
||||
month: row.month,
|
||||
monthLabel: row.monthLabel,
|
||||
catKey: catKey,
|
||||
catLabel: catLabel,
|
||||
perf: v,
|
||||
sourceRow: (catKey === 'self' || catKey === 'lease') ? row : null
|
||||
});
|
||||
setView('sales');
|
||||
}, []);
|
||||
|
||||
var openProjectDrill = useCallback(function (r) {
|
||||
setProjectModal({
|
||||
salesperson: r.salesperson,
|
||||
catKey: salesModal.catKey,
|
||||
amount: r.amount
|
||||
});
|
||||
setView('project');
|
||||
}, [salesModal.catKey]);
|
||||
|
||||
var openProjectDrillFromDetail = useCallback(function (r) {
|
||||
setProjectModal({
|
||||
salesperson: r.salesperson,
|
||||
catKey: salesModal.catKey,
|
||||
amount: r.mainPerf
|
||||
});
|
||||
setView('project');
|
||||
}, [salesModal.catKey]);
|
||||
|
||||
var salesModalColumns = useMemo(function () {
|
||||
return [
|
||||
{ title: '业务员', dataIndex: 'salesperson', key: 'salesperson', width: 120 },
|
||||
{
|
||||
title: '业绩金额',
|
||||
dataIndex: 'amount',
|
||||
key: 'amount',
|
||||
align: 'right',
|
||||
render: function (v, r) {
|
||||
return React.createElement('button', {
|
||||
type: 'button',
|
||||
className: 'biz-standbook-perf-link',
|
||||
onClick: function () { openProjectDrill(r); }
|
||||
}, fmtMoney(v));
|
||||
}
|
||||
},
|
||||
{ title: '说明', key: 'hint', width: 200, render: function () { return React.createElement('span', { style: { color: 'rgba(0,0,0,0.45)', fontSize: 12 } }, '点击金额查看项目明细'); } }
|
||||
];
|
||||
}, [openProjectDrill]);
|
||||
|
||||
var selfLeaseDrillColumns = useMemo(function () {
|
||||
var mainTitle = salesModal.catKey === 'lease' ? '租赁业绩' : '自营业绩';
|
||||
return [
|
||||
{
|
||||
title: '月份',
|
||||
dataIndex: 'ym',
|
||||
key: 'ym',
|
||||
width: 104,
|
||||
align: 'center',
|
||||
onCell: function (record) {
|
||||
return { rowSpan: record.monthRowSpan };
|
||||
}
|
||||
},
|
||||
{ title: '业务部门', dataIndex: 'deptName', key: 'deptName', width: 124 },
|
||||
{ title: '业务人员', dataIndex: 'salesperson', key: 'salesperson', width: 100 },
|
||||
{
|
||||
title: mainTitle,
|
||||
dataIndex: 'mainPerf',
|
||||
key: 'mainPerf',
|
||||
align: 'right',
|
||||
width: 120,
|
||||
render: function (v, r) {
|
||||
if (v === null || v === undefined || v === '' || numOrZero(v) === 0) {
|
||||
return fmtMoney(v);
|
||||
}
|
||||
return React.createElement('button', {
|
||||
type: 'button',
|
||||
className: 'biz-standbook-perf-link',
|
||||
onClick: function () { openProjectDrillFromDetail(r); }
|
||||
}, fmtMoney(v));
|
||||
}
|
||||
},
|
||||
{ title: '氢费业绩', dataIndex: 'h2PerfCol', key: 'h2PerfCol', align: 'right', width: 118, render: function (v) { return fmtMoney(v); } },
|
||||
{ title: '电费业绩', dataIndex: 'elecPerf', key: 'elecPerf', align: 'right', width: 118, render: function (v) { return fmtMoney(v); } },
|
||||
{ title: 'ETC业绩', dataIndex: 'etcPerfCol', key: 'etcPerfCol', align: 'right', width: 112, render: function (v) { return fmtMoney(v); } },
|
||||
{ title: '其他', dataIndex: 'otherAmt', key: 'otherAmt', align: 'right', width: 100, render: function (v) { return fmtMoney(v); } }
|
||||
];
|
||||
}, [salesModal.catKey, openProjectDrillFromDetail]);
|
||||
|
||||
var projectModalColumns = useMemo(function () {
|
||||
return [
|
||||
{ title: '项目编号', dataIndex: 'projectCode', key: 'projectCode', width: 130 },
|
||||
{ title: '项目名称', dataIndex: 'projectName', key: 'projectName', ellipsis: true },
|
||||
{ title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 110 },
|
||||
{ title: '业绩金额', dataIndex: 'amount', key: 'amount', align: 'right', render: function (v) { return fmtMoney(v); } },
|
||||
{ title: '业务日期', dataIndex: 'bizDate', key: 'bizDate', width: 110 },
|
||||
{ title: '备注', dataIndex: 'remark', key: 'remark', width: 80 }
|
||||
];
|
||||
}, []);
|
||||
|
||||
var handleExport = useCallback(function () {
|
||||
if (!dataSource || dataSource.length === 0) {
|
||||
message.warning('当前无数据可导出,请先查询');
|
||||
return;
|
||||
}
|
||||
var y = yearApplied && yearApplied.format ? yearApplied.format('YYYY') : 'ledger';
|
||||
var headers = ['月份'];
|
||||
TRIPLE_DEFS.forEach(function (c) {
|
||||
headers.push(c.short + '业绩', c.short + '成本', c.short + '利润');
|
||||
});
|
||||
headers.push('其他');
|
||||
var body = [headers];
|
||||
dataSource.forEach(function (r) {
|
||||
var line = [r.monthLabel];
|
||||
TRIPLE_KEYS.forEach(function (k) {
|
||||
line.push(fmtMoney(r[k + 'Perf']), fmtMoney(r[k + 'Cost']), fmtMoney(r[k + 'Profit']));
|
||||
});
|
||||
line.push(fmtMoney(r.otherAmount));
|
||||
body.push(line);
|
||||
});
|
||||
var deptCsv = (deptApplied && deptApplied.length)
|
||||
? deptApplied.map(function (v) {
|
||||
var o = deptOptions.find(function (x) { return x.value === v; });
|
||||
return o ? o.label : v;
|
||||
}).join('、')
|
||||
: '全部';
|
||||
body.push(['业务部', deptCsv]);
|
||||
downloadCsv('业务台账_' + y + '_' + new Date().getTime() + '.csv', body);
|
||||
message.success('已导出');
|
||||
}, [dataSource, yearApplied, deptApplied, deptOptions]);
|
||||
|
||||
var ledgerColumns = useMemo(function () {
|
||||
var cols = [
|
||||
{
|
||||
title: '月份',
|
||||
dataIndex: 'monthLabel',
|
||||
key: 'monthLabel',
|
||||
fixed: 'left',
|
||||
width: 76,
|
||||
align: 'center',
|
||||
render: function (t, r) {
|
||||
if (r.rowType === 'total') return React.createElement('span', { style: { fontWeight: 700 } }, t);
|
||||
return t;
|
||||
}
|
||||
}
|
||||
];
|
||||
TRIPLE_DEFS.forEach(function (cat) {
|
||||
var ck = cat.key;
|
||||
var s = cat.short;
|
||||
cols.push({
|
||||
title: cat.groupTitle,
|
||||
key: 'grp-' + ck,
|
||||
align: 'center',
|
||||
children: [
|
||||
{
|
||||
title: s + '业绩',
|
||||
dataIndex: ck + 'Perf',
|
||||
key: ck + 'Perf',
|
||||
width: 112,
|
||||
align: 'right',
|
||||
render: function (v, row) {
|
||||
if (row.rowType === 'total' || v === null || v === undefined || v === '' || numOrZero(v) === 0) {
|
||||
return fmtMoney(v);
|
||||
}
|
||||
return React.createElement('button', {
|
||||
type: 'button',
|
||||
className: 'biz-standbook-perf-link',
|
||||
onClick: function () { openPerfDrill(row, ck); }
|
||||
}, fmtMoney(v));
|
||||
}
|
||||
},
|
||||
{
|
||||
title: s + '成本',
|
||||
dataIndex: ck + 'Cost',
|
||||
key: ck + 'Cost',
|
||||
width: 112,
|
||||
align: 'right',
|
||||
render: function (vmt) { return fmtMoney(vmt); }
|
||||
},
|
||||
{
|
||||
title: s + '利润',
|
||||
dataIndex: ck + 'Profit',
|
||||
key: ck + 'Profit',
|
||||
width: 112,
|
||||
align: 'right',
|
||||
render: function (vp) { return fmtMoney(vp); }
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
cols.push({
|
||||
title: '其他',
|
||||
dataIndex: 'otherAmount',
|
||||
key: 'otherAmount',
|
||||
width: 108,
|
||||
align: 'right',
|
||||
render: function (vo) { return fmtMoney(vo); }
|
||||
});
|
||||
return cols;
|
||||
}, [openPerfDrill]);
|
||||
|
||||
var rowClassName = useCallback(function (record) {
|
||||
if (record.rowType === 'total') return '';
|
||||
return 'biz-row-month';
|
||||
}, []);
|
||||
|
||||
var renderMainView = function () {
|
||||
return React.createElement(React.Fragment, null,
|
||||
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(DatePicker, {
|
||||
picker: 'year',
|
||||
style: filterControlStyle,
|
||||
placeholder: '请选择年份',
|
||||
format: 'YYYY',
|
||||
value: yearDraft,
|
||||
onChange: function (v) { setYearDraft(v); }
|
||||
})
|
||||
)
|
||||
),
|
||||
React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 6 },
|
||||
React.createElement('div', { style: filterItemStyle },
|
||||
React.createElement('div', { style: filterLabelStyle }, '业务部'),
|
||||
React.createElement(Select, {
|
||||
mode: 'multiple',
|
||||
placeholder: '全部',
|
||||
style: filterControlStyle,
|
||||
value: deptDraft,
|
||||
onChange: function (v) { setDeptDraft(v || []); },
|
||||
options: deptOptions,
|
||||
showSearch: true,
|
||||
allowClear: true,
|
||||
maxTagCount: 2,
|
||||
filterOption: filterOption
|
||||
})
|
||||
)
|
||||
),
|
||||
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' } }, tableTitle),
|
||||
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 } },
|
||||
'业务部:',
|
||||
deptDisplayLabel
|
||||
),
|
||||
React.createElement('div', { className: 'biz-standbook-table-wrap' },
|
||||
React.createElement(Table, {
|
||||
className: 'biz-standbook-table',
|
||||
size: 'small',
|
||||
bordered: true,
|
||||
rowKey: 'key',
|
||||
columns: ledgerColumns,
|
||||
dataSource: dataSource,
|
||||
pagination: false,
|
||||
rowClassName: rowClassName,
|
||||
scroll: { x: 'max-content', y: 520 },
|
||||
sticky: true
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
var renderSalesView = function () {
|
||||
var titleText = showSelfLeaseDrill ? selfLeaseDrillTitle : ('业务员业绩 — ' + (salesModal.monthLabel || '') + ' / ' + (salesModal.catLabel || ''));
|
||||
return React.createElement(Card, { style: tableCardStyle, bodyStyle: { padding: '24px' } },
|
||||
React.createElement('div', { style: { display: 'flex', alignItems: 'center', marginBottom: 20 } },
|
||||
React.createElement(Button, {
|
||||
onClick: function () { setView('main'); },
|
||||
style: { marginRight: 16 }
|
||||
}, '返回上一步'),
|
||||
React.createElement('div', { style: { fontSize: 18, fontWeight: 700, color: '#0f172a' } }, titleText)
|
||||
),
|
||||
showSelfLeaseDrill
|
||||
? React.createElement('div', { className: 'biz-standbook-table-wrap' },
|
||||
React.createElement(Table, {
|
||||
className: 'biz-standbook-table',
|
||||
size: 'small',
|
||||
bordered: true,
|
||||
rowKey: 'key',
|
||||
columns: selfLeaseDrillColumns,
|
||||
dataSource: selfLeaseDrillPayload.rows,
|
||||
pagination: false,
|
||||
scroll: { x: 'max-content' },
|
||||
summary: function () {
|
||||
var totals = selfLeaseDrillPayload.totals;
|
||||
var Sm = Table.Summary;
|
||||
var Row = Sm.Row;
|
||||
var Cell = Sm.Cell;
|
||||
return React.createElement(Sm, null,
|
||||
React.createElement(Row, null,
|
||||
React.createElement(Cell, { index: 0, colSpan: 3, align: 'center' }, '合计'),
|
||||
React.createElement(Cell, { index: 3, align: 'right' }, fmtMoney(totals.main)),
|
||||
React.createElement(Cell, { index: 4, align: 'right' }, fmtMoney(totals.h2)),
|
||||
React.createElement(Cell, { index: 5, align: 'right' }, fmtMoney(totals.elec)),
|
||||
React.createElement(Cell, { index: 6, align: 'right' }, fmtMoney(totals.etc)),
|
||||
React.createElement(Cell, { index: 7, align: 'right' }, fmtMoney(totals.other))
|
||||
)
|
||||
);
|
||||
}
|
||||
})
|
||||
)
|
||||
: React.createElement(React.Fragment, null,
|
||||
React.createElement('div', { style: { marginBottom: 16, color: '#64748b', fontSize: 14 } },
|
||||
'从总表钻取:本业务线下各业务员业绩构成。点击「业绩金额」继续查看项目明细。'
|
||||
),
|
||||
React.createElement('div', { className: 'biz-standbook-table-wrap' },
|
||||
React.createElement(Table, {
|
||||
className: 'biz-standbook-table',
|
||||
size: 'small',
|
||||
bordered: true,
|
||||
rowKey: 'key',
|
||||
columns: salesModalColumns,
|
||||
dataSource: salesModalRows,
|
||||
pagination: false,
|
||||
scroll: { x: 'max-content' }
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
var renderProjectView = function () {
|
||||
var titleText = '项目明细 — ' + (projectModal.salesperson || '') + ' · ' + ((TRIPLE_DEFS.find(function (c) { return c.key === projectModal.catKey; }) || {}).groupTitle || '');
|
||||
return React.createElement(Card, { style: tableCardStyle, bodyStyle: { padding: '24px' } },
|
||||
React.createElement('div', { style: { display: 'flex', alignItems: 'center', marginBottom: 20 } },
|
||||
React.createElement(Button, {
|
||||
onClick: function () { setView('sales'); },
|
||||
style: { marginRight: 16 }
|
||||
}, '返回上一步'),
|
||||
React.createElement('div', { style: { fontSize: 18, fontWeight: 700, color: '#0f172a' } }, titleText)
|
||||
),
|
||||
React.createElement('div', { style: { marginBottom: 16, color: '#64748b', fontSize: 14 } },
|
||||
'二级钻取:该项目业务员名下具体项目/车辆维度业绩(演示数据)。'
|
||||
),
|
||||
React.createElement('div', { className: 'biz-standbook-table-wrap' },
|
||||
React.createElement(Table, {
|
||||
className: 'biz-standbook-table',
|
||||
size: 'small',
|
||||
bordered: true,
|
||||
rowKey: 'key',
|
||||
columns: projectModalColumns,
|
||||
dataSource: projectModalRows,
|
||||
pagination: false,
|
||||
scroll: { x: 'max-content' }
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
var breadcrumbItems = [{ title: '数据分析' }, { title: '业务台账' }];
|
||||
if (view === 'sales' || view === 'project') {
|
||||
breadcrumbItems.push({ title: showSelfLeaseDrill ? '业务员汇总' : '业务员业绩' });
|
||||
}
|
||||
if (view === 'project') {
|
||||
breadcrumbItems.push({ title: '项目明细' });
|
||||
}
|
||||
|
||||
return React.createElement(App, null,
|
||||
React.createElement('style', null, ledgerTableStyle),
|
||||
React.createElement('div', { style: layoutStyle },
|
||||
React.createElement(Breadcrumb, { style: { marginBottom: 14 }, items: breadcrumbItems }),
|
||||
view === 'main' ? renderMainView() : null,
|
||||
view === 'sales' ? renderSalesView() : null,
|
||||
view === 'project' ? renderProjectView() : null
|
||||
)
|
||||
);
|
||||
};
|
||||
2468
web端/数据分析/业务部台账.jsx
Normal file
2468
web端/数据分析/业务部台账.jsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -108,53 +108,59 @@ const Component = function () {
|
||||
*/
|
||||
function buildMockYear2026() {
|
||||
var rows = [];
|
||||
var template = [
|
||||
{
|
||||
month: 1,
|
||||
selfPerf: 285000.5, selfCost: 240000, selfProfit: 45000.5,
|
||||
leasePerf: 188000, leaseCost: 120000, leaseProfit: 68000,
|
||||
salesPerf: 420000, salesCost: 310000, salesProfit: 110000,
|
||||
inspectionPerf: 131241.59, inspectionCost: 88000, inspectionProfit: 43241.59,
|
||||
agencyPerf: 4004.73, agencyCost: 1200, agencyProfit: 2804.73,
|
||||
etcPerf: 79750.92, etcCost: 45000, etcProfit: 34750.92,
|
||||
otherPerf: 12000, otherCost: 5000, otherProfit: 7000
|
||||
},
|
||||
{
|
||||
month: 2,
|
||||
selfPerf: 260000, selfCost: 230000, selfProfit: 30000,
|
||||
leasePerf: 195000, leaseCost: 125000, leaseProfit: 70000,
|
||||
salesPerf: null, salesCost: null, salesProfit: null,
|
||||
inspectionPerf: 98000, inspectionCost: 60000, inspectionProfit: 38000,
|
||||
agencyPerf: 3200, agencyCost: 1000, agencyProfit: 2200,
|
||||
etcPerf: 72000, etcCost: 40000, etcProfit: 32000,
|
||||
otherPerf: null, otherCost: null, otherProfit: null
|
||||
},
|
||||
{
|
||||
month: 3,
|
||||
selfPerf: 270000, selfCost: 235000, selfProfit: 35000,
|
||||
leasePerf: 200000, leaseCost: 128000, leaseProfit: 72000,
|
||||
salesPerf: 380000, salesCost: 290000, salesProfit: 90000,
|
||||
inspectionPerf: 105000, inspectionCost: 70000, inspectionProfit: 35000,
|
||||
agencyPerf: 4100, agencyCost: 1100, agencyProfit: 3000,
|
||||
etcPerf: 81000, etcCost: 43000, etcProfit: 38000,
|
||||
otherPerf: 8500, otherCost: 3000, otherProfit: 5500
|
||||
}
|
||||
];
|
||||
var i;
|
||||
for (i = 1; i <= 12; i++) {
|
||||
var src = template[i - 1];
|
||||
if (!src) {
|
||||
src = {
|
||||
month: i,
|
||||
selfPerf: null, selfCost: null, selfProfit: null,
|
||||
leasePerf: null, leaseCost: null, leaseProfit: null,
|
||||
salesPerf: null, salesCost: null, salesProfit: null,
|
||||
inspectionPerf: null, inspectionCost: null, inspectionProfit: null,
|
||||
agencyPerf: null, agencyCost: null, agencyProfit: null,
|
||||
etcPerf: null, etcCost: null, etcProfit: null,
|
||||
otherPerf: null, otherCost: null, otherProfit: null
|
||||
};
|
||||
}
|
||||
var selfPerf = 250000 + Math.random() * 100000;
|
||||
var selfCost = selfPerf * (0.8 + Math.random() * 0.1);
|
||||
|
||||
var leasePerf = 180000 + Math.random() * 50000;
|
||||
var leaseCost = leasePerf * (0.6 + Math.random() * 0.1);
|
||||
|
||||
var salesPerf = 300000 + Math.random() * 200000;
|
||||
var salesCost = salesPerf * (0.7 + Math.random() * 0.15);
|
||||
|
||||
var inspectionPerf = 80000 + Math.random() * 60000;
|
||||
var inspectionCost = inspectionPerf * (0.6 + Math.random() * 0.2);
|
||||
|
||||
var agencyPerf = 3000 + Math.random() * 2000;
|
||||
var agencyCost = agencyPerf * (0.3 + Math.random() * 0.1);
|
||||
|
||||
var etcPerf = 70000 + Math.random() * 20000;
|
||||
var etcCost = etcPerf * (0.5 + Math.random() * 0.1);
|
||||
|
||||
var otherPerf = 8000 + Math.random() * 5000;
|
||||
var otherCost = otherPerf * (0.4 + Math.random() * 0.2);
|
||||
|
||||
var src = {
|
||||
selfPerf: Math.round(selfPerf * 100) / 100,
|
||||
selfCost: Math.round(selfCost * 100) / 100,
|
||||
selfProfit: Math.round((selfPerf - selfCost) * 100) / 100,
|
||||
|
||||
leasePerf: Math.round(leasePerf * 100) / 100,
|
||||
leaseCost: Math.round(leaseCost * 100) / 100,
|
||||
leaseProfit: Math.round((leasePerf - leaseCost) * 100) / 100,
|
||||
|
||||
salesPerf: Math.round(salesPerf * 100) / 100,
|
||||
salesCost: Math.round(salesCost * 100) / 100,
|
||||
salesProfit: Math.round((salesPerf - salesCost) * 100) / 100,
|
||||
|
||||
inspectionPerf: Math.round(inspectionPerf * 100) / 100,
|
||||
inspectionCost: Math.round(inspectionCost * 100) / 100,
|
||||
inspectionProfit: Math.round((inspectionPerf - inspectionCost) * 100) / 100,
|
||||
|
||||
agencyPerf: Math.round(agencyPerf * 100) / 100,
|
||||
agencyCost: Math.round(agencyCost * 100) / 100,
|
||||
agencyProfit: Math.round((agencyPerf - agencyCost) * 100) / 100,
|
||||
|
||||
etcPerf: Math.round(etcPerf * 100) / 100,
|
||||
etcCost: Math.round(etcCost * 100) / 100,
|
||||
etcProfit: Math.round((etcPerf - etcCost) * 100) / 100,
|
||||
|
||||
otherPerf: Math.round(otherPerf * 100) / 100,
|
||||
otherCost: Math.round(otherCost * 100) / 100,
|
||||
otherProfit: Math.round((otherPerf - otherCost) * 100) / 100
|
||||
};
|
||||
|
||||
rows.push({
|
||||
key: 'm' + i,
|
||||
month: i,
|
||||
|
||||
752
web端/数据分析/客户服务部业务统计报表.jsx
Normal file
752
web端/数据分析/客户服务部业务统计报表.jsx
Normal file
@@ -0,0 +1,752 @@
|
||||
// 【重要】必须使用 const Component 作为组件变量名
|
||||
// 数据分析 - 客户服务部业务统计报表(依据业务台账模版数据统计方案)
|
||||
// 经营总览 KPI + 租赁/物流/能源/成本账户/未收预警;支持汇总→明细钻取(原型演示数据)
|
||||
|
||||
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 Tabs = antd.Tabs;
|
||||
var Space = antd.Space;
|
||||
var Modal = antd.Modal;
|
||||
var Tag = antd.Tag;
|
||||
var Statistic = antd.Statistic;
|
||||
var message = antd.message;
|
||||
|
||||
function filterOption(input, option) {
|
||||
var label = (option && (option.label || option.children)) || '';
|
||||
return String(label).toLowerCase().indexOf(String(input || '').toLowerCase()) >= 0;
|
||||
}
|
||||
|
||||
function fmtMoney(n) {
|
||||
if (n === null || n === undefined || n === '') return '—';
|
||||
var x = Number(n);
|
||||
if (isNaN(x)) return '—';
|
||||
return x.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
}
|
||||
|
||||
function fmtPct(n) {
|
||||
if (n === null || n === undefined || n === '') return '—';
|
||||
var x = Number(n);
|
||||
if (isNaN(x)) return '—';
|
||||
return (x * 100).toFixed(1) + '%';
|
||||
}
|
||||
|
||||
function numOrZero(v) {
|
||||
if (v === null || v === undefined || v === '') return 0;
|
||||
var n = Number(v);
|
||||
return isNaN(n) ? 0 : n;
|
||||
}
|
||||
|
||||
function escapeCsv(v) {
|
||||
var s = v == null ? '' : String(v);
|
||||
if (s.indexOf(',') !== -1 || s.indexOf('"') !== -1 || s.indexOf('\n') !== -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 initialYear() {
|
||||
try {
|
||||
if (window.dayjs) return window.dayjs('2026-01-01');
|
||||
} catch (e1) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getAppliedYearMonth(yearApplied, monthApplied) {
|
||||
var y = yearApplied && yearApplied.format ? yearApplied.format('YYYY') : '2026';
|
||||
var m = monthApplied != null && monthApplied !== '' ? Number(monthApplied) : null;
|
||||
return { year: y, month: m };
|
||||
}
|
||||
|
||||
var SALESPERSONS = ['谈云', '刘念念', '谯云', '董剑煜', '尚建华'];
|
||||
var CUSTOMERS = [
|
||||
'嘉兴古道物流有限公司', '杭州绿道城配科技有限公司', '宁波港联氢运物流有限公司',
|
||||
'上海虹钦物流有限公司', '嘉兴市乍浦港口经营有限公司', '荣达餐饮(广东)集团有限公司'
|
||||
];
|
||||
|
||||
function buildMockKpi(ym) {
|
||||
var seed = Number(ym.year) * 100 + (ym.month || 5);
|
||||
return {
|
||||
totalRevenue: 4280000 + (seed % 17) * 12000,
|
||||
totalProfit: 612000 + (seed % 11) * 8000,
|
||||
uncollected: 386500 + (seed % 9) * 5000,
|
||||
accountBalance: 1258000 + (seed % 13) * 3000,
|
||||
activeVehicles: 186 + (seed % 7)
|
||||
};
|
||||
}
|
||||
|
||||
function buildOverviewRows(ym) {
|
||||
var lines = [
|
||||
{ key: 'lease', line: '租赁业务', receivable: 1280000, received: 1120000, cost: 720000, profit: 400000 },
|
||||
{ key: 'logistics', line: '物流业务', receivable: 2100000, received: 1980000, cost: 1650000, profit: 330000 },
|
||||
{ key: 'energy', line: '能源销售', receivable: 680000, received: 620000, cost: 480000, profit: 140000 },
|
||||
{ key: 'etc', line: 'ETC及其他', receivable: 220000, received: 210000, cost: 95000, profit: 115000 }
|
||||
];
|
||||
return lines.map(function (r) {
|
||||
var uncollected = r.receivable - r.received;
|
||||
return Object.assign({}, r, {
|
||||
uncollected: uncollected,
|
||||
profitRate: r.receivable > 0 ? r.profit / r.receivable : 0,
|
||||
year: ym.year,
|
||||
monthLabel: ym.month ? ym.month + '月' : '全年累计'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function buildLeaseSummary(ym, filters) {
|
||||
var rows = [];
|
||||
var i;
|
||||
for (i = 0; i < 8; i++) {
|
||||
var sp = SALESPERSONS[i % SALESPERSONS.length];
|
||||
var cu = CUSTOMERS[i % CUSTOMERS.length];
|
||||
if (filters.salesperson && sp !== filters.salesperson) continue;
|
||||
if (filters.customer && cu !== filters.customer) continue;
|
||||
var recv = 120000 + i * 18500;
|
||||
var got = recv - (i % 3 === 0 ? 22000 : 0);
|
||||
var cost = recv * 0.58;
|
||||
rows.push({
|
||||
key: 'lease-' + i,
|
||||
year: ym.year,
|
||||
month: ym.month || 5,
|
||||
salesperson: sp,
|
||||
customerName: cu,
|
||||
receivable: recv,
|
||||
uncollected: recv - got,
|
||||
cost: cost,
|
||||
profit: got - cost,
|
||||
profitRate: recv > 0 ? (got - cost) / recv : 0
|
||||
});
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
function buildLogisticsSummary(ym, filters) {
|
||||
var projects = ['沪浙干线', '嘉兴冷链城配', '宁波港区短驳', '盒马城配专线'];
|
||||
var rows = [];
|
||||
projects.forEach(function (p, i) {
|
||||
var sp = SALESPERSONS[i % SALESPERSONS.length];
|
||||
var cu = CUSTOMERS[(i + 1) % CUSTOMERS.length];
|
||||
if (filters.salesperson && sp !== filters.salesperson) return;
|
||||
if (filters.customer && cu !== filters.customer) return;
|
||||
var orders = 120 + i * 35;
|
||||
var recv = 280000 + i * 95000;
|
||||
var got = recv - (i === 2 ? 45000 : 8000);
|
||||
rows.push({
|
||||
key: 'log-' + i,
|
||||
month: ym.month || 5,
|
||||
salesperson: sp,
|
||||
projectName: p,
|
||||
customerName: cu,
|
||||
orderCount: orders,
|
||||
receivable: recv,
|
||||
received: got,
|
||||
uncollected: recv - got,
|
||||
invoiceAmount: got * 0.95
|
||||
});
|
||||
});
|
||||
return rows;
|
||||
}
|
||||
|
||||
function buildEnergySummary(ym) {
|
||||
return CUSTOMERS.slice(0, 5).map(function (c, i) {
|
||||
var kg = 4200 + i * 680;
|
||||
var cost = kg * 32;
|
||||
var sales = kg * 38;
|
||||
return {
|
||||
key: 'eng-' + i,
|
||||
year: ym.year,
|
||||
month: ym.month || 5,
|
||||
customerName: c,
|
||||
salesperson: SALESPERSONS[i % SALESPERSONS.length],
|
||||
h2Kg: kg,
|
||||
costAmount: cost,
|
||||
salesAmount: sales,
|
||||
grossProfit: sales - cost,
|
||||
collectStatus: i % 2 === 0 ? '已收款' : '待收款'
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function buildAccountRows() {
|
||||
return CUSTOMERS.slice(0, 6).map(function (c, i) {
|
||||
var open = 50000 + i * 12000;
|
||||
var recharge = 200000 + i * 30000;
|
||||
var usedH2 = 120000 + i * 22000;
|
||||
var usedElec = 45000 + i * 8000;
|
||||
return {
|
||||
key: 'acc-' + i,
|
||||
customerName: c,
|
||||
salesperson: SALESPERSONS[i % SALESPERSONS.length],
|
||||
openBalance: open,
|
||||
recharge: recharge,
|
||||
usedH2: usedH2,
|
||||
usedElec: usedElec,
|
||||
closeBalance: open + recharge - usedH2 - usedElec
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function buildUncollectedRows(ym) {
|
||||
var rows = [];
|
||||
var i;
|
||||
for (i = 0; i < 10; i++) {
|
||||
var recv = 80000 + i * 12000;
|
||||
var uncol = 15000 + (i % 4) * 8000;
|
||||
rows.push({
|
||||
key: 'unc-' + i,
|
||||
line: i % 2 === 0 ? '租赁' : '物流',
|
||||
salesperson: SALESPERSONS[i % SALESPERSONS.length],
|
||||
customerName: CUSTOMERS[i % CUSTOMERS.length],
|
||||
plateNo: '浙F0' + (3280 + i) + 'F',
|
||||
dueDate: '2026-0' + ((i % 5) + 1) + '-15',
|
||||
receivable: recv,
|
||||
uncollected: uncol,
|
||||
overdueDays: (i % 5) * 7 + 3
|
||||
});
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
function mockLeaseDetail(row) {
|
||||
return [
|
||||
{ key: 'd1', plateNo: '浙F03298F', receivable: row.receivable * 0.4, received: row.receivable * 0.35, uncollected: row.receivable * 0.05 },
|
||||
{ key: 'd2', plateNo: '粤AGP3649', receivable: row.receivable * 0.35, received: row.receivable * 0.32, uncollected: row.receivable * 0.03 },
|
||||
{ key: 'd3', plateNo: '沪A62261F', receivable: row.receivable * 0.25, received: row.receivable * 0.2, uncollected: row.receivable * 0.05 }
|
||||
];
|
||||
}
|
||||
|
||||
function mockLogisticsDetail(row) {
|
||||
return [
|
||||
{ key: 'd1', plateNo: '浙F05519F', tripDate: '2026-05-08', amount: row.receivable * 0.45, h2Fee: 3200, etcFee: 890 },
|
||||
{ key: 'd2', plateNo: '浙F02698F', tripDate: '2026-05-10', amount: row.receivable * 0.35, h2Fee: 2800, etcFee: 720 },
|
||||
{ key: 'd3', plateNo: '粤AGR5099', tripDate: '2026-05-12', amount: row.receivable * 0.2, h2Fee: 1500, etcFee: 410 }
|
||||
];
|
||||
}
|
||||
|
||||
var layoutStyle = { padding: '16px 24px', background: '#f5f5f5', minHeight: '100vh' };
|
||||
var filterLabelStyle = { marginBottom: 6, fontSize: 14, color: 'rgba(0,0,0,0.65)' };
|
||||
var filterItemStyle = { marginBottom: 12 };
|
||||
var filterControlStyle = { width: '100%' };
|
||||
var profitNegStyle = { background: '#fff1f0', padding: '2px 8px', borderRadius: 4, display: 'inline-block' };
|
||||
var drillLinkStyle = { cursor: 'pointer', color: '#1677ff', border: 'none', background: 'none', padding: 0, font: 'inherit' };
|
||||
|
||||
var tableStyle =
|
||||
'.cs-dept-stat-table .ant-table-thead>tr>th{background:#e6f4ff!important;font-weight:500;font-size:12px}' +
|
||||
'.cs-dept-stat-table .ant-table-tbody>tr>td{white-space:nowrap}' +
|
||||
'.cs-dept-kpi .ant-statistic-title{font-size:13px;color:rgba(0,0,0,0.55)}';
|
||||
|
||||
var yearDraftState = useState(initialYear);
|
||||
var yearDraft = yearDraftState[0];
|
||||
var setYearDraft = yearDraftState[1];
|
||||
var yearAppliedState = useState(initialYear);
|
||||
var yearApplied = yearAppliedState[0];
|
||||
var setYearApplied = yearAppliedState[1];
|
||||
|
||||
var monthDraftState = useState(5);
|
||||
var monthDraft = monthDraftState[0];
|
||||
var setMonthDraft = monthDraftState[1];
|
||||
var monthAppliedState = useState(5);
|
||||
var monthApplied = monthAppliedState[0];
|
||||
var setMonthApplied = monthAppliedState[1];
|
||||
|
||||
var spDraftState = useState(undefined);
|
||||
var spDraft = spDraftState[0];
|
||||
var setSpDraft = spDraftState[1];
|
||||
var spAppliedState = useState(undefined);
|
||||
var spApplied = spAppliedState[0];
|
||||
var setSpApplied = spAppliedState[1];
|
||||
|
||||
var cuDraftState = useState(undefined);
|
||||
var cuDraft = cuDraftState[0];
|
||||
var setCuDraft = cuDraftState[1];
|
||||
var cuAppliedState = useState(undefined);
|
||||
var cuApplied = cuAppliedState[0];
|
||||
var setCuApplied = cuAppliedState[1];
|
||||
|
||||
var activeTabState = useState('overview');
|
||||
var activeTab = activeTabState[0];
|
||||
var setActiveTab = activeTabState[1];
|
||||
|
||||
var drillState = useState({ open: false, type: '', title: '', rows: [] });
|
||||
var drill = drillState[0];
|
||||
var setDrill = drillState[1];
|
||||
|
||||
var ym = useMemo(function () {
|
||||
return getAppliedYearMonth(yearApplied, monthApplied);
|
||||
}, [yearApplied, monthApplied]);
|
||||
|
||||
var filters = useMemo(function () {
|
||||
return { salesperson: spApplied, customer: cuApplied };
|
||||
}, [spApplied, cuApplied]);
|
||||
|
||||
var kpi = useMemo(function () { return buildMockKpi(ym); }, [ym]);
|
||||
var overviewRows = useMemo(function () { return buildOverviewRows(ym); }, [ym]);
|
||||
var leaseRows = useMemo(function () { return buildLeaseSummary(ym, filters); }, [ym, filters]);
|
||||
var logisticsRows = useMemo(function () { return buildLogisticsSummary(ym, filters); }, [ym, filters]);
|
||||
var energyRows = useMemo(function () { return buildEnergySummary(ym); }, [ym]);
|
||||
var accountRows = useMemo(function () { return buildAccountRows(); }, []);
|
||||
var uncollectedRows = useMemo(function () { return buildUncollectedRows(ym); }, [ym]);
|
||||
|
||||
var monthOptions = useMemo(function () {
|
||||
var opts = [{ value: '', label: '全年' }];
|
||||
var m;
|
||||
for (m = 1; m <= 12; m++) opts.push({ value: m, label: m + '月' });
|
||||
return opts;
|
||||
}, []);
|
||||
|
||||
var spOptions = useMemo(function () {
|
||||
return [{ value: '', label: '全部业务员' }].concat(SALESPERSONS.map(function (s) { return { value: s, label: s }; }));
|
||||
}, []);
|
||||
|
||||
var cuOptions = useMemo(function () {
|
||||
return [{ value: '', label: '全部客户' }].concat(CUSTOMERS.map(function (c) { return { value: c, label: c }; }));
|
||||
}, []);
|
||||
|
||||
var handleQuery = useCallback(function () {
|
||||
setYearApplied(yearDraft);
|
||||
setMonthApplied(monthDraft === '' ? null : monthDraft);
|
||||
setSpApplied(spDraft || undefined);
|
||||
setCuApplied(cuDraft || undefined);
|
||||
}, [yearDraft, monthDraft, spDraft, cuDraft]);
|
||||
|
||||
var handleReset = useCallback(function () {
|
||||
var y0 = initialYear();
|
||||
setYearDraft(y0);
|
||||
setYearApplied(y0);
|
||||
setMonthDraft(5);
|
||||
setMonthApplied(5);
|
||||
setSpDraft(undefined);
|
||||
setSpApplied(undefined);
|
||||
setCuDraft(undefined);
|
||||
setCuApplied(undefined);
|
||||
}, []);
|
||||
|
||||
var openDrill = useCallback(function (type, row) {
|
||||
var rows = [];
|
||||
var title = '';
|
||||
if (type === 'lease') {
|
||||
rows = mockLeaseDetail(row);
|
||||
title = '租赁车辆明细 · ' + row.customerName;
|
||||
} else if (type === 'logistics') {
|
||||
rows = mockLogisticsDetail(row);
|
||||
title = '物流运单明细 · ' + row.projectName;
|
||||
}
|
||||
setDrill({ open: true, type: type, title: title, rows: rows });
|
||||
}, []);
|
||||
|
||||
var closeDrill = useCallback(function () {
|
||||
setDrill({ open: false, type: '', title: '', rows: [] });
|
||||
}, []);
|
||||
|
||||
var pageTitle = useMemo(function () {
|
||||
var m = monthApplied ? monthApplied + '月' : '全年';
|
||||
return ym.year + '年' + m + ' · 客户服务部业务统计';
|
||||
}, [ym, monthApplied]);
|
||||
|
||||
var overviewColumns = useMemo(function () {
|
||||
return [
|
||||
{ title: '业务条线', dataIndex: 'line', key: 'line', width: 100, fixed: 'left' },
|
||||
{ title: '应收', dataIndex: 'receivable', key: 'receivable', align: 'right', render: fmtMoney },
|
||||
{ title: '实收', dataIndex: 'received', key: 'received', align: 'right', render: fmtMoney },
|
||||
{ title: '未收', dataIndex: 'uncollected', key: 'uncollected', align: 'right', render: function (v) {
|
||||
var n = Number(v);
|
||||
if (!isNaN(n) && n > 0) return React.createElement('span', { style: { color: '#f53f3f' } }, fmtMoney(v));
|
||||
return fmtMoney(v);
|
||||
}},
|
||||
{ title: '成本', dataIndex: 'cost', key: 'cost', align: 'right', render: fmtMoney },
|
||||
{ title: '利润', dataIndex: 'profit', key: 'profit', align: 'right', render: function (v) {
|
||||
var n = Number(v);
|
||||
var neg = !isNaN(n) && n < 0;
|
||||
return neg ? React.createElement('span', { style: profitNegStyle }, fmtMoney(v)) : fmtMoney(v);
|
||||
}},
|
||||
{ title: '利润率', dataIndex: 'profitRate', key: 'profitRate', align: 'right', width: 88, render: fmtPct }
|
||||
];
|
||||
}, []);
|
||||
|
||||
var leaseColumns = useMemo(function () {
|
||||
return [
|
||||
{ title: '业务员', dataIndex: 'salesperson', key: 'salesperson', width: 88 },
|
||||
{ title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 200, ellipsis: true },
|
||||
{ title: '应收', dataIndex: 'receivable', key: 'receivable', align: 'right', render: fmtMoney },
|
||||
{ title: '未收', dataIndex: 'uncollected', key: 'uncollected', align: 'right', render: fmtMoney },
|
||||
{ title: '成本', dataIndex: 'cost', key: 'cost', align: 'right', render: fmtMoney },
|
||||
{ title: '利润', dataIndex: 'profit', key: 'profit', align: 'right', render: function (v) { return fmtMoney(v); } },
|
||||
{ title: '利润率', dataIndex: 'profitRate', key: 'profitRate', align: 'right', render: fmtPct },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 88,
|
||||
fixed: 'right',
|
||||
render: function (_, r) {
|
||||
return React.createElement('button', { type: 'button', style: drillLinkStyle, onClick: function () { openDrill('lease', r); } }, '明细');
|
||||
}
|
||||
}
|
||||
];
|
||||
}, [openDrill]);
|
||||
|
||||
var logisticsColumns = useMemo(function () {
|
||||
return [
|
||||
{ title: '业务员', dataIndex: 'salesperson', key: 'salesperson', width: 88 },
|
||||
{ title: '项目名称', dataIndex: 'projectName', key: 'projectName', width: 140 },
|
||||
{ title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 180, ellipsis: true },
|
||||
{ title: '运单量', dataIndex: 'orderCount', key: 'orderCount', align: 'right', width: 80 },
|
||||
{ title: '应收', dataIndex: 'receivable', key: 'receivable', align: 'right', render: fmtMoney },
|
||||
{ title: '实收', dataIndex: 'received', key: 'received', align: 'right', render: fmtMoney },
|
||||
{ title: '未收', dataIndex: 'uncollected', key: 'uncollected', align: 'right', render: fmtMoney },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 88,
|
||||
fixed: 'right',
|
||||
render: function (_, r) {
|
||||
return React.createElement('button', { type: 'button', style: drillLinkStyle, onClick: function () { openDrill('logistics', r); } }, '明细');
|
||||
}
|
||||
}
|
||||
];
|
||||
}, [openDrill]);
|
||||
|
||||
var energyColumns = useMemo(function () {
|
||||
return [
|
||||
{ title: '客户名', dataIndex: 'customerName', key: 'customerName', width: 200, ellipsis: true },
|
||||
{ title: '业务员', dataIndex: 'salesperson', key: 'salesperson', width: 88 },
|
||||
{ title: '加氢量(kg)', dataIndex: 'h2Kg', key: 'h2Kg', align: 'right', render: function (v) { return fmtMoney(v); } },
|
||||
{ title: '成本金额', dataIndex: 'costAmount', key: 'costAmount', align: 'right', render: fmtMoney },
|
||||
{ title: '销售金额', dataIndex: 'salesAmount', key: 'salesAmount', align: 'right', render: fmtMoney },
|
||||
{ title: '毛利', dataIndex: 'grossProfit', key: 'grossProfit', align: 'right', render: fmtMoney },
|
||||
{
|
||||
title: '收款状态',
|
||||
dataIndex: 'collectStatus',
|
||||
key: 'collectStatus',
|
||||
width: 96,
|
||||
render: function (v) {
|
||||
return React.createElement(Tag, { color: v === '已收款' ? 'success' : 'warning' }, v);
|
||||
}
|
||||
}
|
||||
];
|
||||
}, []);
|
||||
|
||||
var accountColumns = useMemo(function () {
|
||||
return [
|
||||
{ title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 200, ellipsis: true },
|
||||
{ title: '业务员', dataIndex: 'salesperson', key: 'salesperson', width: 88 },
|
||||
{ title: '上年结存', dataIndex: 'openBalance', key: 'openBalance', align: 'right', render: fmtMoney },
|
||||
{ title: '本年充值', dataIndex: 'recharge', key: 'recharge', align: 'right', render: fmtMoney },
|
||||
{ title: '已用氢费', dataIndex: 'usedH2', key: 'usedH2', align: 'right', render: fmtMoney },
|
||||
{ title: '已用电费', dataIndex: 'usedElec', key: 'usedElec', align: 'right', render: fmtMoney },
|
||||
{ title: '期末结余', dataIndex: 'closeBalance', key: 'closeBalance', align: 'right', render: fmtMoney }
|
||||
];
|
||||
}, []);
|
||||
|
||||
var uncollectedColumns = useMemo(function () {
|
||||
return [
|
||||
{ title: '条线', dataIndex: 'line', key: 'line', width: 72 },
|
||||
{ title: '业务员', dataIndex: 'salesperson', key: 'salesperson', width: 88 },
|
||||
{ title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 180, ellipsis: true },
|
||||
{ title: '车牌', dataIndex: 'plateNo', key: 'plateNo', width: 110 },
|
||||
{ title: '应付款日', dataIndex: 'dueDate', key: 'dueDate', width: 110 },
|
||||
{ title: '应收', dataIndex: 'receivable', key: 'receivable', align: 'right', render: fmtMoney },
|
||||
{ title: '未收', dataIndex: 'uncollected', key: 'uncollected', align: 'right', render: fmtMoney },
|
||||
{
|
||||
title: '超期天数',
|
||||
dataIndex: 'overdueDays',
|
||||
key: 'overdueDays',
|
||||
align: 'right',
|
||||
width: 96,
|
||||
render: function (v) {
|
||||
var n = Number(v);
|
||||
if (n > 14) return React.createElement(Tag, { color: 'error' }, v + ' 天');
|
||||
if (n > 0) return React.createElement(Tag, { color: 'warning' }, v + ' 天');
|
||||
return v;
|
||||
}
|
||||
}
|
||||
];
|
||||
}, []);
|
||||
|
||||
var drillColumns = useMemo(function () {
|
||||
if (drill.type === 'lease') {
|
||||
return [
|
||||
{ title: '车牌号码', dataIndex: 'plateNo', key: 'plateNo', width: 110 },
|
||||
{ title: '应收', dataIndex: 'receivable', key: 'receivable', align: 'right', render: fmtMoney },
|
||||
{ title: '实收', dataIndex: 'received', key: 'received', align: 'right', render: fmtMoney },
|
||||
{ title: '未收', dataIndex: 'uncollected', key: 'uncollected', align: 'right', render: fmtMoney }
|
||||
];
|
||||
}
|
||||
return [
|
||||
{ title: '车牌', dataIndex: 'plateNo', key: 'plateNo', width: 110 },
|
||||
{ title: '出车日期', dataIndex: 'tripDate', key: 'tripDate', width: 110 },
|
||||
{ title: '金额', dataIndex: 'amount', key: 'amount', align: 'right', render: fmtMoney },
|
||||
{ title: '氢费', dataIndex: 'h2Fee', key: 'h2Fee', align: 'right', render: fmtMoney },
|
||||
{ title: 'ETC', dataIndex: 'etcFee', key: 'etcFee', align: 'right', render: fmtMoney }
|
||||
];
|
||||
}, [drill.type]);
|
||||
|
||||
var handleExport = useCallback(function () {
|
||||
var headers = ['业务条线', '应收', '实收', '未收', '成本', '利润'];
|
||||
var body = [headers].concat(overviewRows.map(function (r) {
|
||||
return [r.line, r.receivable, r.received, r.uncollected, r.cost, r.profit];
|
||||
}));
|
||||
downloadCsv('客户服务部业务统计_' + ym.year + '_' + new Date().getTime() + '.csv', body);
|
||||
message.success('已导出经营总览');
|
||||
}, [overviewRows, ym.year]);
|
||||
|
||||
var tabItems = useMemo(function () {
|
||||
return [
|
||||
{
|
||||
key: 'overview',
|
||||
label: '经营总览',
|
||||
children: React.createElement(Table, {
|
||||
className: 'cs-dept-stat-table',
|
||||
rowKey: 'key',
|
||||
size: 'small',
|
||||
bordered: true,
|
||||
pagination: false,
|
||||
scroll: { x: 900 },
|
||||
columns: overviewColumns,
|
||||
dataSource: overviewRows
|
||||
})
|
||||
},
|
||||
{
|
||||
key: 'lease',
|
||||
label: '租赁经营',
|
||||
children: React.createElement(Table, {
|
||||
className: 'cs-dept-stat-table',
|
||||
rowKey: 'key',
|
||||
size: 'small',
|
||||
bordered: true,
|
||||
pagination: { pageSize: 10, showSizeChanger: true },
|
||||
scroll: { x: 1100 },
|
||||
columns: leaseColumns,
|
||||
dataSource: leaseRows
|
||||
})
|
||||
},
|
||||
{
|
||||
key: 'logistics',
|
||||
label: '物流经营',
|
||||
children: React.createElement(Table, {
|
||||
className: 'cs-dept-stat-table',
|
||||
rowKey: 'key',
|
||||
size: 'small',
|
||||
bordered: true,
|
||||
pagination: { pageSize: 10, showSizeChanger: true },
|
||||
scroll: { x: 1100 },
|
||||
columns: logisticsColumns,
|
||||
dataSource: logisticsRows
|
||||
})
|
||||
},
|
||||
{
|
||||
key: 'energy',
|
||||
label: '能源销售',
|
||||
children: React.createElement(Table, {
|
||||
className: 'cs-dept-stat-table',
|
||||
rowKey: 'key',
|
||||
size: 'small',
|
||||
bordered: true,
|
||||
pagination: { pageSize: 10 },
|
||||
scroll: { x: 1000 },
|
||||
columns: energyColumns,
|
||||
dataSource: energyRows
|
||||
})
|
||||
},
|
||||
{
|
||||
key: 'account',
|
||||
label: '成本与账户',
|
||||
children: React.createElement(Table, {
|
||||
className: 'cs-dept-stat-table',
|
||||
rowKey: 'key',
|
||||
size: 'small',
|
||||
bordered: true,
|
||||
pagination: { pageSize: 10 },
|
||||
scroll: { x: 1000 },
|
||||
columns: accountColumns,
|
||||
dataSource: accountRows
|
||||
})
|
||||
},
|
||||
{
|
||||
key: 'uncollected',
|
||||
label: '未收预警',
|
||||
children: React.createElement(Table, {
|
||||
className: 'cs-dept-stat-table',
|
||||
rowKey: 'key',
|
||||
size: 'small',
|
||||
bordered: true,
|
||||
pagination: { pageSize: 10 },
|
||||
scroll: { x: 1100 },
|
||||
columns: uncollectedColumns,
|
||||
dataSource: uncollectedRows
|
||||
})
|
||||
}
|
||||
];
|
||||
}, [
|
||||
overviewColumns, overviewRows, leaseColumns, leaseRows, logisticsColumns, logisticsRows,
|
||||
energyColumns, energyRows, accountColumns, accountRows, uncollectedColumns, uncollectedRows
|
||||
]);
|
||||
|
||||
return React.createElement(
|
||||
App,
|
||||
null,
|
||||
React.createElement('style', null, tableStyle),
|
||||
React.createElement(
|
||||
'div',
|
||||
{ style: layoutStyle },
|
||||
React.createElement(Breadcrumb, {
|
||||
style: { marginBottom: 12 },
|
||||
items: [
|
||||
{ title: '数据分析' },
|
||||
{ title: '客户服务部业务统计' }
|
||||
]
|
||||
}),
|
||||
React.createElement(
|
||||
Card,
|
||||
{ bordered: false, style: { marginBottom: 16 } },
|
||||
React.createElement('div', { style: { fontSize: 18, fontWeight: 600, marginBottom: 16, color: 'rgba(0,0,0,0.88)' } }, pageTitle),
|
||||
React.createElement(
|
||||
Row,
|
||||
{ gutter: 16, align: 'bottom' },
|
||||
React.createElement(Col, { xs: 24, sm: 12, md: 6, lg: 4 },
|
||||
React.createElement('div', { style: filterItemStyle },
|
||||
React.createElement('div', { style: filterLabelStyle }, '统计年份'),
|
||||
React.createElement(DatePicker, {
|
||||
picker: 'year',
|
||||
style: filterControlStyle,
|
||||
value: yearDraft,
|
||||
onChange: setYearDraft,
|
||||
allowClear: false
|
||||
})
|
||||
)
|
||||
),
|
||||
React.createElement(Col, { xs: 24, sm: 12, md: 6, lg: 4 },
|
||||
React.createElement('div', { style: filterItemStyle },
|
||||
React.createElement('div', { style: filterLabelStyle }, '统计月份'),
|
||||
React.createElement(Select, {
|
||||
style: filterControlStyle,
|
||||
value: monthDraft,
|
||||
onChange: setMonthDraft,
|
||||
options: monthOptions,
|
||||
allowClear: false
|
||||
})
|
||||
)
|
||||
),
|
||||
React.createElement(Col, { xs: 24, sm: 12, md: 6, lg: 5 },
|
||||
React.createElement('div', { style: filterItemStyle },
|
||||
React.createElement('div', { style: filterLabelStyle }, '业务员'),
|
||||
React.createElement(Select, {
|
||||
style: filterControlStyle,
|
||||
value: spDraft,
|
||||
onChange: setSpDraft,
|
||||
options: spOptions,
|
||||
allowClear: true,
|
||||
showSearch: true,
|
||||
filterOption: filterOption,
|
||||
placeholder: '全部业务员'
|
||||
})
|
||||
)
|
||||
),
|
||||
React.createElement(Col, { xs: 24, sm: 12, md: 6, lg: 5 },
|
||||
React.createElement('div', { style: filterItemStyle },
|
||||
React.createElement('div', { style: filterLabelStyle }, '客户名称'),
|
||||
React.createElement(Select, {
|
||||
style: filterControlStyle,
|
||||
value: cuDraft,
|
||||
onChange: setCuDraft,
|
||||
options: cuOptions,
|
||||
allowClear: true,
|
||||
showSearch: true,
|
||||
filterOption: filterOption,
|
||||
placeholder: '全部客户'
|
||||
})
|
||||
)
|
||||
),
|
||||
React.createElement(Col, { xs: 24, sm: 24, md: 24, lg: 6 },
|
||||
React.createElement(Space, { style: { marginBottom: 12 } },
|
||||
React.createElement(Button, { type: 'primary', onClick: handleQuery }, '查询'),
|
||||
React.createElement(Button, { onClick: handleReset }, '重置'),
|
||||
React.createElement(Button, { onClick: handleExport }, '导出总览')
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
React.createElement(
|
||||
Row,
|
||||
{ gutter: 16, style: { marginBottom: 16 }, className: 'cs-dept-kpi' },
|
||||
React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 4 },
|
||||
React.createElement(Card, { size: 'small', bordered: false },
|
||||
React.createElement(Statistic, { title: '总收入(应收口径)', value: kpi.totalRevenue, precision: 2, suffix: '元' })
|
||||
)
|
||||
),
|
||||
React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 4 },
|
||||
React.createElement(Card, { size: 'small', bordered: false },
|
||||
React.createElement(Statistic, { title: '总利润', value: kpi.totalProfit, precision: 2, suffix: '元', valueStyle: { color: '#00b42a' } })
|
||||
)
|
||||
),
|
||||
React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 4 },
|
||||
React.createElement(Card, { size: 'small', bordered: false },
|
||||
React.createElement(Statistic, { title: '未收余额', value: kpi.uncollected, precision: 2, suffix: '元', valueStyle: { color: '#f53f3f' } })
|
||||
)
|
||||
),
|
||||
React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 4 },
|
||||
React.createElement(Card, { size: 'small', bordered: false },
|
||||
React.createElement(Statistic, { title: '氢电账户结余', value: kpi.accountBalance, precision: 2, suffix: '元' })
|
||||
)
|
||||
),
|
||||
React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 4 },
|
||||
React.createElement(Card, { size: 'small', bordered: false },
|
||||
React.createElement(Statistic, { title: '在营车辆', value: kpi.activeVehicles, suffix: '台' })
|
||||
)
|
||||
)
|
||||
),
|
||||
React.createElement(
|
||||
Card,
|
||||
{ bordered: false, bodyStyle: { paddingTop: 12 } },
|
||||
React.createElement(Tabs, {
|
||||
activeKey: activeTab,
|
||||
onChange: setActiveTab,
|
||||
items: tabItems,
|
||||
destroyInactiveTabPane: false
|
||||
}),
|
||||
React.createElement('div', { style: { marginTop: 12, fontSize: 12, color: 'rgba(0,0,0,0.45)' } },
|
||||
'说明:数据为原型演示;联调后由租赁/物流/能源/收支管控等台账子表汇总写入。租赁、物流汇总行可点击「明细」钻取至车辆/运单层级。'
|
||||
)
|
||||
),
|
||||
React.createElement(Modal, {
|
||||
title: drill.title || '明细',
|
||||
open: drill.open,
|
||||
onCancel: closeDrill,
|
||||
footer: null,
|
||||
width: 720,
|
||||
destroyOnClose: true
|
||||
},
|
||||
React.createElement(Table, {
|
||||
rowKey: 'key',
|
||||
size: 'small',
|
||||
bordered: true,
|
||||
pagination: false,
|
||||
columns: drillColumns,
|
||||
dataSource: drill.rows
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user