diff --git a/web端/业务管理/保险采购.jsx b/web端/业务管理/保险采购.jsx new file mode 100644 index 0000000..8681d95 --- /dev/null +++ b/web端/业务管理/保险采购.jsx @@ -0,0 +1,5327 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 业务管理 - 保险采购 +// 模块:① 比价单(选车报价 → 保存 → 按最晚付费日临期/超期提醒 → 勾选提交采购工作流) +// ② 保单管理(一车一档台账,OCR/导入/逐条录入,与比价单不关联) +// 与车辆管理「保险状态」联动:交强险 + 商业险均存在且在有效期内为正常,否则异常(禁止交车) + +const { useState, useMemo, useCallback } = React; +const moment = window.moment || window.dayjs; +const antd = window.antd; +const { + Input, + Select, + Button, + Card, + Table, + Badge, + Tooltip, + Modal, + Tag, + message, + Popover, + Alert, + Checkbox, + DatePicker, + InputNumber, + Space, + Radio, + Steps, + Upload, + Progress, + Tabs, + Timeline, +} = antd; + +const ANCHOR_TODAY = '2026-06-01'; +const IPC_STORAGE_KEY = 'oneos_ipc_insurance_v1'; +const IPC_COMPARE_SHEETS_KEY = 'oneos_ipc_compare_sheets_v1'; +const IPC_POLICY_RECOGN_TASKS_KEY = 'oneos_ipc_policy_recogn_tasks_v1'; +const IPC_INSURANCE_HISTORY_EDITS_KEY = 'oneos_ipc_insurance_history_edits_v1'; +const IPC_EDIT_PLATE_KEY = 'oneos_ipc_edit_plate'; +const NO_PLATE_LABEL = '暂无车牌'; +const PROTO_COMPARE_CREATOR = '张明辉'; + +const INSURANCE_TYPE_ITEMS = [ + { key: 'compulsory', label: '交强', fullLabel: '交强险' }, + { key: 'commercial', label: '商业', fullLabel: '商业险' }, + { key: 'excess', label: '超赔', fullLabel: '超赔险' }, + { key: 'cargo', label: '货物', fullLabel: '货物险' }, + { key: 'driverAccident', label: '驾意', fullLabel: '驾意险' }, +]; + +const CORE_INSURANCE_KEYS = ['compulsory', 'commercial']; +const INSURANCE_WARN_DAYS = 30; +/** 比价单:最晚付费日期 ≤ 该天数视为临期 */ +const LATEST_PAY_WARN_DAYS = 3; + +const INSURANCE_LABEL_TO_KEY = { + 交强险: 'compulsory', + 商业险: 'commercial', + 超赔险: 'excess', + 货物险: 'cargo', + 驾意险: 'driverAccident', +}; + +const POLICY_OCR_MODES = [ + { key: 'policy', label: '保单录入', desc: '选择险种后上传附件,自动识别保单要素并匹配台账' }, + { key: 'suspend', label: '停保', desc: '上传停保/停驶批单,识别保单号与车牌后停保' }, + { key: 'resume', label: '复驶', desc: '上传复驶批单,识别后更新生效与到期日' }, + { key: 'cancel', label: '退保', desc: '上传退保批单,识别保单号与退费金额' }, +]; + +const POLICY_BIZ_TYPE_OPTIONS = [ + { value: 'policy', label: '保单录入' }, + { value: 'suspend', label: '停租' }, + { value: 'resume', label: '复驶' }, + { value: 'cancel', label: '退保' }, +]; + +const EMPTY_POLICY_DETAIL = { + plateNo: '', + vin: '', + insuranceType: '交强险', + bizType: 'policy', + company: '', + policyNo: '', + endorsementNo: '', + payTime: '', + startDate: '', + endDate: '', + reinstateDate: '', + premium: '', + coverageItems: [], + applicant: '', + insured: '', + signDate: '', +}; + +/** 保单项目/责任限额:表单为字符串数组,导入/OCR/台账可为分号拼接文本 */ +const parseCoverageItemsInput = (raw) => { + if (Array.isArray(raw)) { + return raw.map((s) => String(s ?? '').trim()).filter(Boolean); + } + const text = String(raw ?? '').trim(); + if (!text) return []; + const segments = text.split(/[;;\n]+/).map((s) => s.trim()).filter(Boolean); + const expanded = []; + segments.forEach((part) => { + const sub = part.split(/、/).map((s) => s.trim()).filter(Boolean); + if (sub.length > 1 && part.length <= 120) { + expanded.push(...sub); + } else { + expanded.push(part); + } + }); + return expanded; +}; + +const serializeCoverageItems = (items) => parseCoverageItemsInput(items).join(';'); + +const getCoverageItemsFormRows = (items) => { + const list = Array.isArray(items) ? items : parseCoverageItemsInput(items); + return list.length ? list : ['']; +}; + +/** 基于用户提供的真实保单/批单样本(PDF 解析 + 文件名) */ +const REFERENCE_POLICY_OCR_MOCKS = [ + { + test: (n) => /沪BDB9161.*交强险/i.test(n), + detail: { + plateNo: '沪BDB9161', vin: 'LC0DF4CD8S0303140', insuranceType: '交强险', bizType: 'policy', + policyNo: 'ASHZ001CTP26B187065J', endorsementNo: 'DZQA26480000279515', + company: '中国太平洋财产保险股份有限公司', + payTime: '2026-06-01 17:42:10', signDate: '2026-05-27', startDate: '2026-06-05', endDate: '2027-06-04', + premium: '1243.00', + coverageItems: '死亡伤残赔偿限额180000元;医疗费用赔偿限额18000元;财产损失赔偿限额2000元', + applicant: '上海羚牛氢运物联网科技有限公司', insured: '上海羚牛氢运物联网科技有限公司', + }, + }, + { + test: (n) => /粤AGP9827.*商业险/i.test(n), + detail: { + plateNo: '粤AGP9827', insuranceType: '商业险', bizType: 'policy', + policyNo: '2050AA330400260000GV', company: '紫金财产保险股份有限公司', + payTime: '2026-05-28 10:00:00', startDate: '2026-06-06', endDate: '2027-05-27', premium: '12800.00', + coverageItems: '机动车损失险、第三者责任险、车上人员责任险', + }, + }, + { + test: (n) => /粤AGP3071.*驾意/i.test(n), + detail: { + plateNo: '粤AGP3071', insuranceType: '驾意险', bizType: 'policy', + policyNo: 'JY2026AGP3071001', company: '中国平安财产保险股份有限公司', + startDate: '2026-06-06', endDate: '2027-05-27', premium: '380.00', + coverageItems: '驾乘意外险,每座身故伤残/医疗限额', + }, + }, + { + test: (n) => /粤AGR9766.*超赔/i.test(n), + detail: { + plateNo: '粤AGR9766', vin: 'LB9A32A21R0LS1478', insuranceType: '超赔险', bizType: 'policy', + policyNo: '6260828000909X006408', company: '国任财产保险股份有限公司广州市番禺支公司', + payTime: '2026-04-16 14:37:37', signDate: '2026-04-16', startDate: '2026-04-17', endDate: '2027-04-16', + premium: '1500.00', + coverageItems: '公路货物运输定额保险;累计赔偿限额10001000元;主险货物保险金额1000元', + applicant: '羚牛氢能科技(广东)有限公司', insured: '羚牛氢能科技(广东)有限公司', + }, + }, + { + test: (n) => /货物险|20208A330400240001QX/i.test(n), + detail: { + plateNo: '浙F05178F', insuranceType: '货物险', bizType: 'policy', + policyNo: '20208A330400240001QX', company: '紫金财产保险股份有限公司', + payTime: '2024-10-17 15:58:05', startDate: '2024-10-18', endDate: '2025-10-17', premium: '1500.00', + coverageItems: '公路货物运输定额保险 CNY500000;集装箱货物及其箱体', + applicant: '嘉兴羚牛汽车服务有限公司', insured: '嘉兴羚牛汽车服务有限公司', + }, + }, + { + test: (n) => /粤A03423F.*停驶/i.test(n), + detail: { + plateNo: '粤A03423F', insuranceType: '商业险', bizType: 'suspend', + policyNo: '2050AA3304002600002EM', endorsementNo: '3050AA3304002600002EM01', + company: '紫金财产保险股份有限公司', payTime: '2026-04-16 16:13:53', + startDate: '2026-04-17', endDate: '2026-04-17', reinstateDate: '2027-03-31', + coverageItems: '停驶批单:保险车辆停驶,停驶期间保险责任中止', + }, + }, + { + test: (n) => /粤A06290F.*复驶/i.test(n), + detail: { + plateNo: '粤A06290F', insuranceType: '商业险', bizType: 'resume', + policyNo: '2050AA33040026000226', endorsementNo: '3050AA3304002600022602', + company: '紫金财产保险股份有限公司', payTime: '2026-04-30 14:25:43', + startDate: '2026-05-06', endDate: '2027-03-27', reinstateDate: '2027-03-16', + coverageItems: '复驶批单:停驶车辆恢复行驶,保险责任自复驶日起恢复', + }, + }, + { + test: (n) => /浙F03220F.*复驶|BSHZ001S2024B005477B/i.test(n), + detail: { + plateNo: '浙F03220F', insuranceType: '商业险', bizType: 'resume', + policyNo: 'BSHZ001S2024B005477B', endorsementNo: 'BSHZ001S2024B005477E', + company: '中国太平洋财产保险股份有限公司', startDate: '2026-05-01', endDate: '2027-04-30', + coverageItems: '复驶批单', + }, + }, + { + test: (n) => /沪A06192F.*停保|BSHZ001S2024B005054V/i.test(n), + detail: { + plateNo: '沪A06192F', insuranceType: '商业险', bizType: 'suspend', + policyNo: 'BSHZ001S2024B005054V', endorsementNo: 'BSHZ001S2024B005054E', + company: '中国太平洋财产保险股份有限公司', startDate: '2025-12-05', endDate: '2026-03-05', + reinstateDate: '2026-06-01', + coverageItems: '停保批单', + }, + }, + { + test: (n) => /粤A03331F.*退保/i.test(n), + detail: { + plateNo: '粤A03331F', insuranceType: '商业险', bizType: 'cancel', + policyNo: '2050AA330400260000GV', endorsementNo: '3050AA330400260000GV02', + company: '紫金财产保险股份有限公司', payTime: '2026-05-27 17:51:20', + startDate: '2026-05-28', endDate: '2026-05-28', premium: '7853.27', + coverageItems: '商业险退保批单,退还保费', + }, + }, + { + test: (n) => /粤AGR0772.*停保/i.test(n), + detail: { + plateNo: '粤AGR0772', insuranceType: '商业险', bizType: 'suspend', + policyNo: 'PAIC-SY-AGR0772-2025', company: '中国平安财产保险股份有限公司', + startDate: '2025-12-05', endDate: '2026-03-05', reinstateDate: '2026-06-01', + coverageItems: '商业险停保', + }, + }, +]; + +const normalizePolicyDetail = (raw = {}) => ({ + ...EMPTY_POLICY_DETAIL, + ...raw, + plateNo: (raw.plateNo || '').trim(), + vin: (raw.vin || '').trim(), + insuranceType: raw.insuranceType || '交强险', + bizType: raw.bizType || 'policy', + coverageItems: parseCoverageItemsInput(raw.coverageItems), +}); + +const inferPolicyDetailFromFileName = (fileName) => { + const name = fileName || ''; + const plateMatch = name.match(/[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼][A-Z][A-Z0-9]{4,6}[A-Z0-9挂学警港澳]?/i); + const plateNo = plateMatch ? plateMatch[0].toUpperCase() : ''; + let insuranceType = '交强险'; + if (/商业险|商业/.test(name)) insuranceType = '商业险'; + else if (/超赔/.test(name)) insuranceType = '超赔险'; + else if (/驾意/.test(name)) insuranceType = '驾意险'; + else if (/货物/.test(name)) insuranceType = '货物险'; + else if (/交强/.test(name)) insuranceType = '交强险'; + let bizType = 'policy'; + if (/停驶|停保/.test(name)) bizType = 'suspend'; + else if (/复驶/.test(name)) bizType = 'resume'; + else if (/退保/.test(name)) bizType = 'cancel'; + const policyNoMatch = name.match(/BSHZ\d+[A-Z0-9]+|ASHZ\d+[A-Z0-9]+|202\d{2}A\d+QX|2050AA\d+[A-Z0-9]+|6260\d+X\d+/i); + const rangeMatch = name.match(/(\d{4})[.\-](\d{1,2})[.\-](\d{1,2})/); + let startDate = ''; + let endDate = ''; + if (rangeMatch) { + const y = rangeMatch[1]; + const m = String(rangeMatch[2]).padStart(2, '0'); + const d = String(rangeMatch[3]).padStart(2, '0'); + startDate = `${y}-${m}-${d}`; + const parts = name.match(/(\d{4})[.\-](\d{1,2})[.\-](\d{1,2}).*?(\d{4})[.\-](\d{1,2})[.\-](\d{1,2})/); + if (parts) { + endDate = `${parts[4]}-${String(parts[5]).padStart(2, '0')}-${String(parts[6]).padStart(2, '0')}`; + } + } + return normalizePolicyDetail({ + plateNo, + insuranceType, + bizType, + policyNo: policyNoMatch ? policyNoMatch[0] : '', + startDate, + endDate, + }); +}; + +const resolvePolicyDetailFromFileName = (fileName) => { + const ref = REFERENCE_POLICY_OCR_MOCKS.find((m) => m.test(fileName)); + if (ref) return normalizePolicyDetail(ref.detail); + return inferPolicyDetailFromFileName(fileName); +}; + +const bizTypeToRecognMode = (bizType) => ( + bizType === 'suspend' ? 'suspend' : bizType === 'resume' ? 'resume' : bizType === 'cancel' ? 'cancel' : 'policy' +); + +const applyPolicyDetailToInsuranceItem = (item, detail, mode) => { + const d = normalizePolicyDetail(detail); + const next = { ...item }; + next.company = d.company || next.company; + next.policyNo = d.policyNo || next.policyNo; + next.endorsementNo = d.endorsementNo || next.endorsementNo || ''; + next.startDate = d.startDate || next.startDate; + next.endDate = d.endDate || next.endDate; + next.premium = d.premium || next.premium; + next.payTime = d.payTime || next.payTime || ''; + next.signDate = d.signDate || next.signDate || ''; + next.coverageItems = serializeCoverageItems(d.coverageItems) || next.coverageItems || ''; + next.applicant = d.applicant || next.applicant || ''; + next.insured = d.insured || next.insured || ''; + if (mode === 'policy') { + next.policyTag = ''; + next.reinstateDate = ''; + } else if (mode === 'suspend') { + next.policyTag = 'suspended'; + next.reinstateDate = d.reinstateDate || next.reinstateDate || '2026-09-01'; + } else if (mode === 'resume') { + next.policyTag = ''; + next.reinstateDate = ''; + } else if (mode === 'cancel') { + next.policyTag = 'cancelled'; + next.reinstateDate = ''; + } + return next; +}; + +const createPolicyRecognTaskId = () => `TASK-${Date.now().toString().slice(-8)}`; + +const isPolicyRecognImageOrPdf = (file) => { + const name = (file?.name || '').toLowerCase(); + const type = (file?.type || '').toLowerCase(); + return type.includes('pdf') || type.startsWith('image/') || /\.(pdf|png|jpe?g|webp|bmp|gif|tiff?)$/i.test(name); +}; + +const isPolicyImportExcelFile = (file) => { + const name = (file?.name || '').toLowerCase(); + const type = (file?.type || '').toLowerCase(); + return /\.(csv|xlsx|xls)$/i.test(name) + || type.includes('csv') + || type.includes('spreadsheet') + || type.includes('excel'); +}; + +const POLICY_IMPORT_TEMPLATE_HEADERS = [ + '车牌号', + 'VIN码', + '业务类型', + '险种', + '保险公司', + '保单号', + '批单号', + '付款时间', + '生效日期', + '到期日期', + '复驶日期', + '保费(元)', + '保单项目', + '投保人', + '被保险人', + '签单日期', +]; + +const POLICY_IMPORT_TEMPLATE_SAMPLE_ROW = [ + '沪BDB9161', + 'LC0DF4CD8S0303140', + '保单录入', + '交强险', + '中国太平洋财产保险股份有限公司', + 'ASHZ001CTP26B187065J', + 'DZQA26480000279515', + '2026-06-01 17:42:10', + '2026-06-05', + '2027-06-04', + '', + '1243.00', + '死亡伤残180000元;医疗18000元;财产2000元', + '上海羚牛氢运物联网科技有限公司', + '上海羚牛氢运物联网科技有限公司', + '2026-05-27', +]; + +const downloadPolicyImportTemplate = () => { + const escapeCsvCell = (val) => { + const s = String(val ?? ''); + return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s; + }; + const csv = `\uFEFF${[ + POLICY_IMPORT_TEMPLATE_HEADERS.map(escapeCsvCell).join(','), + POLICY_IMPORT_TEMPLATE_SAMPLE_ROW.map(escapeCsvCell).join(','), + ].join('\n')}`; + const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = '保单批量导入模板.csv'; + a.click(); + URL.revokeObjectURL(url); +}; + +const parseCsvLine = (line) => { + const result = []; + let cur = ''; + let inQuote = false; + for (let i = 0; i < line.length; i += 1) { + const c = line[i]; + if (c === '"') { + inQuote = !inQuote; + continue; + } + if (c === ',' && !inQuote) { + result.push(cur.trim()); + cur = ''; + continue; + } + cur += c; + } + result.push(cur.trim()); + return result; +}; + +const normalizeImportHeaderKey = (header) => { + const h = String(header || '').trim().replace(/^\uFEFF/, ''); + const map = { + 车牌号: 'plateNo', + 车牌: 'plateNo', + VIN码: 'vin', + VIN: 'vin', + 业务类型: 'bizTypeLabel', + 险种: 'insuranceType', + 保险类型: 'insuranceType', + 保险公司: 'company', + 保单号: 'policyNo', + 批单号: 'endorsementNo', + 付款时间: 'payTime', + 生效日期: 'startDate', + 起保日期: 'startDate', + 到期日期: 'endDate', + 复驶日期: 'reinstateDate', + '保费(元)': 'premium', + 保费: 'premium', + 保单项目: 'coverageItems', + 投保人: 'applicant', + 被保险人: 'insured', + 签单日期: 'signDate', + }; + return map[h] || null; +}; + +const parsePolicyImportFileText = (text) => { + const lines = String(text || '').split(/\r?\n/).map((l) => l.trim()).filter(Boolean); + if (!lines.length) return []; + const headerCells = parseCsvLine(lines[0]); + const colIndex = {}; + headerCells.forEach((cell, idx) => { + const key = normalizeImportHeaderKey(cell); + if (key) colIndex[key] = idx; + }); + const hasHeader = Object.keys(colIndex).length >= 3; + const dataLines = hasHeader ? lines.slice(1) : lines; + const pick = (cells, key, fallbackIdx) => { + if (hasHeader && colIndex[key] != null) return (cells[colIndex[key]] || '').trim(); + return (cells[fallbackIdx] || '').trim(); + }; + return dataLines.map((line) => { + const cells = parseCsvLine(line); + if (!cells.some((c) => c)) return null; + const bizLabel = pick(cells, 'bizTypeLabel', 2); + const bizMap = { 保单录入: 'policy', 停保: 'suspend', 停租: 'suspend', 复驶: 'resume', 退保: 'cancel' }; + return { + plateNo: pick(cells, 'plateNo', 0), + vin: pick(cells, 'vin', 1), + bizType: bizMap[bizLabel] || 'policy', + insuranceType: pick(cells, 'insuranceType', 3), + company: pick(cells, 'company', 4), + policyNo: pick(cells, 'policyNo', 5), + endorsementNo: pick(cells, 'endorsementNo', 6), + payTime: pick(cells, 'payTime', 7), + startDate: pick(cells, 'startDate', 8), + endDate: pick(cells, 'endDate', 9), + reinstateDate: pick(cells, 'reinstateDate', 10), + premium: pick(cells, 'premium', 11), + coverageItems: pick(cells, 'coverageItems', 12), + applicant: pick(cells, 'applicant', 13), + insured: pick(cells, 'insured', 14), + signDate: pick(cells, 'signDate', 15), + }; + }).filter(Boolean); +}; + +const resolveImportRowLedgerKey = (row) => { + const plate = (row?.plateNo || '').trim(); + const vin = (row?.vin || '').trim(); + if (plate) { + const v = findVehicleByPlate(plate); + if (v) return getVehicleLedgerKey(v); + } + if (vin) { + const v = findVehicleByVin(vin); + if (v) return getVehicleLedgerKey(v); + } + return null; +}; + +const isImportRowLedgerMatched = (ledgerKey) => ( + !!ledgerKey && MOCK_VEHICLES.some((v) => getVehicleLedgerKey(v) === ledgerKey) +); + +const buildRecognResultFromDetail = (fileMeta, detail, allInsurance, forcedMode) => { + const d = normalizePolicyDetail(detail); + const mode = forcedMode || bizTypeToRecognMode(d.bizType); + const plate = d.plateNo; + const vin = d.vin; + const vehicle = (plate && findVehicleByPlate(plate)) + || (vin && findVehicleByVin(vin)) + || { plateNo: plate, vin }; + const ledgerKey = resolveImportRowLedgerKey({ plateNo: plate, vin }) + || (plate ? getVehicleLedgerKey(vehicle) : '') + || (vin ? getVehicleLedgerKey(vehicle) : ''); + let typeKey = INSURANCE_LABEL_TO_KEY[d.insuranceType]; + const record = ledgerKey ? allInsurance[ledgerKey] : null; + let existing = typeKey && record ? record[typeKey] : null; + if (mode !== 'policy' && d.policyNo && ledgerKey) { + const policyMatch = findPolicyMatchInLedger(allInsurance, ledgerKey, d.policyNo); + if (policyMatch) { + typeKey = policyMatch.typeKey; + existing = policyMatch.item; + } + } + const typeLabel = INSURANCE_TYPE_ITEMS.find((it) => it.key === typeKey)?.fullLabel || d.insuranceType || '—'; + const ocrPolicyNo = d.policyNo || existing?.policyNo || ''; + let ocrEndDate = d.endDate || existing?.endDate || ''; + let reinstateDate = d.reinstateDate || ''; + if (!ocrEndDate) { + if (mode === 'suspend' || mode === 'cancel') ocrEndDate = ANCHOR_TODAY; + else if (mode === 'resume') ocrEndDate = '2027-06-30'; + else ocrEndDate = '2027-12-31'; + } + const matched = isImportRowLedgerMatched(ledgerKey) && !!typeKey && !!(ocrPolicyNo || ocrEndDate); + const bizLabel = POLICY_BIZ_TYPE_OPTIONS.find((o) => o.value === d.bizType)?.label || '保单录入'; + return { + id: fileMeta.id || `ocr-r-${Date.now()}`, + fileUid: fileMeta.fileUid || fileMeta.uid || `f-${Date.now()}`, + fileName: fileMeta.fileName || fileMeta.name || '导入记录', + fileType: fileMeta.fileType || '', + policyDetail: d, + ocrPlateNo: (vehicle.plateNo || plate || '').trim(), + ocrVin: vehicle.vin || vin || '', + displayPlate: formatVehiclePlateDisplay(vehicle.plateNo || plate), + ocrPolicyNo, + ocrEndDate, + ocrStartDate: d.startDate || '', + ocrPremium: d.premium || '', + ocrPayTime: d.payTime || '', + ocrEndorsementNo: d.endorsementNo || '', + ocrCoverageItems: serializeCoverageItems(d.coverageItems), + ocrBizType: d.bizType, + ocrBizTypeLabel: bizLabel, + ocrCompany: d.company || existing?.company || INSURANCE_MGMT_COMPANIES[0], + reinstateDate, + ledgerKey: ledgerKey || '', + typeKey: typeKey || '', + insuranceTypeLabel: typeLabel, + matched, + matchTip: matched + ? '已与台账车辆、险种匹配,可核对后确认' + : !ledgerKey + ? '未匹配到台账车辆,请检查车牌或 VIN' + : !typeKey + ? '险种填写有误' + : '请填写保单号或到期日期', + confirmed: false, + recognMode: mode, + }; +}; + +const buildImportResultsFromRows = (rows, allInsurance) => ( + (rows || []).map((row, idx) => buildRecognResultFromDetail( + { id: `import-r-${idx}-${Date.now()}`, fileUid: `import-row-${idx}`, fileName: `导入_${row.plateNo || row.vin || `第${idx + 1}行`}.csv`, fileType: 'text/csv' }, + normalizePolicyDetail(row), + allInsurance + )) +); + +const readPolicyImportFileAsText = (file) => new Promise((resolve, reject) => { + const name = (file?.name || '').toLowerCase(); + if (/\.(xlsx|xls)$/i.test(name)) { + message.warning('当前原型请使用 CSV 模板(在 Excel 中打开模板后另存为 CSV UTF-8 再上传)'); + resolve(''); + return; + } + const reader = new FileReader(); + reader.onload = (e) => resolve(String(e?.target?.result || '')); + reader.onerror = () => reject(new Error('read failed')); + reader.readAsText(file, 'UTF-8'); +}); + +const findPolicyMatchInLedger = (allInsurance, ledgerKey, policyNo) => { + const record = allInsurance?.[ledgerKey]; + if (!record || !policyNo) return null; + for (let i = 0; i < INSURANCE_TYPE_ITEMS.length; i += 1) { + const item = INSURANCE_TYPE_ITEMS[i]; + if (record[item.key]?.policyNo === policyNo) { + return { typeKey: item.key, item: record[item.key], label: item.fullLabel }; + } + } + return null; +}; + +const buildMockOcrResults = (files, mode, insuranceTypeLabel, allInsurance) => ( + (files || []).filter((f) => f.status === 'done').map((file, idx) => { + const fromFile = resolvePolicyDetailFromFileName(file.name); + const detail = normalizePolicyDetail({ + ...fromFile, + insuranceType: mode === 'policy' ? (insuranceTypeLabel || fromFile.insuranceType) : fromFile.insuranceType, + bizType: mode !== 'policy' ? mode : (fromFile.bizType || 'policy'), + }); + if (!detail.plateNo && !detail.vin) { + const vehicle = MOCK_VEHICLES[idx % MOCK_VEHICLES.length] || {}; + detail.plateNo = vehicle.plateNo || ''; + detail.vin = vehicle.vin || ''; + } + if (!detail.policyNo) { + detail.policyNo = `PDZA${String(20260000 + idx)}`; + } + return buildRecognResultFromDetail( + { id: `ocr-r-${file.uid}`, fileUid: file.uid, fileName: file.name, fileType: file.type || '' }, + detail, + allInsurance, + mode + ); + }) +); + +const recognResultToPolicyDetail = (result) => normalizePolicyDetail({ + plateNo: result.ocrPlateNo, + vin: result.ocrVin, + insuranceType: result.insuranceTypeLabel, + bizType: result.ocrBizType || result.policyDetail?.bizType || 'policy', + company: result.ocrCompany, + policyNo: result.ocrPolicyNo, + endorsementNo: result.ocrEndorsementNo, + payTime: result.ocrPayTime, + startDate: result.ocrStartDate, + endDate: result.ocrEndDate, + reinstateDate: result.reinstateDate, + premium: result.ocrPremium, + coverageItems: parseCoverageItemsInput(result.policyDetail?.coverageItems ?? result.ocrCoverageItems), + ...(result.policyDetail || {}), +}); + +const applyPolicyOcrResultToLedger = (result, mode) => { + const { ledgerKey, typeKey } = result; + if (!ledgerKey || !typeKey) return false; + const effectiveMode = result.recognMode || mode; + const detail = result.policyDetail ? normalizePolicyDetail(result.policyDetail) : recognResultToPolicyDetail(result); + const nowStr = formatCompareSheetNow(); + return (prev) => { + const record = ensureInsuranceRecordShape(prev[ledgerKey] || createEmptyInsuranceRecord()); + const item = applyPolicyDetailToInsuranceItem({ ...record[typeKey] }, detail, effectiveMode); + item.updateTime = nowStr; + item.updateUser = PROTO_COMPARE_CREATOR; + return { ...prev, [ledgerKey]: { ...record, [typeKey]: item } }; + }; +}; + +/** 保险公司管理模块 — 保险公司名称枚举(原型 mock) */ +const INSURANCE_MGMT_COMPANIES = [ + '中国人民财产保险股份有限公司', + '中国平安财产保险股份有限公司', + '中国太平洋财产保险股份有限公司', + '中国人寿财产保险股份有限公司', + '阳光财产保险股份有限公司', + '中华联合财产保险股份有限公司', + '太平财产保险有限公司', + '大地财产保险股份有限公司', + '上海某某保险公司', +]; + +const QUOTE_INSURANCE_TYPES = ['交强险', '商业险', '超赔险', '货物险', '驾意险']; + +const sanitizePremiumInput = (raw) => { + let s = String(raw || '').replace(/[^\d.]/g, ''); + const dotIdx = s.indexOf('.'); + if (dotIdx >= 0) { + s = s.slice(0, dotIdx + 1) + s.slice(dotIdx + 1).replace(/\./g, '').slice(0, 2); + } + return s; +}; + +const isValidPremium = (s) => { + const v = (s || '').trim(); + if (!v) return false; + if (!/^\d+(\.\d{1,2})?$/.test(v)) return false; + return parseFloat(v) > 0; +}; + +const formatPremiumDisplay = (s) => { + if (!isValidPremium(s)) return s || ''; + return parseFloat(s).toFixed(2); +}; + +const createEmptyQuoteDraft = () => ({ company: undefined, premium: '' }); + +const shortInsuranceCompanyName = (name) => ( + (name || '').replace(/股份有限公司/g, '').replace(/有限公司/g, '').trim() +); + +/** 比价单:汇总各行已确认报价金额 */ +const calcCompareSheetConfirmedTotal = (rows) => { + let total = 0; + let count = 0; + (rows || []).forEach((row) => { + if (!row.confirmedQuoteId) return; + const quote = (row.quotes || []).find((q) => q.id === row.confirmedQuoteId); + if (!quote?.premium) return; + const amount = parseFloat(quote.premium); + if (!Number.isNaN(amount) && amount > 0) { + total += amount; + count += 1; + } + }); + return { total, count }; +}; + +const VEHICLE_PROFILES = { + '沪A03561F': { customer: '上海迅杰物流有限公司', ownerCompany: '浙江羚牛氢能科技有限公司', color: '白色', regDate: '2024-06-05', inspectExpire: '2026-06-30' }, + '粤B58888F': { customer: '深圳冷链运输有限公司', ownerCompany: '羚牛运营(广东)', color: '蓝色', regDate: '2024-07-20', inspectExpire: '2026-07-20' }, + '苏E33333': { customer: '苏州港务集团', ownerCompany: '浙江羚牛氢能科技有限公司', color: '红色', regDate: '2024-05-16', inspectExpire: '2026-05-15' }, + '京A12345': { customer: '—', ownerCompany: '某某科技有限公司', color: '灰色', regDate: '2020-10-01', inspectExpire: '2024-10-01' }, + '浙A88888': { customer: '—', ownerCompany: '浙江羚牛氢能科技有限公司', color: '绿色', regDate: '2025-01-01', inspectExpire: '2027-12-31' }, + '沪D66666': { customer: '客户C', ownerCompany: '羚牛运营(上海)', color: '白色', regDate: '2021-06-15', inspectExpire: '2025-01-31' }, + '粤A12345': { customer: '客户A', ownerCompany: '羚牛运营(广东)', color: '白色', regDate: '2023-07-01', inspectExpire: '2026-02-28' }, + '苏A55678': { customer: '—', ownerCompany: '羚牛运营(嘉兴)', color: '黄色', regDate: '2025-05-01', inspectExpire: '2026-04-30' }, + LZYTBACR2M9999001: { customer: '嘉兴某某物流有限公司', ownerCompany: '浙江羚牛氢能科技有限公司', color: '白色', regDate: '2025-11-01', inspectExpire: '2026-10-31' }, +}; + +const createCompareRowId = () => `cr-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`; +const createCompareSheetId = () => `cs-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`; +const createCompareAttachmentId = () => `att-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`; +const createQuoteId = () => `qt-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`; + +const formatFileSize = (bytes) => { + if (bytes == null || Number.isNaN(bytes)) return ''; + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / (1024 * 1024)).toFixed(2)} MB`; +}; + +const attachmentsToUploadFileList = (attachments) => (attachments || []).map((a) => ({ + uid: a.uid || a.id || createCompareAttachmentId(), + name: a.name, + size: a.size, + type: a.type, + status: 'done', + uploadedAt: a.uploadedAt, +})); + +const uploadFileListToAttachments = (fileList) => (fileList || []).map((f) => ({ + id: f.uid || createCompareAttachmentId(), + uid: f.uid, + name: f.name, + size: f.size, + type: f.type || '', + uploadedAt: f.uploadedAt || formatCompareSheetNow(), +})); + +const formatCompareSheetNow = () => { + if (moment) return moment(ANCHOR_TODAY).hour(10).minute(30).second(0).format('YYYY-MM-DD HH:mm:ss'); + return `${ANCHOR_TODAY} 10:30:00`; +}; + +const calcCompareSheetStats = (rows) => { + const vehicleKeys = new Set(); + (rows || []).forEach((row) => { + const plate = (row.plateNo || '').trim(); + const vin = (row.vin || '').trim(); + if (plate) vehicleKeys.add(`plate:${plate.toUpperCase()}`); + else if (vin) vehicleKeys.add(`vin:${vin.toUpperCase()}`); + }); + return { + totalVehicles: vehicleKeys.size, + insuranceCount: (rows || []).length, + }; +}; + +const countCompareRowsWithConfirmedQuote = (rows) => ( + (rows || []).filter((r) => r.confirmedQuoteId).length +); + +const getLatestPayDateDiffDays = (dateStr) => { + if (!dateStr || !moment) return null; + const today = moment(ANCHOR_TODAY).startOf('day'); + const pay = moment(dateStr).startOf('day'); + if (!pay.isValid()) return null; + return pay.diff(today, 'days'); +}; + +const getLatestPayDateStatus = (dateStr) => { + const diff = getLatestPayDateDiffDays(dateStr); + if (diff === null) return { type: 'none', text: '未填写最晚付费日期' }; + if (diff < 0) return { type: 'overdue', text: `最晚付费已超期 ${Math.abs(diff)} 天`, diffDays: diff }; + if (diff <= LATEST_PAY_WARN_DAYS) return { type: 'warning', text: `最晚付费临期,剩余 ${diff} 天`, diffDays: diff }; + return { type: 'normal', text: `距离最晚付费 ${diff} 天`, diffDays: diff }; +}; + +const calcCompareSheetPayAlerts = (sheet) => { + let warning = 0; + let overdue = 0; + (sheet?.rows || []).forEach((row) => { + const st = getLatestPayDateStatus(row.latestPayDate); + if (st.type === 'warning') warning += 1; + if (st.type === 'overdue') overdue += 1; + }); + return { warning, overdue }; +}; + +const syncCompareSheetProcurementCounts = (sheet) => { + const rows = sheet?.rows || []; + const submittedProcurementCount = rows.filter((r) => r.procurementStatus === 'submitted' || r.procurementStatus === 'completed').length; + const completedCount = rows.filter((r) => r.procurementStatus === 'completed').length; + return { submittedProcurementCount, completedCount }; +}; + +const normalizeCompareRows = (rows) => (rows || []).map((row) => ({ + ...row, + procurementStatus: row.procurementStatus || 'none', +})); + +const normalizeCompareSheet = (sheet) => { + const rows = normalizeCompareRows(sheet.rows); + const attachments = Array.isArray(sheet.attachments) ? sheet.attachments : []; + return { + ...sheet, + rows, + attachments, + ...calcCompareSheetStats(rows), + ...syncCompareSheetProcurementCounts({ rows }), + }; +}; + +const createEmptyInsuranceItem = () => ({ + company: '', + policyNo: '', + endorsementNo: '', + startDate: '', + endDate: '', + premium: '', + payTime: '', + signDate: '', + coverageItems: '', + applicant: '', + insured: '', + updateTime: '', + updateUser: '', + policyTag: '', + reinstateDate: '', +}); + +const createEmptyCompareRow = () => ({ + id: createCompareRowId(), + plateNo: '', + vin: '', + customer: '', + ownerCompany: '', + brand: '', + model: '', + bodyColor: '', + regDate: '', + inspectExpire: '', + insureMode: '续保', + insuranceType: '交强险', + jqValidUntil: '', + syValidUntil: '', + latestPayDate: '', + quotes: [], + confirmedQuoteId: '', + procurementStatus: 'none', + procurementSubmittedAt: '', +}); + +const buildCompareRowFromVehicle = (v, insuranceData) => ({ + id: createCompareRowId(), + ...buildVehicleComparePatch(v, insuranceData), + latestPayDate: '', + quotes: [], + confirmedQuoteId: '', + procurementStatus: 'none', + procurementSubmittedAt: '', +}); + +const cloneCompareRow = (row) => ({ + ...JSON.parse(JSON.stringify(row)), + id: createCompareRowId(), + quotes: (row.quotes || []).map((q) => ({ ...q, id: createQuoteId() })), + confirmedQuoteId: '', +}); + +const MOCK_VEHICLES = [ + { plateNo: '沪A03561F', brand: '宇通', model: '49吨牵引车头', vin: 'LMRKH9AC0R1004086', status: '自营' }, + { plateNo: '粤B58888F', brand: '福田', model: '奥铃4.5吨冷藏车', vin: 'LGHXCAE28M6789012', status: '租赁' }, + { plateNo: '苏E33333', brand: '陕汽', model: '德龙X3000混动牵引车', vin: 'LSXCH9AE8M1094857', status: '自营' }, + { plateNo: '京A12345', brand: '东风', model: 'DFH1180厢式货车', vin: 'LKLG7C4E4NA774759', status: '退出运营' }, + { plateNo: '浙A88888', brand: '宇通', model: '氢燃料电池大巴', vin: 'LMRKH9AE2P9876543', status: '库存' }, + { plateNo: '沪D66666', brand: '比亚迪', model: 'T5轻卡', vin: 'LSVAU2BR3NS567890', status: '租赁' }, + { plateNo: '粤A12345', brand: '比亚迪', model: '汉EV', vin: 'LGWEF4A59NS123456', status: '租赁' }, + { plateNo: '苏A55678', brand: '福田', model: '欧马可4.2米', vin: 'LVBV3JBB8NY123456', status: '库存' }, + { plateNo: '', brand: '东风', model: '氢燃料电池牵引车(待上牌)', vin: 'LZYTBACR2M9999001', status: '库存' }, + { plateNo: '沪BDB9161', brand: '腾势', model: 'QCJ6520MBEV1纯电动', vin: 'LC0DF4CD8S0303140', status: '自营' }, + { plateNo: '粤AGR9766', brand: '帕力安', model: '燃料电池翼开启厢式车', vin: 'LB9A32A21R0LS1478', status: '自营' }, + { plateNo: '粤AGP9827', brand: '比亚迪', model: '轻卡', vin: 'LGXAGP98270000001', status: '租赁' }, + { plateNo: '粤AGP3071', brand: '福田', model: '欧马可', vin: 'LGXAGP30710000001', status: '租赁' }, + { plateNo: '粤A03423F', brand: '宇通', model: '49吨牵引', vin: 'LZYTBACR2A03423F01', status: '自营' }, + { plateNo: '粤A06290F', brand: '陕汽', model: '牵引车', vin: 'LZYTBACR2A06290F01', status: '自营' }, + { plateNo: '粤A03331F', brand: '东风', model: '厢式货车', vin: 'LZYTBACR2A03331F01', status: '自营' }, + { plateNo: '浙F03220F', brand: '福田', model: '冷藏车', vin: 'LZYTBACR2F03220F01', status: '租赁' }, + { plateNo: '沪A06192F', brand: '比亚迪', model: 'T5', vin: 'LZYTBACR2A06192F01', status: '自营' }, + { plateNo: '浙F05178F', brand: '福田', model: '牵引车', vin: 'LZYTBACR2F05178F01', status: '自营' }, +]; + +const hasVehiclePlate = (vehicle) => !!(vehicle?.plateNo || '').trim(); + +const getVehicleLedgerKey = (vehicleOrKey) => { + if (!vehicleOrKey) return ''; + if (typeof vehicleOrKey === 'object') { + const plate = (vehicleOrKey.plateNo || '').trim(); + if (plate) return plate; + return (vehicleOrKey.vin || '').trim(); + } + return String(vehicleOrKey).trim(); +}; + +const formatVehiclePlateDisplay = (plateNo) => { + const p = (plateNo || '').trim(); + return p || NO_PLATE_LABEL; +}; + +const isCompareRowVehicleLinked = (row) => !!(row?.plateNo || '').trim() || !!(row?.vin || '').trim(); + +const getVehicleProfile = (vehicle) => { + if (!vehicle) return {}; + const plate = (vehicle.plateNo || '').trim(); + if (plate && VEHICLE_PROFILES[plate]) return VEHICLE_PROFILES[plate]; + const vin = (vehicle.vin || '').trim(); + return VEHICLE_PROFILES[vin] || {}; +}; + +const getInitialInsuranceSeed = (vehicle) => { + const plate = (vehicle.plateNo || '').trim(); + if (plate && INITIAL_INSURANCE_DATA[plate]) return INITIAL_INSURANCE_DATA[plate]; + const vin = (vehicle.vin || '').trim(); + return INITIAL_INSURANCE_DATA[vin] || null; +}; + +const findVehicleByPlate = (plate) => { + const key = (plate || '').trim().toUpperCase(); + if (!key) return null; + return MOCK_VEHICLES.find((v) => (v.plateNo || '').trim().toUpperCase() === key) || null; +}; + +const findVehicleByVin = (vin) => { + const key = (vin || '').trim().toUpperCase(); + return MOCK_VEHICLES.find((v) => v.vin.toUpperCase() === key) || null; +}; + +const PLATE_SELECT_OPTIONS = MOCK_VEHICLES + .filter((v) => hasVehiclePlate(v)) + .map((v) => ({ label: v.plateNo, value: v.plateNo })); +const VIN_SELECT_OPTIONS = MOCK_VEHICLES.map((v) => ({ label: v.vin, value: v.vin })); + +const buildVehicleComparePatch = (vehicle, insuranceData) => { + if (!vehicle) return {}; + const profile = getVehicleProfile(vehicle); + const ins = insuranceData[getVehicleLedgerKey(vehicle)] || {}; + const jqEnd = ins.compulsory?.endDate || ''; + const syEnd = ins.commercial?.endDate || ''; + return { + plateNo: vehicle.plateNo, + vin: vehicle.vin, + customer: profile.customer || '', + ownerCompany: profile.ownerCompany || '', + brand: vehicle.brand, + model: vehicle.model, + bodyColor: profile.color || '', + regDate: profile.regDate || '', + inspectExpire: profile.inspectExpire || '', + insureMode: jqEnd || syEnd ? '续保' : '新保', + insuranceType: '交强险', + jqValidUntil: jqEnd, + syValidUntil: syEnd, + }; +}; + +const clearVehicleComparePatch = () => ({ + plateNo: '', + vin: '', + customer: '', + ownerCompany: '', + brand: '', + model: '', + bodyColor: '', + regDate: '', + inspectExpire: '', + insureMode: '续保', + insuranceType: '交强险', + jqValidUntil: '', + syValidUntil: '', +}); + +const createEmptyInsuranceRecord = () => ({ + compulsory: createEmptyInsuranceItem(), + commercial: createEmptyInsuranceItem(), + excess: createEmptyInsuranceItem(), + cargo: createEmptyInsuranceItem(), + driverAccident: createEmptyInsuranceItem(), +}); + +const ensureInsuranceRecordShape = (record) => { + const base = createEmptyInsuranceRecord(); + const next = { ...base }; + INSURANCE_TYPE_ITEMS.forEach(({ key }) => { + next[key] = { ...base[key], ...(record?.[key] || {}) }; + }); + return next; +}; + +const VEHICLE_INSURANCE_MGMT_TABS = [ + { key: 'timeline', label: '保险采购全周期记录' }, + { key: 'compulsory', label: '交强险' }, + { key: 'commercial', label: '商业险' }, + { key: 'excess', label: '超赔险' }, + { key: 'driverAccident', label: '驾意险' }, + { key: 'cargo', label: '货物险' }, +]; + +/** 管理页记录类型:新保 / 续保 / 停租 / 复驶 / 退保 */ +const POLICY_PURCHASE_TYPE_META = { + new: { label: '新保', color: 'success', timelineColor: 'green', chipClass: 'lc-purchase-type--new' }, + renew: { label: '续保', color: 'processing', timelineColor: 'blue', chipClass: 'lc-purchase-type--renew' }, + rentStop: { label: '停租', color: 'warning', timelineColor: 'orange', chipClass: 'lc-purchase-type--rent-stop' }, + resume: { label: '复驶', color: 'cyan', timelineColor: 'cyan', chipClass: 'lc-purchase-type--resume' }, + cancel: { label: '退保', color: 'default', timelineColor: 'gray', chipClass: 'lc-purchase-type--cancel' }, +}; + +const POLICY_PURCHASE_TYPE_LEGEND = ['new', 'renew', 'rentStop', 'resume', 'cancel']; + +const POLICY_LIKE_EVENT_TYPES = new Set(['purchase', 'renew', 'procurement', 'recognize']); + +const eventTypeToDefaultPurchaseType = (eventType) => { + if (eventType === 'suspend') return 'rentStop'; + if (eventType === 'resume') return 'resume'; + if (eventType === 'cancel') return 'cancel'; + if (eventType === 'renew') return 'renew'; + return 'new'; +}; + +const assignPurchaseTypesToRecords = (records) => { + const counters = {}; + const chronological = [...records].sort((a, b) => String(a.time).localeCompare(String(b.time))); + chronological.forEach((rec) => { + const typeKey = rec.typeKey || '_'; + if (!counters[typeKey]) counters[typeKey] = 0; + let purchaseType = eventTypeToDefaultPurchaseType(rec.eventType); + if (POLICY_LIKE_EVENT_TYPES.has(rec.eventType)) { + purchaseType = counters[typeKey] > 0 ? 'renew' : 'new'; + counters[typeKey] += 1; + } + rec.purchaseType = purchaseType; + }); + return records; +}; + +const subtractInsuranceYears = (dateStr, years) => { + if (!moment || !dateStr) return ''; + const d = moment(dateStr, 'YYYY-MM-DD', true); + if (!d.isValid()) return ''; + return d.subtract(years, 'year').format('YYYY-MM-DD'); +}; + +const vehicleMatchesCompareRow = (vehicle, row) => { + if (!vehicle || !row) return false; + const ledgerKey = getVehicleLedgerKey(vehicle); + const rowKey = getVehicleLedgerKey({ plateNo: row.plateNo, vin: row.vin }); + return !!ledgerKey && ledgerKey === rowKey; +}; + +const createInsuranceHistoryRecord = (payload) => ({ + id: payload.id || `ih-${payload.typeKey}-${payload.eventType}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, + typeKey: payload.typeKey, + typeLabel: payload.typeLabel, + eventType: payload.eventType, + purchaseType: payload.purchaseType || eventTypeToDefaultPurchaseType(payload.eventType), + time: payload.time || '', + payTime: payload.payTime || '', + purchaseTime: payload.time || '', + policyNo: payload.policyNo || '', + company: payload.company || '', + premium: payload.premium || '', + startDate: payload.startDate || '', + endDate: payload.endDate || '', + source: payload.source || 'ledger', + sourceLabel: payload.sourceLabel || '', + policyTag: payload.policyTag || '', + reinstateDate: payload.reinstateDate || '', + policyDetail: payload.policyDetail || null, + fileName: payload.fileName || (payload.policyNo ? `${payload.policyNo}_${payload.typeLabel}.pdf` : '保单附件.pdf'), +}); + +const purchaseTypeToBizType = (purchaseType, eventType) => { + if (purchaseType === 'rentStop' || eventType === 'suspend') return 'suspend'; + if (purchaseType === 'resume' || eventType === 'resume') return 'resume'; + if (purchaseType === 'cancel' || eventType === 'cancel') return 'cancel'; + return 'policy'; +}; + +const historyRecordToPolicyDetail = (record, vehicle) => { + if (record?.policyDetail) { + return normalizePolicyDetail({ + ...record.policyDetail, + plateNo: vehicle?.plateNo || record.policyDetail.plateNo, + vin: vehicle?.vin || record.policyDetail.vin, + }); + } + return normalizePolicyDetail({ + plateNo: vehicle?.plateNo || '', + vin: vehicle?.vin || '', + insuranceType: record?.typeLabel || '交强险', + bizType: purchaseTypeToBizType(record?.purchaseType, record?.eventType), + company: record?.company || '', + policyNo: record?.policyNo || '', + payTime: record?.payTime || '', + startDate: record?.startDate || '', + endDate: record?.endDate || '', + reinstateDate: record?.reinstateDate || '', + premium: record?.premium || '', + coverageItems: parseCoverageItemsInput(record?.policyDetail?.coverageItems), + applicant: '', + insured: '', + signDate: '', + }); +}; + +const applyPolicyDetailToHistoryRecord = (record, detail) => { + const d = normalizePolicyDetail(detail); + const time = d.startDate || record.time; + const typeLabel = d.insuranceType || record.typeLabel; + const next = { + ...record, + policyDetail: d, + typeLabel, + policyNo: d.policyNo || record.policyNo, + company: d.company, + payTime: d.payTime, + startDate: d.startDate, + endDate: d.endDate, + premium: d.premium, + reinstateDate: d.reinstateDate, + time, + purchaseTime: time, + fileName: d.policyNo ? `${d.policyNo}_${typeLabel}.pdf` : record.fileName, + }; + next.summary = getInsuranceEventSummary(next); + return next; +}; + +const applyHistoryEditsToVehicleHistory = (history, edits) => { + if (!history || !edits || typeof edits !== 'object') return history; + const patch = (r) => { + const detail = edits[r.id]; + if (!detail) return r; + return applyPolicyDetailToHistoryRecord(r, detail); + }; + const byType = {}; + Object.keys(history.byType || {}).forEach((k) => { + byType[k] = (history.byType[k] || []).map(patch); + }); + return { + ...history, + timeline: (history.timeline || []).map(patch), + byType, + }; +}; + +const buildPolicyDetailFromLedgerItem = (vehicle, typeLabel, item, bizType = 'policy') => normalizePolicyDetail({ + plateNo: vehicle?.plateNo || '', + vin: vehicle?.vin || '', + insuranceType: typeLabel, + bizType, + company: item?.company || '', + policyNo: item?.policyNo || '', + endorsementNo: item?.endorsementNo || '', + payTime: item?.payTime || '', + signDate: item?.signDate || '', + startDate: item?.startDate || '', + endDate: item?.endDate || '', + reinstateDate: item?.reinstateDate || '', + premium: item?.premium || '', + coverageItems: parseCoverageItemsInput(item?.coverageItems), + applicant: item?.applicant || '', + insured: item?.insured || '', +}); + +const getInsuranceEventSummary = (record) => { + const premiumText = record.premium ? `,金额 ¥${record.premium}` : ''; + const typeLabel = POLICY_PURCHASE_TYPE_META[record.purchaseType]?.label || '记录'; + const period = record.startDate && record.endDate + ? `,期间 ${record.startDate} 至 ${record.endDate}` + : (record.endDate ? `,到期 ${record.endDate}` : ''); + switch (record.purchaseType) { + case 'new': + return `${typeLabel} ${record.typeLabel},保单号 ${record.policyNo || '—'}${period}${premiumText}`; + case 'renew': + return `${typeLabel} ${record.typeLabel},保单号 ${record.policyNo || '—'}${period}${premiumText}`; + case 'rentStop': + return `${typeLabel} · ${record.typeLabel},保单号 ${record.policyNo || '—'}${record.reinstateDate ? `,预计复驶 ${record.reinstateDate}` : ''}`; + case 'resume': + return `${typeLabel} · ${record.typeLabel},保单号 ${record.policyNo || '—'}${period}`; + case 'cancel': + return `${typeLabel} · ${record.typeLabel},保单号 ${record.policyNo || '—'}${premiumText}`; + default: + return `${record.typeLabel} · ${record.policyNo || '—'}`; + } +}; + +const buildVehicleInsuranceHistory = (vehicle, allInsurance, compareSheets, policyRecognTasks) => { + const ledgerKey = getVehicleLedgerKey(vehicle); + const record = ensureInsuranceRecordShape(allInsurance[ledgerKey] || createEmptyInsuranceRecord()); + const records = []; + + INSURANCE_TYPE_ITEMS.forEach(({ key: typeKey, fullLabel: typeLabel }) => { + const item = record[typeKey]; + if (!item?.policyNo) return; + + const purchaseTime = item.startDate || item.updateTime || item.endDate; + records.push(createInsuranceHistoryRecord({ + id: `ih-${ledgerKey}-${typeKey}-ledger-current`, + typeKey, + typeLabel, + eventType: 'purchase', + time: purchaseTime, + payTime: item.payTime || '', + policyNo: item.policyNo, + company: item.company, + premium: item.premium, + startDate: item.startDate, + endDate: item.endDate, + policyDetail: buildPolicyDetailFromLedgerItem(vehicle, typeLabel, item, 'policy'), + source: 'ledger', + sourceLabel: '台账当前保单', + fileName: `${item.policyNo}_${typeLabel}.pdf`, + })); + + if (item.startDate) { + const prevStart = subtractInsuranceYears(item.startDate, 1); + const prevEnd = item.endDate ? subtractInsuranceYears(item.endDate, 1) : ''; + if (prevStart) { + records.push(createInsuranceHistoryRecord({ + id: `ih-${ledgerKey}-${typeKey}-history-${prevStart}`, + typeKey, + typeLabel, + eventType: 'renew', + time: prevStart, + policyNo: `${String(item.policyNo).slice(0, 12)}-H1`, + company: item.company, + premium: item.premium, + startDate: prevStart, + endDate: prevEnd, + source: 'history', + sourceLabel: '历史续保', + fileName: `${item.policyNo}_续保_${typeLabel}.pdf`, + })); + } + } + + if (item.policyTag === 'suspended') { + records.push(createInsuranceHistoryRecord({ + id: `ih-${ledgerKey}-${typeKey}-ledger-suspend`, + typeKey, + typeLabel, + eventType: 'suspend', + time: item.updateTime || item.endDate || purchaseTime, + policyNo: item.policyNo, + company: item.company, + premium: item.premium, + startDate: item.startDate, + endDate: item.endDate, + policyTag: 'suspended', + reinstateDate: item.reinstateDate, + policyDetail: buildPolicyDetailFromLedgerItem(vehicle, typeLabel, item, 'suspend'), + source: 'ledger', + sourceLabel: '停租', + fileName: `${item.policyNo}_停租_${typeLabel}.pdf`, + })); + } + if (item.policyTag === 'cancelled') { + records.push(createInsuranceHistoryRecord({ + id: `ih-${ledgerKey}-${typeKey}-ledger-cancel`, + typeKey, + typeLabel, + eventType: 'cancel', + time: item.updateTime || item.endDate || purchaseTime, + policyNo: item.policyNo, + company: item.company, + premium: item.premium, + startDate: item.startDate, + endDate: item.endDate, + policyTag: 'cancelled', + policyDetail: buildPolicyDetailFromLedgerItem(vehicle, typeLabel, item, 'cancel'), + source: 'ledger', + sourceLabel: '退保', + fileName: `${item.policyNo}_退保_${typeLabel}.pdf`, + })); + } + }); + + (compareSheets || []).forEach((sheet) => { + (sheet.rows || []).forEach((row) => { + if (!vehicleMatchesCompareRow(vehicle, row)) return; + const typeKey = INSURANCE_LABEL_TO_KEY[row.insuranceType] || 'compulsory'; + const typeLabel = row.insuranceType || INSURANCE_TYPE_ITEMS.find((it) => it.key === typeKey)?.fullLabel || '—'; + const confirmed = (row.quotes || []).find((q) => q.id === row.confirmedQuoteId); + if (!confirmed) return; + const eventType = row.procurementStatus === 'completed' ? 'procurement' : 'procurement'; + records.push(createInsuranceHistoryRecord({ + id: `ih-compare-${sheet.id}-${row.id}`, + typeKey, + typeLabel, + eventType, + time: row.procurementSubmittedAt || sheet.createdAt, + policyNo: confirmed.policyNo || `CG-${String(row.id).slice(-8)}`, + company: confirmed.company, + premium: confirmed.premium, + source: 'compare', + sourceLabel: sheet.periodLabel ? `比价单 · ${sheet.periodLabel}` : '比价单采购', + fileName: `${sheet.id || 'sheet'}_${typeLabel}_采购单.pdf`, + })); + }); + }); + + (policyRecognTasks || []).forEach((task) => { + (task.results || []).forEach((r) => { + if (!r.confirmed || r.ledgerKey !== ledgerKey) return; + const typeKey = r.typeKey || INSURANCE_LABEL_TO_KEY[r.insuranceTypeLabel]; + if (!typeKey) return; + let eventType = 'recognize'; + if (task.mode === 'suspend') eventType = 'suspend'; + else if (task.mode === 'cancel') eventType = 'cancel'; + else if (task.mode === 'resume') eventType = 'resume'; + records.push(createInsuranceHistoryRecord({ + id: `ih-recognize-${task.id}-${r.id}`, + typeKey, + typeLabel: r.insuranceTypeLabel || INSURANCE_TYPE_ITEMS.find((it) => it.key === typeKey)?.fullLabel, + eventType, + time: task.completedAt || task.createdAt, + policyNo: r.ocrPolicyNo, + company: r.ocrCompany, + premium: r.ocrPremium || '', + payTime: r.ocrPayTime || '', + startDate: r.ocrStartDate || '', + endDate: r.ocrEndDate, + reinstateDate: r.reinstateDate, + policyTag: eventType === 'suspend' ? 'suspended' : eventType === 'cancel' ? 'cancelled' : '', + policyDetail: r.policyDetail ? normalizePolicyDetail(r.policyDetail) : recognResultToPolicyDetail(r), + source: 'recognize', + sourceLabel: task.entryLabel || '批量识别', + recognizeTaskId: task.id, + recognizeResultId: r.id, + fileName: r.fileName || `${r.ocrPolicyNo}_${r.insuranceTypeLabel}.pdf`, + })); + }); + }); + + assignPurchaseTypesToRecords(records); + records.sort((a, b) => String(b.time).localeCompare(String(a.time))); + + const timeline = records.map((r) => ({ + ...r, + summary: getInsuranceEventSummary(r), + })); + + const byType = {}; + INSURANCE_TYPE_ITEMS.forEach(({ key }) => { byType[key] = []; }); + records.forEach((r) => { + if (byType[r.typeKey]) byType[r.typeKey].push(r); + }); + Object.keys(byType).forEach((k) => { + byType[k].sort((a, b) => String(b.time).localeCompare(String(a.time))); + }); + + return { timeline, byType, ledgerKey }; +}; + +const INITIAL_INSURANCE_DATA = { + '沪A03561F': { + compulsory: { company: '中国人民财产保险', policyNo: 'PDZA202533048200000123', startDate: '2025-01-01', endDate: '2026-12-31', premium: '950.00', updateTime: '2025-01-05 10:00', updateUser: '张明辉' }, + commercial: { company: '中国人民财产保险', policyNo: 'PDAA202533048200000456', startDate: '2025-01-01', endDate: '2026-12-31', premium: '12800.50', updateTime: '2025-01-05 10:00', updateUser: '张明辉' }, + excess: { company: '中国平安财产保险', policyNo: 'PAIC-CP-2025-8899', startDate: '2025-07-01', endDate: '2026-06-30', premium: '3200.00', updateTime: '2025-06-28 14:20', updateUser: '李专员' }, + cargo: { company: '中国太平洋财产保险', policyNo: 'CPIC-HW-2025-1122', startDate: '2025-03-15', endDate: '2026-03-14', premium: '1800.00', updateTime: '2025-03-10 09:15', updateUser: '李专员' }, + driverAccident: { company: '', policyNo: '', startDate: '', endDate: '', premium: '', updateTime: '', updateUser: '' }, + }, + '粤B58888F': { + compulsory: { company: '阳光财产保险', policyNo: 'YGCI-JQ-2025-3301', startDate: '2025-09-01', endDate: '2026-08-31', premium: '950.00', updateTime: '2025-08-28 11:00', updateUser: '王专员' }, + commercial: { company: '', policyNo: '', startDate: '', endDate: '', premium: '', updateTime: '', updateUser: '' }, + excess: { company: '', policyNo: '', startDate: '', endDate: '', premium: '', updateTime: '', updateUser: '' }, + cargo: { company: '中国人寿财产保险', policyNo: 'GPIC-HW-2025-7788', startDate: '2025-04-01', endDate: '2026-03-31', premium: '1600.00', updateTime: '2025-03-28 16:40', updateUser: '王专员' }, + driverAccident: { company: '中华联合财产保险', policyNo: 'CIC-JY-2025-001', startDate: '2025-05-01', endDate: '2026-04-30', premium: '560.00', updateTime: '2025-04-28 10:00', updateUser: '王专员' }, + }, + '苏E33333': { + compulsory: { company: '中国太平洋财产保险', policyNo: 'CPIC-JQ-2024-7788', startDate: '2024-06-01', endDate: '2025-05-31', premium: '880.00', updateTime: '2024-05-28 09:00', updateUser: '陈高伟' }, + commercial: { company: '中国人寿财产保险', policyNo: 'GPIC-SY-2025-1122', startDate: '2025-07-01', endDate: '2026-06-30', premium: '9850.00', updateTime: '2025-06-25 15:30', updateUser: '陈高伟' }, + excess: { company: '', policyNo: '', startDate: '', endDate: '', premium: '', updateTime: '', updateUser: '' }, + cargo: { company: '', policyNo: '', startDate: '', endDate: '', premium: '', updateTime: '', updateUser: '' }, + driverAccident: { company: '', policyNo: '', startDate: '', endDate: '', premium: '', updateTime: '', updateUser: '' }, + }, + '京A12345': { + compulsory: { company: '', policyNo: '', startDate: '', endDate: '', premium: '', updateTime: '', updateUser: '' }, + commercial: { company: '', policyNo: '', startDate: '', endDate: '', premium: '', updateTime: '', updateUser: '' }, + excess: { company: '', policyNo: '', startDate: '', endDate: '', premium: '', updateTime: '', updateUser: '' }, + cargo: { company: '', policyNo: '', startDate: '', endDate: '', premium: '', updateTime: '', updateUser: '' }, + driverAccident: { company: '', policyNo: '', startDate: '', endDate: '', premium: '', updateTime: '', updateUser: '' }, + }, + '浙A88888': { + compulsory: { company: '中国人民财产保险', policyNo: 'PDZA202533048200000789', startDate: '2025-07-01', endDate: '2026-06-30', premium: '950.00', updateTime: '2025-06-28 10:00', updateUser: '张小凡' }, + commercial: { company: '中国人民财产保险', policyNo: 'PDAA202533048200000790', startDate: '2025-01-01', endDate: '2027-12-31', premium: '15600.00', updateTime: '2025-01-05 10:00', updateUser: '张小凡' }, + excess: { company: '中国平安财产保险', policyNo: 'PAIC-CP-2025-9900', startDate: '2025-01-01', endDate: '2026-12-31', premium: '2800.00', updateTime: '2025-01-03 11:00', updateUser: '张小凡' }, + cargo: { company: '', policyNo: '', startDate: '', endDate: '', premium: '', updateTime: '', updateUser: '' }, + driverAccident: { company: '阳光财产保险', policyNo: 'YGCI-JY-2025-2200', startDate: '2025-09-01', endDate: '2026-08-31', premium: '480.00', updateTime: '2025-08-30 09:00', updateUser: '张小凡' }, + }, + '沪D66666': { + compulsory: { company: '中国人民财产保险', policyNo: 'PDZA202533048200000321', startDate: '2025-02-01', endDate: '2026-01-31', premium: '950.00', updateTime: '2025-01-28 14:00', updateUser: '赵六' }, + commercial: { company: '中国人民财产保险', policyNo: 'PDAA202533048200000322', startDate: '2025-02-01', endDate: '2026-01-31', premium: '11200.00', updateTime: '2025-01-28 14:00', updateUser: '赵六' }, + excess: { company: '', policyNo: '', startDate: '', endDate: '', premium: '', updateTime: '', updateUser: '' }, + cargo: { company: '', policyNo: '', startDate: '', endDate: '', premium: '', updateTime: '', updateUser: '' }, + driverAccident: { company: '', policyNo: '', startDate: '', endDate: '', premium: '', updateTime: '', updateUser: '' }, + }, + '粤A12345': { + compulsory: { company: '中国平安财产保险', policyNo: 'PAIC-JQ-2025-4455', startDate: '2025-03-01', endDate: '2026-02-28', premium: '950.00', updateTime: '2025-02-25 10:00', updateUser: '张三' }, + commercial: { company: '中国平安财产保险', policyNo: 'PAIC-SY-2025-4456', startDate: '2025-03-01', endDate: '2026-02-28', premium: '10500.00', updateTime: '2025-02-25 10:00', updateUser: '张三' }, + excess: { company: '中国太平洋财产保险', policyNo: 'CPIC-CP-2025-5566', startDate: '2025-03-01', endDate: '2026-02-28', premium: '2400.00', updateTime: '2025-02-25 10:00', updateUser: '张三' }, + cargo: { company: '中华联合财产保险', policyNo: 'CIC-HW-2025-7788', startDate: '2025-03-01', endDate: '2026-02-28', premium: '1500.00', updateTime: '2025-02-25 10:00', updateUser: '张三' }, + driverAccident: { company: '阳光财产保险', policyNo: 'YGCI-JY-2025-9900', startDate: '2025-03-01', endDate: '2026-02-28', premium: '520.00', updateTime: '2025-02-25 10:00', updateUser: '张三' }, + }, + '苏A55678': { + compulsory: { company: '中国人寿财产保险', policyNo: 'GPIC-JQ-2025-0011', startDate: '2025-05-01', endDate: '2026-04-30', premium: '880.00', updateTime: '2025-04-28 09:00', updateUser: '孙七' }, + commercial: { company: '', policyNo: '', startDate: '', endDate: '', premium: '', updateTime: '', updateUser: '' }, + excess: { company: '', policyNo: '', startDate: '', endDate: '', premium: '', updateTime: '', updateUser: '' }, + cargo: { company: '', policyNo: '', startDate: '', endDate: '', premium: '', updateTime: '', updateUser: '' }, + driverAccident: { company: '', policyNo: '', startDate: '', endDate: '', premium: '', updateTime: '', updateUser: '' }, + }, + LZYTBACR2M9999001: { + compulsory: { company: '中国人民财产保险', policyNo: 'PDZA202533048200000901', startDate: '2025-10-01', endDate: '2026-09-30', premium: '950.00', updateTime: '2025-09-28 10:00', updateUser: '李专员' }, + commercial: { company: '', policyNo: '', startDate: '', endDate: '', premium: '', updateTime: '', updateUser: '' }, + excess: { company: '中国平安财产保险', policyNo: 'PAIC-CP-2025-7701', startDate: '2025-10-01', endDate: '2026-09-30', premium: '2600.00', updateTime: '2025-09-28 10:00', updateUser: '李专员' }, + cargo: { company: '', policyNo: '', startDate: '', endDate: '', premium: '', updateTime: '', updateUser: '' }, + driverAccident: { company: '', policyNo: '', startDate: '', endDate: '', premium: '', updateTime: '', updateUser: '' }, + }, +}; + +const loadInsuranceFromStorage = () => { + try { + const raw = localStorage.getItem(IPC_STORAGE_KEY); + if (!raw) return null; + const parsed = JSON.parse(raw); + return parsed && typeof parsed === 'object' ? parsed : null; + } catch { + return null; + } +}; + +const persistInsuranceToStorage = (data) => { + try { + localStorage.setItem(IPC_STORAGE_KEY, JSON.stringify(data)); + } catch { + /* ignore */ + } +}; + +const loadCompareSheetsFromStorage = () => { + try { + const raw = localStorage.getItem(IPC_COMPARE_SHEETS_KEY); + if (!raw) return null; + const parsed = JSON.parse(raw); + return Array.isArray(parsed) ? parsed : null; + } catch { + return null; + } +}; + +const persistCompareSheetsToStorage = (sheets) => { + try { + localStorage.setItem(IPC_COMPARE_SHEETS_KEY, JSON.stringify(sheets)); + } catch { + /* ignore */ + } +}; + +const POLICY_RECOGN_ENTRY_LABEL = { ocr: '保单批量识别', import: '批量导入' }; +const POLICY_RECOGN_MODE_LABEL = Object.fromEntries(POLICY_OCR_MODES.map((m) => [m.key, m.label])); +const POLICY_RECOGN_STATUS_META = { + pending_confirm: { label: '待确认', color: 'warning' }, + partial: { label: '部分确认', color: 'processing' }, + completed: { label: '已完成', color: 'success' }, +}; + +const derivePolicyRecognTaskStatus = (results) => { + const list = results || []; + const matched = list.filter((r) => r.matched); + const confirmedMatched = matched.filter((r) => r.confirmed); + if (matched.length > 0 && confirmedMatched.length >= matched.length) return 'completed'; + if (list.some((r) => r.confirmed)) return 'partial'; + return 'pending_confirm'; +}; + +const summarizePolicyRecognTask = (results) => { + const list = results || []; + return { + fileCount: list.length, + matchedCount: list.filter((r) => r.matched).length, + confirmedCount: list.filter((r) => r.confirmed).length, + }; +}; + +const buildPolicyRecognTaskRecord = ({ + id, + entry, + mode, + insuranceType, + results, + createdAt, + creator, + status, + completedAt, + phase, +}) => { + const stats = summarizePolicyRecognTask(results); + return { + id, + createdAt: createdAt || formatCompareSheetNow(), + completedAt: completedAt || '', + entry, + entryLabel: POLICY_RECOGN_ENTRY_LABEL[entry] || entry, + mode, + modeLabel: POLICY_RECOGN_MODE_LABEL[mode] || mode, + insuranceType: insuranceType || '', + creator: creator || PROTO_COMPARE_CREATOR, + status: status || derivePolicyRecognTaskStatus(results), + phase: phase || 'results', + ...stats, + results: (results || []).map((r) => ({ ...r })), + fileNames: (results || []).map((r) => r.fileName).filter(Boolean), + }; +}; + +const loadPolicyRecognTasksFromStorage = () => { + try { + const raw = localStorage.getItem(IPC_POLICY_RECOGN_TASKS_KEY); + if (!raw) return null; + const parsed = JSON.parse(raw); + return Array.isArray(parsed) ? parsed : null; + } catch { + return null; + } +}; + +const persistPolicyRecognTasksToStorage = (tasks) => { + try { + localStorage.setItem(IPC_POLICY_RECOGN_TASKS_KEY, JSON.stringify(tasks)); + } catch { + /* ignore */ + } +}; + +const loadInsuranceHistoryEditsFromStorage = () => { + try { + const raw = localStorage.getItem(IPC_INSURANCE_HISTORY_EDITS_KEY); + if (!raw) return {}; + const parsed = JSON.parse(raw); + return parsed && typeof parsed === 'object' ? parsed : {}; + } catch { + return {}; + } +}; + +const persistInsuranceHistoryEditsToStorage = (edits) => { + try { + localStorage.setItem(IPC_INSURANCE_HISTORY_EDITS_KEY, JSON.stringify(edits)); + } catch { + /* ignore */ + } +}; + +const createMockPolicyRecognTasks = () => { + const insMap = buildMockInsuranceMap(); + const files1 = [ + { uid: 'demo-f1', name: '粤BDG9701_交强险.pdf', status: 'done' }, + { uid: 'demo-f2', name: '粤AGR9766_超赔险.pdf', status: 'done' }, + ]; + const results1 = buildMockOcrResults(files1, 'policy', '交强险', insMap); + results1.forEach((r) => { r.confirmed = true; }); + + const files2 = [ + { uid: 'demo-f3', name: '沪A03561F_商业险.jpg', status: 'done' }, + { uid: 'demo-f4', name: '粤B88888_交强险.pdf', status: 'done' }, + ]; + const results2 = buildMockOcrResults(files2, 'policy', '商业险', insMap); + if (results2[0]) results2[0].confirmed = true; + + return [ + buildPolicyRecognTaskRecord({ + id: 'TASK-83892906', + entry: 'ocr', + mode: 'policy', + insuranceType: '交强险', + results: results1, + createdAt: '2026-05-28 15:20:10', + completedAt: '2026-05-28 15:32:00', + phase: 'results', + }), + buildPolicyRecognTaskRecord({ + id: 'TASK-84120155', + entry: 'import', + mode: 'policy', + insuranceType: '商业险', + results: results2, + createdAt: '2026-05-30 09:15:00', + phase: 'results', + }), + ]; +}; + +const buildMockInsuranceMap = () => { + const map = {}; + MOCK_VEHICLES.forEach((v) => { + const key = getVehicleLedgerKey(v); + const seed = getInitialInsuranceSeed(v); + map[key] = seed ? JSON.parse(JSON.stringify(seed)) : createEmptyInsuranceRecord(); + }); + return map; +}; + +const createMockCompareRowWithQuote = (vehicle, insMap, insuranceType, premium, extra = {}) => { + const row = buildCompareRowFromVehicle(vehicle, insMap); + const quoteId = createQuoteId(); + row.insuranceType = insuranceType; + row.quotes = [{ id: quoteId, company: INSURANCE_MGMT_COMPANIES[0], premium }]; + row.confirmedQuoteId = quoteId; + row.latestPayDate = extra.latestPayDate || '2026-06-15'; + row.procurementStatus = extra.procurementStatus || 'none'; + row.procurementSubmittedAt = extra.procurementSubmittedAt || ''; + return row; +}; + +const createMockCompareSheets = () => { + const insMap = buildMockInsuranceMap(); + const sheet1Rows = normalizeCompareRows([ + createMockCompareRowWithQuote(MOCK_VEHICLES[0], insMap, '交强险', '950.00', { latestPayDate: '2026-06-03', procurementStatus: 'completed' }), + createMockCompareRowWithQuote(MOCK_VEHICLES[0], insMap, '商业险', '12800.50', { latestPayDate: '2026-06-04', procurementStatus: 'submitted' }), + createMockCompareRowWithQuote(MOCK_VEHICLES[1], insMap, '交强险', '950.00', { latestPayDate: '2026-05-28' }), + ]); + const sheet2Rows = normalizeCompareRows([ + createMockCompareRowWithQuote(MOCK_VEHICLES[2], insMap, '交强险', '880.00', { latestPayDate: '2026-06-02', procurementStatus: 'completed' }), + createMockCompareRowWithQuote(MOCK_VEHICLES[2], insMap, '商业险', '9850.00', { latestPayDate: '2026-06-02', procurementStatus: 'completed' }), + createMockCompareRowWithQuote(MOCK_VEHICLES[7], insMap, '交强险', '950.00', { latestPayDate: '2026-06-01', procurementStatus: 'submitted' }), + createMockCompareRowWithQuote(MOCK_VEHICLES[7], insMap, '超赔险', '2600.00', { latestPayDate: '2026-07-01', procurementStatus: 'submitted' }), + ]); + const sheet3Rows = normalizeCompareRows([ + createMockCompareRowWithQuote(MOCK_VEHICLES[4], insMap, '商业险', '15600.00', { latestPayDate: '2026-05-20' }), + ]); + return [ + normalizeCompareSheet({ + id: 'cs-mock-20260528', + createdAt: '2026-05-28 14:20:00', + createdBy: '张明辉', + periodLabel: '2026年5-6月', + remark: '华东区二季度集中采购', + attachments: [ + { id: 'att-demo-1', uid: 'att-demo-1', name: '6月比价询价单.pdf', size: 245760, type: 'application/pdf', uploadedAt: '2026-05-28 14:18:00' }, + { id: 'att-demo-2', uid: 'att-demo-2', name: '保险公司报价截图.zip', size: 1048576, type: 'application/zip', uploadedAt: '2026-05-28 14:19:00' }, + ], + rows: sheet1Rows, + }), + normalizeCompareSheet({ + id: 'cs-mock-20260520', + createdAt: '2026-05-20 09:15:00', + createdBy: '李专员', + periodLabel: '2026年5月', + remark: '苏粤车辆续保比价', + rows: sheet2Rows, + }), + normalizeCompareSheet({ + id: 'cs-mock-20260510', + createdAt: '2026-05-10 16:40:00', + createdBy: '王专员', + periodLabel: '2026年5月', + remark: '', + rows: sheet3Rows, + }), + ]; +}; + +const compareSheetMatchesPlateFilter = (sheet, plateKey) => { + if (!plateKey) return true; + const key = plateKey.trim().toLowerCase(); + return (sheet.rows || []).some((row) => { + const plate = (row.plateNo || '').trim().toLowerCase(); + const vin = (row.vin || '').trim().toLowerCase(); + if (plate && plate.includes(key)) return true; + if (!plate && (NO_PLATE_LABEL.toLowerCase().includes(key) || key.includes('暂无'))) return true; + if (vin && vin.includes(key)) return true; + return false; + }); +}; + +const compareSheetMatchesCreatedRange = (createdAt, range) => { + if (!range || !range[0] || !range[1]) return true; + if (!createdAt || !moment) return true; + const day = moment(String(createdAt).slice(0, 10), 'YYYY-MM-DD', true); + if (!day.isValid()) return true; + const start = range[0].clone().startOf('day'); + const end = range[1].clone().endOf('day'); + return day.isSameOrAfter(start) && day.isSameOrBefore(end); +}; + +const DEFAULT_COMPARE_MGMT_FILTERS = { plateNo: '', createdRange: null }; +const DEFAULT_POLICY_RECOGN_TASK_FILTERS = { taskId: '', entry: '全部', status: '全部', createdRange: null }; + +const tableTitleMultiline = (...lines) => ( +
+ {lines.map((line, idx) => ( + {line} + ))} +
+); + +const listColumnHeaderCell = () => ({ className: 'lc-th-wrap' }); + +const mapInsuranceStatusToBadge = (type) => { + if (type === 'success') return 'success'; + if (type === 'warning') return 'warning'; + if (type === 'expired') return 'error'; + return 'default'; +}; + +/** 到期时间列:剩余 / 过期天数文案 */ +const getInsuranceRemainShortText = (status) => { + const { type, diffDays } = status || {}; + if (type === 'unuploaded') return '未购买'; + if (type === 'expired') return diffDays != null ? `过期${Math.abs(diffDays)}天` : '已过期'; + if (diffDays != null) return `剩余${diffDays}天`; + return '—'; +}; + +const sortVehiclesRetiredLast = (vehicles) => { + const active = []; + const retired = []; + vehicles.forEach((v) => { + if (v.status === '退出运营') retired.push(v); + else active.push(v); + }); + return [...active, ...retired]; +}; + +const parseMultiPlates = (text) => { + const raw = (text || '').trim(); + if (!raw) return []; + const lines = raw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean); + const expanded = lines.flatMap((line) => { + if (/[,,、;;]/.test(line)) { + return line.split(/[,,、;;]+/).map((s) => s.trim()).filter(Boolean); + } + return [line]; + }); + return [...new Set(expanded.map((s) => s.toUpperCase()))]; +}; + +const ICONS = { + vehicle: , + success: , + warning: , + shield: , + policy: , +}; + +const PAGE_STYLE = ` +.lc-edit-page { font-family: system-ui, -apple-system, sans-serif; color: #1e293b; } +.lc-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; } +.lc-filter-card.ant-card { border-radius: 16px !important; border: 1px solid #e2e8f0 !important; box-shadow: 0 4px 20px -4px rgba(15, 23, 42, 0.03) !important; margin-bottom: 16px; } +.lc-filter-card > .ant-card-head { border-bottom: 1px solid #f1f5f9 !important; min-height: auto; padding: 12px 20px !important; } +.lc-filter-card > .ant-card-head .ant-card-head-title { font-size: 15px !important; font-weight: 700 !important; color: #0f172a !important; padding: 0 !important; } +.lc-filter-card > .ant-card-body { padding: 16px 20px 20px !important; } +.lc-filter-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px 24px; } +@media (max-width: 1100px) { .lc-filter-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } } +@media (max-width: 720px) { .lc-filter-grid { grid-template-columns: 1fr; } } +.lc-filter-field { display: flex; align-items: center; gap: 12px; min-width: 0; } +.lc-filter-field-label { flex: 0 0 72px; text-align: right; font-size: 13px; font-weight: 500; color: #475569; line-height: 1.4; white-space: nowrap; } +.lc-filter-field-control { flex: 1; min-width: 0; } +.lc-filter-field-control .ant-input, .lc-filter-field-control .ant-select { width: 100%; } +.lc-multi-plate-pop { width: 320px; padding: 4px 2px; } +.lc-multi-plate-pop-hint { font-size: 12px; color: #64748b; margin-bottom: 8px; line-height: 1.5; } +.lc-multi-plate-pop-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 10px; } +.lc-multi-plate-trigger { cursor: pointer; } +.lc-multi-plate-trigger .ant-input { cursor: pointer; } +.lc-filter-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; padding-top: 16px; border-top: 1px solid #f1f5f9; } +.lc-alert-stats-row { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 12px; margin-bottom: 16px; } +@media (max-width: 1200px) { .lc-alert-stats-row { grid-template-columns: repeat(3, minmax(0, 1fr)); } } +@media (max-width: 768px) { .lc-alert-stats-row { grid-template-columns: repeat(2, minmax(0, 1fr)); } } +.lc-alert-card { display: flex; align-items: flex-start; gap: 12px; padding: 14px 30px 14px 16px; border-radius: 12px; border: 1px solid #e2e8f0; background: #fff; position: relative; overflow: hidden; min-width: 0; } +.lc-alert-card-main { flex: 1; min-width: 0; } +.lc-alert-card-icon { flex-shrink: 0; width: 40px; height: 40px; border-radius: 10px; display: flex; align-items: center; justify-content: center; } +.lc-alert-card-val { font-size: 26px; font-weight: 800; line-height: 1.1; color: #0f172a; font-variant-numeric: tabular-nums; } +.lc-alert-card-title { font-size: 13px; font-weight: 600; color: #334155; margin-top: 2px; } +.lc-alert-card-tip-anchor { position: absolute; top: 8px; right: 8px; z-index: 2; line-height: 0; } +.lc-alert-card-tip { width: 18px; height: 18px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; color: #94a3b8; background: rgba(255,255,255,.92); border: 1px solid #e2e8f0; cursor: help; line-height: 0; } +.lc-alert-card-tip:hover { color: #64748b; border-color: #cbd5e1; background: #fff; } +.lc-alert-card--total { background: linear-gradient(135deg, #f8fafc 0%, #fff 100%); } +.lc-alert-card--total .lc-alert-card-icon { background: #e2e8f0; color: #475569; } +.lc-alert-card--normal { background: linear-gradient(135deg, #ecfdf5 0%, #fff 55%); border-color: #bbf7d0; } +.lc-alert-card--normal .lc-alert-card-icon { background: #d1fae5; color: #059669; } +.lc-alert-card--normal .lc-alert-card-val { color: #047857; } +.lc-alert-card--warning { background: linear-gradient(135deg, #fff7ed 0%, #fff 55%); border-color: #fed7aa; } +.lc-alert-card--warning .lc-alert-card-icon { background: #ffedd5; color: #ea580c; } +.lc-alert-card--warning .lc-alert-card-val { color: #c2410c; } +.lc-alert-card--expired { background: linear-gradient(135deg, #fef2f2 0%, #fff 55%); border-color: #fecaca; } +.lc-alert-card--expired .lc-alert-card-icon { background: #fee2e2; color: #dc2626; } +.lc-alert-card--expired .lc-alert-card-val { color: #b91c1c; } +.lc-alert-card--unuploaded { background: linear-gradient(135deg, #f8fafc 0%, #fff 55%); } +.lc-alert-card--unuploaded .lc-alert-card-icon { background: #f1f5f9; color: #64748b; } +.lc-alert-card--unuploaded .lc-alert-card-val { color: #64748b; } +.lc-alert-card-clickable { cursor: pointer; transition: box-shadow .2s ease, border-color .2s ease; } +.lc-alert-card-clickable:hover { box-shadow: 0 4px 14px rgba(15, 23, 42, 0.08); } +.lc-alert-card-active { box-shadow: 0 0 0 2px rgba(22, 93, 255, 0.2) !important; border-color: #165dff !important; } +.lc-table-section { margin-bottom: 0; } +.lc-table-toolbar { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px 16px; margin-bottom: 8px; min-height: 32px; } +.lc-table-card { background: #fff; border-radius: 16px; border: 1px solid #e2e8f0; box-shadow: 0 4px 20px -4px rgba(15, 23, 42, 0.03); overflow: hidden; } +.lc-table-card .ant-table-thead > tr > th { background: #f8fafc !important; color: #475569 !important; font-weight: 700 !important; font-size: 13px !important; border-bottom: 1px solid #e2e8f0 !important; padding: 12px 16px !important; vertical-align: middle; } +.lc-table-card .ant-table-thead > tr > th.lc-th-wrap { padding: 10px 8px !important; text-align: center; vertical-align: middle; white-space: nowrap; } +.lc-table-th-multiline { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; line-height: 1.3; white-space: normal; } +.lc-table-th-line { display: block; font-size: 12px; font-weight: 700; color: #475569; } +.lc-list-expire-cell { display: flex; flex-direction: column; gap: 4px; min-width: 0; } +.lc-list-expire-date { font-size: 12px; font-weight: 600; line-height: 1.35; } +.lc-list-expire-meta { line-height: 1.2; } +.lc-list-status-badge-wrap { display: inline-flex; max-width: 100%; } +.lc-list-status-badge-wrap .ant-badge { display: inline-flex; align-items: center; max-width: 100%; } +.lc-list-status-badge-text { font-size: 11px; font-weight: 600; white-space: nowrap; } +.lc-list-table .ant-table-wrapper, .lc-list-table .ant-table { width: 100% !important; } +.lc-list-table .ant-table-content table { table-layout: fixed; width: 100% !important; } +.lc-cell-ellipsis { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.lc-table-card .ant-table-tbody > tr:not(.ant-table-measure-row) > td { padding: 10px 8px !important; border-bottom: 1px solid #f1f5f9 !important; } +.lc-table-card .ant-table-tbody > tr:not(.ant-table-measure-row):hover > td { background: #f8fafc !important; } +.lc-table-card .ant-table-tbody > tr.lc-row-retired:not(.ant-table-measure-row) > td { background: #f8fafc !important; color: #94a3b8; } +.lc-list-plate-cell { display: flex; flex-direction: column; gap: 2px; min-width: 0; } +.lc-list-plate-sub { display: block; font-size: 11px; font-weight: 500; line-height: 1.35; } +.lc-list-plate-empty { color: #94a3b8 !important; font-style: italic; font-weight: 500 !important; } +.lc-table-toolbar-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-left: auto; } +.lc-compare-modal .ant-modal-content { border-radius: 16px; overflow: hidden; } +.lc-compare-modal .ant-modal-header { background: linear-gradient(135deg, #f8fafc 0%, #fff 100%); border-bottom: 1px solid #e2e8f0; padding: 16px 20px; } +.lc-compare-modal .ant-modal-title { font-size: 16px; font-weight: 700; color: #0f172a; } +.lc-compare-modal .ant-modal-body { padding: 16px 20px 12px; max-height: calc(100vh - 180px); overflow: auto; } +.lc-compare-modal .ant-modal-footer { border-top: 1px solid #f1f5f9; padding: 12px 20px 16px; } +.lc-compare-toolbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; flex-wrap: wrap; gap: 8px; } +.lc-compare-table-wrap { border-radius: 12px; border: 1px solid #e2e8f0; overflow: hidden; background: #fff; box-shadow: 0 4px 16px -4px rgba(15, 23, 42, 0.06); } +.lc-compare-table .ant-table-thead > tr > th { background: #f1f5f9 !important; color: #334155 !important; font-size: 12px !important; font-weight: 700 !important; padding: 10px 8px !important; white-space: nowrap; border-bottom: 1px solid #e2e8f0 !important; } +.lc-compare-table .ant-table-thead > tr > th.lc-compare-th-key { background: #ecfdf5 !important; color: #065f46 !important; } +.lc-compare-table .ant-table-thead > tr > th.lc-compare-th-auto { background: #f8fafc !important; } +.lc-compare-table .ant-table-thead > tr > th.lc-compare-th-edit { background: #fffbeb !important; color: #92400e !important; } +.lc-compare-table .ant-table-tbody > tr > td { padding: 8px 6px !important; vertical-align: middle !important; font-size: 12px; border-bottom: 1px solid #f1f5f9 !important; } +.lc-compare-table .ant-table-tbody > tr:nth-child(even) > td { background: #fafbfc; } +.lc-compare-table .ant-table-tbody > tr:hover > td { background: #f0fdf4 !important; } +.lc-compare-table .ant-table-tbody > tr.ant-table-row-selected > td { background: #ecfdf5 !important; } +.lc-compare-table .ant-table-thead > tr > th.ant-table-cell-fix-left, +.lc-compare-table .ant-table-thead > tr > th.ant-table-cell-fix-right { background: #f1f5f9 !important; } +.lc-compare-table .ant-table-thead > tr > th.ant-table-cell-fix-left.lc-compare-th-key { background: #ecfdf5 !important; } +.lc-compare-table .ant-table-tbody > tr > td.ant-table-cell-fix-left, +.lc-compare-table .ant-table-tbody > tr > td.ant-table-cell-fix-right { background: #fff !important; } +.lc-compare-table .ant-table-tbody > tr:nth-child(even) > td.ant-table-cell-fix-left, +.lc-compare-table .ant-table-tbody > tr:nth-child(even) > td.ant-table-cell-fix-right { background: #fafbfc !important; } +.lc-compare-table .ant-table-tbody > tr:hover > td.ant-table-cell-fix-left, +.lc-compare-table .ant-table-tbody > tr:hover > td.ant-table-cell-fix-right { background: #f0fdf4 !important; } +.lc-compare-table .ant-table-tbody > tr.ant-table-row-selected > td.ant-table-cell-fix-left, +.lc-compare-table .ant-table-tbody > tr.ant-table-row-selected > td.ant-table-cell-fix-right { background: #ecfdf5 !important; } +.lc-compare-table .ant-table-cell-fix-left-last::after, +.lc-compare-table .ant-table-cell-fix-right-first::after { box-shadow: inset 10px 0 8px -8px rgba(15, 23, 42, 0.12); } +.lc-compare-cell-select { width: 100%; min-width: 100px; } +.lc-compare-cell-select .ant-select-selector { border-radius: 6px !important; font-size: 12px !important; min-height: 28px !important; } +.lc-compare-cell-input { width: 100%; min-width: 88px; border-radius: 6px; } +.lc-compare-cell-input.ant-input-sm, .lc-compare-cell-input.ant-picker-small { font-size: 12px; } +.lc-compare-readonly { display: block; padding: 4px 8px; min-height: 28px; line-height: 20px; border-radius: 6px; background: #f8fafc; border: 1px solid #f1f5f9; color: #334155; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.lc-compare-readonly--wrap { white-space: normal; word-break: break-all; line-height: 1.4; } +.lc-compare-readonly.is-empty { color: #94a3b8; } +.lc-compare-readonly.is-linked { background: #f0fdf4; border-color: #bbf7d0; color: #065f46; font-weight: 500; } +.lc-compare-action-cell { display: flex; align-items: center; gap: 2px; flex-wrap: wrap; } +.lc-compare-action-cell .ant-btn-link { font-size: 12px; font-weight: 600; padding: 0 4px; height: auto; } +.lc-compare-quote-btn { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 6px; background: #ecfdf5; border: 1px solid #a7f3d0; color: #059669 !important; font-weight: 600 !important; font-size: 12px !important; } +.lc-compare-quote-btn:hover { background: #d1fae5 !important; } +.lc-quote-popover-overlay .ant-popover-inner { padding: 0 !important; border-radius: 14px !important; overflow: hidden; box-shadow: 0 16px 48px -12px rgba(15, 23, 42, 0.22) !important; border: 1px solid #e2e8f0; } +.lc-quote-popover-overlay .ant-popover-arrow { display: none; } +.lc-quote-card { width: 400px; max-width: 92vw; } +.lc-quote-card-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 14px 16px; background: linear-gradient(135deg, #ecfdf5 0%, #f8fafc 55%, #fff 100%); border-bottom: 1px solid #e2e8f0; } +.lc-quote-card-title { font-size: 15px; font-weight: 700; color: #0f172a; } +.lc-quote-card-plate { font-size: 11px; font-weight: 600; color: #059669; background: #d1fae5; border: 1px solid #a7f3d0; padding: 2px 8px; border-radius: 999px; white-space: nowrap; } +.lc-quote-card-body { padding: 14px 16px 16px; max-height: 420px; overflow-y: auto; } +.lc-quote-card-body::-webkit-scrollbar { width: 4px; } +.lc-quote-card-body::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 2px; } +.lc-quote-list-label { font-size: 11px; font-weight: 700; color: #64748b; text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 8px; } +.lc-quote-empty { text-align: center; padding: 20px 12px; border-radius: 10px; background: #f8fafc; border: 1px dashed #e2e8f0; color: #94a3b8; font-size: 12px; margin-bottom: 14px; } +.lc-quote-item-card { display: flex; align-items: flex-start; gap: 10px; padding: 10px 12px; margin-bottom: 8px; border-radius: 10px; border: 1px solid #e2e8f0; background: #fff; transition: border-color .2s, box-shadow .2s; } +.lc-quote-item-card:hover { border-color: #cbd5e1; box-shadow: 0 2px 8px rgba(15, 23, 42, 0.04); } +.lc-quote-item-card.is-selected { border-color: #10b981; background: linear-gradient(135deg, #f0fdf4 0%, #fff 100%); box-shadow: 0 0 0 1px rgba(16, 185, 129, 0.15); } +.lc-quote-item-card .ant-radio { margin-top: 2px; } +.lc-quote-item-main { flex: 1; min-width: 0; } +.lc-quote-item-company { font-size: 13px; font-weight: 600; color: #0f172a; line-height: 1.4; margin-bottom: 4px; } +.lc-quote-item-row { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; font-size: 12px; color: #64748b; } +.lc-quote-type-tag { display: inline-flex; align-items: center; padding: 1px 7px; border-radius: 4px; background: #eff6ff; border: 1px solid #bfdbfe; color: #1d4ed8; font-size: 11px; font-weight: 600; } +.lc-quote-price { font-size: 14px; font-weight: 700; color: #059669; font-variant-numeric: tabular-nums; } +.lc-quote-price-unit { font-size: 11px; font-weight: 500; color: #64748b; margin-left: 2px; } +.lc-quote-item-del { flex-shrink: 0; padding: 0 4px !important; height: auto !important; font-size: 12px !important; } +.lc-quote-form-wrap { margin-top: 14px; padding-top: 14px; border-top: 1px solid #f1f5f9; } +.lc-quote-form-title { font-size: 13px; font-weight: 700; color: #0f172a; margin-bottom: 12px; display: flex; align-items: center; gap: 6px; } +.lc-quote-form-title::before { content: ''; width: 3px; height: 14px; border-radius: 2px; background: #10b981; } +.lc-quote-form-field { margin-bottom: 10px; } +.lc-quote-form-label { display: block; font-size: 12px; font-weight: 600; color: #475569; margin-bottom: 5px; } +.lc-quote-form-label-required::after { content: ' *'; color: #ef4444; } +.lc-quote-form-actions { display: flex; justify-content: flex-end; margin-top: 4px; } +.lc-quote-form-actions .ant-btn-primary { border-radius: 8px; font-weight: 600; background: linear-gradient(135deg, #10b981 0%, #059669 100%); border: none; box-shadow: 0 2px 8px rgba(16, 185, 129, 0.25); } +.lc-compare-quote-cell { display: flex; flex-direction: column; gap: 6px; min-width: 0; } +.lc-compare-quote-inline-list { display: flex; flex-direction: column; gap: 4px; width: 100%; } +.lc-compare-quote-inline-item { display: flex; align-items: center; gap: 4px; padding: 4px 6px; border-radius: 6px; border: 1px solid #e2e8f0; background: #fff; cursor: pointer; transition: border-color .15s, background .15s; margin: 0 !important; } +.lc-compare-quote-inline-item:hover { border-color: #cbd5e1; background: #f8fafc; } +.lc-compare-quote-inline-item.is-selected { border-color: #10b981; background: #f0fdf4; box-shadow: 0 0 0 1px rgba(16, 185, 129, 0.12); } +.lc-compare-quote-inline-item .ant-radio { margin-right: 0; top: 0; } +.lc-compare-quote-inline-company { flex: 1; min-width: 0; font-size: 11px; font-weight: 600; color: #334155; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.lc-compare-quote-inline-price { flex-shrink: 0; font-size: 12px; font-weight: 700; color: #059669; font-variant-numeric: tabular-nums; } +.lc-compare-quote-inline-del { flex-shrink: 0; padding: 0 2px !important; height: 18px !important; min-width: 18px !important; font-size: 14px !important; line-height: 1 !important; color: #94a3b8 !important; } +.lc-compare-quote-inline-del:hover { color: #ef4444 !important; } +.lc-compare-quote-add { padding: 0 !important; height: auto !important; font-size: 12px !important; font-weight: 600 !important; color: #059669 !important; align-self: flex-start; } +.lc-compare-quote-empty-hint { font-size: 11px; color: #94a3b8; line-height: 1.4; } +.lc-quote-card-type-badge { font-size: 11px; font-weight: 600; color: #1d4ed8; background: #eff6ff; border: 1px solid #bfdbfe; padding: 2px 8px; border-radius: 999px; white-space: nowrap; } +.lc-compare-footer { margin-top: 14px; padding-top: 14px; border-top: 1px solid #e2e8f0; display: flex; flex-direction: column; gap: 12px; } +.lc-compare-remark-field { display: flex; flex-direction: column; gap: 6px; } +.lc-compare-remark-label { font-size: 13px; font-weight: 600; color: #334155; } +.lc-compare-total-bar { display: flex; align-items: baseline; flex-wrap: wrap; gap: 8px 16px; padding: 12px 16px; border-radius: 10px; background: linear-gradient(135deg, #ecfdf5 0%, #f8fafc 100%); border: 1px solid #bbf7d0; } +.lc-compare-total-label { font-size: 13px; font-weight: 600; color: #334155; } +.lc-compare-total-amount { font-size: 22px; font-weight: 800; color: #059669; font-variant-numeric: tabular-nums; line-height: 1.2; } +.lc-compare-total-unit { font-size: 13px; font-weight: 600; color: #059669; margin-left: 2px; } +.lc-compare-total-hint { font-size: 12px; color: #64748b; margin-left: auto; } +.lc-copy-pop { width: 200px; } +.lc-copy-pop-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 10px; } +.lc-compare-mgmt-modal .ant-modal-body { padding: 16px 20px 20px; } +.lc-compare-mgmt-filter { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px 24px; margin-bottom: 14px; padding-bottom: 14px; border-bottom: 1px solid #f1f5f9; } +@media (max-width: 720px) { .lc-compare-mgmt-filter { grid-template-columns: 1fr; } } +.lc-compare-mgmt-toolbar { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px; margin-bottom: 12px; } +.lc-compare-mgmt-table .ant-table-thead > tr > th { background: #f8fafc !important; font-weight: 700 !important; font-size: 13px !important; } +.lc-compare-mgmt-table .ant-table-tbody > tr > td { font-size: 13px; } +.lc-compare-mgmt-count { font-variant-numeric: tabular-nums; font-weight: 700; color: #0f172a; } +.lc-compare-pay-alert-row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin-bottom: 14px; } +.lc-compare-pay-alert { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 10px 14px; border-radius: 10px; border: 1px solid #e2e8f0; background: #fff; } +.lc-compare-pay-alert--warning { border-color: #fed7aa; background: linear-gradient(135deg, #fff7ed 0%, #fff 80%); } +.lc-compare-pay-alert--overdue { border-color: #fecaca; background: linear-gradient(135deg, #fef2f2 0%, #fff 80%); } +.lc-compare-pay-alert-val { font-size: 22px; font-weight: 800; font-variant-numeric: tabular-nums; line-height: 1; } +.lc-compare-pay-alert--warning .lc-compare-pay-alert-val { color: #c2410c; } +.lc-compare-pay-alert--overdue .lc-compare-pay-alert-val { color: #b91c1c; } +.lc-compare-editor-meta { display: flex; flex-wrap: wrap; gap: 12px 20px; margin-bottom: 12px; padding: 10px 14px; border-radius: 10px; background: #f8fafc; border: 1px solid #e2e8f0; } +.lc-compare-editor-meta-field { display: flex; align-items: center; gap: 8px; min-width: 200px; flex: 1; } +.lc-compare-editor-meta-label { font-size: 12px; font-weight: 600; color: #64748b; white-space: nowrap; } +.lc-compare-procurement-hint { font-size: 12px; color: #64748b; } +.lc-compare-total-bar--procurement { margin-top: 8px; border-color: #bfdbfe; background: linear-gradient(135deg, #eff6ff 0%, #f8fafc 100%); } +.lc-compare-total-bar--procurement .lc-compare-total-amount { color: #1d4ed8; } +.lc-module-tabs.ant-tabs > .ant-tabs-nav { margin-bottom: 12px; } +.lc-module-tabs .ant-tabs-tab { font-weight: 600; font-size: 14px; } +.lc-policy-toolbar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; } +.lc-policy-ocr-upload { margin: 12px 0; } +.lc-policy-import-template-bar { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; padding: 12px 14px; margin-bottom: 12px; border-radius: 10px; background: linear-gradient(135deg, #ecfdf5 0%, #f8fafc 100%); border: 1px solid #a7f3d0; } +.lc-policy-import-template-bar-text { font-size: 12px; color: #047857; line-height: 1.55; flex: 1; min-width: 200px; } +.lc-policy-import-excel-upload { margin: 0; } +.lc-policy-recogn-modal .ant-modal-body { padding: 16px 20px 20px; max-height: calc(100vh - 160px); overflow-y: auto; } +.lc-policy-recogn-task { padding: 10px 14px; border-radius: 10px; background: #f8fafc; border: 1px solid #e2e8f0; margin-bottom: 14px; font-size: 12px; color: #64748b; } +.lc-policy-recogn-task strong { color: #0f172a; } +.lc-policy-recogn-progress { margin: 16px 0; } +.lc-policy-recogn-file-list { margin-top: 10px; max-height: 140px; overflow-y: auto; } +.lc-policy-recogn-file-item { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 6px 0; border-bottom: 1px solid #f1f5f9; font-size: 12px; } +.lc-policy-recogn-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 14px; flex-wrap: wrap; } +.lc-policy-recogn-tasks-filter { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px 16px; margin-bottom: 12px; } +@media (max-width: 900px) { .lc-policy-recogn-tasks-filter { grid-template-columns: repeat(2, minmax(0, 1fr)); } } +.lc-policy-recogn-tasks-toolbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; flex-wrap: wrap; gap: 8px; } +.lc-policy-recogn-task-id { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; color: #334155; } +.lc-policy-recogn-preview { width: 100%; min-height: 360px; border: 1px solid #e2e8f0; border-radius: 8px; background: #f8fafc; display: flex; align-items: center; justify-content: center; } +.lc-policy-recogn-preview img { max-width: 100%; max-height: 70vh; object-fit: contain; } +.lc-policy-recogn-preview iframe { width: 100%; height: 70vh; border: none; border-radius: 8px; } +.lc-vehicle-ins-mgmt-modal .ant-modal-content { border-radius: 16px; overflow: hidden; box-shadow: 0 24px 64px -16px rgba(15, 23, 42, 0.28); } +.lc-vehicle-ins-mgmt-modal .ant-modal-header { display: none; } +.lc-vehicle-ins-mgmt-modal .ant-modal-body { padding: 0; max-height: calc(100vh - 96px); overflow: hidden; display: flex; flex-direction: column; } +.lc-vehicle-ins-mgmt-modal .ant-modal-footer { border-top: 1px solid #e2e8f0; padding: 12px 20px; background: #f8fafc; } +.lc-vehicle-ins-mgmt-shell { display: flex; flex-direction: column; min-height: 0; flex: 1; } +.lc-vehicle-ins-mgmt-hero { padding: 20px 24px 18px; background: linear-gradient(135deg, #ecfdf5 0%, #f8fafc 42%, #fff 100%); border-bottom: 1px solid #e2e8f0; } +.lc-vehicle-ins-mgmt-hero-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; flex-wrap: wrap; margin-bottom: 16px; } +.lc-vehicle-ins-mgmt-hero-title { font-size: 11px; font-weight: 700; color: #059669; letter-spacing: 0.06em; text-transform: uppercase; margin-bottom: 6px; } +.lc-vehicle-ins-mgmt-plate { font-size: 26px; font-weight: 800; color: #0f172a; letter-spacing: 0.02em; line-height: 1.2; } +.lc-vehicle-ins-mgmt-subtitle { font-size: 13px; color: #64748b; margin-top: 6px; } +.lc-vehicle-ins-mgmt-status-pill { display: inline-flex; align-items: center; gap: 6px; padding: 6px 14px; border-radius: 999px; font-size: 13px; font-weight: 700; border: 1px solid transparent; } +.lc-vehicle-ins-mgmt-status-pill--success { background: #ecfdf5; border-color: #a7f3d0; color: #047857; } +.lc-vehicle-ins-mgmt-status-pill--warning { background: #fffbeb; border-color: #fde68a; color: #b45309; } +.lc-vehicle-ins-mgmt-status-pill--error { background: #fef2f2; border-color: #fecaca; color: #b91c1c; } +.lc-vehicle-ins-mgmt-meta-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px 14px; } +@media (max-width: 768px) { .lc-vehicle-ins-mgmt-meta-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } } +.lc-vehicle-ins-mgmt-meta-card { padding: 10px 12px; border-radius: 10px; background: rgba(255,255,255,.85); border: 1px solid #e2e8f0; min-width: 0; } +.lc-vehicle-ins-mgmt-meta-label { font-size: 11px; color: #94a3b8; font-weight: 600; margin-bottom: 4px; } +.lc-vehicle-ins-mgmt-meta-val { font-size: 13px; font-weight: 600; color: #0f172a; word-break: break-all; line-height: 1.35; } +.lc-vehicle-ins-mgmt-body { padding: 16px 20px 20px; overflow-y: auto; flex: 1; min-height: 0; } +.lc-vehicle-ins-mgmt-legend { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 14px; padding: 10px 12px; border-radius: 10px; background: #f8fafc; border: 1px solid #e2e8f0; } +.lc-vehicle-ins-mgmt-legend-hint { font-size: 12px; color: #64748b; margin-right: 8px; align-self: center; } +.lc-purchase-type-chip { display: inline-flex; align-items: center; padding: 2px 10px; border-radius: 999px; font-size: 11px; font-weight: 700; border: 1px solid transparent; line-height: 1.5; } +.lc-purchase-type--new { background: #ecfdf5; border-color: #a7f3d0; color: #047857; } +.lc-purchase-type--renew { background: #eff6ff; border-color: #bfdbfe; color: #1d4ed8; } +.lc-purchase-type--rent-stop { background: #fff7ed; border-color: #fed7aa; color: #c2410c; } +.lc-purchase-type--resume { background: #ecfeff; border-color: #a5f3fc; color: #0e7490; } +.lc-purchase-type--cancel { background: #f1f5f9; border-color: #cbd5e1; color: #475569; } +.lc-vehicle-ins-mgmt-tabs .ant-tabs-nav { margin-bottom: 0 !important; padding: 0 4px; } +.lc-vehicle-ins-mgmt-tabs .ant-tabs-tab { font-weight: 600; font-size: 13px; padding: 10px 14px !important; transition: color 0.15s; } +.lc-vehicle-ins-mgmt-tabs .ant-tabs-tab-active .ant-tabs-tab-btn { color: #059669 !important; } +.lc-vehicle-ins-mgmt-tabs .ant-tabs-ink-bar { background: #10b981 !important; height: 3px !important; border-radius: 3px 3px 0 0; } +.lc-vehicle-ins-mgmt-tabs .ant-tabs-content-holder { padding-top: 14px; } +.lc-ins-tab-badge { display: inline-flex; align-items: center; justify-content: center; min-width: 18px; height: 18px; padding: 0 5px; margin-left: 6px; border-radius: 999px; background: #e2e8f0; color: #475569; font-size: 11px; font-weight: 700; } +.lc-vehicle-ins-mgmt-tabs .ant-tabs-tab-active .lc-ins-tab-badge { background: #d1fae5; color: #047857; } +.lc-vehicle-ins-timeline { padding: 4px 8px 12px; max-height: 440px; overflow-y: auto; } +.lc-vehicle-ins-timeline-item { cursor: pointer; padding: 12px 14px; margin: 0 0 10px; border-radius: 12px; border: 1px solid #e2e8f0; background: #fff; transition: border-color 0.15s, box-shadow 0.15s, transform 0.15s; } +.lc-vehicle-ins-timeline-item:hover { border-color: #a7f3d0; box-shadow: 0 4px 14px -6px rgba(16, 185, 129, 0.35); transform: translateY(-1px); } +.lc-vehicle-ins-timeline-item:focus-visible { outline: 2px solid #10b981; outline-offset: 2px; } +.lc-vehicle-ins-timeline-title { font-size: 13px; font-weight: 700; color: #0f172a; margin-bottom: 6px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } +.lc-vehicle-ins-timeline-desc { font-size: 12px; color: #475569; line-height: 1.55; } +.lc-vehicle-ins-timeline-meta { font-size: 11px; color: #94a3b8; margin-top: 6px; } +.lc-vehicle-ins-mgmt-table-card { border-radius: 12px; border: 1px solid #e2e8f0; overflow: hidden; background: #fff; } +.lc-vehicle-ins-mgmt-table-card .ant-table-thead > tr > th { background: #f8fafc !important; font-weight: 700 !important; font-size: 12px !important; color: #475569 !important; } +.lc-vehicle-ins-mgmt-empty { padding: 48px 24px; text-align: center; border-radius: 12px; border: 1px dashed #cbd5e1; background: #f8fafc; } +.lc-ins-history-row--active > td { background: #ecfdf5 !important; } +.lc-ins-history-row--active > td:first-child { box-shadow: inset 3px 0 0 #10b981; } +.lc-policy-detail-form { display: grid; grid-template-columns: 1fr 1fr; gap: 12px 16px; } +@media (max-width: 720px) { .lc-policy-detail-form { grid-template-columns: 1fr; } } +.lc-policy-detail-form-full { grid-column: 1 / -1; } +.lc-policy-detail-section-title { font-size: 13px; font-weight: 700; color: #334155; margin: 4px 0 8px; grid-column: 1 / -1; } +.lc-coverage-items-editor { display: flex; flex-direction: column; gap: 8px; width: 100%; } +.lc-coverage-items-row { display: flex; align-items: center; gap: 8px; } +.lc-coverage-items-row .ant-input { flex: 1; min-width: 0; } +.lc-coverage-items-index { flex-shrink: 0; width: 22px; font-size: 12px; font-weight: 700; color: #94a3b8; text-align: right; font-variant-numeric: tabular-nums; } +.lc-coverage-items-add { margin-top: 2px; border-radius: 8px !important; font-weight: 600; color: #059669 !important; border-color: #a7f3d0 !important; background: #f0fdf4 !important; } +.lc-coverage-items-add:hover { border-color: #6ee7b7 !important; color: #047857 !important; } +.lc-list-policy-tag { margin-top: 4px; } +.lc-compare-attach-field { display: flex; flex-direction: column; gap: 8px; } +.lc-compare-attach-label { font-size: 13px; font-weight: 600; color: #334155; } +.lc-compare-attach-hint { font-size: 12px; color: #94a3b8; line-height: 1.5; } +.lc-compare-attach-upload .ant-upload-list { max-height: 160px; overflow-y: auto; margin-top: 8px; } +.lc-compare-attach-upload .ant-upload-list-item { border-radius: 8px; } +.lc-compare-attach-upload .ant-upload-select { display: block; } +`; + +const goInsuranceEditPage = (ledgerKey, master, vehicle) => { + const label = vehicle + ? (hasVehiclePlate(vehicle) ? vehicle.plateNo : `${NO_PLATE_LABEL}(${vehicle.vin})`) + : ledgerKey; + try { + sessionStorage.setItem(IPC_EDIT_PLATE_KEY, ledgerKey); + persistInsuranceToStorage(master); + } catch { + /* ignore */ + } + if (typeof window.__axhubNavigate === 'function') { + window.__axhubNavigate('保险采购-编辑'); + message.success(`已进入 [${label}] 保险维护`); + return; + } + message.info(`已带入 [${label}] 车辆信息,请打开「保险采购-编辑」页面继续维护`); +}; + +const Component = function () { + const [allInsurance, setAllInsurance] = useState(() => { + const stored = loadInsuranceFromStorage(); + if (stored) { + const normalized = {}; + Object.keys(stored).forEach((k) => { + normalized[k] = ensureInsuranceRecordShape(stored[k]); + }); + return normalized; + } + const merged = {}; + MOCK_VEHICLES.forEach((v) => { + const key = getVehicleLedgerKey(v); + const seed = getInitialInsuranceSeed(v); + merged[key] = ensureInsuranceRecordShape( + seed ? JSON.parse(JSON.stringify(seed)) : createEmptyInsuranceRecord() + ); + }); + return merged; + }); + + const updateAllInsurance = useCallback((updater) => { + setAllInsurance((prev) => { + const next = typeof updater === 'function' ? updater(prev) : updater; + persistInsuranceToStorage(next); + return next; + }); + }, []); + + const DEFAULT_LIST_FILTERS = { + plateNo: '', + plateNos: '', + vin: '', + brand: '', + model: '', + operateStatus: '全部', + insuranceStatus: '全部', + }; + const [listFilters, setListFilters] = useState(() => ({ ...DEFAULT_LIST_FILTERS })); + const [appliedFilters, setAppliedFilters] = useState(() => ({ ...DEFAULT_LIST_FILTERS })); + const [multiPlateOpen, setMultiPlateOpen] = useState(false); + const [multiPlateDraft, setMultiPlateDraft] = useState(''); + const [kpiFilter, setKpiFilter] = useState('total'); + const [prdOpen, setPrdOpen] = useState(false); + const [compareMgmtOpen, setCompareMgmtOpen] = useState(false); + const [compareSheets, setCompareSheets] = useState(() => { + const stored = loadCompareSheetsFromStorage(); + const list = stored && stored.length ? stored : createMockCompareSheets(); + return list.map(normalizeCompareSheet); + }); + const [compareMgmtFilters, setCompareMgmtFilters] = useState(() => ({ ...DEFAULT_COMPARE_MGMT_FILTERS })); + const [appliedCompareMgmtFilters, setAppliedCompareMgmtFilters] = useState(() => ({ ...DEFAULT_COMPARE_MGMT_FILTERS })); + const [compareModalOpen, setCompareModalOpen] = useState(false); + const [editingCompareSheetId, setEditingCompareSheetId] = useState(null); + const [compareRows, setCompareRows] = useState([]); + const [selectedCompareKeys, setSelectedCompareKeys] = useState([]); + const [copyPopoverRowId, setCopyPopoverRowId] = useState(null); + const [copyCountDraft, setCopyCountDraft] = useState(1); + const [quoteDraft, setQuoteDraft] = useState(createEmptyQuoteDraft); + const [quoteEditRowId, setQuoteEditRowId] = useState(null); + const [compareRemark, setCompareRemark] = useState(''); + const [compareSheetPeriod, setCompareSheetPeriod] = useState(''); + const [compareAttachmentFileList, setCompareAttachmentFileList] = useState([]); + const [procurementFlowOpen, setProcurementFlowOpen] = useState(false); + const [procurementFlowStep, setProcurementFlowStep] = useState(0); + const [procurementFlowLoading, setProcurementFlowLoading] = useState(false); + const [policyRecognOpen, setPolicyRecognOpen] = useState(false); + const [policyRecognEntry, setPolicyRecognEntry] = useState('ocr'); + const [policyRecognMode, setPolicyRecognMode] = useState('policy'); + const [policyRecognInsuranceType, setPolicyRecognInsuranceType] = useState('交强险'); + const [policyRecognPhase, setPolicyRecognPhase] = useState('upload'); + const [policyRecognFiles, setPolicyRecognFiles] = useState([]); + const [policyRecognProgress, setPolicyRecognProgress] = useState(0); + const [policyRecognTaskId, setPolicyRecognTaskId] = useState(''); + const [policyRecognResults, setPolicyRecognResults] = useState([]); + const [policyRecognViewOnly, setPolicyRecognViewOnly] = useState(false); + const [policyPreview, setPolicyPreview] = useState(null); + const [policyRecognTasks, setPolicyRecognTasks] = useState(() => { + const stored = loadPolicyRecognTasksFromStorage(); + return stored && stored.length ? stored : createMockPolicyRecognTasks(); + }); + const [policyRecognTasksOpen, setPolicyRecognTasksOpen] = useState(false); + const [policyRecognTasksFilters, setPolicyRecognTasksFilters] = useState(() => ({ ...DEFAULT_POLICY_RECOGN_TASK_FILTERS })); + const [appliedPolicyRecognTasksFilters, setAppliedPolicyRecognTasksFilters] = useState(() => ({ ...DEFAULT_POLICY_RECOGN_TASK_FILTERS })); + const [policyAddOpen, setPolicyAddOpen] = useState(false); + const [vehicleInsMgmtOpen, setVehicleInsMgmtOpen] = useState(false); + const [vehicleInsMgmtVehicle, setVehicleInsMgmtVehicle] = useState(null); + const [vehicleInsMgmtActiveTab, setVehicleInsMgmtActiveTab] = useState('timeline'); + const [vehicleInsMgmtHighlightId, setVehicleInsMgmtHighlightId] = useState(''); + const [policyAddDraft, setPolicyAddDraft] = useState(() => ({ ...EMPTY_POLICY_DETAIL })); + const [policyRecognEditOpen, setPolicyRecognEditOpen] = useState(false); + const [policyRecognEditId, setPolicyRecognEditId] = useState(''); + const [policyRecognEditDraft, setPolicyRecognEditDraft] = useState(() => ({ ...EMPTY_POLICY_DETAIL })); + const [insuranceHistoryEdits, setInsuranceHistoryEdits] = useState(() => loadInsuranceHistoryEditsFromStorage()); + const [vehicleInsHistoryEditOpen, setVehicleInsHistoryEditOpen] = useState(false); + const [vehicleInsHistoryEditRecord, setVehicleInsHistoryEditRecord] = useState(null); + const [vehicleInsHistoryEditDraft, setVehicleInsHistoryEditDraft] = useState(() => ({ ...EMPTY_POLICY_DETAIL })); + + const compareSheetSummary = useMemo( + () => calcCompareSheetConfirmedTotal(compareRows), + [compareRows] + ); + + const selectedProcurementSummary = useMemo(() => { + const selected = compareRows.filter((r) => selectedCompareKeys.includes(r.id)); + return calcCompareSheetConfirmedTotal(selected); + }, [compareRows, selectedCompareKeys]); + + const compareMgmtPayAlerts = useMemo(() => { + let warning = 0; + let overdue = 0; + compareSheets.forEach((sheet) => { + const alert = calcCompareSheetPayAlerts(sheet); + warning += alert.warning; + overdue += alert.overdue; + }); + return { warning, overdue }; + }, [compareSheets]); + + const saveCompareSheets = useCallback((nextSheets) => { + setCompareSheets(nextSheets); + persistCompareSheetsToStorage(nextSheets); + }, []); + + const filteredCompareSheets = useMemo(() => { + const plateKey = (appliedCompareMgmtFilters.plateNo || '').trim(); + const range = appliedCompareMgmtFilters.createdRange; + return compareSheets + .filter((sheet) => compareSheetMatchesCreatedRange(sheet.createdAt, range)) + .filter((sheet) => compareSheetMatchesPlateFilter(sheet, plateKey)) + .sort((a, b) => String(b.createdAt || '').localeCompare(String(a.createdAt || ''))); + }, [compareSheets, appliedCompareMgmtFilters]); + + const updateCompareRow = useCallback((rowId, patch) => { + setCompareRows((prev) => prev.map((r) => (r.id === rowId ? { ...r, ...patch } : r))); + }, []); + + const fillCompareRowFromVehicle = useCallback((rowId, vehicle) => { + if (!vehicle) return; + updateCompareRow(rowId, buildVehicleComparePatch(vehicle, allInsurance)); + }, [allInsurance, updateCompareRow]); + + const fillCompareRowFromPlate = useCallback((rowId, plateNo) => { + const vehicle = findVehicleByPlate(plateNo); + if (!vehicle) return; + fillCompareRowFromVehicle(rowId, vehicle); + }, [fillCompareRowFromVehicle]); + + const handleComparePlateChange = useCallback((rowId, plateNo) => { + if (!plateNo) { + updateCompareRow(rowId, clearVehicleComparePatch()); + return; + } + fillCompareRowFromPlate(rowId, plateNo); + }, [fillCompareRowFromPlate, updateCompareRow]); + + const handleCompareVinChange = useCallback((rowId, vin) => { + if (!vin) { + updateCompareRow(rowId, clearVehicleComparePatch()); + return; + } + const vehicle = findVehicleByVin(vin); + if (!vehicle) { + message.warning('未找到该 VIN 对应车辆'); + return; + } + fillCompareRowFromVehicle(rowId, vehicle); + }, [fillCompareRowFromVehicle, updateCompareRow]); + + const renderReadonlyField = (val, linked) => ( + + {val || '—'} + + ); + + const renderReadonlyDate = (val, linked) => ( + + {val || '—'} + + ); + + const openCompareMgmtModal = () => { + setCompareMgmtFilters({ ...DEFAULT_COMPARE_MGMT_FILTERS }); + setAppliedCompareMgmtFilters({ ...DEFAULT_COMPARE_MGMT_FILTERS }); + setCompareMgmtOpen(true); + }; + + const openCompareEditor = (sheet) => { + if (sheet) { + setEditingCompareSheetId(sheet.id); + setCompareRows(normalizeCompareRows(JSON.parse(JSON.stringify(sheet.rows || [])))); + setCompareRemark(sheet.remark || ''); + setCompareSheetPeriod(sheet.periodLabel || ''); + setCompareAttachmentFileList(attachmentsToUploadFileList(sheet.attachments)); + } else { + setEditingCompareSheetId(null); + setCompareRows([createEmptyCompareRow()]); + setCompareRemark(''); + setCompareSheetPeriod(moment ? moment(ANCHOR_TODAY).format('YYYY年M月') : '2026年6月'); + setCompareAttachmentFileList([]); + } + setSelectedCompareKeys([]); + setQuoteDraft(createEmptyQuoteDraft()); + setQuoteEditRowId(null); + setCompareModalOpen(true); + }; + + const buildSheetPayloadFromEditor = (rowsSnapshot) => normalizeCompareSheet({ + id: editingCompareSheetId || createCompareSheetId(), + createdAt: editingCompareSheetId + ? (compareSheets.find((s) => s.id === editingCompareSheetId)?.createdAt || formatCompareSheetNow()) + : formatCompareSheetNow(), + createdBy: editingCompareSheetId + ? (compareSheets.find((s) => s.id === editingCompareSheetId)?.createdBy || PROTO_COMPARE_CREATOR) + : PROTO_COMPARE_CREATOR, + periodLabel: compareSheetPeriod, + remark: compareRemark, + attachments: uploadFileListToAttachments(compareAttachmentFileList), + rows: rowsSnapshot, + }); + + const handleCompareAttachmentChange = ({ fileList }) => { + setCompareAttachmentFileList( + fileList.map((f) => ({ + ...f, + status: 'done', + uploadedAt: f.uploadedAt || formatCompareSheetNow(), + })) + ); + }; + + const handleCompareMgmtQuery = () => { + setAppliedCompareMgmtFilters({ ...compareMgmtFilters }); + message.success('查询完成'); + }; + + const handleCompareMgmtReset = () => { + setCompareMgmtFilters({ ...DEFAULT_COMPARE_MGMT_FILTERS }); + setAppliedCompareMgmtFilters({ ...DEFAULT_COMPARE_MGMT_FILTERS }); + message.info('筛选条件已重置'); + }; + + const handleDeleteCompareSheet = (sheet) => { + Modal.confirm({ + title: '删除比价单', + content: `确定删除 ${sheet.createdAt || ''} 由 ${sheet.createdBy || '—'} 创建的比价单吗?删除后不可恢复。`, + okText: '删除', + okType: 'danger', + cancelText: '取消', + centered: true, + onOk: () => { + const next = compareSheets.filter((s) => s.id !== sheet.id); + saveCompareSheets(next); + message.success('比价单已删除'); + }, + }); + }; + + const handleAddCompareRow = () => { + setCompareRows((prev) => [...prev, createEmptyCompareRow()]); + }; + + const handleDeleteCompareRow = (rowId) => { + setCompareRows((prev) => prev.filter((r) => r.id !== rowId)); + setSelectedCompareKeys((prev) => prev.filter((k) => k !== rowId)); + }; + + const handleCopyCompareRow = (row, count) => { + const n = Math.max(1, Math.min(50, Number(count) || 1)); + const clones = Array.from({ length: n }, () => cloneCompareRow(row)); + setCompareRows((prev) => { + const idx = prev.findIndex((r) => r.id === row.id); + if (idx < 0) return [...prev, ...clones]; + const next = [...prev]; + next.splice(idx + 1, 0, ...clones); + return next; + }); + setCopyPopoverRowId(null); + setCopyCountDraft(1); + message.success(`已复制 ${n} 条记录`); + }; + + const handleAddQuote = (rowId, rowInsuranceType) => { + if (!quoteDraft.company) { + message.warning('请选择保险公司'); + return; + } + if (!isValidPremium(quoteDraft.premium)) { + message.warning('请输入大于 0 的报价,最多两位小数'); + return; + } + const premium = formatPremiumDisplay(quoteDraft.premium); + const newQuote = { + id: createQuoteId(), + company: quoteDraft.company, + premium, + }; + let added = false; + setCompareRows((prev) => prev.map((r) => { + if (r.id !== rowId) return r; + const exists = (r.quotes || []).some((q) => q.company === quoteDraft.company); + if (exists) { + message.warning('该保险公司报价已存在'); + return r; + } + added = true; + return { ...r, quotes: [...(r.quotes || []), newQuote] }; + })); + if (added) { + setQuoteDraft(createEmptyQuoteDraft()); + message.success(`已添加${rowInsuranceType || ''}报价`); + } + }; + + const handleRemoveQuote = (rowId, quoteId) => { + setCompareRows((prev) => prev.map((r) => { + if (r.id !== rowId) return r; + const quotes = (r.quotes || []).filter((q) => q.id !== quoteId); + const confirmedQuoteId = r.confirmedQuoteId === quoteId ? '' : r.confirmedQuoteId; + return { ...r, quotes, confirmedQuoteId }; + })); + }; + + const handleSubmitCompareSheet = (options = {}) => { + const { closeModal = true } = options; + if (!compareRows.length) { + message.warning('请至少添加一条购买记录'); + return null; + } + const missingVehicle = compareRows.find((r) => !(r.plateNo || '').trim() && !(r.vin || '').trim()); + if (missingVehicle) { + message.warning('存在未选择车辆的记录,请填写车牌或 VIN'); + return null; + } + const rowsSnapshot = normalizeCompareRows(JSON.parse(JSON.stringify(compareRows))); + const payload = buildSheetPayloadFromEditor(rowsSnapshot); + let nextSheets; + let savedId = payload.id; + if (editingCompareSheetId) { + nextSheets = compareSheets.map((s) => (s.id === editingCompareSheetId ? payload : s)); + message.success(`比价单已保存,共 ${rowsSnapshot.length} 条购买记录`); + } else { + nextSheets = [payload, ...compareSheets]; + message.success(`比价单已创建,共 ${rowsSnapshot.length} 条购买记录`); + savedId = payload.id; + } + saveCompareSheets(nextSheets); + setEditingCompareSheetId(savedId); + if (closeModal) { + setCompareModalOpen(false); + } + return savedId; + }; + + const handleSubmitProcurement = () => { + if (!selectedCompareKeys.length) { + message.warning('请勾选需要提交采购的购买记录'); + return; + } + const selectedRows = compareRows.filter((r) => selectedCompareKeys.includes(r.id)); + const invalid = selectedRows.find((r) => !r.confirmedQuoteId || !r.latestPayDate); + if (invalid) { + message.warning('勾选记录须已选定确认报价并填写最晚付费日期'); + return; + } + const alreadySubmitted = selectedRows.find((r) => r.procurementStatus === 'submitted' || r.procurementStatus === 'completed'); + if (alreadySubmitted) { + message.warning('勾选记录中包含已提交采购项,请重新选择'); + return; + } + if (!editingCompareSheetId) { + Modal.confirm({ + title: '保存并提交采购', + content: '提交采购前将先保存当前比价单,是否继续?', + okText: '继续', + cancelText: '取消', + centered: true, + onOk: () => { + const savedId = handleSubmitCompareSheet({ closeModal: false }); + if (savedId) { + setProcurementFlowStep(0); + setProcurementFlowLoading(false); + setProcurementFlowOpen(true); + } + }, + }); + return; + } + setProcurementFlowStep(0); + setProcurementFlowLoading(false); + setProcurementFlowOpen(true); + }; + + const runProcurementWorkflow = () => { + setProcurementFlowLoading(true); + setProcurementFlowStep(1); + window.setTimeout(() => setProcurementFlowStep(2), 650); + window.setTimeout(() => { + const submittedAt = formatCompareSheetNow(); + const rowsSnapshot = compareRows.map((r) => ( + selectedCompareKeys.includes(r.id) + ? { ...r, procurementStatus: 'submitted', procurementSubmittedAt: submittedAt } + : r + )); + const payload = buildSheetPayloadFromEditor(rowsSnapshot); + const nextSheets = compareSheets.map((s) => (s.id === payload.id ? payload : s)); + saveCompareSheets(nextSheets); + setCompareRows(rowsSnapshot); + setProcurementFlowStep(3); + setProcurementFlowLoading(false); + message.success(`采购申请已发起(原型),已提交 ${selectedCompareKeys.length} 条,工作流单号 WF-INS-${Date.now().toString().slice(-6)}`); + setSelectedCompareKeys([]); + }, 1300); + }; + + const resolvePolicyVehicleKey = (plateOrVin) => { + const key = (plateOrVin || '').trim(); + if (!key) return null; + const byPlate = findVehicleByPlate(key); + if (byPlate) return getVehicleLedgerKey(byPlate); + const byVin = findVehicleByVin(key); + if (byVin) return getVehicleLedgerKey(byVin); + return null; + }; + + const savePolicyRecognTaskSnapshot = useCallback((snapshot) => { + const { + taskId, + entry, + mode, + insuranceType, + results, + phase, + completedAt, + } = snapshot; + if (!taskId || !results?.length) return; + setPolicyRecognTasks((prev) => { + const existing = prev.find((t) => t.id === taskId); + const record = buildPolicyRecognTaskRecord({ + id: taskId, + entry: entry ?? existing?.entry ?? 'ocr', + mode: mode ?? existing?.mode ?? 'policy', + insuranceType: insuranceType ?? existing?.insuranceType ?? '', + results, + createdAt: existing?.createdAt, + creator: existing?.creator, + completedAt: completedAt ?? existing?.completedAt ?? '', + phase: phase ?? existing?.phase ?? 'results', + }); + const idx = prev.findIndex((t) => t.id === taskId); + const next = idx >= 0 + ? prev.map((t, i) => (i === idx ? record : t)) + : [record, ...prev]; + persistPolicyRecognTasksToStorage(next); + return next; + }); + }, []); + + const vehicleInsuranceHistory = useMemo(() => { + if (!vehicleInsMgmtVehicle) return { timeline: [], byType: {}, ledgerKey: '' }; + const built = buildVehicleInsuranceHistory( + vehicleInsMgmtVehicle, + allInsurance, + compareSheets, + policyRecognTasks + ); + return applyHistoryEditsToVehicleHistory(built, insuranceHistoryEdits); + }, [vehicleInsMgmtVehicle, allInsurance, compareSheets, policyRecognTasks, insuranceHistoryEdits]); + + const openVehicleInsuranceMgmt = (vehicle) => { + setVehicleInsMgmtVehicle(vehicle); + setVehicleInsMgmtActiveTab('timeline'); + setVehicleInsMgmtHighlightId(''); + setVehicleInsMgmtOpen(true); + }; + + const jumpToVehicleInsuranceRecord = (typeKey, recordId) => { + setVehicleInsMgmtActiveTab(typeKey); + setVehicleInsMgmtHighlightId(recordId); + window.setTimeout(() => setVehicleInsMgmtHighlightId(''), 3200); + }; + + const handleInsuranceRecordPreview = (record) => { + Modal.info({ + title: `预览 · ${record.fileName}`, + width: 520, + centered: true, + content: ( +
+
险种:{record.typeLabel}
+
保单号:{record.policyNo || '—'}
+
保险公司:{record.company || '—'}
+
正式环境将内嵌 PDF / 图片预览;原型仅展示附件名称。
+
+ ), + okText: '关闭', + }); + }; + + const handleInsuranceRecordDownload = (record) => { + message.success(`已开始下载:${record.fileName || '保单附件'}(原型)`); + }; + + const syncVehicleInsHistoryEditToLedger = (record, detail) => { + if (!vehicleInsMgmtVehicle || record.source !== 'ledger') return; + const ledgerKey = getVehicleLedgerKey(vehicleInsMgmtVehicle); + if (!ledgerKey) return; + const ledgerEvents = new Set(['purchase', 'suspend', 'cancel']); + if (!ledgerEvents.has(record.eventType)) return; + const mode = bizTypeToRecognMode(detail.bizType); + updateAllInsurance((prev) => { + const rec = ensureInsuranceRecordShape(prev[ledgerKey] || createEmptyInsuranceRecord()); + const item = rec[record.typeKey]; + if (!item?.policyNo) return prev; + if (record.eventType === 'purchase' && item.policyNo !== record.policyNo) return prev; + const nextItem = applyPolicyDetailToInsuranceItem({ ...item }, detail, mode); + nextItem.updateTime = formatCompareSheetNow(); + nextItem.updateUser = PROTO_COMPARE_CREATOR; + return { ...prev, [ledgerKey]: { ...rec, [record.typeKey]: nextItem } }; + }); + }; + + const syncVehicleInsHistoryEditToRecognTask = (record, detail) => { + if (record.source !== 'recognize' || !record.recognizeTaskId || !record.recognizeResultId) return; + setPolicyRecognTasks((prev) => { + const next = prev.map((task) => { + if (task.id !== record.recognizeTaskId) return task; + const results = (task.results || []).map((r) => ( + r.id === record.recognizeResultId + ? mergeRecognResultWithDetail(r, detail) + : r + )); + return { ...task, results }; + }); + persistPolicyRecognTasksToStorage(next); + return next; + }); + }; + + const openVehicleInsHistoryEdit = (record) => { + if (!vehicleInsMgmtVehicle) return; + setVehicleInsHistoryEditRecord(record); + setVehicleInsHistoryEditDraft(historyRecordToPolicyDetail(record, vehicleInsMgmtVehicle)); + setVehicleInsHistoryEditOpen(true); + }; + + const saveVehicleInsHistoryEdit = () => { + if (!vehicleInsHistoryEditRecord) return; + const detail = normalizePolicyDetail(vehicleInsHistoryEditDraft); + if (!detail.policyNo && !detail.endDate) { + message.warning('请至少填写保单号或到期日期'); + return; + } + const record = vehicleInsHistoryEditRecord; + setInsuranceHistoryEdits((prev) => { + const next = { ...prev, [record.id]: detail }; + persistInsuranceHistoryEditsToStorage(next); + return next; + }); + syncVehicleInsHistoryEditToLedger(record, detail); + syncVehicleInsHistoryEditToRecognTask(record, detail); + setVehicleInsHistoryEditOpen(false); + setVehicleInsHistoryEditRecord(null); + message.success('已保存保单要素'); + }; + + const renderPurchaseTypeChip = (purchaseType) => { + const meta = POLICY_PURCHASE_TYPE_META[purchaseType] || { label: purchaseType, chipClass: '' }; + return ( + {meta.label} + ); + }; + + const vehicleInsMgmtTabCounts = useMemo(() => { + const counts = { timeline: vehicleInsuranceHistory.timeline.length }; + VEHICLE_INSURANCE_MGMT_TABS.filter((t) => t.key !== 'timeline').forEach((tab) => { + counts[tab.key] = (vehicleInsuranceHistory.byType[tab.key] || []).length; + }); + return counts; + }, [vehicleInsuranceHistory]); + + const vehicleInsuranceHistoryColumns = [ + { + title: '业务时间', + dataIndex: 'purchaseTime', + width: 104, + render: (val) => {val || '—'}, + }, + { + title: '类型', + dataIndex: 'purchaseType', + width: 76, + render: (val) => renderPurchaseTypeChip(val), + }, + { + title: '保单号', + dataIndex: 'policyNo', + width: 140, + ellipsis: true, + }, + { + title: '保险公司', + dataIndex: 'company', + width: 168, + ellipsis: true, + render: (val) => val || '—', + }, + { + title: '付款时间', + dataIndex: 'payTime', + width: 108, + ellipsis: true, + render: (val) => val || '—', + }, + { + title: '生效日', + dataIndex: 'startDate', + width: 96, + render: (val) => val || '—', + }, + { + title: '到期日', + dataIndex: 'endDate', + width: 96, + render: (val) => val || '—', + }, + { + title: '金额', + dataIndex: 'premium', + width: 88, + align: 'right', + render: (val, record) => ( + val ? ( + + {record.purchaseType === 'cancel' ? '-' : ''}¥{val} + + ) : '—' + ), + }, + { + title: '操作', + key: 'action', + width: 148, + fixed: 'right', + render: (_, record) => ( + + + + + + ), + }, + ]; + + const renderVehicleInsuranceTypeTab = (typeKey) => { + const rows = vehicleInsuranceHistory.byType[typeKey] || []; + const tabLabel = VEHICLE_INSURANCE_MGMT_TABS.find((t) => t.key === typeKey)?.label || ''; + if (!rows.length) { + return ( +
+
{tabLabel}暂无记录
+
该险种首次购买为「新保」,此前已购后再投保记为「续保」
+
+ ); + } + return ( +
+ 8 ? { pageSize: 8, showSizeChanger: false, size: 'small' } : false} + scroll={{ x: 1020 }} + rowClassName={(record) => (record.id === vehicleInsMgmtHighlightId ? 'lc-ins-history-row--active' : '')} + /> + + ); + }; + + const renderVehicleInsMgmtTabLabel = (tab) => { + const count = vehicleInsMgmtTabCounts[tab.key] || 0; + return ( + + {tab.label} + {count > 0 ? {count} : null} + + ); + }; + + const filteredPolicyRecognTasks = useMemo(() => { + const taskKey = (appliedPolicyRecognTasksFilters.taskId || '').trim().toUpperCase(); + const entry = appliedPolicyRecognTasksFilters.entry; + const status = appliedPolicyRecognTasksFilters.status; + const range = appliedPolicyRecognTasksFilters.createdRange; + return [...policyRecognTasks] + .filter((task) => { + if (taskKey && !(task.id || '').toUpperCase().includes(taskKey)) return false; + if (entry && entry !== '全部' && task.entryLabel !== entry) return false; + if (status && status !== '全部') { + const meta = POLICY_RECOGN_STATUS_META[task.status]; + if ((meta?.label || task.status) !== status) return false; + } + return compareSheetMatchesCreatedRange(task.createdAt, range); + }) + .sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt))); + }, [policyRecognTasks, appliedPolicyRecognTasksFilters]); + + const openPolicyRecognTasksModal = () => { + setPolicyRecognTasksFilters({ ...DEFAULT_POLICY_RECOGN_TASK_FILTERS }); + setAppliedPolicyRecognTasksFilters({ ...DEFAULT_POLICY_RECOGN_TASK_FILTERS }); + setPolicyRecognTasksOpen(true); + }; + + const handlePolicyRecognTasksQuery = () => { + setAppliedPolicyRecognTasksFilters({ ...policyRecognTasksFilters }); + message.success('已刷新任务列表'); + }; + + const handlePolicyRecognTasksReset = () => { + setPolicyRecognTasksFilters({ ...DEFAULT_POLICY_RECOGN_TASK_FILTERS }); + setAppliedPolicyRecognTasksFilters({ ...DEFAULT_POLICY_RECOGN_TASK_FILTERS }); + }; + + const openPolicyRecogn = (entry, initialMode = 'policy') => { + setPolicyRecognEntry(entry); + setPolicyRecognMode(entry === 'import' ? 'policy' : initialMode); + setPolicyRecognInsuranceType('交强险'); + setPolicyRecognPhase('upload'); + setPolicyRecognFiles([]); + setPolicyRecognProgress(0); + setPolicyRecognTaskId(''); + setPolicyRecognResults([]); + setPolicyRecognViewOnly(false); + setPolicyPreview(null); + setPolicyRecognOpen(true); + }; + + const openPolicyRecognTaskRecord = (task) => { + if (!task?.id || !task.results?.length) { + message.warning('任务记录无效'); + return; + } + setPolicyRecognEntry(task.entry || 'ocr'); + setPolicyRecognMode(task.mode || 'policy'); + setPolicyRecognInsuranceType(task.insuranceType || '交强险'); + setPolicyRecognTaskId(task.id); + setPolicyRecognResults(task.results.map((r) => ({ ...r }))); + setPolicyRecognFiles([]); + setPolicyRecognProgress(100); + setPolicyRecognViewOnly(task.status === 'completed'); + const phase = task.status === 'completed' + ? 'results' + : (task.phase === 'recognized' ? 'recognized' : 'results'); + setPolicyRecognPhase(phase); + setPolicyPreview(null); + setPolicyRecognOpen(true); + setPolicyRecognTasksOpen(false); + }; + + const closePolicyRecogn = () => { + if (policyRecognTaskId && policyRecognResults.length) { + const status = derivePolicyRecognTaskStatus(policyRecognResults); + savePolicyRecognTaskSnapshot({ + taskId: policyRecognTaskId, + entry: policyRecognEntry, + mode: policyRecognMode, + insuranceType: policyRecognInsuranceType, + results: policyRecognResults, + phase: policyRecognPhase, + completedAt: status === 'completed' ? formatCompareSheetNow() : undefined, + }); + } + if (policyPreview?.url) URL.revokeObjectURL(policyPreview.url); + setPolicyPreview(null); + setPolicyRecognOpen(false); + }; + + const policyRecognAllUploaded = policyRecognFiles.length > 0 + && policyRecognFiles.every((f) => f.status === 'done'); + + const handlePolicyRecognUploadChange = ({ fileList }) => { + const incoming = fileList.filter((f) => f.status !== 'removed'); + const valid = incoming.filter((f) => isPolicyRecognImageOrPdf(f)); + if (valid.length < incoming.length) { + message.warning('已忽略不支持格式,仅支持 PDF / 图片'); + } + const next = valid.map((f) => { + if (f.status === 'done') return f; + return { ...f, status: 'uploading', percent: f.percent || 0 }; + }); + setPolicyRecognFiles(next); + next.forEach((f, i) => { + if (f.status === 'uploading') { + window.setTimeout(() => { + setPolicyRecognFiles((prev) => prev.map((p) => ( + p.uid === f.uid ? { ...p, status: 'done', percent: 100 } : p + ))); + }, 500 + i * 280); + } + }); + }; + + const handlePolicyImportUploadChange = ({ fileList }) => { + const incoming = fileList.filter((f) => f.status !== 'removed').slice(-1); + const valid = incoming.filter((f) => isPolicyImportExcelFile(f)); + if (incoming.length && !valid.length) { + message.warning('请上传 Excel 模板文件(.csv、.xlsx、.xls)'); + return; + } + const next = valid.map((f) => ( + f.status === 'done' ? f : { ...f, status: 'done', percent: 100 } + )); + setPolicyRecognFiles(next); + }; + + const startPolicyExcelImportTask = async () => { + const fileItem = policyRecognFiles.find((f) => f.status === 'done'); + const file = fileItem?.originFileObj; + if (!file) { + message.warning('请先上传 Excel 导入文件'); + return; + } + const taskId = createPolicyRecognTaskId(); + setPolicyRecognPhase('recognizing'); + setPolicyRecognTaskId(taskId); + setPolicyRecognProgress(0); + let progress = 0; + const progTimer = window.setInterval(() => { + progress = Math.min(92, progress + 14); + setPolicyRecognProgress(progress); + }, 220); + try { + const text = await readPolicyImportFileAsText(file); + window.clearInterval(progTimer); + if (!String(text).trim()) { + setPolicyRecognPhase('upload'); + return; + } + const rows = parsePolicyImportFileText(text); + if (!rows.length) { + message.error('未解析到有效数据,请按模板填写车牌/VIN、险种、保单号等字段'); + setPolicyRecognPhase('upload'); + return; + } + const results = buildImportResultsFromRows(rows, allInsurance); + setPolicyRecognProgress(100); + setPolicyRecognResults(results); + setPolicyRecognPhase('recognized'); + savePolicyRecognTaskSnapshot({ + taskId, + entry: 'import', + mode: 'policy', + insuranceType: '', + results, + phase: 'recognized', + }); + const matchedN = results.filter((r) => r.matched).length; + message.success(`已解析 ${results.length} 条,${matchedN} 条已匹配台账,请点击「处理」核对`); + } catch { + window.clearInterval(progTimer); + message.error('导入文件读取失败,请重试'); + setPolicyRecognPhase('upload'); + } + }; + + const startPolicyRecognTask = () => { + if (!policyRecognAllUploaded) { + message.warning(policyRecognEntry === 'import' ? '请先上传 Excel 文件' : '请等待全部文件上传完成'); + return; + } + if (policyRecognEntry === 'import') { + startPolicyExcelImportTask(); + return; + } + if (policyRecognMode === 'policy' && !policyRecognInsuranceType) { + message.warning('请选择保险类型'); + return; + } + const taskId = createPolicyRecognTaskId(); + const entrySnap = policyRecognEntry; + const modeSnap = policyRecognMode; + const insuranceSnap = policyRecognInsuranceType; + const filesSnap = policyRecognFiles; + setPolicyRecognPhase('recognizing'); + setPolicyRecognTaskId(taskId); + setPolicyRecognProgress(0); + let progress = 0; + const timer = window.setInterval(() => { + progress += 10 + Math.floor(Math.random() * 8); + if (progress >= 100) { + window.clearInterval(timer); + setPolicyRecognProgress(100); + const results = buildMockOcrResults( + filesSnap, + modeSnap, + insuranceSnap, + allInsurance + ); + setPolicyRecognResults(results); + setPolicyRecognPhase('recognized'); + savePolicyRecognTaskSnapshot({ + taskId, + entry: entrySnap, + mode: modeSnap, + insuranceType: insuranceSnap, + results, + phase: 'recognized', + }); + message.success('识别完成,已写入任务记录,请点击「处理」查看结果'); + } else { + setPolicyRecognProgress(Math.min(99, progress)); + } + }, 380); + }; + + const openPolicyRecognResults = () => { + if (!policyRecognResults.length) { + message.warning('暂无识别结果'); + return; + } + setPolicyRecognPhase('results'); + if (policyRecognTaskId) { + savePolicyRecognTaskSnapshot({ + taskId: policyRecognTaskId, + entry: policyRecognEntry, + mode: policyRecognMode, + insuranceType: policyRecognInsuranceType, + results: policyRecognResults, + phase: 'results', + }); + } + }; + + const handlePreviewPolicyResult = (result) => { + const file = policyRecognFiles.find((f) => f.uid === result.fileUid); + if (policyPreview?.url) URL.revokeObjectURL(policyPreview.url); + if (file?.originFileObj && (file.type || '').startsWith('image/')) { + const url = URL.createObjectURL(file.originFileObj); + setPolicyPreview({ url, fileName: result.fileName, isImage: true }); + return; + } + setPolicyPreview({ + url: null, + fileName: result.fileName, + isImage: false, + hint: policyRecognFiles.length + ? 'PDF 预览(原型):正式环境将内嵌预览识别原件' + : '任务记录未保存原件,正式环境可从附件库查看', + }); + }; + + const mergeRecognResultWithDetail = (result, detail) => { + const rebuilt = buildRecognResultFromDetail( + { + id: result.id, + fileUid: result.fileUid, + fileName: result.fileName, + fileType: result.fileType, + }, + detail, + allInsurance, + bizTypeToRecognMode(detail.bizType) + ); + return { ...result, ...rebuilt, id: result.id, confirmed: result.confirmed }; + }; + + const renderPolicyDetailForm = (draft, setDraft, options = {}) => { + const { showBizType = true } = options; + const coverageRows = getCoverageItemsFormRows(draft.coverageItems); + const updateCoverageRow = (idx, value) => { + const next = [...coverageRows]; + next[idx] = value; + setDraft((p) => ({ ...p, coverageItems: next })); + }; + const addCoverageRow = () => { + setDraft((p) => ({ + ...p, + coverageItems: [...getCoverageItemsFormRows(p.coverageItems), ''], + })); + }; + const removeCoverageRow = (idx) => { + const next = coverageRows.filter((_, i) => i !== idx); + setDraft((p) => ({ ...p, coverageItems: next.length ? next : [''] })); + }; + return ( +
+
车辆与险种
+ {renderFilterField('车牌号', ( + setDraft((p) => ({ ...p, plateNo: e.target.value }))} + placeholder="与 VIN 至少填一项" + /> + ))} + {renderFilterField('VIN码', ( + setDraft((p) => ({ ...p, vin: e.target.value }))} /> + ))} + {showBizType ? renderFilterField('业务类型', ( + setDraft((p) => ({ ...p, insuranceType: v }))} + style={{ width: '100%' }} + options={QUOTE_INSURANCE_TYPES.map((t) => ({ label: t, value: t }))} + /> + ))} +
保单要素
+ {renderFilterField('保险公司', ( + setDraft((p) => ({ ...p, policyNo: e.target.value }))} /> + ))} + {renderFilterField('批单号', ( + setDraft((p) => ({ ...p, endorsementNo: e.target.value }))} placeholder="停保/复驶/退保批单号" /> + ))} + {renderFilterField('付款时间', ( + setDraft((p) => ({ ...p, payTime: e.target.value }))} placeholder="如 2026-06-01 17:42:10" /> + ))} + {renderFilterField('签单日期', ( + setDraft((p) => ({ ...p, signDate: ds || '' }))} + /> + ))} + {renderFilterField('生效日期', ( + setDraft((p) => ({ ...p, startDate: ds || '' }))} + /> + ))} + {renderFilterField('到期日期', ( + setDraft((p) => ({ ...p, endDate: ds || '' }))} + /> + ))} + {(draft.bizType === 'suspend' || draft.bizType === 'resume') ? renderFilterField('复驶日期', ( + setDraft((p) => ({ ...p, reinstateDate: ds || '' }))} + /> + )) : null} + {renderFilterField(draft.bizType === 'cancel' ? '退费金额(元)' : '保费(元)', ( + setDraft((p) => ({ ...p, premium: e.target.value }))} placeholder="元" /> + ))} +
+ {renderFilterField('保单项目/责任限额', ( +
+ {coverageRows.map((row, idx) => ( +
+ {idx + 1} + updateCoverageRow(idx, e.target.value)} + placeholder="如:死亡伤残赔偿限额180000元、机动车损失险" + /> + +
+ ))} + +
+ 一条保单可维护多项责任/险种;批量导入时同一单元格可用「;」分隔多项 +
+
+ ))} +
+ {renderFilterField('投保人', ( + setDraft((p) => ({ ...p, applicant: e.target.value }))} /> + ))} + {renderFilterField('被保险人', ( + setDraft((p) => ({ ...p, insured: e.target.value }))} /> + ))} +
+ ); + }; + + const openPolicyRecognResultEdit = (result) => { + setPolicyRecognEditId(result.id); + setPolicyRecognEditDraft(recognResultToPolicyDetail(result)); + setPolicyRecognEditOpen(true); + }; + + const savePolicyRecognResultEdit = () => { + const result = policyRecognResults.find((r) => r.id === policyRecognEditId); + if (!result) return; + const merged = mergeRecognResultWithDetail(result, policyRecognEditDraft); + setPolicyRecognResults((prev) => prev.map((r) => (r.id === policyRecognEditId ? merged : r))); + setPolicyRecognEditOpen(false); + message.success('已更新识别结果,请确认后写入台账'); + }; + + const confirmPolicyRecognResult = (resultId) => { + const result = policyRecognResults.find((r) => r.id === resultId); + if (!result) return; + if (!result.matched) { + message.warning('该条未匹配台账,无法确认'); + return; + } + if (result.confirmed) { + message.info('该条已确认'); + return; + } + const mode = result.recognMode || policyRecognMode; + updateAllInsurance(applyPolicyOcrResultToLedger(result, mode)); + const nextResults = policyRecognResults.map((r) => ( + r.id === resultId ? { ...r, confirmed: true } : r + )); + setPolicyRecognResults(nextResults); + if (derivePolicyRecognTaskStatus(nextResults) === 'completed') { + setPolicyRecognViewOnly(true); + } + if (policyRecognTaskId) { + savePolicyRecognTaskSnapshot({ + taskId: policyRecognTaskId, + entry: policyRecognEntry, + mode: policyRecognMode, + insuranceType: policyRecognInsuranceType, + results: nextResults, + phase: 'results', + completedAt: derivePolicyRecognTaskStatus(nextResults) === 'completed' ? formatCompareSheetNow() : undefined, + }); + } + message.success(`已更新 ${result.displayPlate || result.ocrVin} 的到期时间`); + }; + + const confirmAllPolicyRecognResults = () => { + const pending = policyRecognResults.filter((r) => r.matched && !r.confirmed); + if (!pending.length) { + message.info('没有可批量确认的记录'); + return; + } + let nextInsurance = { ...allInsurance }; + pending.forEach((result) => { + const mode = result.recognMode || policyRecognMode; + nextInsurance = applyPolicyOcrResultToLedger(result, mode)(nextInsurance); + }); + updateAllInsurance(() => nextInsurance); + const nextResults = policyRecognResults.map((r) => ( + r.matched ? { ...r, confirmed: true } : r + )); + setPolicyRecognResults(nextResults); + const allDone = derivePolicyRecognTaskStatus(nextResults) === 'completed'; + if (allDone) setPolicyRecognViewOnly(true); + if (policyRecognTaskId) { + savePolicyRecognTaskSnapshot({ + taskId: policyRecognTaskId, + entry: policyRecognEntry, + mode: policyRecognMode, + insuranceType: policyRecognInsuranceType, + results: nextResults, + phase: 'results', + completedAt: allDone ? formatCompareSheetNow() : undefined, + }); + } + message.success(`已批量确认 ${pending.length} 条,台账到期时间已更新`); + }; + + const policyRecognResultColumns = useMemo(() => { + const canEdit = !policyRecognViewOnly; + return [ + { + title: '文件', + dataIndex: 'fileName', + width: 140, + ellipsis: true, + }, + { + title: '类型', + key: 'biz', + width: 72, + render: (_, r) => ( + {r.ocrBizTypeLabel || '保单录入'} + ), + }, + { + title: '车牌号', + key: 'plate', + width: 96, + render: (_, r) => r.displayPlate || r.ocrPlateNo || r.ocrVin || '—', + }, + { + title: '保单号', + dataIndex: 'ocrPolicyNo', + width: 130, + ellipsis: true, + }, + { + title: '险种', + dataIndex: 'insuranceTypeLabel', + width: 72, + }, + { + title: '付款时间', + dataIndex: 'ocrPayTime', + width: 108, + ellipsis: true, + render: (v) => v || '—', + }, + { + title: '生效日', + dataIndex: 'ocrStartDate', + width: 96, + render: (v) => v || '—', + }, + { + title: '到期日', + dataIndex: 'ocrEndDate', + width: 96, + }, + { + title: '金额', + dataIndex: 'ocrPremium', + width: 80, + align: 'right', + render: (v) => (v ? `¥${v}` : '—'), + }, + { + title: '匹配', + key: 'matched', + width: 88, + render: (_, r) => ( + + {r.matched ? '已匹配' : '未匹配'} + + ), + }, + { + title: '状态', + key: 'confirmed', + width: 80, + render: (_, r) => ( + r.confirmed ? 已确认 : 待确认 + ), + }, + { + title: '操作', + key: 'action', + width: 168, + fixed: 'right', + render: (_, r) => ( + + + {canEdit ? ( + <> + + + + ) : null} + + ), + }, + ]; + }, [policyRecognViewOnly, policyRecognResults, policyRecognFiles, allInsurance]); + + const handlePolicyAddSubmit = () => { + const detail = normalizePolicyDetail(policyAddDraft); + const ledgerKey = resolvePolicyVehicleKey(detail.plateNo || detail.vin); + if (!ledgerKey) { + message.warning('请填写台账中存在的车牌或 VIN'); + return; + } + const typeKey = INSURANCE_LABEL_TO_KEY[detail.insuranceType]; + if (!typeKey || !detail.endDate) { + message.warning('请填写险种与到期日期'); + return; + } + const mode = bizTypeToRecognMode(detail.bizType); + updateAllInsurance((prev) => { + const record = ensureInsuranceRecordShape(prev[ledgerKey] || createEmptyInsuranceRecord()); + const item = applyPolicyDetailToInsuranceItem( + { ...record[typeKey] }, + { ...detail, policyNo: detail.policyNo || `MAN-${Date.now().toString().slice(-6)}` }, + mode + ); + item.updateTime = formatCompareSheetNow(); + item.updateUser = PROTO_COMPARE_CREATOR; + return { ...prev, [ledgerKey]: { ...record, [typeKey]: item } }; + }); + message.success('保单已录入台账'); + setPolicyAddOpen(false); + setPolicyAddDraft({ ...EMPTY_POLICY_DETAIL }); + }; + + const renderPolicyStatusTag = (item) => { + if (!item?.policyTag) return null; + if (item.policyTag === 'suspended') { + return ( + + 已停保 + + ); + } + if (item.policyTag === 'cancelled') { + return ( + 已退保 + ); + } + return null; + }; + + const renderDateCell = (rowId, field, value) => ( + updateCompareRow(rowId, { [field]: ds || '' })} + placeholder="选择日期" + style={{ width: '100%' }} + /> + ); + + const renderQuoteAddPopover = (row) => ( + { + setQuoteEditRowId(open ? row.id : null); + if (!open) setQuoteDraft(createEmptyQuoteDraft()); + }} + content={( +
+
+ 添加报价 + {row.insuranceType || '交强险'} +
+
+
+ + setQuoteDraft((d) => ({ ...d, premium: sanitizePremiumInput(e.target.value) }))} + onBlur={() => { + if (quoteDraft.premium && isValidPremium(quoteDraft.premium)) { + setQuoteDraft((d) => ({ ...d, premium: formatPremiumDisplay(d.premium) })); + } + }} + /> +
+
+ +
+
+
+ )} + > + +
+ ); + + const renderQuoteCell = (row) => { + const quotes = row.quotes || []; + return ( +
+ {quotes.length > 0 ? ( + updateCompareRow(row.id, { confirmedQuoteId: e.target.value })} + className="lc-compare-quote-inline-list" + > + {quotes.map((q) => { + const selected = row.confirmedQuoteId === q.id; + return ( + + ); + })} + + ) : ( + 暂无报价 + )} + {renderQuoteAddPopover(row)} +
+ ); + }; + + const compareColumns = [ + { + title: '车牌号', + dataIndex: 'plateNo', + width: 118, + fixed: 'left', + onHeaderCell: () => ({ className: 'lc-compare-th-key' }), + render: (val, row) => ( + (option?.label || '').toLowerCase().includes(input.toLowerCase())} + onChange={(v) => handleCompareVinChange(row.id, v)} + /> + + ), + }, + { + title: '客户', + dataIndex: 'customer', + width: 128, + onHeaderCell: () => ({ className: 'lc-compare-th-auto' }), + render: (val, row) => renderReadonlyField(val, isCompareRowVehicleLinked(row)), + }, + { + title: '归属公司', + dataIndex: 'ownerCompany', + width: 148, + onHeaderCell: () => ({ className: 'lc-compare-th-auto' }), + render: (val, row) => renderReadonlyField(val, isCompareRowVehicleLinked(row)), + }, + { + title: '品牌', + dataIndex: 'brand', + width: 80, + onHeaderCell: () => ({ className: 'lc-compare-th-auto' }), + render: (val, row) => renderReadonlyField(val, isCompareRowVehicleLinked(row)), + }, + { + title: '型号', + dataIndex: 'model', + width: 120, + onHeaderCell: () => ({ className: 'lc-compare-th-auto' }), + render: (val, row) => renderReadonlyField(val, isCompareRowVehicleLinked(row)), + }, + { + title: '车身颜色', + dataIndex: 'bodyColor', + width: 80, + onHeaderCell: () => ({ className: 'lc-compare-th-auto' }), + render: (val, row) => renderReadonlyField(val, isCompareRowVehicleLinked(row)), + }, + { + title: '行驶证注册日期', + dataIndex: 'regDate', + width: 118, + onHeaderCell: () => ({ className: 'lc-compare-th-auto' }), + render: (val, row) => renderReadonlyDate(val, isCompareRowVehicleLinked(row)), + }, + { + title: '年检有效期', + dataIndex: 'inspectExpire', + width: 108, + onHeaderCell: () => ({ className: 'lc-compare-th-auto' }), + render: (val, row) => renderReadonlyDate(val, isCompareRowVehicleLinked(row)), + }, + { + title: '投保方式', + dataIndex: 'insureMode', + width: 96, + onHeaderCell: () => ({ className: 'lc-compare-th-edit' }), + render: (val, row) => ( + updateCompareRow(row.id, { + insuranceType: v, + quotes: [], + confirmedQuoteId: '', + })} + options={QUOTE_INSURANCE_TYPES.map((t) => ({ label: t, value: t }))} + /> + ), + }, + { + title: '交强险有效期', + dataIndex: 'jqValidUntil', + width: 108, + onHeaderCell: () => ({ className: 'lc-compare-th-auto' }), + render: (val, row) => renderReadonlyDate(val, isCompareRowVehicleLinked(row)), + }, + { + title: '商业险有效期', + dataIndex: 'syValidUntil', + width: 108, + onHeaderCell: () => ({ className: 'lc-compare-th-auto' }), + render: (val, row) => renderReadonlyDate(val, isCompareRowVehicleLinked(row)), + }, + { + title: '最晚付费日期', + dataIndex: 'latestPayDate', + width: 128, + onHeaderCell: () => ({ className: 'lc-compare-th-edit' }), + render: (val, row) => { + const paySt = getLatestPayDateStatus(val); + return ( +
+ {renderDateCell(row.id, 'latestPayDate', val)} + {val ? ( +
+ + {paySt.type === 'overdue' ? `超期${Math.abs(paySt.diffDays)}天` : paySt.type === 'warning' ? `临期${paySt.diffDays}天` : `剩余${paySt.diffDays}天`} + +
+ ) : null} +
+ ); + }, + }, + { + title: '采购状态', + dataIndex: 'procurementStatus', + width: 92, + onHeaderCell: () => ({ className: 'lc-compare-th-auto' }), + render: (st) => { + if (st === 'completed') return 已完结; + if (st === 'submitted') return 已提交; + return 未提交; + }, + }, + { + title: '报价情况', + key: 'quotes', + width: 220, + onHeaderCell: () => ({ className: 'lc-compare-th-edit' }), + render: (_, row) => renderQuoteCell(row), + }, + { + title: '操作', + key: 'action', + width: 96, + fixed: 'right', + render: (_, row) => ( +
+ { + setCopyPopoverRowId(open ? row.id : null); + if (!open) setCopyCountDraft(1); + }} + content={( +
+
复制条数
+ setCopyCountDraft(v || 1)} style={{ width: '100%' }} /> +
+ + +
+
+ )} + > + +
+ +
+ ), + }, + ]; + + const getInsuranceItemStatus = (ledgerKey, typeKey) => { + const item = allInsurance[ledgerKey]?.[typeKey]; + if (!item || !item.endDate || !item.policyNo) { + return { type: 'unuploaded', text: '未购买', diffDays: null }; + } + const today = new Date(ANCHOR_TODAY); + today.setHours(0, 0, 0, 0); + const expDate = new Date(item.endDate); + expDate.setHours(0, 0, 0, 0); + const diffDays = Math.ceil((expDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)); + if (diffDays <= 0) { + return { type: 'expired', text: `已到期 (逾期 ${Math.abs(diffDays)} 天)`, diffDays }; + } + if (diffDays <= INSURANCE_WARN_DAYS) { + return { type: 'warning', text: `临期 (${diffDays} 天后)`, diffDays }; + } + return { type: 'success', text: '正常', diffDays }; + }; + + /** 与车辆管理「保险状态」一致:交强险 + 商业险均存在且在有效期内(临期仍算有效) */ + const getVehicleInsuranceStatus = (ledgerKey) => { + const jq = getInsuranceItemStatus(ledgerKey, 'compulsory'); + const sy = getInsuranceItemStatus(ledgerKey, 'commercial'); + const isValid = (st) => st.type === 'success' || st.type === 'warning'; + const isNormal = isValid(jq) && isValid(sy); + if (isNormal) { + const hasWarning = jq.type === 'warning' || sy.type === 'warning'; + return { + label: hasWarning ? '临期' : '正常', + color: hasWarning ? 'warning' : 'success', + abnormal: false, + tip: hasWarning + ? '交强险或商业险临期,仍在有效期内,可交车但需尽快续保' + : '交强险、商业险均在有效期内', + }; + } + const reasons = []; + if (jq.type === 'unuploaded') reasons.push('交强险未购买'); + else if (jq.type === 'expired') reasons.push('交强险已到期'); + if (sy.type === 'unuploaded') reasons.push('商业险未购买'); + else if (sy.type === 'expired') reasons.push('商业险已到期'); + return { + label: '异常', + color: 'error', + abnormal: true, + tip: `${reasons.join(';')}。保险状态异常的车辆禁止交车`, + }; + }; + + const isCoreInsuranceNormal = (ledgerKey) => !getVehicleInsuranceStatus(ledgerKey).abnormal; + + const isAnyInsuranceWarning = (ledgerKey) => ( + INSURANCE_TYPE_ITEMS.some((item) => getInsuranceItemStatus(ledgerKey, item.key).type === 'warning') + ); + + const isCoreInsuranceExpired = (ledgerKey) => ( + CORE_INSURANCE_KEYS.some((key) => getInsuranceItemStatus(ledgerKey, key).type === 'expired') + ); + + const isCoreInsuranceMissing = (ledgerKey) => ( + CORE_INSURANCE_KEYS.some((key) => getInsuranceItemStatus(ledgerKey, key).type === 'unuploaded') + ); + + const matchKpiFilter = (ledgerKey, filterKey) => { + if (filterKey === 'total') return true; + if (filterKey === 'normal') return isCoreInsuranceNormal(ledgerKey); + if (filterKey === 'warning') return isAnyInsuranceWarning(ledgerKey); + if (filterKey === 'expired') return isCoreInsuranceExpired(ledgerKey); + if (filterKey === 'unuploaded') return isCoreInsuranceMissing(ledgerKey); + return true; + }; + + const stats = useMemo(() => { + let normal = 0; + let warning = 0; + let expired = 0; + let unuploaded = 0; + MOCK_VEHICLES.forEach((v) => { + const ledgerKey = getVehicleLedgerKey(v); + if (isCoreInsuranceNormal(ledgerKey)) normal += 1; + if (isAnyInsuranceWarning(ledgerKey)) warning += 1; + if (isCoreInsuranceExpired(ledgerKey)) expired += 1; + if (isCoreInsuranceMissing(ledgerKey)) unuploaded += 1; + }); + return { total: MOCK_VEHICLES.length, normal, warning, expired, unuploaded }; + }, [allInsurance]); + + const brandOptions = useMemo( + () => [...new Set(MOCK_VEHICLES.map((v) => v.brand))].map((b) => ({ label: b, value: b })), + [] + ); + const modelOptions = useMemo( + () => [...new Set(MOCK_VEHICLES.map((v) => v.model))].map((m) => ({ label: m, value: m })), + [] + ); + + const appliedMultiPlates = useMemo(() => parseMultiPlates(appliedFilters.plateNos), [appliedFilters.plateNos]); + + const filterVehiclesByFilters = (vehicles, f, kpi) => { + const plateKey = (f.plateNo || '').trim().toLowerCase(); + const multiPlates = parseMultiPlates(f.plateNos); + const vinKey = (f.vin || '').trim().toLowerCase(); + const brandKey = (f.brand || '').trim().toLowerCase(); + const modelKey = (f.model || '').trim().toLowerCase(); + + return vehicles.filter((v) => { + const ledgerKey = getVehicleLedgerKey(v); + const plateText = (v.plateNo || '').trim(); + if (multiPlates.length) { + if (!plateText) return false; + if (!multiPlates.includes(plateText.toUpperCase())) return false; + } else if (plateKey) { + if (!plateText) { + if (!NO_PLATE_LABEL.toLowerCase().includes(plateKey) && !plateKey.includes('暂无')) return false; + } else if (!plateText.toLowerCase().includes(plateKey)) return false; + } + if (vinKey && !v.vin.toLowerCase().includes(vinKey)) return false; + if (brandKey && !v.brand.toLowerCase().includes(brandKey)) return false; + if (modelKey && !v.model.toLowerCase().includes(modelKey)) return false; + if (f.operateStatus !== '全部' && v.status !== f.operateStatus) return false; + if (f.insuranceStatus === '正常' && getVehicleInsuranceStatus(ledgerKey).abnormal) return false; + if (f.insuranceStatus === '异常' && !getVehicleInsuranceStatus(ledgerKey).abnormal) return false; + if (!matchKpiFilter(ledgerKey, kpi)) return false; + return true; + }); + }; + + const filteredVehicles = useMemo( + () => sortVehiclesRetiredLast(filterVehiclesByFilters(MOCK_VEHICLES, appliedFilters, kpiFilter)), + [appliedFilters, allInsurance, kpiFilter] + ); + + const handleListFilterQuery = () => { + const plates = parseMultiPlates(multiPlateDraft); + const next = { + ...listFilters, + plateNos: multiPlateDraft.trim(), + plateNo: plates.length ? '' : listFilters.plateNo, + }; + setListFilters(next); + setAppliedFilters(next); + setMultiPlateOpen(false); + const hitCount = filterVehiclesByFilters(MOCK_VEHICLES, next, kpiFilter).length; + if (plates.length) { + message.success(`已按 ${plates.length} 个车牌筛选,命中 ${hitCount} 条记录`); + } else { + message.success(`查询完成,命中 ${hitCount} 条记录`); + } + }; + + const handleListFilterReset = () => { + setListFilters({ ...DEFAULT_LIST_FILTERS }); + setAppliedFilters({ ...DEFAULT_LIST_FILTERS }); + setMultiPlateDraft(''); + setKpiFilter('total'); + message.info('筛选条件已重置'); + }; + + const handleMultiPlateOpenChange = (open) => { + setMultiPlateOpen(open); + if (open) setMultiPlateDraft(listFilters.plateNos || ''); + }; + + const renderFilterField = (label, control) => ( +
+ {label} +
{control}
+
+ ); + + const isRetiredVehicle = (record) => record?.status === '退出运营'; + + const renderInsuranceDateCell = (record, typeKey) => { + const ledgerKey = getVehicleLedgerKey(record); + const item = allInsurance[ledgerKey]?.[typeKey]; + const status = getInsuranceItemStatus(ledgerKey, typeKey); + const dateVal = item?.endDate; + const muted = isRetiredVehicle(record); + const policyTagEl = renderPolicyStatusTag(item); + return ( +
+
+ {dateVal || '—'} +
+ {!muted ? ( +
+ {policyTagEl || ( + + + + {getInsuranceRemainShortText(status)} + + )} + /> + + + )} +
+ ) : null} +
+ ); + }; + + const listColumns = [ + { + title: '车牌号', + dataIndex: 'plateNo', + key: 'plateNo', + width: 148, + onHeaderCell: listColumnHeaderCell, + align: 'left', + render: (plate, record) => { + const muted = isRetiredVehicle(record); + const noPlate = !hasVehiclePlate(record); + const displayPlate = formatVehiclePlateDisplay(plate); + const sub = [record.brand, record.model].filter(Boolean).join(' · '); + return ( +
+ + {displayPlate} + + {sub ? ( + + + {sub} + + + ) : null} +
+ ); + }, + }, + { + title: 'VIN码', + dataIndex: 'vin', + key: 'vin', + width: 112, + onHeaderCell: listColumnHeaderCell, + render: (vin, record) => ( + + + {vin} + + + ), + }, + { + title: '运营状态', + dataIndex: 'status', + key: 'status', + width: 80, + onHeaderCell: listColumnHeaderCell, + render: (status) => ( + + {status} + + ), + }, + { + title: '保险状态', + key: 'insuranceStatus', + width: 80, + onHeaderCell: listColumnHeaderCell, + render: (record) => { + const st = getVehicleInsuranceStatus(getVehicleLedgerKey(record)); + return ( + + + {st.label} + + + ); + }, + }, + { + title: '交强险到期时间', + key: 'compulsory', + width: 118, + onHeaderCell: listColumnHeaderCell, + render: (record) => renderInsuranceDateCell(record, 'compulsory'), + }, + { + title: '商业险到期时间', + key: 'commercial', + width: 118, + onHeaderCell: listColumnHeaderCell, + render: (record) => renderInsuranceDateCell(record, 'commercial'), + }, + { + title: '超赔险到期时间', + key: 'excess', + width: 118, + onHeaderCell: listColumnHeaderCell, + render: (record) => renderInsuranceDateCell(record, 'excess'), + }, + { + title: '货物险到期时间', + key: 'cargo', + width: 118, + onHeaderCell: listColumnHeaderCell, + render: (record) => renderInsuranceDateCell(record, 'cargo'), + }, + { + title: '驾意险到期时间', + key: 'driverAccident', + width: 118, + onHeaderCell: listColumnHeaderCell, + render: (record) => renderInsuranceDateCell(record, 'driverAccident'), + }, + { + title: '操作', + key: 'action', + width: 64, + onHeaderCell: listColumnHeaderCell, + render: (record) => ( + + ), + }, + ]; + + return ( +
+ + +
+
+
+
保险采购
+
保单管理(一车一档)· 比价单独立管理,互不关联
+
+ +
+ + +
+ {renderFilterField('车牌号', ( + 0} + value={listFilters.plateNo} + onChange={(e) => setListFilters((prev) => ({ ...prev, plateNo: e.target.value }))} + onPressEnter={handleListFilterQuery} + style={{ borderRadius: 8 }} + /> + ))} + {renderFilterField('多车牌', ( + +
每行一个车牌,或同一行内用逗号分隔
+ setMultiPlateDraft(e.target.value)} + placeholder={'沪A03561F\n粤B58888F'} + style={{ borderRadius: 8 }} + /> +
+ + +
+
+ )} + > + + + ))} + {renderFilterField('VIN码', ( + setListFilters((prev) => ({ ...prev, vin: e.target.value }))} + onPressEnter={handleListFilterQuery} + style={{ borderRadius: 8 }} + /> + ))} + {renderFilterField('品牌', ( + setListFilters((prev) => ({ ...prev, model: val || '' }))} + options={modelOptions} + filterOption={(input, option) => (option?.label || '').toLowerCase().includes(input.toLowerCase())} + style={{ width: '100%' }} + /> + ))} + {renderFilterField('运营状态', ( + + ))} + {renderFilterField('保险状态', ( + + ))} +
+
+ + +
+ + +
+ {[ + { key: 'total', type: 'total', title: '台账车辆总数', desc: '纳入保险采购台账管理的车辆(一车一档)', val: stats.total, icon: ICONS.vehicle }, + { key: 'normal', type: 'normal', title: '保险状态正常', desc: '交强险、商业险均已购买且在有效期内,与车辆管理「保险状态=正常」一致', val: stats.normal, icon: ICONS.success }, + { key: 'warning', type: 'warning', title: '险种临期预警', desc: `任一类险种止期 ≤ ${INSURANCE_WARN_DAYS} 天(含交强险、商业险、超赔险、货物险、驾意险)`, val: stats.warning, icon: ICONS.warning }, + { key: 'expired', type: 'expired', title: '核心险种逾期', desc: '交强险或商业险已到期,车辆管理保险状态为异常,禁止交车', val: stats.expired, icon: ICONS.warning }, + { key: 'unuploaded', type: 'unuploaded', title: '核心险种待购', desc: '交强险或商业险未录入/未购买,车辆管理保险状态为异常,禁止交车', val: stats.unuploaded, icon: ICONS.shield }, + ].map((card) => ( +
setKpiFilter(card.key)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + setKpiFilter(card.key); + } + }} + > +
+ + e.stopPropagation()} + onMouseDown={(e) => e.stopPropagation()} + > + + + + + + + +
+
{card.icon}
+
+
{card.val}
+
{card.title}
+
+
+ ))} +
+ +
+
+ 保单录入 +
+ + + + + +
+
+
+
getVehicleLedgerKey(record)} + rowClassName={(record) => (record.status === '退出运营' ? 'lc-row-retired' : '')} + pagination={false} + scroll={{ x: 1040 }} + locale={{ + emptyText: ( +
+ +
暂无符合检索条件的保险台账车辆
+
+ ), + }} + /> + + + + + setVehicleInsMgmtOpen(false)}> + 关闭 + + )} + onCancel={() => setVehicleInsMgmtOpen(false)} + > + {vehicleInsMgmtVehicle ? (() => { + const profile = getVehicleProfile(vehicleInsMgmtVehicle); + const ledgerKey = getVehicleLedgerKey(vehicleInsMgmtVehicle); + const insStatus = getVehicleInsuranceStatus(ledgerKey); + const statusPillClass = insStatus.color === 'success' || insStatus.color === 'warning' + ? `lc-vehicle-ins-mgmt-status-pill--${insStatus.color}` + : 'lc-vehicle-ins-mgmt-status-pill--error'; + return ( +
+
+
+
+
车辆保险档案
+
{formatVehiclePlateDisplay(vehicleInsMgmtVehicle.plateNo)}
+
+ {vehicleInsMgmtVehicle.brand} {vehicleInsMgmtVehicle.model} +
+
+ + + 保险状态 · {insStatus.label} + + +
+
+
+
VIN码
+
{vehicleInsMgmtVehicle.vin || '—'}
+
+
+
运营状态
+
{vehicleInsMgmtVehicle.status || '—'}
+
+
+
客户
+
{profile.customer || '—'}
+
+
+
产权方
+
{profile.ownerCompany || '—'}
+
+
+
注册日期
+
{profile.regDate || '—'}
+
+
+
年审到期
+
{profile.inspectExpire || '—'}
+
+
+
+
+
+ 记录类型 + {POLICY_PURCHASE_TYPE_LEGEND.map((key) => renderPurchaseTypeChip(key))} + + 新保:该险种首次购买;续保:此前已购后再投保 + +
+ + + {vehicleInsuranceHistory.timeline.length ? ( + + {vehicleInsuranceHistory.timeline.map((item) => { + const meta = POLICY_PURCHASE_TYPE_META[item.purchaseType] || {}; + return ( + + {item.time || '—'} + + )} + > +
jumpToVehicleInsuranceRecord(item.typeKey, item.id)} + onKeyDown={(e) => { + if (e.key === 'Enter') jumpToVehicleInsuranceRecord(item.typeKey, item.id); + }} + > +
+ {renderPurchaseTypeChip(item.purchaseType)} + {item.typeLabel} + {item.policyNo ? ( + {item.policyNo} + ) : null} +
+
{item.summary}
+
+ {item.sourceLabel ? `${item.sourceLabel} · ` : ''} + 点击查看 {item.typeLabel} 明细 → +
+
+
+ ); + })} +
+ ) : ( +
+
暂无全周期记录
+
可通过批量识别、Excel 导入或比价单采购产生记录
+
+ )} +
+ {VEHICLE_INSURANCE_MGMT_TABS.filter((t) => t.key !== 'timeline').map((tab) => ( + + {renderVehicleInsuranceTypeTab(tab.key)} + + ))} +
+
+
+ ); + })() : null} +
+ + setPrdOpen(false)} + > + +
+

1. 面包屑:业务管理 → 保险采购

+

2. 保险状态规则(交车拦截)

+
    +
  • 核验交强险商业险是否已购买且在有效期内
  • +
  • 两项均满足(含临期未到期)→ 车辆管理「保险状态 = 正常/临期」
  • +
  • 任一项缺失或已到期 → 「保险状态 = 异常」,禁止交车
  • +
+

3. 管理险种:交强险、商业险、超赔险、货物险、驾意险

+

4. 列表字段:车牌号(下附品牌型号)、VIN、运营状态、保险状态、各险种到期时间;操作「管理」打开全周期保险记录(时间轴 + 分险种 Tab,支持预览/下载)

+

5. 筛选:车牌号、多车牌、VIN、品牌、型号、运营状态、保险状态;KPI 看板点击可联动筛选列表

+

6. 交互参照:页面布局、KPI 看板、状态图示与证照管理模块保持一致

+

7. 比价单:按月管理批次;选车(可无车牌仅 VIN)→ 报价 → 保存;最晚付费日临期/超期看板提醒;勾选记录提交采购工作流(原型);列表统计已提交采购与流程完结数量

+

8. 保单管理:新增/识别/导入统一维护保单号、批单号、付款时间、生效/到期、保费、保单项目等;批量导入模板与新增字段一致;识别任务可回看

+
+
+ + setCompareMgmtOpen(false)} + > +
+ {renderFilterField('创建时间', ( + setCompareMgmtFilters((prev) => ({ ...prev, createdRange: range }))} + placeholder={['开始日期', '结束日期']} + allowClear + /> + ))} + {renderFilterField('车牌号', ( + setCompareMgmtFilters((prev) => ({ ...prev, plateNo: e.target.value }))} + onPressEnter={handleCompareMgmtQuery} + style={{ borderRadius: 8 }} + /> + ))} +
+
+ + +
+
+
+
+
最晚付费临期
+
距最晚付费日 ≤ {LATEST_PAY_WARN_DAYS} 天
+
+ {compareMgmtPayAlerts.warning} +
+
+
+
最晚付费超期
+
已超过最晚付费日
+
+ {compareMgmtPayAlerts.overdue} +
+
+
+ + 共 {filteredCompareSheets.length} 条比价单 + + +
+
`共 ${t} 条` }} + scroll={{ x: 1040 }} + locale={{ emptyText: '暂无比价单,请点击「新建比价单」' }} + columns={[ + { + title: '创建日期', + dataIndex: 'createdAt', + key: 'createdAt', + width: 168, + render: (val) => {val || '—'}, + }, + { + title: '创建人', + dataIndex: 'createdBy', + key: 'createdBy', + width: 96, + render: (val) => val || '—', + }, + { + title: '采购周期', + dataIndex: 'periodLabel', + key: 'periodLabel', + width: 108, + render: (val) => val || '—', + }, + { + title: '总车辆', + dataIndex: 'totalVehicles', + key: 'totalVehicles', + width: 88, + align: 'center', + render: (val) => {val ?? 0}, + }, + { + title: '保险数量', + dataIndex: 'insuranceCount', + key: 'insuranceCount', + width: 96, + align: 'center', + render: (val) => {val ?? 0}, + }, + { + title: '附件', + key: 'attachments', + width: 72, + align: 'center', + render: (_, record) => { + const n = record.attachments?.length || 0; + return n > 0 ? ( + a.name).join('、')}> + {n} + + ) : ( + + ); + }, + }, + { + title: '已提交采购数量', + dataIndex: 'submittedProcurementCount', + key: 'submittedProcurementCount', + width: 128, + align: 'center', + render: (val) => ( + 0 ? 'processing' : 'default'} style={{ margin: 0, fontWeight: 600 }}> + {val ?? 0} + + ), + }, + { + title: '流程完结数量', + dataIndex: 'completedCount', + key: 'completedCount', + width: 120, + align: 'center', + render: (val) => ( + 0 ? 'success' : 'default'} style={{ margin: 0, fontWeight: 600 }}> + {val ?? 0} + + ), + }, + { + title: '操作', + key: 'action', + width: 120, + fixed: 'right', + render: (_, record) => ( + + + + + ), + }, + ]} + /> + + + { + setCompareModalOpen(false); + setEditingCompareSheetId(null); + }} + footer={( +
+ + 勾选购买记录后可提交采购申请;须已确认报价并填写最晚付费日期 + +
+ + + +
+
+ )} + > +
+
+ 采购周期 + setCompareSheetPeriod(e.target.value)} + style={{ flex: 1, borderRadius: 8 }} + /> +
+ +
+
+ + + {selectedCompareKeys.length ? ( + + ) : null} + + 共 {compareRows.length} 条购买记录 +
+
+
({ + disabled: record.procurementStatus === 'completed', + }), + }} + locale={{ emptyText: '暂无购买记录,请点击「新增一行」' }} + /> + +
+
+ + setCompareRemark(e.target.value)} + placeholder="选填,可填写比价说明、采购要求等" + maxLength={500} + showCount + style={{ borderRadius: 8 }} + /> +
+
+ 附件 + 不限制文件格式与上传数量;保存比价单时一并存储附件信息(原型仅存元数据) + false} + onChange={handleCompareAttachmentChange} + itemRender={(originNode, file) => ( + + {originNode} + + )} + > + + +
+
+ 整单确认金额 + + {compareSheetSummary.total.toFixed(2)} + + + + 全单已确认 {compareSheetSummary.count} 项 + +
+
+ 勾选提交采购金额 + + {selectedProcurementSummary.total.toFixed(2)} + + + + 已勾选 {selectedCompareKeys.length} 条 · 可提交 {selectedProcurementSummary.count} 项确认报价 + +
+
+ + + + + + + )} + onCancel={() => !procurementFlowLoading && setProcurementFlowOpen(false)} + > + + {procurementFlowStep >= 3 ? ( + + ) : ( + + )} + + + setPolicyRecognTasksOpen(false)} + > +
+ {renderFilterField('创建时间', ( + setPolicyRecognTasksFilters((prev) => ({ ...prev, createdRange: range }))} + placeholder={['开始日期', '结束日期']} + allowClear + /> + ))} + {renderFilterField('任务编号', ( + setPolicyRecognTasksFilters((prev) => ({ ...prev, taskId: e.target.value }))} + onPressEnter={handlePolicyRecognTasksQuery} + style={{ borderRadius: 8 }} + /> + ))} + {renderFilterField('业务类型', ( + setPolicyRecognTasksFilters((prev) => ({ ...prev, status: v }))} + style={{ width: '100%' }} + options={[ + { label: '全部', value: '全部' }, + { label: '待确认', value: '待确认' }, + { label: '部分确认', value: '部分确认' }, + { label: '已完成', value: '已完成' }, + ]} + /> + ))} +
+
+ + +
+
+ + 共 {filteredPolicyRecognTasks.length} 条识别任务 + + +
+
`共 ${t} 条` }} + scroll={{ x: 960 }} + locale={{ emptyText: '暂无识别任务,请发起「保单批量识别」或「批量导入」' }} + columns={[ + { + title: '创建时间', + dataIndex: 'createdAt', + width: 168, + render: (val) => {val || '—'}, + }, + { + title: '任务编号', + dataIndex: 'id', + width: 148, + render: (val) => {val}, + }, + { + title: '业务类型', + dataIndex: 'entryLabel', + width: 120, + }, + { + title: '业务模式', + dataIndex: 'modeLabel', + width: 96, + render: (val, record) => ( + + {val || '—'} + {record.insuranceType ? ( + {record.insuranceType} + ) : null} + + ), + }, + { + title: '文件数', + dataIndex: 'fileCount', + width: 72, + align: 'center', + }, + { + title: '确认进度', + key: 'progress', + width: 100, + align: 'center', + render: (_, record) => ( + + {record.confirmedCount}/{record.matchedCount || record.fileCount} + + ), + }, + { + title: '状态', + dataIndex: 'status', + width: 96, + render: (status) => { + const meta = POLICY_RECOGN_STATUS_META[status] || { label: status, color: 'default' }; + return {meta.label}; + }, + }, + { + title: '操作', + key: 'action', + width: 88, + fixed: 'right', + render: (_, record) => ( + + ), + }, + ]} + /> + + + + {policyRecognTaskId ? ( +
+ 任务编号:{policyRecognTaskId} + {policyRecognPhase === 'recognizing' ? (policyRecognEntry === 'import' ? ' · 导入中…' : ' · 识别中…') : null} + {policyRecognPhase === 'recognized' ? ' · 识别完成' : null} + {policyRecognPhase === 'results' ? ' · 核对确认' : null} +
+ ) : null} + + {policyRecognPhase === 'upload' ? ( + <> + {policyRecognEntry === 'ocr' ? ( + <> + +
+
业务类型
+ setPolicyRecognMode(e.target.value)} + style={{ display: 'flex', flexDirection: 'column', gap: 8 }} + > + {POLICY_OCR_MODES.map((m) => ( + + {m.label} + {m.desc} + + ))} + +
+ {policyRecognMode === 'policy' ? ( +
+ {renderFilterField('保险类型', ( +
+
+ {!policyRecognViewOnly ? ( + + ) : null} + {!policyRecognViewOnly ? ( + + ) : null} + +
+ + ) : null} + + + { if (policyPreview?.url) URL.revokeObjectURL(policyPreview.url); setPolicyPreview(null); }}>关闭} + onCancel={() => { if (policyPreview?.url) URL.revokeObjectURL(policyPreview.url); setPolicyPreview(null); }} + > +
+ {policyPreview?.isImage && policyPreview.url ? ( + {policyPreview.fileName} + ) : ( +
+
PDF
+
{policyPreview?.fileName}
+
{policyPreview?.hint}
+
+ )} +
+
+ + setPolicyRecognEditOpen(false)} + onOk={savePolicyRecognResultEdit} + okText="保存" + cancelText="取消" + > + + {renderPolicyDetailForm(policyRecognEditDraft, setPolicyRecognEditDraft)} + + + { + setVehicleInsHistoryEditOpen(false); + setVehicleInsHistoryEditRecord(null); + }} + onOk={saveVehicleInsHistoryEdit} + okText="保存" + cancelText="取消" + > + + {renderPolicyDetailForm(vehicleInsHistoryEditDraft, setVehicleInsHistoryEditDraft)} + + + setPolicyAddOpen(false)} + onOk={handlePolicyAddSubmit} + okText="保存" + cancelText="取消" + > + + {renderPolicyDetailForm(policyAddDraft, setPolicyAddDraft)} + + + ); +}; + +export default Component; diff --git a/web端/业务管理/文档/_build_租赁账单手册.py b/web端/业务管理/文档/_build_租赁账单手册.py new file mode 100644 index 0000000..846fb4f --- /dev/null +++ b/web端/业务管理/文档/_build_租赁账单手册.py @@ -0,0 +1,187 @@ +# -*- coding: utf-8 -*- +"""生成「租赁账单」用户说明书 Word,输出到用户桌面。""" +from pathlib import Path + +from docx import Document +from docx.shared import Pt, Cm +from docx.enum.text import WD_ALIGN_PARAGRAPH +from docx.oxml.ns import qn +from docx.oxml import OxmlElement + + +def set_cell_shading(cell, fill_hex): + shading = OxmlElement("w:shd") + shading.set(qn("w:fill"), fill_hex) + cell._tc.get_or_add_tcPr().append(shading) + + +def add_heading_cn(doc, text, level): + h = doc.add_heading(text, level=level) + for r in h.runs: + r.font.name = "PingFang SC" + r._element.rPr.rFonts.set(qn("w:eastAsia"), "PingFang SC") + return h + + +def add_para_cn(doc, text, bold=False): + p = doc.add_paragraph() + run = p.add_run(text) + run.font.size = Pt(11) + run.font.name = "PingFang SC" + run._element.rPr.rFonts.set(qn("w:eastAsia"), "PingFang SC") + run.bold = bold + return p + + +def add_bullet(doc, text): + p = doc.add_paragraph(style="List Bullet") + r = p.add_run(text) + r.font.size = Pt(11) + r.font.name = "PingFang SC" + r._element.rPr.rFonts.set(qn("w:eastAsia"), "PingFang SC") + + +def build(out_path: Path): + doc = Document() + sect = doc.sections[0] + sect.left_margin = Cm(2.2) + sect.right_margin = Cm(2.2) + + t = doc.add_paragraph() + t.alignment = WD_ALIGN_PARAGRAPH.CENTER + r = t.add_run("数字化资产 ONEOS 运管平台\n租赁账单 · 功能说明(用户操作)") + r.bold = True + r.font.size = Pt(18) + r.font.name = "PingFang SC" + r._element.rPr.rFonts.set(qn("w:eastAsia"), "PingFang SC") + + add_para_cn( + doc, + "本文档依据原型页面「租赁账单.jsx」「租赁账单-查看.jsx」「租赁账单-收费明细.jsx」整理," + "说明从列表进入查看、再进入收费明细的常规操作与字段含义。正式系统以线上版本为准。", + ) + + add_heading_cn(doc, "一、模块概述", 1) + add_para_cn( + doc, + "「租赁账单」用于按租赁合同聚合展示多期账单,支持筛选、展开查看每期账单明细与金额," + "在账单未提交前维护收费明细及列表中的部分成本字段;提交后收费明细不可再改。", + ) + + add_heading_cn(doc, "二、如何进入", 1) + add_bullet(doc, "登录系统后,左侧菜单选择「业务管理」→「租赁账单」。") + add_bullet(doc, "列表页面包屑为:业务管理 / 租赁账单。右上角可点「查看需求说明」打开产品需求全文。") + + add_heading_cn(doc, "三、列表页操作", 1) + + add_heading_cn(doc, "3.1 筛选区", 2) + add_para_cn(doc, "第一行默认展示:合同编码、项目名称、客户名称(均为可搜索下拉,支持输入关键字匹配)。") + add_para_cn(doc, "点击「展开」后增加:业务部门、业务负责人(下拉选择)、交车任务编码(文本框,模糊匹配)。") + add_bullet(doc, "重置:清空所有筛选条件。") + add_bullet(doc, "查询:按条件刷新列表(原型中与筛选联动)。") + + add_heading_cn(doc, "3.2 主表(合同维度)", 2) + add_para_cn( + doc, + "每行代表一份租赁合同。点击行首「+」展开子表,查看该合同下各期账单。字段包括:" + "合同编码、合同类型(正式/试用)、项目名称、客户名称、合同生效日期、交车任务编码、业务部门、业务负责人。", + ) + add_para_cn(doc, "表格底部分页:可切换页码、每页条数,并显示总条数。") + + add_heading_cn(doc, "3.3 子表(账单期维度)", 2) + add_para_cn(doc, "子表列说明(与原型一致):") + tbl = doc.add_table(rows=13, cols=2) + tbl.style = "Table Grid" + hdr = ["字段", "说明"] + for j, h in enumerate(hdr): + tbl.rows[0].cells[j].text = h + set_cell_shading(tbl.rows[0].cells[j], "E6F4EA") + rows = [ + ("账单编号", "合同编码 + ZD + 四位期号。例:HT-ZL-2025-001 第1期 → …ZD0001。用于后期与用友 YS 等对账取票。"), + ("账单期数", "该合同下第几期账单。"), + ("状态", "已提交 / 待提交等。需求中还规划「已结清」(财务到账等于实收后)。"), + ("账单开始/结束日期", "本期计费区间,格式 YYYY-MM-DD。"), + ("提车数量", "显示为「N辆」链接,点击弹出车辆列表:品牌、型号、车牌号。"), + ("应收款总额", "系统计算的应收金额(需求:各车月租金总和 + 各车服务费总和)。"), + ("实收款总额", "需求:月租金+服务费合计减去减免总金额。"), + ("减免总金额", "各减免项合计。"), + ("车辆成本", "按车型日成本 × 账单天数 × 车辆数汇总展示(只读计算)。"), + ("氢费成本", "点击金额可改为输入框,两位小数,失焦保存(列表内快速维护)。"), + ("其他成本", "同上,可点击编辑。"), + ("操作", "见下文。"), + ] + for i, (a, b) in enumerate(rows, start=1): + tbl.rows[i].cells[0].text = a + tbl.rows[i].cells[1].text = b + + add_heading_cn(doc, "3.4 列表行操作:查看 / 收费明细", 2) + add_bullet(doc, "查看:任意状态均可进入「租赁账单 - 查看」页,浏览该期账单的只读明细与汇总。") + add_bullet( + doc, + "收费明细:仅当该期状态为「待提交」时显示;「已提交」后入口隐藏,表示收费项已确认不可再改。", + ) + + add_heading_cn(doc, "四、查看页(只读)", 1) + add_para_cn(doc, "面包屑:业务管理 / 租赁账单 / 查看。") + add_heading_cn(doc, "4.1 账单信息", 2) + add_para_cn( + doc, + "展示合同编码、合同类型、项目名称、客户名称、交车任务编码、账单编码、账单期数、" + "账单开始日期、账单结束日期(与列表规则一致)。", + ) + add_heading_cn(doc, "4.2 账单明细汇总", 2) + add_bullet( + doc, + "应收款总额(可点击):展开包含「应收月租金合计、应收保证金合计、应收服务费合计」," + "首期账单另含「氢费预付款应收」。", + ) + add_bullet( + doc, + "实收款总额(可点击):包含实收租金、保证金、实收服务费、租金减免、服务费减免等;" + "首期另含氢费预收与氢费减免。", + ) + add_bullet(doc, "开票总额:与实收口径一致但不含「应收车辆保证金」,用于开票参考。") + add_heading_cn(doc, "4.3 车辆明细表", 2) + add_para_cn( + doc, + "每车一行:序号、品牌、型号、车牌、应收/实收月租金、租金备注、减免金额及备注、减免证明(可点预览)、" + "应收保证金(首期按合同,非首期为 0)、服务费项目(点「管理」弹出各服务项目应收/实收/减免/备注)、" + "应收服务费与实收服务费合计。", + ) + add_heading_cn(doc, "4.4 氢费预付款(仅首期)", 2) + add_para_cn(doc, "首期账单在表格下方展示氢费预付款应收、实收、减免金额及备注;第二期起整块不显示。") + add_heading_cn(doc, "4.5 返回", 2) + add_para_cn(doc, "点击「返回」回到租赁账单列表。") + + add_heading_cn(doc, "五、收费明细页(编辑提交)", 1) + add_para_cn(doc, "面包屑:业务管理 / 租赁账单 / 收费明细。由列表子表「收费明细」进入。") + add_heading_cn(doc, "5.1 账单信息与汇总", 2) + add_para_cn(doc, "顶部「账单信息」与查看页相同。汇总区同样可点击「应收款总额」「实收款总额」查看分项;「开票金额」规则同查看页。") + add_heading_cn(doc, "5.2 可编辑内容", 2) + add_bullet(doc, "实收车辆月租金:输入框,两位小数,默认与应收一致,可按实收修改。") + add_bullet(doc, "车辆租金备注、减免金额、减免备注:文本或金额输入。") + add_bullet(doc, "减免证明:附件上传(多文件),单行展示文件名,可预览、可删除。") + add_bullet(doc, "服务费项目:点「管理」,在气泡内维护每项「实收费用」(必填 *)、「减免费用」、「备注」;实收服务费自动按实收费用汇总。") + add_bullet(doc, "氢费预付款(仅首期):实收金额、减免金额、减免备注可编辑;应收金额为合同带出只读。") + add_heading_cn(doc, "5.3 底部按钮", 2) + add_bullet(doc, "提交:弹出「请确认账单金额无误」,确认后提交成功并返回(原型逻辑)。") + add_bullet(doc, "保存:保存当前填写,不做完整校验,保存后返回。") + add_bullet(doc, "取消:提示未保存将丢失修改,确认后返回列表。") + + add_heading_cn(doc, "六、操作建议流程(培训口径)", 1) + add_para_cn(doc, "1)在列表用筛选定位合同 → 展开子表找到目标期数 → 看「状态」。") + add_para_cn(doc, "2)先点「查看」核对应收与合同是否一致。") + add_para_cn(doc, "3)若为期初「待提交」,点「收费明细」补全实收、减免、服务费与附件 → 保存或提交。") + add_para_cn(doc, "4)提交后仅能通过「查看」复核;列表中可继续维护氢费成本、其他成本(与收费明细提交状态独立,以实际上线规则为准)。") + + add_heading_cn(doc, "七、文档信息", 1) + add_para_cn(doc, "版本:与原型标注一致(租赁账单 2026-03-10;查看/收费明细 2026-03-11)。生成工具:项目内 web端/业务管理/文档/_build_租赁账单手册.py") + + out_path.parent.mkdir(parents=True, exist_ok=True) + doc.save(out_path) + print("Saved:", out_path) + + +if __name__ == "__main__": + desktop = Path.home() / "Desktop" + build(desktop / "租赁账单功能说明.docx") diff --git a/web端/加氢站管理/站点信息.jsx b/web端/加氢站管理/站点信息.jsx new file mode 100644 index 0000000..1645dcb --- /dev/null +++ b/web端/加氢站管理/站点信息.jsx @@ -0,0 +1,3353 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 加氢站管理 - 站点信息(列表 + KPI 分类 + 独立新建页 / 编辑查看抽屉) + +var H2_REGION_CASCADER_OPTIONS = [ + { value: '浙江省', label: '浙江省', children: [{ value: '嘉兴市', label: '嘉兴市' }, { value: '杭州市', label: '杭州市' }, { value: '宁波市', label: '宁波市' }] }, + { value: '上海市', label: '上海市', children: [{ value: '上海市', label: '上海市' }] }, + { value: '江苏省', label: '江苏省', children: [{ value: '南京市', label: '南京市' }, { value: '苏州市', label: '苏州市' }] }, + { value: '广东省', label: '广东省', children: [{ value: '广州市', label: '广州市' }, { value: '深圳市', label: '深圳市' }] } +]; + +var H2_STATION_KPI_CARDS = [ + { key: 'all', type: 'total', title: '全部加氢站', desc: '纳入台账管理的全部站点' }, + { key: 'high', type: 'normal', title: '高频加氢站点', desc: '加氢次数 ≥ 3 次的站点' }, + { key: 'low', type: 'warning', title: '低频加氢站点', desc: '加氢次数 1~2 次的站点' }, + { key: 'none', type: 'unuploaded', title: '无加氢站点', desc: '暂无加氢记录的站点' } +]; + +var H2_SIGNED_FILTER_CARDS = [ + { key: 'yes', type: 'normal', title: '已签约站点', desc: '已签约的加氢站点,点击快捷筛选' }, + { key: 'no', type: 'unuploaded', title: '未签约站点', desc: '尚未签约的加氢站点,点击快捷筛选' } +]; + +var H2_BUSINESS_STATUS_OPTIONS = [ + { value: '营业中', label: '营业中' }, + { value: '暂停营业', label: '暂停营业' }, + { value: '停止营业', label: '停止营业' } +]; + +var H2_PAGE_STYLE = [ + '.h2-station-page { padding: 24px 24px 80px; height: 100vh; display: flex; flex-direction: column; background: linear-gradient(165deg, #f1f5f9 0%, #f8fafc 50%, #f1f5f9 100%); overflow: hidden; box-sizing: border-box; }', + '.h2-station-page .lc-filter-card.ant-card { border-radius: 16px !important; border: 1px solid #e2e8f0 !important; box-shadow: 0 4px 20px -4px rgba(15, 23, 42, 0.03) !important; margin-bottom: 16px; }', + '.h2-station-page .lc-filter-card > .ant-card-head { border-bottom: 1px solid #f1f5f9 !important; min-height: auto; padding: 12px 20px !important; }', + '.h2-station-page .lc-filter-card > .ant-card-head .ant-card-head-title { font-size: 15px !important; font-weight: 700 !important; color: #0f172a !important; padding: 0 !important; }', + '.h2-station-page .lc-filter-card > .ant-card-body { padding: 16px 20px 20px !important; }', + '.h2-station-page .lc-filter-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px 24px; }', + '@media (max-width: 1100px) { .h2-station-page .lc-filter-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }', + '@media (max-width: 720px) { .h2-station-page .lc-filter-grid { grid-template-columns: 1fr; } }', + '.h2-station-page .lc-filter-field { display: flex; align-items: center; gap: 12px; min-width: 0; }', + '.h2-station-page .lc-filter-field-label { flex: 0 0 100px; text-align: right; font-size: 13px; font-weight: 500; color: #475569; line-height: 1.4; white-space: nowrap; }', + '.h2-station-page .lc-filter-field-control { flex: 1; min-width: 0; }', + '.h2-station-page .lc-filter-field-control .ant-input, .h2-station-page .lc-filter-field-control .ant-select { width: 100%; }', + '.h2-station-page .lc-filter-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; padding-top: 16px; border-top: 1px solid #f1f5f9; }', + '.h2-station-page .lc-alert-stats-row { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 12px; margin-bottom: 16px; }', + '@media (max-width: 1400px) { .h2-station-page .lc-alert-stats-row { grid-template-columns: repeat(3, minmax(0, 1fr)); } }', + '@media (max-width: 900px) { .h2-station-page .lc-alert-stats-row { grid-template-columns: repeat(2, minmax(0, 1fr)); } }', + '@media (max-width: 640px) { .h2-station-page .lc-alert-stats-row { grid-template-columns: 1fr; } }', + '.h2-station-page .lc-alert-card { display: flex; align-items: flex-start; gap: 12px; padding: 14px 30px 14px 16px; border-radius: 12px; border: 1px solid #e2e8f0; background: #fff; position: relative; overflow: hidden; min-width: 0; min-height: 44px; }', + '.h2-station-page .lc-alert-card-main { flex: 1; min-width: 0; }', + '.h2-station-page .lc-alert-card-icon { flex-shrink: 0; width: 40px; height: 40px; border-radius: 10px; display: flex; align-items: center; justify-content: center; }', + '.h2-station-page .lc-alert-card-val { font-size: 26px; font-weight: 800; line-height: 1.1; color: #0f172a; font-variant-numeric: tabular-nums; }', + '.h2-station-page .lc-alert-card-title { font-size: 13px; font-weight: 600; color: #334155; margin-top: 2px; }', + '.h2-station-page .lc-alert-card-tip-anchor { position: absolute; top: 8px; right: 8px; z-index: 2; line-height: 0; }', + '.h2-station-page .lc-alert-card-tip { width: 18px; height: 18px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; color: #94a3b8; background: rgba(255,255,255,0.92); border: 1px solid #e2e8f0; cursor: help; line-height: 0; }', + '.h2-station-page .lc-alert-card-tip:hover { color: #64748b; border-color: #cbd5e1; background: #fff; }', + '.h2-station-page .lc-alert-card--total { background: linear-gradient(135deg, #f8fafc 0%, #fff 100%); }', + '.h2-station-page .lc-alert-card--total .lc-alert-card-icon { background: #e2e8f0; color: #475569; }', + '.h2-station-page .lc-alert-card--normal { background: linear-gradient(135deg, #ecfdf5 0%, #fff 55%); border-color: #bbf7d0; }', + '.h2-station-page .lc-alert-card--normal .lc-alert-card-icon { background: #d1fae5; color: #059669; }', + '.h2-station-page .lc-alert-card--normal .lc-alert-card-val { color: #047857; }', + '.h2-station-page .lc-alert-card--warning { background: linear-gradient(135deg, #fff7ed 0%, #fff 55%); border-color: #fed7aa; }', + '.h2-station-page .lc-alert-card--warning .lc-alert-card-icon { background: #ffedd5; color: #ea580c; }', + '.h2-station-page .lc-alert-card--warning .lc-alert-card-val { color: #c2410c; }', + '.h2-station-page .lc-alert-card--unuploaded { background: linear-gradient(135deg, #f8fafc 0%, #fff 55%); }', + '.h2-station-page .lc-alert-card--unuploaded .lc-alert-card-icon { background: #f1f5f9; color: #64748b; }', + '.h2-station-page .lc-alert-card--unuploaded .lc-alert-card-val { color: #64748b; }', + '.h2-station-page .lc-alert-card-clickable { cursor: pointer; transition: box-shadow 0.2s ease, border-color 0.2s ease, transform 0.2s ease; }', + '.h2-station-page .lc-alert-card-clickable:hover { box-shadow: 0 4px 14px rgba(15, 23, 42, 0.08); }', + '.h2-station-page .lc-alert-card-clickable:focus-visible { outline: 2px solid #10b981; outline-offset: 2px; }', + '.h2-station-page .lc-alert-card-active { box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.25) !important; border-color: #10b981 !important; }', + '.h2-station-page .lc-table-section { margin-bottom: 0; flex: 1; display: flex; flex-direction: column; min-height: 0; }', + '.h2-station-page .lc-table-toolbar { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px 16px; margin-bottom: 8px; min-height: 32px; }', + '.h2-station-page .lc-table-legend-outer { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; padding: 6px 4px; font-size: 12px; color: #64748b; }', + '.h2-station-page .lc-table-legend-label { font-weight: 600; color: #64748b; }', + '.h2-station-page .lc-table-legend-item { display: inline-flex; align-items: center; gap: 6px; }', + '.h2-station-page .lc-table-legend-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }', + '.h2-station-page .lc-table-toolbar-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-left: auto; }', + '.h2-station-page .lc-table-card { background: #fff; border-radius: 16px; border: 1px solid #e2e8f0; box-shadow: 0 4px 20px -4px rgba(15, 23, 42, 0.03); overflow: hidden; flex: 1; overflow-y: auto; display: flex; flex-direction: column; }', + '.h2-station-page .lc-list-table .ant-table-wrapper, .h2-station-page .lc-list-table .ant-table { width: 100% !important; }', + '.h2-station-page .lc-table-card .ant-table-thead > tr > th { background: #f8fafc !important; color: #475569 !important; font-weight: 700 !important; font-size: 13px !important; border-bottom: 1px solid #e2e8f0 !important; padding: 12px 16px !important; }', + '.h2-station-page .lc-table-card .ant-table-tbody > tr.ant-table-measure-row, .h2-station-page .lc-table-card .ant-table-tbody > tr.ant-table-measure-row > td { height: 0 !important; max-height: 0 !important; padding: 0 !important; margin: 0 !important; border: none !important; line-height: 0 !important; font-size: 0 !important; overflow: hidden !important; visibility: hidden !important; }', + '.h2-station-page .lc-table-card .ant-table-tbody > tr:not(.ant-table-measure-row) > td { padding: 12px 16px !important; font-size: 13px; }', + '.h2-station-page .lc-table-card .ant-table-tbody > tr:not(.ant-table-measure-row):hover > td { background: #f8fafc !important; }', + '.h2-station-page .lc-table-card .ant-pagination { margin: 0 !important; padding: 12px 16px !important; border-top: 1px solid #f1f5f9; }', + '.h2-station-page .lc-action-btn { font-weight: 600 !important; color: #10b981 !important; padding: 0 !important; min-height: 44px; }', + '.h2-station-page .lc-action-btn-danger { color: #ef4444 !important; }', + '.h2-station-page .h2-action-more-btn { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 8px; color: #64748b; cursor: pointer; transition: background 0.15s ease, color 0.15s ease; }', + '.h2-station-page .h2-action-more-btn:hover { background: #f1f5f9; color: #334155; }', + '.h2-station-page .h2-action-more-btn:focus-visible { outline: 2px solid #10b981; outline-offset: 2px; }', + '.h2-station-page .h2-row-actions { display: inline-flex; align-items: center; gap: 4px; }', + '.h2-station-page .lc-station-name { font-weight: 700; color: #0f172a; font-size: 13px; }', + '.h2-station-page .lc-station-name-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-width: 0; }', + '.h2-station-page .lc-station-name-row .lc-station-name { min-width: 0; }', + '.h2-station-page .lc-station-signed-tag { margin: 0 !important; border-radius: 6px !important; font-weight: 600 !important; flex-shrink: 0; line-height: 20px !important; }', + '.h2-station-page .lc-refuel-kg-row { display: flex; align-items: center; justify-content: flex-end; gap: 8px; flex-wrap: wrap; min-width: 0; }', + '.h2-station-page .lc-refuel-freq-tag { margin: 0 !important; border-radius: 6px !important; font-weight: 600 !important; flex-shrink: 0; line-height: 20px !important; }', + '.h2-station-page .lc-station-region { color: #64748b; font-size: 12px; margin-bottom: 2px; }', + '.h2-station-page .lc-station-contact-name { font-weight: 600; color: #0f172a; font-size: 13px; line-height: 1.35; }', + '.h2-station-page .lc-station-contact-phone { color: #64748b; font-size: 12px; margin-top: 2px; line-height: 1.35; font-variant-numeric: tabular-nums; }', + '.h2-station-page .h2-drawer-section-title { font-size: 14px; font-weight: 700; color: #0f172a; margin: 4px 0 12px; display: flex; align-items: center; gap: 8px; }', + '.h2-station-page .h2-drawer-section-title::before { content: ""; width: 3px; height: 14px; border-radius: 2px; background: linear-gradient(180deg, #10b981, #34d399); flex-shrink: 0; }', + '.h2-station-page .h2-region-address { display: flex; flex-direction: column; gap: 12px; width: 100%; }', + '.h2-station-page .h2-region-address .ant-select, .h2-station-page .h2-region-address .ant-input { width: 100%; }', + '.h2-station-page .h2-region-address--inline { flex-direction: row; align-items: center; gap: 8px; }', + '.h2-station-page .h2-region-address--inline .h2-region-address-cascader { flex: 0 0 38%; max-width: 220px; min-width: 140px; }', + '.h2-station-page .h2-region-address--inline .h2-region-address-detail { flex: 1; min-width: 0; }', + '.h2-station-page .h2-create-radio-group { display: flex; flex-wrap: wrap; align-items: center; gap: 16px; min-height: 32px; }', + '.h2-station-page .h2-create-radio-group .ant-radio-wrapper { margin-inline-end: 0; font-size: 14px; color: #1d2129; }', + '.h2-station-page .h2-contract-dates { display: flex; align-items: center; gap: 8px; width: 100%; flex-wrap: wrap; }', + '.h2-station-page .h2-contract-dates-sep { color: #94a3b8; font-size: 13px; flex-shrink: 0; }', + '.h2-station-page .h2-contract-upload { display: flex; flex-direction: column; gap: 10px; width: 100%; }', + '.h2-station-page .h2-contract-upload-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }', + '.h2-station-page .h2-contract-file-list { display: flex; flex-direction: column; gap: 6px; }', + '.h2-station-page .h2-contract-file-item { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 6px 10px; border-radius: 6px; background: #f8fafc; border: 1px solid #e2e8f0; font-size: 13px; }', + '.h2-station-page .h2-contract-file-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #334155; }', + '.h2-station-page .h2-contract-file-btns { display: flex; align-items: center; gap: 4px; flex-shrink: 0; }', + '.h2-station-page--create .h2-create-form .h2-create-upload-btn.ant-upload-wrapper { width: auto; }', + '.h2-station-page--create .h2-create-form .h2-region-address { gap: 12px; margin-top: 0; }', + '.h2-station-page .h2-business-hours { display: flex; flex-direction: column; gap: 12px; width: 100%; }', + '.h2-station-page .h2-business-hours-range { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; width: 100%; }', + '.h2-station-page .h2-business-hours-range-item { display: flex; flex-direction: column; gap: 6px; min-width: 0; }', + '.h2-station-page .h2-business-hours-range-label { font-size: 12px; color: #64748b; line-height: 1.2; }', + '.h2-station-page--create .h2-create-form .h2-business-hours .ant-picker { width: 100% !important; height: 32px !important; border-radius: 2px !important; border: 1px solid #e5e6eb !important; background: #fff !important; }', + '.h2-station-page--create .h2-create-form .h2-business-hours .ant-picker .ant-picker-input > input { font-size: 14px !important; color: #1d2129 !important; }', + '.h2-station-page--create .h2-create-form .h2-business-hours .ant-picker:not(.ant-picker-disabled):hover { border-color: #c9cdd4 !important; background: #fff !important; }', + '.h2-station-page--create .h2-create-form .h2-business-hours .ant-picker-focused { border-color: #10b981 !important; box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2) !important; background: #fff !important; }', + '.h2-station-page--create .h2-create-form .h2-business-hours--col4 { gap: 8px; max-width: 100%; }', + '.h2-station-page--create .h2-create-form .h2-business-hours--col4 .ant-select { width: 100% !important; max-width: 100%; }', + '.h2-station-page--create .h2-create-form .h2-business-hours--col4 .h2-business-hours-range { grid-template-columns: 1fr; gap: 8px; }', + '.h2-station-page--create .h2-create-form .ant-col-6 .ant-form-item-control-input { max-width: 100%; }', + '.h2-station-page .h2-prd-modal .ant-modal-body { max-height: 70vh; overflow: auto; }', + '.h2-station-page .h2-prd-content { padding: 8px 0; white-space: pre-wrap; font-size: 13px; line-height: 1.65; color: #475569; }', + '.h2-station-page .h2-import-template-bar { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; padding: 14px 16px; margin-bottom: 14px; border-radius: 12px; background: linear-gradient(135deg, #ecfdf5 0%, #f8fafc 100%); border: 1px solid #bbf7d0; }', + '.h2-station-page .h2-import-template-bar-text { font-size: 13px; color: #475569; line-height: 1.55; flex: 1; min-width: 0; }', + '.h2-station-page .h2-import-preview { margin-top: 14px; padding: 12px 14px; border-radius: 10px; background: #f8fafc; border: 1px solid #e2e8f0; font-size: 13px; color: #334155; }', + '.h2-station-page .h2-import-error-list { margin: 8px 0 0; padding-left: 18px; max-height: 120px; overflow: auto; font-size: 12px; color: #b91c1c; }', + '.h2-station-page .h2-status-log-section { margin-top: 20px; padding-top: 16px; border-top: 1px solid #f1f5f9; }', + '.h2-station-page .h2-status-log-title { font-size: 14px; font-weight: 700; color: #0f172a; margin-bottom: 10px; }', + '.h2-station-page .h2-status-log-table .ant-table-thead > tr > th { background: #f8fafc !important; font-size: 12px !important; padding: 8px 12px !important; }', + '.h2-station-page .h2-status-log-table .ant-table-tbody > tr > td { font-size: 12px !important; padding: 8px 12px !important; }', + '.h2-station-page .h2-refuel-drill-link { border: none; background: none; padding: 0; font-weight: 700; color: #059669; cursor: pointer; font-variant-numeric: tabular-nums; font-size: 13px; }', + '.h2-station-page .h2-refuel-drill-link:hover { color: #047857; text-decoration: underline; }', + '.h2-station-page .h2-refuel-drill-link:focus-visible { outline: 2px solid #10b981; outline-offset: 2px; border-radius: 4px; }', + '.h2-station-page .h2-ledger-totals-bar { display: flex; align-items: stretch; gap: 0; margin-bottom: 12px; border: 1px solid #bae6fd; border-radius: 10px; overflow: hidden; background: #f8fafc; box-shadow: 0 1px 0 rgba(15, 23, 42, 0.04); }', + '.h2-station-page .h2-ledger-totals-bar__title { display: flex; align-items: center; justify-content: center; min-width: 72px; padding: 10px 14px; font-size: 14px; font-weight: 700; color: #0f172a; background: #e8f4fc; border-right: 1px solid #bae6fd; flex-shrink: 0; }', + '.h2-station-page .h2-ledger-totals-bar__items { display: flex; flex: 1; flex-wrap: wrap; }', + '.h2-station-page .h2-ledger-totals-bar__item { flex: 1; min-width: 140px; padding: 8px 20px; border-right: 1px solid #e2e8f0; display: flex; flex-direction: column; justify-content: center; gap: 4px; }', + '.h2-station-page .h2-ledger-totals-bar__item:last-child { border-right: none; }', + '.h2-station-page .h2-ledger-totals-bar__label { font-size: 12px; color: rgba(15, 23, 42, 0.55); font-weight: 500; line-height: 1.2; }', + '.h2-station-page .h2-ledger-totals-bar__value { font-size: 16px; font-weight: 700; color: #0f172a; font-variant-numeric: tabular-nums; line-height: 1.3; }', + '.h2-station-page .h2-refuel-record-table .ant-table-thead > tr > th { background: #f8fafc !important; font-size: 12px !important; padding: 8px 12px !important; }', + '.h2-station-page .h2-refuel-record-table .ant-table-tbody > tr > td { font-size: 12px !important; padding: 8px 12px !important; }', + '.h2-station-page .h2-prepaid-balance-cell { display: flex; align-items: center; justify-content: flex-end; gap: 6px; flex-wrap: nowrap; box-sizing: border-box; }', + '.h2-station-page .h2-prepaid-balance-cell .h2-prepaid-balance-amount { flex: 0 0 auto; white-space: nowrap; border: none; background: none; padding: 0; font: inherit; font-weight: 700; cursor: pointer; text-decoration: underline; text-underline-offset: 2px; font-variant-numeric: tabular-nums; }', + '.h2-station-page .h2-prepaid-balance-cell .h2-prepaid-balance-amount:focus-visible { outline: 2px solid #10b981; outline-offset: 2px; border-radius: 4px; }', + '.h2-station-page .h2-prepaid-balance-cell .lc-station-signed-tag { flex-shrink: 0; }', + '.h2-station-page .lc-table-card .ant-table-tbody > tr:not(.ant-table-measure-row) > td.h2-cell-prepaid-balance { overflow: visible !important; }', + '.h2-station-page .h2-balance-record-table .ant-table-thead > tr > th { background: #f8fafc !important; font-size: 12px !important; padding: 8px 12px !important; }', + '.h2-station-page .h2-balance-record-table .ant-table-tbody > tr > td { font-size: 12px !important; padding: 8px 12px !important; }', + '.h2-station-page.h2-station-page--create { padding: 0 0 96px; height: auto; min-height: 100dvh; overflow: auto; }', + '.h2-station-page--create .h2-create-shell { width: 100%; max-width: none; margin: 0; padding: 16px 20px 0; box-sizing: border-box; }', + '.h2-station-page--create .h2-create-topbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 16px; padding: 12px 16px; background: #fff; border-radius: 4px; border: 1px solid #e2e8f0; }', + '.h2-station-page--create .h2-create-back-btn { display: inline-flex !important; align-items: center; gap: 6px; height: 32px; padding: 0 12px !important; border-radius: 4px !important; font-weight: 500; color: #475569 !important; border: 1px solid #e2e8f0 !important; background: #fff !important; }', + '.h2-station-page--create .h2-create-back-btn:hover { color: #059669 !important; border-color: #10b981 !important; background: #f0fdf4 !important; }', + '.h2-station-page--create .h2-create-back-btn:focus-visible { outline: 2px solid #10b981; outline-offset: 2px; }', + '.h2-station-page--create .h2-create-topbar-actions { display: flex; align-items: center; gap: 10px; }', + '.h2-station-page .h2-create-card.ant-card { border-radius: 16px !important; border: 1px solid #e2e8f0 !important; box-shadow: 0 4px 20px -4px rgba(15, 23, 42, 0.05) !important; margin-bottom: 16px; transition: box-shadow 0.2s ease, border-color 0.2s ease; }', + '.h2-station-page .h2-create-card:hover { box-shadow: 0 8px 28px -6px rgba(15, 23, 42, 0.08) !important; }', + '.h2-station-page .h2-create-card > .ant-card-head { border-bottom: 1px solid #f1f5f9 !important; min-height: auto; padding: 16px 22px !important; background: linear-gradient(180deg, #fafbfc 0%, #fff 100%); }', + '.h2-station-page .h2-create-card > .ant-card-head .ant-card-head-title { font-size: 15px !important; font-weight: 700 !important; color: #0f172a !important; padding: 0 !important; }', + '.h2-station-page .h2-create-card > .ant-card-body { padding: 8px 22px 20px !important; }', + '.h2-station-page .h2-card-title-bar { display: inline-flex; align-items: center; gap: 10px; }', + '.h2-station-page .h2-card-title-bar::before { content: ""; width: 3px; height: 16px; border-radius: 2px; background: linear-gradient(180deg, #10b981, #34d399); flex-shrink: 0; }', + '.h2-station-page .h2-card-title-icon { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 10px; background: #ecfdf5; color: #059669; flex-shrink: 0; }', + '.h2-station-page .h2-form-section { margin-bottom: 4px; }', + '.h2-station-page .h2-form-section + .h2-form-section { margin-top: 8px; padding-top: 8px; border-top: 1px solid #f1f5f9; }', + '.h2-station-page--create .h2-create-form .ant-row { width: 100%; }', + '.h2-station-page--create .h2-create-form .ant-col-6 { min-width: 0; }', + '.h2-station-page--create .h2-create-form .ant-form-item { margin-bottom: 24px; }', + '.h2-station-page--create .h2-create-form .ant-form-item-label { padding: 0 0 8px; }', + '.h2-station-page--create .h2-create-form .ant-form-item-label > label { font-size: 14px; font-weight: 400; color: #1d2129; height: auto; }', + '.h2-station-page--create .h2-create-form .ant-form-item-label > label::after { display: none !important; }', + '.h2-station-page--create .h2-create-input.ant-input:not(.ant-input-disabled):not(:disabled), .h2-station-page--create .h2-create-form .ant-select:not(.ant-select-disabled) .ant-select-selector, .h2-station-page--create .h2-create-form .ant-cascader:not(.ant-select-disabled) .ant-select-selector { height: 32px !important; min-height: 32px !important; border-radius: 2px !important; font-size: 14px !important; border: 1px solid #e5e6eb !important; background: #fff !important; color: #1d2129 !important; }', + '.h2-station-page--create .h2-create-form .ant-input { padding: 4px 12px !important; }', + '.h2-station-page--create .h2-create-form .ant-input-textarea:not(.ant-input-disabled):not(:disabled) { height: auto !important; min-height: auto !important; border-radius: 2px !important; background: #fff !important; color: #1d2129 !important; border: 1px solid #e5e6eb !important; }', + '.h2-station-page--create .h2-create-form .ant-input:not(:disabled):not(.ant-input-disabled):hover, .h2-station-page--create .h2-create-form .ant-select:not(.ant-select-disabled):hover .ant-select-selector, .h2-station-page--create .h2-create-form .ant-cascader:not(.ant-select-disabled):hover .ant-select-selector { background: #fff !important; border-color: #c9cdd4 !important; }', + '.h2-station-page--create .h2-create-form .ant-input:focus, .h2-station-page--create .h2-create-form .ant-input-focused, .h2-station-page--create .h2-create-form .ant-select-focused .ant-select-selector, .h2-station-page--create .h2-create-form .ant-cascader-focused .ant-select-selector { background: #fff !important; border-color: #10b981 !important; box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2) !important; }', + '.h2-station-page--create .h2-create-form .ant-input-disabled, .h2-station-page--create .h2-create-form .ant-input:disabled, .h2-station-page--create .h2-create-form .ant-select-disabled .ant-select-selector, .h2-station-page--create .h2-create-form .ant-cascader.ant-select-disabled .ant-select-selector { background: #f2f3f5 !important; color: rgba(0, 0, 0, 0.25) !important; cursor: not-allowed !important; }', + '.h2-station-page--create .h2-create-form .ant-select-selection-item { line-height: 30px !important; }', + '.h2-station-page--create .h2-signed-toggle { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px; padding: 14px 16px; border-radius: 12px; background: #f8fafc; border: 1px solid #e2e8f0; margin-bottom: 4px; }', + '.h2-station-page--create .h2-signed-toggle-label { font-size: 13px; font-weight: 600; color: #334155; }', + '.h2-station-page--create .h2-signed-panel { display: grid; grid-template-rows: 0fr; opacity: 0; transition: grid-template-rows 0.28s ease-out, opacity 0.22s ease-out; }', + '.h2-station-page--create .h2-signed-panel--open { grid-template-rows: 1fr; opacity: 1; }', + '.h2-station-page--create .h2-signed-panel-inner { overflow: hidden; min-height: 0; }', + '.h2-station-page--create .h2-signed-panel-content { padding-top: 12px; }', + '.h2-station-page--create .h2-create-upload.ant-upload-wrapper .ant-upload-drag { border-radius: 12px !important; border: 1px dashed #cbd5e1 !important; background: #fafbfc !important; padding: 16px 12px !important; transition: border-color 0.2s ease, background 0.2s ease; }', + '.h2-station-page--create .h2-create-upload.ant-upload-wrapper .ant-upload-drag:hover { border-color: #10b981 !important; background: #f0fdf4 !important; }', + '.h2-station-page--create .h2-create-upload-hint { font-size: 12px; color: #64748b; margin-top: 4px; }', + '.h2-station-page .h2-supplier-mode-bar { margin-bottom: 0; }', + '.h2-station-page .h2-supplier-mode-card { position: relative; display: flex; align-items: flex-start; gap: 12px; width: 100%; padding: 14px 16px; min-height: 72px; border-radius: 12px; border: 2px solid #e2e8f0; background: #fff; cursor: pointer; text-align: left; transition: border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease, transform 0.15s ease; }', + '.h2-station-page .h2-supplier-mode-card:hover { border-color: #86efac; background: #fafffe; }', + '.h2-station-page .h2-supplier-mode-card:active { transform: scale(0.99); }', + '.h2-station-page .h2-supplier-mode-card:focus-visible { outline: 2px solid #10b981; outline-offset: 2px; }', + '.h2-station-page .h2-supplier-mode-card--active { border-color: #10b981; background: linear-gradient(135deg, #ecfdf5 0%, #fff 70%); box-shadow: 0 4px 16px -4px rgba(16, 185, 129, 0.25); }', + '.h2-station-page .h2-supplier-mode-card-icon { flex-shrink: 0; width: 40px; height: 40px; border-radius: 10px; display: flex; align-items: center; justify-content: center; background: #f1f5f9; color: #64748b; transition: background 0.2s ease, color 0.2s ease; }', + '.h2-station-page .h2-supplier-mode-card--active .h2-supplier-mode-card-icon { background: #d1fae5; color: #047857; }', + '.h2-station-page .h2-supplier-mode-card-title { font-size: 14px; font-weight: 700; color: #0f172a; line-height: 1.3; }', + '.h2-station-page .h2-supplier-mode-card-desc { font-size: 12px; color: #64748b; margin-top: 4px; line-height: 1.45; }', + '.h2-station-page .h2-supplier-mode-meta { display: flex; align-items: center; flex-wrap: wrap; gap: 10px 14px; margin-bottom: 16px; padding: 10px 14px; border-radius: 10px; background: #f8fafc; border: 1px solid #e2e8f0; }', + '.h2-station-page .h2-supplier-type-tag { margin: 0 !important; border-radius: 6px !important; font-weight: 600 !important; }', + '.h2-station-page .h2-supplier-link-select { margin-bottom: 16px; padding: 14px 16px; border-radius: 12px; background: linear-gradient(135deg, #eff6ff 0%, #f8fafc 100%); border: 1px solid #bfdbfe; }', + '.h2-station-page .h2-create-footer { position: fixed; bottom: 0; left: 0; right: 0; z-index: 100; background: rgba(255, 255, 255, 0.92); backdrop-filter: blur(10px); border-top: 1px solid #e2e8f0; box-shadow: 0 -8px 24px rgba(15, 23, 42, 0.06); }', + '.h2-station-page .h2-create-footer-inner { width: 100%; max-width: none; margin: 0; padding: 14px 24px; display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; box-sizing: border-box; }', + '.h2-station-page .h2-create-footer-hint { font-size: 13px; color: #64748b; display: flex; align-items: center; gap: 10px; min-width: 0; }', + '.h2-station-page .h2-create-footer-progress { flex: 1; min-width: 120px; max-width: 200px; }', + '.h2-station-page .h2-create-footer-progress .ant-progress-text { font-size: 12px !important; font-weight: 700; color: #059669 !important; }', + '.h2-station-page .h2-create-footer-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-left: auto; }', + '.h2-station-page .h2-create-footer-actions .ant-btn { min-height: 44px; padding: 0 20px; border-radius: 10px; font-weight: 600; }', + '.h2-station-page .h2-create-footer-actions .ant-btn-primary { min-width: 120px; box-shadow: 0 4px 14px -2px rgba(16, 185, 129, 0.45); }', + '.h2-station-page .h2-create-footer-actions .ant-btn-primary:not(:disabled):hover { box-shadow: 0 6px 18px -2px rgba(16, 185, 129, 0.5); transform: translateY(-1px); }', + '.h2-station-page .h2-field-readonly .ant-input, .h2-station-page .h2-field-readonly .ant-select-selector, .h2-station-page .h2-field-readonly .ant-input-textarea { background: #f1f5f9 !important; color: #64748b !important; cursor: default !important; border-style: dashed !important; }', + '@media (prefers-reduced-motion: no-preference) { .h2-station-page--create .h2-create-card { animation: h2CreateCardIn 0.35s ease-out backwards; } .h2-station-page--create .h2-create-card:nth-of-type(2) { animation-delay: 0.08s; } }', + '@keyframes h2CreateCardIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }', + '@media (prefers-reduced-motion: reduce) { .h2-station-page--create .h2-create-card { animation: none; } .h2-station-page--create .h2-signed-panel { transition: none; } }' +].join('\n'); + +var H2_CURRENT_OPERATOR = '系统管理员'; + +function h2OperateTimestamp() { + return new Date().toISOString().slice(0, 16).replace('T', ' '); +} + +function h2CreateBusinessStatusLog(beforeStatus, afterStatus, operator) { + return { + id: 'bsl-' + Date.now() + '-' + Math.floor(Math.random() * 1000), + operateTime: h2OperateTimestamp(), + operator: operator || H2_CURRENT_OPERATOR, + beforeStatus: beforeStatus || '—', + afterStatus: afterStatus || '—' + }; +} + +var H2_IMPORT_TEMPLATE_HEADERS = [ + '加氢站名称', '省', '市', '详细地址', '是否签约', '签约开始时间', '签约结束时间', '营业状态', '营业时间', '联系人', '联系电话' +]; + +var H2_IMPORT_TEMPLATE_SAMPLE = [ + ['苏州新区加氢站(导入示例)', '江苏省', '苏州市', '工业园区星湖街100号', '是', '2025-01-01', '2027-12-31', '营业中', '08:00-20:00', '周八', '13900001111'] +]; + +var H2_IMPORT_HEADER_MAP = { + '加氢站名称': 'name', + '站点名称': 'name', + '名称': 'name', + '省': 'province', + '省份': 'province', + '市': 'city', + '城市': 'city', + '详细地址': 'detail', + '地址详情': 'detail', + '是否签约': 'isSigned', + '签约状态': 'isSigned', + '签约开始时间': 'contractStart', + '签约开始': 'contractStart', + '签约结束时间': 'contractEnd', + '签约结束': 'contractEnd', + '营业状态': 'businessStatus', + '营业时间': 'businessHours', + '联系人': 'contact', + '联系电话': 'phone', + '电话': 'phone' +}; + +function h2CsvEscape(v) { + var s = String(v == null ? '' : v); + if (/[",\n\r]/.test(s)) return '"' + s.replace(/"/g, '""') + '"'; + return s; +} + +function h2DownloadCsv(filename, headers, rows) { + var lines = [headers.map(h2CsvEscape).join(',')].concat( + (rows || []).map(function (r) { return r.map(h2CsvEscape).join(','); }) + ); + var blob = new Blob(['\uFEFF' + lines.join('\n')], { type: 'text/csv;charset=utf-8;' }); + var a = document.createElement('a'); + a.href = URL.createObjectURL(blob); + a.download = filename; + a.click(); + URL.revokeObjectURL(a.href); +} + +function h2ParseCsvLine(line) { + var out = []; + var cur = ''; + var inQuote = false; + var i; + for (i = 0; i < line.length; i++) { + var ch = line.charAt(i); + if (inQuote) { + if (ch === '"') { + if (line.charAt(i + 1) === '"') { cur += '"'; i++; } + else inQuote = false; + } else cur += ch; + } else if (ch === '"') inQuote = true; + else if (ch === ',') { out.push(cur.trim()); cur = ''; } + else cur += ch; + } + out.push(cur.trim()); + return out; +} + +function h2ParseSignedLabel(val) { + var s = String(val || '').trim(); + if (!s) return false; + var lower = s.toLowerCase(); + if (s === '是' || s === '已签约' || lower === 'yes' || lower === 'y' || lower === 'true' || lower === '1') return true; + if (s === '否' || s === '未签约' || lower === 'no' || lower === 'n' || lower === 'false' || lower === '0') return false; + return false; +} + +function h2NormalizeBusinessStatus(val) { + var s = String(val || '').trim(); + var i; + for (i = 0; i < H2_BUSINESS_STATUS_OPTIONS.length; i++) { + if (H2_BUSINESS_STATUS_OPTIONS[i].value === s) return s; + } + if (s.indexOf('暂停') >= 0) return '暂停营业'; + if (s.indexOf('停止') >= 0 || s.indexOf('停业') >= 0) return '停止营业'; + if (s.indexOf('营业') >= 0) return '营业中'; + return ''; +} + +function h2ParseImportCsv(text) { + var raw = String(text || '').replace(/^\uFEFF/, ''); + var lines = raw.split(/\r?\n/).filter(function (ln) { return ln.trim().length > 0; }); + if (lines.length < 2) return { rows: [], headerError: '文件至少需要表头行与一行数据' }; + var headers = h2ParseCsvLine(lines[0]).map(function (h) { return String(h || '').trim().replace(/^\uFEFF/, ''); }); + var fieldIdx = {}; + var hi; + for (hi = 0; hi < headers.length; hi++) { + var key = H2_IMPORT_HEADER_MAP[headers[hi]]; + if (key && fieldIdx[key] == null) fieldIdx[key] = hi; + } + if (fieldIdx.name == null) return { rows: [], headerError: '缺少必填列「加氢站名称」' }; + var rows = []; + var li; + for (li = 1; li < lines.length; li++) { + var cells = h2ParseCsvLine(lines[li]); + if (!cells.some(function (c) { return String(c || '').trim(); })) continue; + var get = function (field) { + var idx = fieldIdx[field]; + return idx == null || idx < 0 ? '' : String(cells[idx] == null ? '' : cells[idx]).trim(); + }; + rows.push({ + lineNo: li + 1, + name: get('name'), + province: get('province'), + city: get('city'), + detail: get('detail'), + isSigned: h2ParseSignedLabel(get('isSigned')), + contractStart: get('contractStart'), + contractEnd: get('contractEnd'), + businessStatus: h2NormalizeBusinessStatus(get('businessStatus')), + businessHours: get('businessHours'), + contact: get('contact'), + phone: get('phone') + }); + } + return { rows: rows, headerError: null }; +} + +function h2ValidateImportRow(row, existingNames) { + var errors = []; + if (!(row.name || '').trim()) errors.push('加氢站名称不能为空'); + else if (existingNames[(row.name || '').trim()]) errors.push('站点名称「' + row.name + '」已存在'); + if (!(row.province || '').trim()) errors.push('省不能为空'); + if (!(row.city || '').trim()) errors.push('市不能为空'); + if (!(row.detail || '').trim()) errors.push('详细地址不能为空'); + if (!(row.contact || '').trim()) errors.push('联系人不能为空'); + if (!(row.phone || '').trim()) errors.push('联系电话不能为空'); + if (!(row.businessStatus || '').trim()) errors.push('营业状态无效,请填写:营业中 / 暂停营业 / 停止营业'); + if (row.isSigned) { + if (!row.contractStart) errors.push('已签约站点需填写签约开始时间'); + if (!row.contractEnd) errors.push('已签约站点需填写签约结束时间'); + } + return errors; +} + +function h2ImportRowToRecord(row, id) { + var region = [row.province, row.city]; + var form = { + name: row.name, + address: { region: region, detail: row.detail }, + isSigned: row.isSigned, + contractStart: row.isSigned ? (row.contractStart || '') : '', + contractEnd: row.isSigned ? (row.contractEnd || '') : '', + contractFiles: [], + businessStatus: row.businessStatus || '营业中', + businessHours: row.businessHours || '', + contact: row.contact, + phone: row.phone + }; + var record = h2FormToRecord(form, id); + record.businessStatusLogs = []; + return record; +} + +function h2SvgIcon(paths, size) { + var s = size || 18; + return React.createElement('svg', { + width: s, + height: s, + viewBox: '0 0 24 24', + fill: 'none', + stroke: 'currentColor', + strokeWidth: 2, + strokeLinecap: 'round', + strokeLinejoin: 'round', + 'aria-hidden': true + }, paths.map(function (p, i) { + if (p.tag === 'circle') return React.createElement('circle', { key: i, cx: p.cx, cy: p.cy, r: p.r }); + if (p.tag === 'line') return React.createElement('line', { key: i, x1: p.x1, y1: p.y1, x2: p.x2, y2: p.y2 }); + if (p.tag === 'rect') return React.createElement('rect', { key: i, x: p.x, y: p.y, width: p.width, height: p.height, rx: p.rx }); + return React.createElement('path', { key: i, d: p.d }); + })); +} + +var H2_ICONS = { + station: h2SvgIcon([{ d: 'M3 21h18M5 21V7l8-4v18M19 21V11l-6-4' }]), + high: h2SvgIcon([{ d: 'M13 2L3 14h9l-1 8 10-12h-9l1-8z' }], 16), + low: h2SvgIcon([{ d: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z' }], 16), + none: h2SvgIcon([{ d: 'M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z' }, { tag: 'line', x1: 12, y1: 9, x2: 12, y2: 13 }, { tag: 'line', x1: 12, y1: 17, x2: 12.01, y2: 17 }], 16), + doc: h2SvgIcon([{ d: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z' }, { d: 'M14 2v6h6' }, { tag: 'line', x1: 16, y1: 13, x2: 8, y2: 13 }, { tag: 'line', x1: 16, y1: 17, x2: 8, y2: 17 }], 14), + upload: h2SvgIcon([{ d: 'M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4' }, { d: 'M17 8l-5-5-5 5' }, { tag: 'line', x1: 12, y1: 3, x2: 12, y2: 15 }], 14), + empty: h2SvgIcon([{ tag: 'circle', cx: 12, cy: 12, r: 10 }, { tag: 'line', x1: 8, y1: 12, x2: 16, y2: 12 }], 40), + back: h2SvgIcon([{ tag: 'line', x1: 19, y1: 12, x2: 5, y2: 12 }, { d: 'M12 19l-7-7 7-7' }], 16), + mapPin: h2SvgIcon([{ tag: 'path', d: 'M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z' }, { tag: 'circle', cx: 12, cy: 10, r: 3 }], 16), + user: h2SvgIcon([{ tag: 'path', d: 'M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2' }, { tag: 'circle', cx: 12, cy: 7, r: 4 }], 16), + building: h2SvgIcon([{ d: 'M3 21h18M5 21V7l8-4v18M19 21V11l-6-4' }], 18), + truck: h2SvgIcon([{ d: 'M1 3h15v13H1zM16 8h4l3 3v5h-7V8z' }, { tag: 'circle', cx: 5.5, cy: 18.5, r: 2.5 }, { tag: 'circle', cx: 18.5, cy: 18.5, r: 2.5 }], 16), + link: h2SvgIcon([{ tag: 'path', d: 'M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71' }, { tag: 'path', d: 'M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71' }], 16), + plus: h2SvgIcon([{ tag: 'line', x1: 12, y1: 5, x2: 12, y2: 19 }, { tag: 'line', x1: 5, y1: 12, x2: 19, y2: 12 }], 16), + file: h2SvgIcon([{ d: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z' }, { d: 'M14 2v6h6' }], 16) +}; + +function h2MoreIcon() { + return React.createElement('svg', { + viewBox: '0 0 16 16', + width: 16, + height: 16, + fill: 'currentColor', + 'aria-hidden': true + }, + React.createElement('circle', { cx: 8, cy: 3, r: 1.5 }), + React.createElement('circle', { cx: 8, cy: 8, r: 1.5 }), + React.createElement('circle', { cx: 8, cy: 13, r: 1.5 }) + ); +} + +function h2DeriveFrequencyByRefuelCount(refuelCount) { + var n = typeof refuelCount === 'number' ? refuelCount : parseInt(refuelCount, 10); + if (!n || n <= 0) return 'none'; + if (n >= 3) return 'high'; + return 'low'; +} + +function h2FormatRegion(region) { + if (!region || !region.length) return '—'; + return region.join('-'); +} + +function h2MatchRegionFilter(recordRegion, filterRegion) { + if (!filterRegion || !filterRegion.length) return true; + var rr = recordRegion || []; + if (filterRegion[0] && rr[0] !== filterRegion[0]) return false; + if (filterRegion[1] && rr[1] !== filterRegion[1]) return false; + return true; +} + +function h2EmptyListFilters() { + return { name: '', signed: undefined, region: undefined, businessStatus: undefined }; +} + +/** 距签约结束日天数:正=剩余,负=已过期 */ +function h2DaysUntilContractEnd(dateStr) { + if (!dateStr) return null; + var parts = String(dateStr).trim().split('-'); + if (parts.length < 3) return null; + var y = parseInt(parts[0], 10); + var m = parseInt(parts[1], 10) - 1; + var d = parseInt(parts[2], 10); + if (isNaN(y) || isNaN(m) || isNaN(d)) return null; + var end = new Date(y, m, d, 23, 59, 59, 999); + var today = new Date(); + today.setHours(0, 0, 0, 0); + return Math.ceil((end.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)); +} + +function h2RenderContractRemainTag(contractEnd) { + var days = h2DaysUntilContractEnd(contractEnd); + if (days == null) return null; + var Tag = window.antd && window.antd.Tag; + if (!Tag) return null; + var tagProps = { style: { marginTop: 4, borderRadius: 6, fontWeight: 600, fontSize: 11 } }; + if (days < 0) { + return React.createElement(Tag, Object.assign({}, tagProps, { + color: 'error', + 'aria-label': '已过期 ' + Math.abs(days) + ' 天' + }), '已过期 ' + Math.abs(days) + ' 天'); + } + if (days <= 30) { + return React.createElement(Tag, Object.assign({}, tagProps, { + color: 'warning', + 'aria-label': '剩余 ' + days + ' 天' + }), '剩余 ' + days + ' 天'); + } + return React.createElement(Tag, Object.assign({}, tagProps, { + color: 'success', + 'aria-label': '剩余 ' + days + ' 天' + }), '剩余 ' + days + ' 天'); +} + +function h2BuildFullAddress(region, detail) { + var base = h2FormatRegion(region); + if (!detail) return base === '—' ? '' : base; + return base === '—' ? detail : base + detail; +} + +function h2EmptyAddressValue() { + return { region: [], detail: '' }; +} + +function h2KpiIcon(key) { + if (key === 'high') return H2_ICONS.high; + if (key === 'low') return H2_ICONS.low; + if (key === 'none') return H2_ICONS.none; + return H2_ICONS.station; +} + +function h2SignedFilterIcon(key) { + if (key === 'yes') { + return h2SvgIcon([{ d: 'M22 11.08V12a10 10 0 1 1-5.93-9.14' }, { d: 'M22 4 12 14.01l-3-3' }], 16); + } + return h2SvgIcon([{ tag: 'circle', cx: 12, cy: 12, r: 10 }, { tag: 'line', x1: 15, y1: 9, x2: 9, y2: 15 }, { tag: 'line', x1: 9, y1: 9, x2: 15, y2: 15 }], 16); +} + +/** 地址组件:省/市级联 + 详细地址(无子标题,挂在「地址」表单项下) */ +function RegionAddressInput(props) { + var antd = window.antd; + var Cascader = antd.Cascader; + var Input = antd.Input; + var value = props.value || h2EmptyAddressValue(); + var onChange = props.onChange; + var disabled = props.disabled; + var inputClassName = props.inputClassName || ''; + var layout = props.layout || 'stack'; + var detailPlaceholder = props.detailPlaceholder || '请输入街道、门牌号等详细地址'; + var regionPlaceholder = props.regionPlaceholder || '请选择省 / 市'; + var fieldStyle = props.fieldStyle || { width: '100%', borderRadius: 8 }; + var rootCls = 'h2-region-address' + (layout === 'inline' ? ' h2-region-address--inline' : ''); + + return React.createElement('div', { className: rootCls }, + React.createElement(Cascader, { + className: inputClassName + (layout === 'inline' ? ' h2-region-address-cascader' : ''), + options: H2_REGION_CASCADER_OPTIONS, + value: value.region && value.region.length ? value.region : undefined, + onChange: function (v) { onChange && onChange({ region: v || [], detail: value.detail || '' }); }, + placeholder: regionPlaceholder, + style: layout === 'inline' ? { width: '100%', borderRadius: fieldStyle.borderRadius || 8 } : fieldStyle, + disabled: disabled, + allowClear: !disabled + }), + React.createElement(Input, { + className: inputClassName + (layout === 'inline' ? ' h2-region-address-detail' : ''), + value: value.detail || '', + onChange: function (e) { onChange && onChange({ region: value.region || [], detail: e.target.value }); }, + placeholder: detailPlaceholder, + disabled: disabled, + maxLength: 200, + style: layout === 'inline' ? { width: '100%', borderRadius: fieldStyle.borderRadius || 8 } : fieldStyle + }) + ); +} + +/** 供应商通讯地址:省/市 + 详细地址(横向嵌入) */ +function SupplierCityAddressInput(props) { + var antd = window.antd; + var Cascader = antd.Cascader; + var Input = antd.Input; + var city = props.city; + var address = props.address || ''; + var onChange = props.onChange; + var disabled = props.disabled; + var inputClassName = props.inputClassName || ''; + var onCityChange = props.onCityChange; + + return React.createElement('div', { className: 'h2-region-address h2-region-address--inline' }, + React.createElement(Cascader, { + className: inputClassName + ' h2-region-address-cascader', + options: H2_REGION_CASCADER_OPTIONS, + value: city && city.length ? city : undefined, + onChange: onCityChange, + placeholder: '请选择省 / 市', + style: { width: '100%' }, + disabled: disabled, + allowClear: !disabled + }), + React.createElement(Input, { + className: inputClassName + ' h2-region-address-detail', + value: address, + disabled: disabled, + placeholder: '请输入详细地址', + maxLength: 200, + style: { width: '100%' }, + onChange: function (e) { onChange && onChange(e.target.value); } + }) + ); +} + +function h2GetUploadFileUrl(file) { + if (!file) return ''; + if (file.url) return file.url; + if (file.thumbUrl) return file.thumbUrl; + if (file.originFileObj && typeof URL !== 'undefined' && URL.createObjectURL) { + try { return URL.createObjectURL(file.originFileObj); } catch (e) { return ''; } + } + return ''; +} + +function h2DownloadUploadFile(file) { + var url = h2GetUploadFileUrl(file); + if (!url) return; + var a = document.createElement('a'); + a.href = url; + a.download = file.name || '附件'; + a.target = '_blank'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); +} + +/** 合同附件:按钮上传 + 预览/下载 */ +function ContractFilesUpload(props) { + var antd = window.antd; + var Upload = antd.Upload; + var Button = antd.Button; + var fileList = props.fileList || []; + var disabled = props.disabled; + + return React.createElement('div', { className: 'h2-contract-upload' }, + React.createElement('div', { className: 'h2-contract-upload-actions' }, + React.createElement(Upload, { + className: 'h2-create-upload-btn', + multiple: true, + disabled: disabled, + fileList: fileList, + beforeUpload: function () { return false; }, + onChange: props.onChange, + showUploadList: false + }, + React.createElement(Button, { + type: 'default', + icon: H2_ICONS.upload, + disabled: disabled + }, '上传附件') + ), + React.createElement('span', { style: { fontSize: 12, color: '#94a3b8' } }, '支持 PDF、图片,可多选,非必填') + ), + fileList.length + ? React.createElement('div', { className: 'h2-contract-file-list' }, + fileList.map(function (file) { + return React.createElement('div', { key: file.uid, className: 'h2-contract-file-item' }, + React.createElement('span', { className: 'h2-contract-file-name', title: file.name }, file.name || '未命名附件'), + React.createElement('div', { className: 'h2-contract-file-btns' }, + React.createElement(Button, { + type: 'link', + size: 'small', + disabled: disabled, + onClick: function () { + var url = h2GetUploadFileUrl(file); + if (url) window.open(url, '_blank'); + } + }, '预览'), + React.createElement(Button, { + type: 'link', + size: 'small', + disabled: disabled, + onClick: function () { h2DownloadUploadFile(file); } + }, '下载') + ) + ); + }) + ) + : null + ); +} + +function h2StationContactPhone(station) { + return (station.mobilePhone || '').trim() || (station.landlinePhone || '').trim(); +} + +function h2ApplyStationContactPhone(station, text) { + var v = String(text || '').trim(); + if (/^1\d{10}$/.test(v.replace(/\s/g, ''))) { + return Object.assign({}, station, { mobilePhone: v.replace(/\s/g, ''), landlinePhone: '' }); + } + return Object.assign({}, station, { mobilePhone: '', landlinePhone: v }); +} + +var H2_BUSINESS_HOURS_MODE_OPTIONS = [ + { label: '全天营业', value: 'allDay' }, + { label: '非全天营业', value: 'custom' } +]; + +function h2IsAllDayBusinessHours(str) { + var s = String(str || '').trim(); + if (!s || s === '—') return false; + if (s === '全天营业' || s === '24小时' || s === '24 小时') return true; + return /^00:00\s*[-–]\s*24:00$/.test(s); +} + +function h2ParseBusinessHours(str) { + var s = String(str || '').trim(); + if (!s || s === '—') { + return { mode: 'allDay', start: '08:00', end: '22:00' }; + } + if (h2IsAllDayBusinessHours(s)) { + return { mode: 'allDay', start: '08:00', end: '22:00' }; + } + var parts = s.split('-'); + if (parts.length >= 2) { + return { + mode: 'custom', + start: (parts[0] || '').trim(), + end: (parts.slice(1).join('-') || '').trim() + }; + } + return { mode: 'custom', start: '', end: '' }; +} + +function h2FormatBusinessHours(val) { + if (!val || val.mode === 'allDay') return '00:00-24:00'; + var start = (val.start || '').trim(); + var end = (val.end || '').trim(); + if (!start || !end) return ''; + return start + '-' + end; +} + +function h2DisplayBusinessHours(str) { + var s = String(str || '').trim(); + if (!s || s === '—') return '—'; + if (h2IsAllDayBusinessHours(s) || s === '全天营业') return '全天营业'; + var parsed = h2ParseBusinessHours(s); + if (parsed.mode === 'custom' && parsed.start && parsed.end) return parsed.start + '-' + parsed.end; + return s; +} + +function h2NormalizeTimeText(text) { + var s = String(text || '').trim(); + if (!s) return ''; + var m = /^(\d{1,2}):(\d{2})$/.exec(s); + if (!m) return s; + var h = parseInt(m[1], 10); + var min = parseInt(m[2], 10); + if (isNaN(h) || isNaN(min) || h < 0 || h > 23 || min < 0 || min > 59) return s; + return (h < 10 ? '0' : '') + h + ':' + (min < 10 ? '0' : '') + min; +} + +function h2TimeTextToDayjs(text) { + var dayjs = window.dayjs; + if (!dayjs) return null; + var norm = h2NormalizeTimeText(text); + if (!norm) return null; + var parts = norm.split(':'); + return dayjs().hour(parseInt(parts[0], 10)).minute(parseInt(parts[1], 10)).second(0).millisecond(0); +} + +function h2DayjsToTimeText(d) { + var dayjs = window.dayjs; + if (!d || !dayjs) return ''; + return dayjs(d).format('HH:mm'); +} + +/** 营业时间:全天 / 非全天 + 起止时间(时:分,可输入) */ +function BusinessHoursInput(props) { + var antd = window.antd; + var Select = antd.Select; + var TimePicker = antd.TimePicker; + var Input = antd.Input; + var parsed = h2ParseBusinessHours(props.value); + var onChange = props.onChange; + var disabled = props.disabled; + var inputClassName = props.inputClassName || ''; + var fieldStyle = props.fieldStyle || { width: '100%' }; + var layout = props.layout || 'full'; + var dayjs = window.dayjs; + var rootClass = 'h2-business-hours' + (layout === 'col4' ? ' h2-business-hours--col4' : ''); + + var emit = function (next) { + onChange && onChange(h2FormatBusinessHours(next)); + }; + + var renderTimeField = function (label, timeKey, placeholder) { + var timeVal = parsed[timeKey] || ''; + var onTimeChange = function (nextText) { + var patch = { mode: 'custom', start: parsed.start || '', end: parsed.end || '' }; + patch[timeKey] = h2NormalizeTimeText(nextText); + emit(patch); + }; + return React.createElement('div', { className: 'h2-business-hours-range-item', key: timeKey }, + React.createElement('span', { className: 'h2-business-hours-range-label' }, label), + dayjs + ? React.createElement(TimePicker, { + className: inputClassName, + format: 'HH:mm', + minuteStep: 1, + hourStep: 1, + showNow: false, + allowClear: !disabled, + inputReadOnly: false, + disabled: disabled, + style: fieldStyle, + placeholder: placeholder, + value: h2TimeTextToDayjs(timeVal), + onChange: function (d) { onTimeChange(d ? h2DayjsToTimeText(d) : ''); } + }) + : React.createElement(Input, { + className: inputClassName, + disabled: disabled, + style: fieldStyle, + placeholder: placeholder || 'HH:mm', + value: timeVal, + maxLength: 5, + onChange: function (e) { onTimeChange(e.target.value); }, + onBlur: function (e) { onTimeChange(e.target.value); } + }) + ); + }; + + return React.createElement('div', { className: rootClass }, + React.createElement(Select, { + className: inputClassName, + value: parsed.mode, + options: H2_BUSINESS_HOURS_MODE_OPTIONS, + style: fieldStyle, + disabled: disabled, + placeholder: '请选择营业时间', + onChange: function (mode) { + if (mode === 'allDay') emit({ mode: 'allDay', start: parsed.start, end: parsed.end }); + else emit({ + mode: 'custom', + start: parsed.start || '08:00', + end: parsed.end || '22:00' + }); + }, + dropdownStyle: { borderRadius: 8 } + }), + parsed.mode === 'custom' + ? React.createElement('div', { className: 'h2-business-hours-range' }, + renderTimeField('开始时间', 'start', '请选择开始时间'), + renderTimeField('结束时间', 'end', '请选择结束时间') + ) + : null + ); +} + +function h2CreateEmptyForm() { + return { + name: '', + address: h2EmptyAddressValue(), + isSigned: true, + contractStart: '', + contractEnd: '', + contractFiles: [], + businessStatus: '营业中', + businessHours: '', + contact: '', + phone: '' + }; +} + +function h2RecordToForm(record) { + if (!record) return h2CreateEmptyForm(); + return { + name: record.name || '', + address: { + region: (record.region || []).slice(), + detail: record.addressDetail || '' + }, + isSigned: record.isSigned !== false, + contractStart: record.contractStart || '', + contractEnd: record.contractEnd || '', + contractFiles: (record.contractFiles || []).map(function (f, i) { + return Object.assign({}, f, { uid: f.uid || ('f-' + i), status: 'done' }); + }), + businessStatus: record.businessStatus || '营业中', + businessHours: record.businessHours || '', + contact: record.contact || '', + phone: record.phone || '' + }; +} + +function h2FormToRecord(form, id) { + var files = (form.contractFiles || []).map(function (f) { + return { uid: f.uid, name: f.name || '合同附件.pdf', url: f.url || '' }; + }); + return { + id: id, + name: (form.name || '').trim(), + region: form.address.region || [], + addressDetail: (form.address.detail || '').trim(), + fullAddress: h2BuildFullAddress(form.address.region, form.address.detail), + isSigned: !!form.isSigned, + contractStart: form.contractStart || '', + contractEnd: form.contractEnd || '', + contractFiles: files, + businessStatus: form.businessStatus || '营业中', + businessHours: (form.businessHours || '').trim(), + contact: (form.contact || '').trim(), + phone: (form.phone || '').trim(), + updateTime: new Date().toISOString().slice(0, 16).replace('T', ' ') + }; +} + +var H2_MOCK_STATIONS = [ + { + id: 1, + name: '嘉兴加氢站(一期)', + region: ['浙江省', '嘉兴市'], + addressDetail: '南湖区科技大道88号', + fullAddress: '浙江省-嘉兴市南湖区科技大道88号', + isSigned: true, + contractStart: '2024-01-01', + contractEnd: '2026-12-31', + contractFiles: [{ uid: 'c1', name: '嘉兴加氢站签约合同.pdf' }], + businessStatus: '营业中', + businessHours: '08:00-22:00', + costUnitPrice: 42.5, + customerUnitPrice: 45, + prepaidBalance: 85620.0, + contact: '张三', + phone: '13800138001', + businessStatusLogs: [], + updateTime: '2026-05-20 14:30' + }, + { + id: 2, + name: '杭州临平加氢站', + region: ['浙江省', '杭州市'], + addressDetail: '临平区东湖街道能源路16号', + fullAddress: '浙江省-杭州市临平区东湖街道能源路16号', + isSigned: true, + contractStart: '2025-03-01', + contractEnd: '2027-02-28', + contractFiles: [{ uid: 'c2', name: '临平加氢站合作协议.pdf' }, { uid: 'c3', name: '临平加氢站补充协议.pdf' }], + businessStatus: '营业中', + businessHours: '00:00-24:00', + prepaidBalance: 125000.5, + contact: '李四', + phone: '13900139002', + updateTime: '2026-06-01 09:15' + }, + { + id: 3, + name: '上海宝山加氢站', + region: ['上海市', '上海市'], + addressDetail: '宝山区富联路128号', + fullAddress: '上海市-上海市宝山区富联路128号', + isSigned: true, + contractStart: '2023-06-01', + contractEnd: '2025-05-31', + contractFiles: [{ uid: 'c4', name: '宝山加氢站合同(即将到期).pdf' }], + businessStatus: '暂停营业', + businessHours: '09:00-18:00', + prepaidBalance: -3250.0, + businessStatusLogs: [ + { id: 'bsl-3-2', operateTime: '2026-04-01 10:00', operator: '王五', beforeStatus: '营业中', afterStatus: '暂停营业' }, + { id: 'bsl-3-1', operateTime: '2026-03-15 09:30', operator: '系统管理员', beforeStatus: '暂停营业', afterStatus: '营业中' } + ], + contact: '王五', + phone: '13700137003', + updateTime: '2026-04-12 16:40' + }, + { + id: 4, + name: '平湖合作停车场(待建加氢)', + region: ['浙江省', '嘉兴市'], + addressDetail: '平湖市乍嘉公路与平善大道交叉口', + fullAddress: '浙江省-嘉兴市平湖市乍嘉公路与平善大道交叉口', + isSigned: false, + contractStart: '', + contractEnd: '', + contractFiles: [], + businessStatus: '停止营业', + businessHours: '—', + prepaidBalance: 0, + contact: '赵六', + phone: '13600136004', + updateTime: '2026-03-08 11:20' + }, + { + id: 5, + name: '苏州工业园区备用站', + region: ['江苏省', '苏州市'], + addressDetail: '工业园区星湖街328号', + fullAddress: '江苏省-苏州市工业园区星湖街328号', + isSigned: false, + contractStart: '', + contractEnd: '', + contractFiles: [], + businessStatus: '停止营业', + businessHours: '10:00-17:00', + prepaidBalance: 15800.0, + contact: '钱七', + phone: '13500135005', + updateTime: '2026-02-18 08:50' + } +]; + +/** 站点加氢记录 Mock(字段对齐车辆氢费明细) */ +var H2_MOCK_REFUEL_RECORDS = [ + { id: 'rf-1', stationName: '嘉兴加氢站(一期)', hydrogenTime: '2026-05-28 10:21:08', plateNo: '浙A12345F', customerName: '嘉兴市鑫峤供应链科技有限公司', hydrogenKg: 12.5, costUnitPrice: 42.5, costAmount: 531.25, customerUnitPrice: 45, customerAmount: 562.5 }, + { id: 'rf-2', stationName: '嘉兴加氢站(一期)', hydrogenTime: '2026-05-26 14:08:33', plateNo: '浙A67890F', customerName: '浙江绿运物流有限公司', hydrogenKg: 10.0, costUnitPrice: 42.5, costAmount: 425.0, customerUnitPrice: 45, customerAmount: 450.0 }, + { id: 'rf-3', stationName: '嘉兴加氢站(一期)', hydrogenTime: '2026-05-22 09:15:00', plateNo: '浙A88888F', customerName: '嘉兴市鑫峤供应链科技有限公司', hydrogenKg: 18.3, costUnitPrice: 42.5, costAmount: 777.75, customerUnitPrice: 45, customerAmount: 823.5 }, + { id: 'rf-4', stationName: '嘉兴加氢站(一期)', hydrogenTime: '2026-05-18 16:42:11', plateNo: '浙A03561F', customerName: '嘉兴港务氢能运输队', hydrogenKg: 15.6, costUnitPrice: 42.5, costAmount: 663.0, customerUnitPrice: 45, customerAmount: 702.0 }, + { id: 'rf-5', stationName: '杭州临平加氢站', hydrogenTime: '2026-05-30 09:30:22', plateNo: '浙B23456F', customerName: '杭州临平城配中心', hydrogenKg: 15.3, costUnitPrice: 43.0, costAmount: 657.9, customerUnitPrice: 46, customerAmount: 703.8 }, + { id: 'rf-6', stationName: '杭州临平加氢站', hydrogenTime: '2026-05-27 18:10:05', plateNo: '浙B99999F', customerName: '浙江氢运科技', hydrogenKg: 18.2, costUnitPrice: 43.0, costAmount: 782.6, customerUnitPrice: 46, customerAmount: 837.2 }, + { id: 'rf-7', stationName: '杭州临平加氢站', hydrogenTime: '2026-05-24 11:05:40', plateNo: '浙B58888F', customerName: '杭州临平城配中心', hydrogenKg: 11.8, costUnitPrice: 43.0, costAmount: 507.4, customerUnitPrice: 46, customerAmount: 542.8 }, + { id: 'rf-8', stationName: '上海宝山加氢站', hydrogenTime: '2026-04-20 16:45:18', plateNo: '沪A88888F', customerName: '上海羚牛氢运', hydrogenKg: 8.0, costUnitPrice: 44.0, costAmount: 352.0, customerUnitPrice: 47, customerAmount: 376.0 }, + { id: 'rf-9', stationName: '上海宝山加氢站', hydrogenTime: '2026-04-08 09:12:55', plateNo: '沪BDB9161F', customerName: '宝山园区试运车队', hydrogenKg: 9.5, costUnitPrice: 44.0, costAmount: 418.0, customerUnitPrice: 47, customerAmount: 446.5 }, + { id: 'rf-10', stationName: '苏州工业园区备用站', hydrogenTime: '2026-03-15 10:00:00', plateNo: '苏E33333F', customerName: '苏州试运客户', hydrogenKg: 6.2, costUnitPrice: 41.0, costAmount: 254.2, customerUnitPrice: 44, customerAmount: 272.8 } +]; + +function h2FormatKgNum(v) { + var n = typeof v === 'number' ? v : parseFloat(v); + return isNaN(n) ? '0.00' : n.toFixed(2); +} + +function h2FormatYuanNum(v) { + var n = typeof v === 'number' ? v : parseFloat(v); + return isNaN(n) ? '0.00' : n.toFixed(2); +} + +function h2NumOrZero(v) { + var n = typeof v === 'number' ? v : parseFloat(v); + return isNaN(n) ? 0 : n; +} + +function h2FormatLedgerMoney(v) { + if (v === null || v === undefined || v === '' || Number(v) === 0) return '—'; + return h2FormatYuanNum(v); +} + +/** 原型:加氢站预付余额变更明细(联调后对接加氢站打款/余额流水接口) */ +function h2BuildMockPrepaidBalanceRows(record) { + var seed = 0; + var code = String(record && record.id != null ? record.id : ''); + var nameKey = String((record && record.name) || ''); + var i; + for (i = 0; i < code.length; i++) seed += code.charCodeAt(i); + for (i = 0; i < nameKey.length; i++) seed += nameKey.charCodeAt(i); + var stationName = (record && record.name) || '—'; + var finalBalance = h2NumOrZero(record && record.prepaidBalance); + var lineCount = Math.max(5, 6 + (seed % 5)); + var rows = []; + var openingBalance = Math.round((finalBalance + 38000 + seed * 620) * 100) / 100; + if (openingBalance < 0) openingBalance = Math.abs(openingBalance) + 50000; + + rows.push({ + key: code + '-bal-0', + stationName: stationName, + incomeAmount: openingBalance, + expenseAmount: null, + balance: openingBalance, + orderNo: 'QC' + code + '260101' + }); + + var balance = openingBalance; + for (i = 1; i < lineCount - 1; i++) { + var isIncome = (seed + i) % 3 === 0; + var incomeAmount = null; + var expenseAmount = null; + if (isIncome) { + incomeAmount = Math.round((6000 + (seed + i) * 380) * 100) / 100; + balance = Math.round((balance + incomeAmount) * 100) / 100; + } else { + expenseAmount = Math.round((2800 + (seed + i) * 210) * 100) / 100; + balance = Math.round((balance - expenseAmount) * 100) / 100; + } + rows.push({ + key: code + '-bal-' + i, + stationName: stationName, + incomeAmount: incomeAmount, + expenseAmount: expenseAmount, + balance: balance, + orderNo: (isIncome ? 'DK' : 'JQ') + code + String(200 + i).padStart(5, '0') + }); + } + + var diff = Math.round((finalBalance - balance) * 100) / 100; + if (Math.abs(diff) < 0.01) { + rows.push({ + key: code + '-bal-last', + stationName: stationName, + incomeAmount: null, + expenseAmount: null, + balance: finalBalance, + orderNo: 'BZ' + code + String(900).padStart(5, '0') + }); + } else if (diff > 0) { + rows.push({ + key: code + '-bal-last', + stationName: stationName, + incomeAmount: diff, + expenseAmount: null, + balance: finalBalance, + orderNo: 'DK' + code + String(999).padStart(5, '0') + }); + } else { + rows.push({ + key: code + '-bal-last', + stationName: stationName, + incomeAmount: null, + expenseAmount: Math.round(Math.abs(diff) * 100) / 100, + balance: finalBalance, + orderNo: 'JQ' + code + String(999).padStart(5, '0') + }); + } + + return rows; +} + +function h2GetRefuelRecordsByStation(stationName) { + var name = String(stationName || '').trim(); + return H2_MOCK_REFUEL_RECORDS.filter(function (r) { return r.stationName === name; }).slice().sort(function (a, b) { + return String(b.hydrogenTime || '').localeCompare(String(a.hydrogenTime || '')); + }); +} + +function h2CalcRefuelStats(stationName) { + var records = h2GetRefuelRecordsByStation(stationName); + var totalKg = 0; + var i; + for (i = 0; i < records.length; i++) totalKg += records[i].hydrogenKg || 0; + return { count: records.length, totalKg: totalKg }; +} + +var H2_MOCK_EXISTING_SUPPLIERS = [ + { + id: 'sup-h2-1', + name: '嘉兴氢能供应有限公司', + type: '加氢站', + city: ['浙江省', '嘉兴市'], + address: '南湖区科技大道66号', + region: '华东', + dept: '能源部', + manager: '张经理', + remark: '嘉兴区域主力加氢站运营商', + contactName: '李供', + contactMobile: '13800138088', + contactTitle: '站长', + taxId: '91330400MA2ABCDEF1', + invoiceAddress: '浙江省嘉兴市南湖区科技大道66号', + invoicePhone: '0573-88886666', + bankName: '中国工商银行嘉兴南湖支行', + bankAccount: '6222021203001234567', + mailingAddress: '浙江省嘉兴市南湖区科技大道66号', + businessAddress: '浙江省嘉兴市南湖区科技大道66号', + businessLicenseFiles: [{ uid: 'bl-1', name: '营业执照_嘉兴氢能.pdf', status: 'done' }], + fillingLicenseFiles: [{ uid: 'fl-1', name: '充装许可证_嘉兴氢能.pdf', status: 'done' }] + }, + { + id: 'sup-h2-2', + name: '上海羚牛氢能科技', + type: '加氢站', + city: ['上海市', '上海市'], + address: '宝山区富联路200号', + region: '华东', + dept: '能源部', + manager: '王专员', + remark: '', + contactName: '陈氢', + contactMobile: '13900139001', + contactTitle: '运营主管', + taxId: '91310100MA1XYZ7890', + invoiceAddress: '上海市宝山区富联路200号', + invoicePhone: '021-66886688', + bankName: '招商银行上海宝山支行', + bankAccount: '6214830210123456789', + mailingAddress: '上海市宝山区富联路200号', + businessAddress: '上海市宝山区富联路200号', + businessLicenseFiles: [{ uid: 'bl-2', name: '营业执照_羚牛氢能.pdf', status: 'done' }], + fillingLicenseFiles: [{ uid: 'fl-2', name: '充装许可证_羚牛氢能.pdf', status: 'done' }] + }, + { + id: 'sup-h2-3', + name: '苏州工业园区氢源科技', + type: '加氢站', + city: ['江苏省', '苏州市'], + address: '工业园区星湖街328号', + region: '华东', + dept: '华东大区', + manager: '赵经理', + remark: '备用站点供应商', + contactName: '钱七', + contactMobile: '13500135005', + contactTitle: '联系人', + taxId: '91320500MA2SUPPLIER', + invoiceAddress: '江苏省苏州市工业园区星湖街328号', + invoicePhone: '0512-66668888', + bankName: '中国银行苏州工业园区支行', + bankAccount: '6216600100009876543', + mailingAddress: '江苏省苏州市工业园区星湖街328号', + businessAddress: '江苏省苏州市工业园区星湖街328号', + businessLicenseFiles: [], + fillingLicenseFiles: [] + }, + { + id: 'sup-other-1', + name: '杭州绿能备件有限公司', + type: '备件供应商', + city: ['浙江省', '杭州市'], + address: '余杭区文一西路998号', + region: '华东', + dept: '采购部', + manager: '孙采购', + remark: '', + contactName: '周备件', + contactMobile: '13600136001', + contactTitle: '销售', + taxId: '91330100MA3OTHER01', + invoiceAddress: '浙江省杭州市余杭区文一西路998号', + invoicePhone: '0571-88880001', + bankName: '中国建设银行杭州余杭支行', + bankAccount: '6227000012345678901', + mailingAddress: '浙江省杭州市余杭区文一西路998号', + businessAddress: '浙江省杭州市余杭区文一西路998号', + businessLicenseFiles: [], + fillingLicenseFiles: [] + } +]; + +function h2CreateEmptyStationForm() { + return { + name: '', + address: h2EmptyAddressValue(), + isSigned: true, + contractStart: '', + contractEnd: '', + contractFiles: [], + businessStatus: '营业中', + businessHours: '00:00-24:00', + contact: '', + mobilePhone: '', + landlinePhone: '', + costUnitPrice: '', + customerUnitPrice: '', + prepaidBalance: 0 + }; +} + +function h2CreateEmptySupplierForm() { + return { + name: '', + city: undefined, + address: '', + region: '', + dept: '', + manager: '', + remark: '', + contactName: '', + contactMobile: '', + contactTitle: '', + taxId: '', + invoiceAddress: '', + invoicePhone: '', + bankName: '', + bankAccount: '', + mailingAddress: '', + businessAddress: '', + businessLicenseFiles: [], + fillingLicenseFiles: [] + }; +} + +function h2GetRegionByProvince(province) { + var a = String(province || '').trim(); + if (!a) return ''; + if (a === '北京市' || a === '天津市' || a === '河北省' || a === '山西省' || a === '内蒙古') return '华北'; + if (a === '上海市' || a === '江苏省' || a === '浙江省' || a === '安徽省' || a === '福建省' || a === '江西省' || a === '山东省') return '华东'; + if (a === '广东省' || a === '广西省' || a === '海南省') return '华南'; + if (a === '河南省' || a === '湖北省' || a === '湖南省') return '华中'; + if (a === '辽宁省' || a === '吉林省' || a === '黑龙江省') return '东北'; + if (a === '四川省' || a === '云南省' || a === '贵州省' || a === '重庆市') return '西南'; + if (a === '陕西省' || a === '甘肃省' || a === '青海省' || a === '宁夏' || a === '新疆') return '西北'; + return ''; +} + +function h2SupplierToForm(s) { + if (!s) return h2CreateEmptySupplierForm(); + return { + name: s.name || '', + city: s.city ? s.city.slice() : undefined, + address: s.address || '', + region: s.region || '', + dept: s.dept || '', + manager: s.manager || '', + remark: s.remark || '', + contactName: s.contactName || '', + contactMobile: s.contactMobile || '', + contactTitle: s.contactTitle || '', + taxId: s.taxId || '', + invoiceAddress: s.invoiceAddress || '', + invoicePhone: s.invoicePhone || '', + bankName: s.bankName || '', + bankAccount: s.bankAccount || '', + mailingAddress: s.mailingAddress || '', + businessAddress: s.businessAddress || s.mailingAddress || '', + businessLicenseFiles: (s.businessLicenseFiles || []).map(function (f, i) { + return Object.assign({}, f, { uid: f.uid || ('bl-' + i), status: 'done' }); + }), + fillingLicenseFiles: (s.fillingLicenseFiles || []).map(function (f, i) { + return Object.assign({}, f, { uid: f.uid || ('fl-' + i), status: 'done' }); + }) + }; +} + +function getH2StationRequirementDoc() { + return [ + '加氢站管理 - 站点信息', + '', + '一、页面定位', + ' 管理全部加氢站点基础信息,支持 KPI 分类筛选、条件检索、新建、编辑、查看与删除。', + '', + '二、站点分类 KPI', + ' · 全部加氢站:展示全部记录', + ' · 高频 / 低频 / 无加氢站点:按加氢次数自动划分(≥3 高频,1~2 低频,0 无加氢)', + '', + '三、列表字段', + ' 加氢站名称(含签约标签)、地址、签约起止时间、营业状态、营业时间、加氢次数、加氢量(含高频/低频标签)、预付余额、联系方式、操作', + ' · 加氢次数/加氢量可点击,弹窗展示该站点加氢记录(对齐车辆氢费明细字段)', + ' · 预付余额:可点击查看余额变更明细;负值红色展示(已欠费);更多菜单支持生成对账单', + '', + '四、表单字段', + ' · 新建:同页内嵌视图,含加氢站基本信息、供应商相关信息两张卡片;供应商类型固定「加氢站」', + ' · 编辑/查看:抽屉;基本信息含名称、地址、营业时间、签约信息、联系方式;营业状态仅在列表「更多-营业状态」维护', + '', + '五、交互', + ' · 筛选:加氢站名称、是否签约、地区(省-市)、营业状态', + ' · KPI 统计:全部加氢站右侧为已签约/未签约站点快捷筛选,其后为高频/低频/无加氢分类', + ' · 未签约时签约时间与附件可选填;已签约建议填写完整', + ' · 查看模式只读;编辑/新建可保存', + ' · 列表支持分页,默认每页 10 条,可切换 5/10/20/50;筛选或 KPI 切换后回到第 1 页', + ' · 操作列:查看 + 更多(编辑、删除、营业状态、价格配置、生成对账单)', + ' · 营业状态弹窗:修改营业状态/时间,并展示状态变更记录(操作时间、操作人、修改前/后状态)', + '', + '六、批量导入', + ' · 下载 CSV 模板,填写后上传;支持 .csv(推荐)、.xlsx、.xls', + ' · 模板字段:加氢站名称、省、市、详细地址、是否签约、签约起止时间、营业状态、营业时间、联系人、联系电话', + ' · 站点名称不可与现有台账重复;解析后确认导入' + ].join('\n'); +} + +var H2_PRIMARY_BTN_STYLE = { + borderRadius: 8, + fontWeight: 600, + background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)', + border: 'none' +}; + +var H2_REQ_BTN_STYLE = { + borderRadius: 8, + border: '1px solid #cbd5e1', + fontWeight: 600, + display: 'inline-flex', + alignItems: 'center', + boxShadow: '0 1px 2px rgba(0,0,0,0.05)', + color: '#475569' +}; + +function h2CreateFormDirty(station, supplier, supplierMode, linkedSupplierId) { + if ((station.name || '').trim()) return true; + if ((station.contact || '').trim()) return true; + if ((station.mobilePhone || '').trim() || (station.landlinePhone || '').trim()) return true; + if (station.address && ((station.address.region && station.address.region.length) || (station.address.detail || '').trim())) return true; + if ((supplier.name || '').trim()) return true; + if (supplierMode === 'link' && linkedSupplierId) return true; + if ((supplier.businessLicenseFiles || []).length || (supplier.fillingLicenseFiles || []).length) return true; + if ((station.contractFiles || []).length) return true; + return false; +} + +function h2CreateFormProgress(station, supplier, supplierMode, linkedSupplierId) { + var checks = [ + !!(station.name || '').trim(), + station.address && station.address.region && station.address.region.length >= 2 && !!(station.address.detail || '').trim(), + !!(station.contact || '').trim(), + !!((station.mobilePhone || '').trim() || (station.landlinePhone || '').trim()), + supplierMode === 'link' ? !!linkedSupplierId : !!(supplier.name || '').trim(), + supplierMode === 'new' + ? (supplier.businessLicenseFiles || []).length > 0 && (supplier.fillingLicenseFiles || []).length > 0 + : !!linkedSupplierId + ]; + var done = checks.filter(Boolean).length; + return Math.round((done / checks.length) * 100); +} + +function h2BuildStationCreateView(ctx) { + var antd = window.antd; + var Card = antd.Card; + var Button = antd.Button; + var Input = antd.Input; + var Select = antd.Select; + var Radio = antd.Radio; + var DatePicker = antd.DatePicker; + var Upload = antd.Upload; + var Form = antd.Form; + var Progress = antd.Progress; + var Row = antd.Row; + var Col = antd.Col; + var dayjs = window.dayjs; + + var inputCls = 'h2-create-input'; + var progressPct = h2CreateFormProgress(ctx.station, ctx.supplier, ctx.supplierMode, ctx.linkedSupplierId); + var H2_COL_4 = 6; + var H2_COL_HALF = 12; + var H2_COL_FULL = 24; + + var formItem = function (label, required, node, extra) { + return React.createElement(Form.Item, { + label: required ? React.createElement('span', null, React.createElement('span', { style: { color: '#ef4444', marginRight: 4 } }, '*'), label) : label, + extra: extra + }, node); + }; + + var formRow = function () { + return React.createElement.apply(null, [Row, { gutter: 24 }].concat(Array.prototype.slice.call(arguments))); + }; + + var col4 = function (node) { + return React.createElement(Col, { span: H2_COL_4 }, node); + }; + + var col12 = function (node) { + return React.createElement(Col, { span: H2_COL_HALF }, node); + }; + + var colFull = function (node) { + return React.createElement(Col, { span: H2_COL_FULL }, node); + }; + + var cardTitle = function (text, icon) { + return React.createElement('span', { className: 'h2-card-title-bar' }, + icon ? React.createElement('span', { className: 'h2-card-title-icon', 'aria-hidden': true }, icon) : null, + text + ); + }; + + var uploadDragger = function (fileList, onChange, btnText, hint, disabled) { + return React.createElement(Upload.Dragger, { + className: 'h2-create-upload', + multiple: true, + disabled: disabled, + fileList: fileList, + beforeUpload: function () { return false; }, + onChange: onChange, + showUploadList: true + }, + React.createElement('p', { className: 'ant-upload-drag-icon', style: { marginBottom: 8 } }, H2_ICONS.upload), + React.createElement('p', { style: { margin: 0, fontWeight: 600, color: '#334155' } }, btnText), + React.createElement('p', { className: 'h2-create-upload-hint' }, hint) + ); + }; + + var supplierOptions = H2_MOCK_EXISTING_SUPPLIERS.map(function (s) { + return { + value: s.id, + label: s.name + (s.type ? '(' + s.type + ')' : '') + (s.city && s.city.length ? ' · ' + s.city.join('-') : '') + }; + }); + + var contractDateRangeValue = null; + if (dayjs && ctx.station.contractStart && ctx.station.contractEnd) { + var ds = dayjs(ctx.station.contractStart); + var de = dayjs(ctx.station.contractEnd); + if (ds.isValid() && de.isValid()) contractDateRangeValue = [ds, de]; + } + + var renderContractDates = function () { + if (dayjs && DatePicker && DatePicker.RangePicker) { + return React.createElement(DatePicker.RangePicker, { + className: inputCls, + style: { width: '100%' }, + format: 'YYYY-MM-DD', + placeholder: ['开始日期', '结束日期'], + value: contractDateRangeValue, + onChange: function (dates) { + if (!dates || dates.length < 2) { + ctx.updateStation({ contractStart: '', contractEnd: '' }); + return; + } + ctx.updateStation({ + contractStart: dayjs(dates[0]).format('YYYY-MM-DD'), + contractEnd: dayjs(dates[1]).format('YYYY-MM-DD') + }); + } + }); + } + return React.createElement('div', { className: 'h2-contract-dates' }, + React.createElement(Input, { + className: inputCls, + type: 'date', + value: ctx.station.contractStart || '', + style: { flex: 1, minWidth: 0 }, + onChange: function (e) { ctx.updateStation({ contractStart: e.target.value }); } + }), + React.createElement('span', { className: 'h2-contract-dates-sep' }, '至'), + React.createElement(Input, { + className: inputCls, + type: 'date', + value: ctx.station.contractEnd || '', + style: { flex: 1, minWidth: 0 }, + onChange: function (e) { ctx.updateStation({ contractEnd: e.target.value }); } + }) + ); + }; + + return React.createElement(React.Fragment, null, + React.createElement('div', { className: 'h2-create-shell' }, + React.createElement('div', { className: 'h2-create-topbar' }, + React.createElement(Button, { + className: 'h2-create-back-btn', + type: 'default', + icon: H2_ICONS.back, + onClick: ctx.onCancel, + disabled: ctx.submitting + }, '返回'), + React.createElement('div', { className: 'h2-create-topbar-actions' }, + React.createElement(Button, { + type: 'default', + icon: ctx.reqIcon, + style: ctx.reqBtnStyle, + onClick: ctx.onOpenRequirement, + 'aria-label': '查看需求说明' + }, '需求说明') + ) + ), + + React.createElement(Form, { layout: 'vertical', requiredMark: false, className: 'h2-create-form' }, + React.createElement(Card, { + className: 'h2-create-card', + id: 'h2-create-station-card', + title: cardTitle('加氢站基本信息', H2_ICONS.building), + bordered: false + }, + formRow( + col4(formItem('加氢站名称', true, React.createElement(Input, { + className: inputCls, + value: ctx.station.name, + placeholder: '请输入加氢站名称', + maxLength: 80, + onChange: function (e) { ctx.updateStation({ name: e.target.value }); }, + onBlur: ctx.syncSupplierFromStation + }))), + col4(formItem('是否签约站点', false, React.createElement(Radio.Group, { + className: 'h2-create-radio-group', + value: ctx.station.isSigned ? 'signed' : 'unsigned', + onChange: function (e) { + var signed = e.target.value === 'signed'; + ctx.updateStation({ + isSigned: signed, + contractStart: signed ? ctx.station.contractStart : '', + contractEnd: signed ? ctx.station.contractEnd : '' + }); + } + }, + React.createElement(Radio, { value: 'signed' }, '签约'), + React.createElement(Radio, { value: 'unsigned' }, '非签约') + ))), + ctx.station.isSigned + ? col12(formItem('签约时间', true, renderContractDates())) + : null + ), + formRow( + col12(formItem('地址', true, React.createElement(RegionAddressInput, { + value: ctx.station.address, + layout: 'inline', + inputClassName: inputCls, + regionPlaceholder: '省 / 市', + detailPlaceholder: '请输入详细地址', + onChange: function (v) { ctx.updateStation({ address: v }); } + }))), + col12(formItem('合同附件', false, React.createElement(ContractFilesUpload, { + fileList: ctx.station.contractFiles, + onChange: function (info) { ctx.updateStation({ contractFiles: info.fileList }); } + }))) + ), + formRow( + col4(formItem('联系人', true, React.createElement(Input, { + className: inputCls, + value: ctx.station.contact, + placeholder: '请输入联系人', + onChange: function (e) { ctx.updateStation({ contact: e.target.value }); }, + onBlur: ctx.syncSupplierFromStation + }))), + col4(formItem('联系电话', true, React.createElement(Input, { + className: inputCls, + value: h2StationContactPhone(ctx.station), + placeholder: '手机号或固定电话', + type: 'tel', + autoComplete: 'tel', + onChange: function (e) { ctx.updateStation(h2ApplyStationContactPhone(ctx.station, e.target.value)); }, + onBlur: ctx.syncSupplierFromStation + }))) + ) + ), + + React.createElement(Card, { + className: 'h2-create-card', + id: 'h2-create-supplier-card', + title: cardTitle('供应商相关信息', H2_ICONS.truck), + bordered: false + }, + formRow( + col12(formItem('关联方式', true, React.createElement(Radio.Group, { + className: 'h2-create-radio-group', + value: ctx.supplierMode, + onChange: function (e) { ctx.handleSupplierModeChange({ target: { value: e.target.value } }); } + }, + React.createElement(Radio, { value: 'link' }, '关联已有供应商'), + React.createElement(Radio, { value: 'new' }, '新增供应商') + ))), + ctx.supplierMode === 'link' + ? col12(formItem('供应商', true, React.createElement(Select, { + className: inputCls, + showSearch: true, + placeholder: '请搜索并选择供应商', + value: ctx.linkedSupplierId, + options: supplierOptions, + optionFilterProp: 'label', + style: { width: '100%' }, + onChange: ctx.handleLinkSupplierChange, + dropdownStyle: { borderRadius: 8 } + }))) + : null + ), + ctx.supplierMode === 'new' + ? React.createElement(React.Fragment, null, + formRow( + col4(formItem('供应商名称', true, React.createElement(Input, { + className: inputCls, + value: ctx.supplier.name, + placeholder: '请输入供应商名称', + onChange: function (e) { ctx.updateSupplier({ name: e.target.value }); } + }))), + col12(formItem('通讯地址', false, React.createElement(SupplierCityAddressInput, { + city: ctx.supplier.city, + address: ctx.supplier.address, + inputClassName: inputCls, + onCityChange: ctx.handleSupplierCityChange, + onChange: function (v) { ctx.updateSupplier({ address: v }); } + }))), + col4(formItem('区域', false, React.createElement(Input, { + className: inputCls, + value: ctx.supplier.region, + disabled: true, + placeholder: '根据省市自动关联' + }))) + ), + formRow( + col4(formItem('纳税人识别号', false, React.createElement(Input, { + className: inputCls, + value: ctx.supplier.taxId, + placeholder: '请输入纳税人识别号', + onChange: function (e) { ctx.updateSupplier({ taxId: e.target.value }); } + }))), + col4(formItem('注册地址', false, React.createElement(Input, { + className: inputCls, + value: ctx.supplier.invoiceAddress, + placeholder: '请输入注册地址', + onChange: function (e) { ctx.updateSupplier({ invoiceAddress: e.target.value }); } + }))), + col4(formItem('注册电话', false, React.createElement(Input, { + className: inputCls, + value: ctx.supplier.invoicePhone, + placeholder: '手机号或固定电话', + type: 'tel', + onChange: function (e) { ctx.updateSupplier({ invoicePhone: e.target.value }); } + }))) + ), + formRow( + col4(formItem('开户行', false, React.createElement(Input, { + className: inputCls, + value: ctx.supplier.bankName, + placeholder: '请输入开户行', + onChange: function (e) { ctx.updateSupplier({ bankName: e.target.value }); } + }))), + col4(formItem('银行账号', false, React.createElement(Input, { + className: inputCls, + value: ctx.supplier.bankAccount, + placeholder: '请输入银行账号', + onChange: function (e) { ctx.updateSupplier({ bankAccount: e.target.value }); } + }))), + col4(formItem('营业地址', false, React.createElement(Input, { + className: inputCls, + value: ctx.supplier.businessAddress, + placeholder: '请输入营业地址', + onChange: function (e) { ctx.updateSupplier({ businessAddress: e.target.value }); } + }))) + ), + formRow( + col4(formItem('营业执照', true, uploadDragger( + ctx.supplier.businessLicenseFiles, + function (info) { ctx.updateSupplier({ businessLicenseFiles: info.fileList }); }, + '点击或拖拽上传', + '支持 PDF、图片', + false + ))), + col4(formItem('充装许可证', true, uploadDragger( + ctx.supplier.fillingLicenseFiles, + function (info) { ctx.updateSupplier({ fillingLicenseFiles: info.fileList }); }, + '点击或拖拽上传', + '支持 PDF、图片', + false + ))) + ) + ) + : null + ) + ) + ), + + React.createElement('footer', { className: 'h2-create-footer' }, + React.createElement('div', { className: 'h2-create-footer-inner' }, + React.createElement('div', { className: 'h2-create-footer-hint' }, + React.createElement(Progress, { + className: 'h2-create-footer-progress', + type: 'line', + percent: progressPct, + strokeColor: { from: '#34d399', to: '#059669' }, + trailColor: '#e2e8f0', + size: 'small', + 'aria-label': '表单完成度 ' + progressPct + '%' + }), + React.createElement('span', null, progressPct >= 100 ? '必填项已就绪,可提交' : '请完善必填项后提交(' + progressPct + '%)') + ), + React.createElement('div', { className: 'h2-create-footer-actions' }, + React.createElement(Button, { + onClick: ctx.onCancel, + disabled: ctx.submitting, + 'aria-label': '取消并返回列表' + }, '取消'), + React.createElement(Button, { + onClick: ctx.onReset, + disabled: ctx.submitting, + 'aria-label': '重置表单' + }, '重置'), + React.createElement(Button, { + type: 'primary', + onClick: ctx.onSubmit, + loading: ctx.submitting, + style: H2_PRIMARY_BTN_STYLE, + 'aria-label': '提交新建加氢站点' + }, '提交创建') + ) + ) + ) + ); +} + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + var useCallback = React.useCallback; + + var antd = window.antd; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Table = antd.Table; + var Button = antd.Button; + var Select = antd.Select; + var Input = antd.Input; + var Space = antd.Space; + var Modal = antd.Modal; + var Drawer = antd.Drawer; + var Form = antd.Form; + var Switch = antd.Switch; + var Upload = antd.Upload; + var Tag = antd.Tag; + var Tooltip = antd.Tooltip; + var Divider = antd.Divider; + var Alert = antd.Alert; + var Dropdown = antd.Dropdown; + var Cascader = antd.Cascader; + var Radio = antd.Radio; + var message = antd.message; + + var listState = useState(H2_MOCK_STATIONS.slice()); + var listData = listState[0]; + var setListData = listState[1]; + + var categoryTabState = useState('all'); + var categoryTab = categoryTabState[0]; + var setCategoryTab = categoryTabState[1]; + + var listFiltersState = useState(h2EmptyListFilters()); + var listFilters = listFiltersState[0]; + var setListFilters = listFiltersState[1]; + + var appliedFiltersState = useState(h2EmptyListFilters()); + var appliedFilters = appliedFiltersState[0]; + var setAppliedFilters = appliedFiltersState[1]; + + var drawerState = useState({ open: false, mode: 'create', record: null }); + var drawer = drawerState[0]; + var setDrawer = drawerState[1]; + + var formState = useState(h2CreateEmptyForm()); + var form = formState[0]; + var setForm = formState[1]; + + var deleteModalState = useState({ open: false, record: null }); + var deleteModal = deleteModalState[0]; + var setDeleteModal = deleteModalState[1]; + + var requirementModalState = useState(false); + var requirementModalOpen = requirementModalState[0]; + var setRequirementModalOpen = requirementModalState[1]; + + var importModalState = useState(false); + var importModalOpen = importModalState[0]; + var setImportModalOpen = importModalState[1]; + + var importFileListState = useState([]); + var importFileList = importFileListState[0]; + var setImportFileList = importFileListState[1]; + + var importPreviewState = useState(null); + var importPreview = importPreviewState[0]; + var setImportPreview = importPreviewState[1]; + + var pageState = useState(1); + var page = pageState[0]; + var setPage = pageState[1]; + + var pageSizeState = useState(10); + var pageSize = pageSizeState[0]; + var setPageSize = pageSizeState[1]; + + var businessModalState = useState({ open: false, record: null, businessStatus: '营业中', businessHours: '', originBusinessStatus: '营业中', statusLogs: [] }); + var businessModal = businessModalState[0]; + var setBusinessModal = businessModalState[1]; + + var priceModalState = useState({ open: false, record: null, costUnitPrice: '', customerUnitPrice: '' }); + var priceModal = priceModalState[0]; + var setPriceModal = priceModalState[1]; + + var refuelModalState = useState({ open: false, station: null, records: [] }); + var refuelModal = refuelModalState[0]; + var setRefuelModal = refuelModalState[1]; + + var statementModalState = useState({ open: false, record: null, period: '2026-05' }); + var statementModal = statementModalState[0]; + var setStatementModal = statementModalState[1]; + + var prepaidBalanceDrillState = useState({ open: false, stationName: '', endingBalance: 0, rows: [] }); + var prepaidBalanceDrill = prepaidBalanceDrillState[0]; + var setPrepaidBalanceDrill = prepaidBalanceDrillState[1]; + + var subViewState = useState('list'); + var subView = subViewState[0]; + var setSubView = subViewState[1]; + + var createStationState = useState(h2CreateEmptyStationForm()); + var createStation = createStationState[0]; + var setCreateStation = createStationState[1]; + + var createSupplierModeState = useState('new'); + var createSupplierMode = createSupplierModeState[0]; + var setCreateSupplierMode = createSupplierModeState[1]; + + var createLinkedSupplierIdState = useState(undefined); + var createLinkedSupplierId = createLinkedSupplierIdState[0]; + var setCreateLinkedSupplierId = createLinkedSupplierIdState[1]; + + var createSupplierState = useState(h2CreateEmptySupplierForm()); + var createSupplier = createSupplierState[0]; + var setCreateSupplier = createSupplierState[1]; + + var createSubmittingState = useState(false); + var createSubmitting = createSubmittingState[0]; + var setCreateSubmitting = createSubmittingState[1]; + + var createSupplierReadonly = createSupplierMode === 'link' && !!createLinkedSupplierId; + + var readOnly = drawer.mode === 'view'; + + var existingNameMap = useMemo(function () { + var map = {}; + listData.forEach(function (r) { + if (r.name) map[String(r.name).trim()] = true; + }); + return map; + }, [listData]); + + var categoryCounts = useMemo(function () { + var counts = { all: listData.length, high: 0, low: 0, none: 0 }; + listData.forEach(function (r) { + var stats = h2CalcRefuelStats(r.name); + var k = h2DeriveFrequencyByRefuelCount(stats.count); + if (counts[k] != null) counts[k] += 1; + }); + return counts; + }, [listData]); + + var signedStats = useMemo(function () { + var signed = 0; + listData.forEach(function (r) { if (r.isSigned) signed += 1; }); + return { signed: signed, unsigned: listData.length - signed }; + }, [listData]); + + var filteredList = useMemo(function () { + var list = listData.slice(); + if (categoryTab !== 'all') { + list = list.filter(function (r) { + var stats = h2CalcRefuelStats(r.name); + return h2DeriveFrequencyByRefuelCount(stats.count) === categoryTab; + }); + } + var kw = (appliedFilters.name || '').trim().toLowerCase(); + if (kw) list = list.filter(function (r) { return (r.name || '').toLowerCase().indexOf(kw) !== -1; }); + if (appliedFilters.signed === 'yes') list = list.filter(function (r) { return r.isSigned; }); + if (appliedFilters.signed === 'no') list = list.filter(function (r) { return !r.isSigned; }); + if (appliedFilters.region && appliedFilters.region.length) { + list = list.filter(function (r) { return h2MatchRegionFilter(r.region, appliedFilters.region); }); + } + if (appliedFilters.businessStatus) { + list = list.filter(function (r) { return r.businessStatus === appliedFilters.businessStatus; }); + } + return list.map(function (r) { + var stats = h2CalcRefuelStats(r.name); + return Object.assign({}, r, { + refuelCount: stats.count, + refuelTotalKg: stats.totalKg, + refuelFreqKey: h2DeriveFrequencyByRefuelCount(stats.count) + }); + }); + }, [listData, categoryTab, appliedFilters]); + + var totalCount = filteredList.length; + + var displayList = useMemo(function () { + var start = (page - 1) * pageSize; + return filteredList.slice(start, start + pageSize); + }, [filteredList, page, pageSize]); + + var tablePagination = useMemo(function () { + return { + current: page, + pageSize: pageSize, + total: totalCount, + showSizeChanger: true, + pageSizeOptions: ['5', '10', '20', '50'], + showTotal: function (t) { return '共 ' + t + ' 条'; }, + onChange: function (p, size) { + setPage(p); + if (size && size !== pageSize) setPageSize(size); + } + }; + }, [page, pageSize, totalCount]); + + var handleKpiCardClick = useCallback(function (key) { + setCategoryTab(key); + setPage(1); + }, []); + + var handleSignedFilterCardClick = useCallback(function (key) { + var nextSigned = appliedFilters.signed === key ? undefined : key; + setListFilters(function (p) { return Object.assign({}, p, { signed: nextSigned }); }); + setAppliedFilters(function (p) { return Object.assign({}, p, { signed: nextSigned }); }); + setPage(1); + }, [appliedFilters.signed]); + + var handleListFilterQuery = useCallback(function () { + setAppliedFilters({ + name: listFilters.name, + signed: listFilters.signed, + region: listFilters.region, + businessStatus: listFilters.businessStatus + }); + setPage(1); + }, [listFilters]); + + var handleListFilterReset = useCallback(function () { + var empty = h2EmptyListFilters(); + setListFilters(empty); + setAppliedFilters(empty); + setPage(1); + }, []); + + var renderFilterField = useCallback(function (label, control) { + return React.createElement('div', { className: 'lc-filter-field' }, + React.createElement('span', { className: 'lc-filter-field-label' }, label), + React.createElement('div', { className: 'lc-filter-field-control' }, control) + ); + }, []); + + var renderAlertStatCard = useCallback(function (card, options) { + var active = options.active; + var count = options.count; + var onClick = options.onClick; + var icon = options.icon; + return React.createElement('div', { + key: card.key, + role: 'button', + tabIndex: 0, + className: 'lc-alert-card lc-alert-card--' + card.type + ' lc-alert-card-clickable' + (active ? ' lc-alert-card-active' : ''), + onClick: onClick, + onKeyDown: function (e) { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + onClick(); + } + }, + 'aria-pressed': active, + 'aria-label': card.title + ',共 ' + count + ' 个' + }, + React.createElement('div', { className: 'lc-alert-card-tip-anchor' }, + React.createElement(Tooltip, { title: card.desc, placement: 'topRight', overlayStyle: { maxWidth: 320 } }, + React.createElement('span', { + className: 'lc-alert-card-tip', + role: 'img', + 'aria-label': card.title + '说明', + onClick: function (e) { e.stopPropagation(); }, + onMouseDown: function (e) { e.stopPropagation(); } + }, + h2SvgIcon([{ tag: 'circle', cx: 12, cy: 12, r: 10 }, { tag: 'line', x1: 12, y1: 16, x2: 12, y2: 12 }, { tag: 'line', x1: 12, y1: 8, x2: 12.01, y2: 8 }], 12) + ) + ) + ), + React.createElement('div', { className: 'lc-alert-card-icon' }, icon), + React.createElement('div', { className: 'lc-alert-card-main' }, + React.createElement('div', { className: 'lc-alert-card-val' }, count), + React.createElement('div', { className: 'lc-alert-card-title' }, card.title) + ) + ); + }, []); + + var resetCreateForm = useCallback(function () { + setCreateStation(h2CreateEmptyStationForm()); + setCreateSupplierMode('new'); + setCreateLinkedSupplierId(undefined); + setCreateSupplier(h2CreateEmptySupplierForm()); + }, []); + + var openCreate = useCallback(function () { + resetCreateForm(); + setSubView('create'); + }, [resetCreateForm]); + + var handleCreatePageBack = useCallback(function () { + setCreateSubmitting(false); + setSubView('list'); + }, []); + + var handleCreateCancel = useCallback(function () { + if (createSubmitting) return; + if (h2CreateFormDirty(createStation, createSupplier, createSupplierMode, createLinkedSupplierId)) { + Modal.confirm({ + title: '放弃未保存的更改?', + content: '当前填写内容尚未提交,返回列表后将丢失。', + okText: '放弃并返回', + cancelText: '继续填写', + okButtonProps: { danger: true }, + centered: true, + onOk: handleCreatePageBack + }); + return; + } + handleCreatePageBack(); + }, [createSubmitting, createStation, createSupplier, createSupplierMode, createLinkedSupplierId, handleCreatePageBack]); + + var updateCreateStation = useCallback(function (patch) { + setCreateStation(function (prev) { return Object.assign({}, prev, patch); }); + }, []); + + var updateCreateSupplier = useCallback(function (patch) { + setCreateSupplier(function (prev) { return Object.assign({}, prev, patch); }); + }, []); + + var handleCreateSupplierModeChange = useCallback(function (e) { + setCreateSupplierMode(e.target.value); + setCreateLinkedSupplierId(undefined); + setCreateSupplier(h2CreateEmptySupplierForm()); + }, []); + + var handleCreateLinkSupplierChange = useCallback(function (id) { + setCreateLinkedSupplierId(id); + var found = H2_MOCK_EXISTING_SUPPLIERS.filter(function (s) { return s.id === id; })[0]; + setCreateSupplier(h2SupplierToForm(found)); + }, []); + + var handleCreateSupplierCityChange = useCallback(function (v) { + updateCreateSupplier({ + city: v, + region: v && v[0] ? h2GetRegionByProvince(v[0]) : '' + }); + }, [updateCreateSupplier]); + + var syncCreateSupplierFromStation = useCallback(function () { + if (createSupplierMode !== 'new') return; + setCreateSupplier(function (supplier) { + var patch = {}; + if (createStation.name && !supplier.name) patch.name = createStation.name; + if (createStation.address && createStation.address.region && createStation.address.region.length >= 2) { + patch.city = createStation.address.region.slice(); + patch.region = h2GetRegionByProvince(createStation.address.region[0]); + } + if (createStation.address && createStation.address.detail && !supplier.address) patch.address = createStation.address.detail; + if (createStation.contact && !supplier.contactName) patch.contactName = createStation.contact; + if (createStation.mobilePhone && !supplier.contactMobile) patch.contactMobile = createStation.mobilePhone; + return Object.keys(patch).length ? Object.assign({}, supplier, patch) : supplier; + }); + }, [createSupplierMode, createStation]); + + var validateCreateForm = useCallback(function () { + if (!(createStation.name || '').trim()) { + message.warning('请填写加氢站名称'); + return false; + } + if (!createStation.address.region || createStation.address.region.length < 2) { + message.warning('请选择加氢站地址的省 / 市'); + return false; + } + if (!(createStation.address.detail || '').trim()) { + message.warning('请填写加氢站详细地址'); + return false; + } + if (!(createStation.contact || '').trim()) { + message.warning('请填写联系人'); + return false; + } + if (!h2StationContactPhone(createStation)) { + message.warning('请填写联系电话'); + return false; + } + if (createStation.isSigned) { + if (!createStation.contractStart || !createStation.contractEnd) { + message.warning('签约站点请填写签约开始与结束时间'); + return false; + } + } + var hoursParsed = h2ParseBusinessHours(createStation.businessHours); + if (hoursParsed.mode === 'custom') { + if (!(hoursParsed.start || '').trim() || !(hoursParsed.end || '').trim()) { + message.warning('非全天营业请选择开始与结束时间'); + return false; + } + if (!/^(\d{1,2}):(\d{2})$/.test(h2NormalizeTimeText(hoursParsed.start)) || !/^(\d{1,2}):(\d{2})$/.test(h2NormalizeTimeText(hoursParsed.end))) { + message.warning('营业时间格式须为 HH:mm'); + return false; + } + } + if (createSupplierMode === 'link' && !createLinkedSupplierId) { + message.warning('请选择要关联的已有供应商'); + return false; + } + if (createSupplierMode === 'new') { + if (!(createSupplier.name || '').trim()) { + message.warning('请填写供应商名称'); + return false; + } + if (!createSupplier.businessLicenseFiles || !createSupplier.businessLicenseFiles.length) { + message.warning('请上传营业执照'); + return false; + } + if (!createSupplier.fillingLicenseFiles || !createSupplier.fillingLicenseFiles.length) { + message.warning('请上传充装许可证'); + return false; + } + } + return true; + }, [createStation, createSupplierMode, createLinkedSupplierId, createSupplier]); + + var handleCreatePageSubmit = useCallback(function (payload) { + var station = payload && payload.station; + if (!station) return; + var nextId = listData.reduce(function (m, r) { return Math.max(m, r.id || 0); }, 0) + 1; + var created = Object.assign({}, station, { id: nextId }); + setListData(function (prev) { return [created].concat(prev); }); + setPage(1); + setSubView('list'); + var supplierHint = payload.supplierMode === 'link' ? '已关联已有供应商' : '已同步创建加氢站类型供应商'; + message.success('站点已创建;' + supplierHint + '(原型)'); + }, [listData]); + + var handleCreateSubmit = useCallback(function () { + if (createSubmitting) return; + if (!validateCreateForm()) return; + setCreateSubmitting(true); + syncCreateSupplierFromStation(); + var now = h2OperateTimestamp(); + var phoneDisplay = createStation.mobilePhone || createStation.landlinePhone || ''; + if (createStation.mobilePhone && createStation.landlinePhone) phoneDisplay = createStation.mobilePhone; + var stationRecord = { + name: (createStation.name || '').trim(), + region: createStation.address.region || [], + addressDetail: (createStation.address.detail || '').trim(), + fullAddress: h2BuildFullAddress(createStation.address.region, createStation.address.detail), + isSigned: !!createStation.isSigned, + contractStart: createStation.isSigned ? createStation.contractStart : '', + contractEnd: createStation.isSigned ? createStation.contractEnd : '', + contractFiles: (createStation.contractFiles || []).map(function (f) { + return { uid: f.uid, name: f.name || '合同附件.pdf', url: f.url || '' }; + }), + businessStatus: createStation.businessStatus || '营业中', + businessHours: (createStation.businessHours || '').trim(), + contact: (createStation.contact || '').trim(), + phone: phoneDisplay, + mobilePhone: (createStation.mobilePhone || '').trim(), + landlinePhone: (createStation.landlinePhone || '').trim(), + costUnitPrice: createStation.costUnitPrice ? parseFloat(createStation.costUnitPrice) : undefined, + customerUnitPrice: createStation.customerUnitPrice ? parseFloat(createStation.customerUnitPrice) : undefined, + prepaidBalance: 0, + businessStatusLogs: [], + updateTime: now, + supplierMode: createSupplierMode, + linkedSupplierId: createSupplierMode === 'link' ? createLinkedSupplierId : undefined, + supplier: Object.assign({ type: '加氢站' }, createSupplier) + }; + window.setTimeout(function () { + handleCreatePageSubmit({ station: stationRecord, supplier: createSupplier, supplierMode: createSupplierMode }); + setCreateSubmitting(false); + }, 380); + }, [createSubmitting, validateCreateForm, syncCreateSupplierFromStation, createStation, createSupplier, createSupplierMode, createLinkedSupplierId, handleCreatePageSubmit]); + + var handleCreateReset = useCallback(function () { + resetCreateForm(); + message.info('表单已重置'); + }, [resetCreateForm]); + + var openRefuelModal = useCallback(function (record) { + setRefuelModal({ + open: true, + station: record, + records: h2GetRefuelRecordsByStation(record.name) + }); + }, []); + + var closeRefuelModal = useCallback(function () { + setRefuelModal({ open: false, station: null, records: [] }); + }, []); + + var openPrepaidBalanceDrill = useCallback(function (record) { + setPrepaidBalanceDrill({ + open: true, + stationName: record.name || '', + endingBalance: h2NumOrZero(record.prepaidBalance), + rows: h2BuildMockPrepaidBalanceRows(record) + }); + }, []); + + var closePrepaidBalanceDrill = useCallback(function () { + setPrepaidBalanceDrill({ open: false, stationName: '', endingBalance: 0, rows: [] }); + }, []); + + var openStatementModal = useCallback(function (record) { + setStatementModal({ open: true, record: record, period: '2026-05' }); + }, []); + + var closeStatementModal = useCallback(function () { + setStatementModal({ open: false, record: null, period: '2026-05' }); + }, []); + + var handleGenerateStatement = useCallback(function () { + if (!statementModal.record) return; + if (!statementModal.period) { + message.warning('请选择对账周期'); + return; + } + var periodLabel = statementModal.period.replace('-', '年') + '月'; + message.success('已生成「' + statementModal.record.name + '」' + periodLabel + '氢费对账单(原型)'); + closeStatementModal(); + }, [statementModal, closeStatementModal]); + + var openEdit = useCallback(function (record) { + setForm(h2RecordToForm(record)); + setDrawer({ open: true, mode: 'edit', record: record }); + }, []); + + var openView = useCallback(function (record) { + setForm(h2RecordToForm(record)); + setDrawer({ open: true, mode: 'view', record: record }); + }, []); + + var openBusinessSetting = useCallback(function (record) { + setBusinessModal({ + open: true, + record: record, + businessStatus: record.businessStatus || '营业中', + businessHours: record.businessHours === '—' ? '' : (record.businessHours || ''), + originBusinessStatus: record.businessStatus || '营业中', + statusLogs: (record.businessStatusLogs || []).slice() + }); + }, []); + + var closeBusinessModal = useCallback(function () { + setBusinessModal({ open: false, record: null, businessStatus: '营业中', businessHours: '', originBusinessStatus: '营业中', statusLogs: [] }); + }, []); + + var handleSaveBusinessSetting = useCallback(function () { + if (!businessModal.record) return; + var nextStatus = businessModal.businessStatus || '营业中'; + var statusChanged = nextStatus !== businessModal.originBusinessStatus; + var newLog = statusChanged + ? h2CreateBusinessStatusLog(businessModal.originBusinessStatus, nextStatus, H2_CURRENT_OPERATOR) + : null; + setListData(function (prev) { + return prev.map(function (r) { + if (r.id !== businessModal.record.id) return r; + var logs = (r.businessStatusLogs || []).slice(); + if (newLog) logs.unshift(newLog); + return Object.assign({}, r, { + businessStatus: nextStatus, + businessHours: (businessModal.businessHours || '').trim() || '—', + businessStatusLogs: logs, + updateTime: h2OperateTimestamp() + }); + }); + }); + message.success(statusChanged ? '营业状态已更新(原型)' : '营业时间已保存(原型)'); + closeBusinessModal(); + }, [businessModal, closeBusinessModal]); + + var openPriceConfig = useCallback(function (record) { + setPriceModal({ + open: true, + record: record, + costUnitPrice: record.costUnitPrice != null ? String(record.costUnitPrice) : '', + customerUnitPrice: record.customerUnitPrice != null ? String(record.customerUnitPrice) : '' + }); + }, []); + + var closePriceModal = useCallback(function () { + setPriceModal({ open: false, record: null, costUnitPrice: '', customerUnitPrice: '' }); + }, []); + + var handleSavePriceConfig = useCallback(function () { + if (!priceModal.record) return; + var cost = parseFloat(priceModal.costUnitPrice); + var customer = parseFloat(priceModal.customerUnitPrice); + if (priceModal.costUnitPrice !== '' && isNaN(cost)) { + message.warning('请输入有效的成本单价'); + return; + } + if (priceModal.customerUnitPrice !== '' && isNaN(customer)) { + message.warning('请输入有效的对客单价'); + return; + } + setListData(function (prev) { + return prev.map(function (r) { + if (r.id !== priceModal.record.id) return r; + return Object.assign({}, r, { + costUnitPrice: priceModal.costUnitPrice === '' ? null : cost, + customerUnitPrice: priceModal.customerUnitPrice === '' ? null : customer, + updateTime: new Date().toISOString().slice(0, 16).replace('T', ' ') + }); + }); + }); + message.success('价格配置已保存(原型)'); + closePriceModal(); + }, [priceModal, closePriceModal]); + + var getRowMoreMenuItems = useCallback(function (record) { + return [ + { key: 'edit', label: '编辑', onClick: function () { openEdit(record); } }, + { key: 'business', label: '营业状态', onClick: function () { openBusinessSetting(record); } }, + { key: 'price', label: '价格配置', onClick: function () { openPriceConfig(record); } }, + { key: 'statement', label: '生成对账单', onClick: function () { openStatementModal(record); } }, + { type: 'divider' }, + { key: 'delete', label: '删除', danger: true, onClick: function () { setDeleteModal({ open: true, record: record }); } } + ]; + }, [openEdit, openBusinessSetting, openPriceConfig, openStatementModal]); + + var closeDrawer = useCallback(function () { + setDrawer({ open: false, mode: 'create', record: null }); + }, []); + + var validateForm = useCallback(function () { + if (!(form.name || '').trim()) { + message.warning('请填写加氢站名称'); + return false; + } + if (!form.address.region || form.address.region.length < 2) { + message.warning('请选择省 / 市'); + return false; + } + if (!(form.address.detail || '').trim()) { + message.warning('请填写详细地址'); + return false; + } + if (!(form.contact || '').trim()) { + message.warning('请填写联系人'); + return false; + } + if (!(form.phone || '').trim()) { + message.warning('请填写联系电话'); + return false; + } + if (form.isSigned) { + if (!form.contractStart || !form.contractEnd) { + message.warning('已签约站点请填写签约开始与结束时间'); + return false; + } + } + return true; + }, [form]); + + var handleSave = useCallback(function () { + if (!validateForm()) return; + if (!drawer.record) return; + var updated = h2FormToRecord(form, drawer.record.id); + updated.businessStatus = drawer.record.businessStatus || '营业中'; + updated.costUnitPrice = drawer.record.costUnitPrice; + updated.customerUnitPrice = drawer.record.customerUnitPrice; + updated.businessStatusLogs = drawer.record.businessStatusLogs || []; + updated.prepaidBalance = drawer.record.prepaidBalance; + updated.mobilePhone = drawer.record.mobilePhone; + updated.landlinePhone = drawer.record.landlinePhone; + updated.supplier = drawer.record.supplier; + updated.linkedSupplierId = drawer.record.linkedSupplierId; + setListData(function (prev) { + return prev.map(function (r) { return r.id === updated.id ? updated : r; }); + }); + message.success('站点已保存(原型)'); + closeDrawer(); + }, [validateForm, drawer, form, closeDrawer]); + + var closeImportModal = useCallback(function () { + setImportModalOpen(false); + setImportFileList([]); + setImportPreview(null); + }, []); + + var downloadImportTemplate = useCallback(function () { + h2DownloadCsv('加氢站点批量导入模板.csv', H2_IMPORT_TEMPLATE_HEADERS, H2_IMPORT_TEMPLATE_SAMPLE); + message.success('已下载导入模板'); + }, []); + + var parseImportFile = useCallback(function (file) { + if (!file) return; + var name = String(file.name || '').toLowerCase(); + if (name.endsWith('.xls') || name.endsWith('.xlsx')) { + message.warning('原型环境请使用 CSV 模板导入;联调后将支持 Excel 直接上传'); + setImportFileList([file]); + setImportPreview(null); + return; + } + if (!name.endsWith('.csv')) { + message.error('仅支持 .csv、.xls、.xlsx 格式'); + return; + } + setImportFileList([file]); + var reader = new FileReader(); + reader.onload = function (ev) { + var text = (ev && ev.target && ev.target.result) || ''; + var parsed = h2ParseImportCsv(text); + if (parsed.headerError) { + message.error(parsed.headerError); + setImportPreview(null); + return; + } + if (!parsed.rows.length) { + message.warning('未解析到有效数据,请检查文件内容'); + setImportPreview(null); + return; + } + var nameMap = Object.assign({}, existingNameMap); + var valid = []; + var errors = []; + var ri; + for (ri = 0; ri < parsed.rows.length; ri++) { + var row = parsed.rows[ri]; + var rowErrors = h2ValidateImportRow(row, nameMap); + if (rowErrors.length) { + errors.push({ lineNo: row.lineNo, name: row.name || '—', reasons: rowErrors }); + } else { + valid.push(row); + if (row.name) nameMap[String(row.name).trim()] = true; + } + } + setImportPreview({ valid: valid, errors: errors, fileName: file.name }); + }; + reader.onerror = function () { + message.error('文件读取失败'); + }; + reader.readAsText(file, 'UTF-8'); + }, [existingNameMap]); + + var handleConfirmImport = useCallback(function () { + if (!importPreview || !importPreview.valid.length) { + message.warning('没有可导入的有效数据'); + return; + } + setListData(function (prev) { + var maxId = prev.reduce(function (m, r) { return Math.max(m, r.id || 0); }, 0); + var imported = importPreview.valid.map(function (row, idx) { + return h2ImportRowToRecord(row, maxId + idx + 1); + }); + return imported.concat(prev); + }); + var errN = (importPreview.errors || []).length; + if (errN) { + message.success('成功导入 ' + importPreview.valid.length + ' 条,' + errN + ' 条校验失败已跳过'); + } else { + message.success('成功导入 ' + importPreview.valid.length + ' 条站点'); + } + setPage(1); + closeImportModal(); + }, [importPreview, closeImportModal]); + + React.useEffect(function () { + var maxPage = Math.max(1, Math.ceil(totalCount / pageSize) || 1); + if (page > maxPage) setPage(maxPage); + }, [totalCount, pageSize, page]); + + var renderRefuelFreqShortTag = function (freqKey) { + if (freqKey === 'high') { + return React.createElement(Tag, { + color: 'success', + className: 'lc-refuel-freq-tag' + }, '高频'); + } + if (freqKey === 'low') { + return React.createElement(Tag, { + color: 'warning', + className: 'lc-refuel-freq-tag' + }, '低频'); + } + return null; + }; + + var renderBusinessStatusTag = function (v) { + if (v === '营业中') return React.createElement(Tag, { color: 'success', style: { borderRadius: 6, fontWeight: 600 } }, v); + if (v === '暂停营业') return React.createElement(Tag, { color: 'warning', style: { borderRadius: 6, fontWeight: 600 } }, v); + if (v === '停止营业') return React.createElement(Tag, { color: 'error', style: { borderRadius: 6, fontWeight: 600 } }, v); + return React.createElement(Tag, { style: { borderRadius: 6, fontWeight: 600, color: '#64748b', background: '#f1f5f9', border: '1px solid #e2e8f0' } }, v || '—'); + }; + + var renderBusinessStatusText = function (v, tone) { + var color = v === '营业中' ? '#059669' : v === '暂停营业' ? '#ea580c' : v === '停止营业' ? '#dc2626' : '#64748b'; + if (tone === 'muted') color = '#94a3b8'; + return React.createElement('span', { style: { fontWeight: 600, color: color } }, v || '—'); + }; + + var renderRefuelDrillLink = function (text, record, ariaLabel) { + return React.createElement('button', { + type: 'button', + className: 'h2-refuel-drill-link', + onClick: function (e) { + e.stopPropagation(); + openRefuelModal(record); + }, + 'aria-label': ariaLabel + }, text); + }; + + var renderPrepaidBalance = useCallback(function (v, record) { + if (v == null || v === '') return React.createElement('span', { style: { color: '#94a3b8' } }, '—'); + var n = typeof v === 'number' ? v : parseFloat(v); + if (isNaN(n)) return '—'; + if (!record) { + var plainColor = n < 0 ? '#dc2626' : '#0f172a'; + return React.createElement('span', { + style: { fontWeight: 700, color: plainColor, fontVariantNumeric: 'tabular-nums' }, + title: n < 0 ? '预付余额不足,已欠费' : undefined + }, h2FormatYuanNum(n)); + } + var isArrears = n < 0; + var amountStyle = isArrears + ? { color: '#dc2626', fontVariantNumeric: 'tabular-nums' } + : { color: '#059669', fontVariantNumeric: 'tabular-nums' }; + return React.createElement('div', { className: 'h2-prepaid-balance-cell' }, + isArrears ? React.createElement(Tag, { color: 'error', className: 'lc-station-signed-tag' }, '已欠费') : null, + React.createElement('button', { + type: 'button', + className: 'h2-prepaid-balance-amount', + style: amountStyle, + title: '点击查看余额变更明细', + 'aria-label': '查看「' + (record.name || '') + '」余额变更明细', + onClick: function (e) { + e.stopPropagation(); + openPrepaidBalanceDrill(record); + } + }, h2FormatYuanNum(n)) + ); + }, [openPrepaidBalanceDrill]); + + var prepaidBalanceDrillColumns = [ + { title: '加氢站名称', dataIndex: 'stationName', key: 'stationName', width: 220, ellipsis: true }, + { title: '收入金额(元)', dataIndex: 'incomeAmount', key: 'incomeAmount', width: 120, align: 'right', render: function (v) { return h2FormatLedgerMoney(v); } }, + { title: '支出金额(元)', dataIndex: 'expenseAmount', key: 'expenseAmount', width: 120, align: 'right', render: function (v) { return h2FormatLedgerMoney(v); } }, + { + title: '余额(元)', + dataIndex: 'balance', + key: 'balance', + width: 120, + align: 'right', + render: function (v) { + var bn = h2NumOrZero(v); + var style = bn < 0 ? { color: '#dc2626', fontWeight: 600, fontVariantNumeric: 'tabular-nums' } : { fontVariantNumeric: 'tabular-nums' }; + return React.createElement('span', { style: style }, h2FormatYuanNum(v)); + } + }, + { + title: '订单编号', + dataIndex: 'orderNo', + key: 'orderNo', + width: 168, + align: 'center', + render: function (v) { + return React.createElement('span', { style: { fontFamily: 'monospace', fontSize: 12 } }, v || '—'); + } + } + ]; + + var prepaidBalanceDrillSummary = useMemo(function () { + if (!prepaidBalanceDrill.rows || !prepaidBalanceDrill.rows.length) { + return { incomeTotal: 0, expenseTotal: 0, endingBalance: 0 }; + } + var last = prepaidBalanceDrill.rows[prepaidBalanceDrill.rows.length - 1]; + return prepaidBalanceDrill.rows.reduce(function (acc, r) { + acc.incomeTotal += h2NumOrZero(r.incomeAmount); + acc.expenseTotal += h2NumOrZero(r.expenseAmount); + return acc; + }, { incomeTotal: 0, expenseTotal: 0, endingBalance: h2NumOrZero(last.balance) }); + }, [prepaidBalanceDrill.rows]); + + var renderPrepaidBalanceDrillSummary = useCallback(function () { + return React.createElement(Table.Summary, null, + React.createElement(Table.Summary.Row, null, + React.createElement(Table.Summary.Cell, { index: 0, align: 'center' }, '合计'), + React.createElement(Table.Summary.Cell, { index: 1, align: 'right' }, h2FormatYuanNum(prepaidBalanceDrillSummary.incomeTotal)), + React.createElement(Table.Summary.Cell, { index: 2, align: 'right' }, h2FormatYuanNum(prepaidBalanceDrillSummary.expenseTotal)), + React.createElement(Table.Summary.Cell, { index: 3, align: 'right' }, h2FormatYuanNum(prepaidBalanceDrillSummary.endingBalance)), + React.createElement(Table.Summary.Cell, { index: 4 }) + ) + ); + }, [prepaidBalanceDrillSummary]); + + var refuelRecordColumns = [ + { title: '序号', key: 'seq', width: 52, align: 'center', render: function (_, __, index) { return index + 1; } }, + { title: '加氢时间', dataIndex: 'hydrogenTime', key: 'hydrogenTime', width: 168 }, + { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 110 }, + { title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 180, ellipsis: true }, + { title: '加氢量(kg)', dataIndex: 'hydrogenKg', key: 'hydrogenKg', width: 100, align: 'right', render: function (v) { return h2FormatKgNum(v); } }, + { title: '成本单价', dataIndex: 'costUnitPrice', key: 'costUnitPrice', width: 92, align: 'right', render: function (v) { return h2FormatYuanNum(v); } }, + { title: '成本总价', dataIndex: 'costAmount', key: 'costAmount', width: 92, align: 'right', render: function (v) { return h2FormatYuanNum(v); } }, + { title: '加氢单价', dataIndex: 'customerUnitPrice', key: 'customerUnitPrice', width: 92, align: 'right', render: function (v) { return h2FormatYuanNum(v); } }, + { title: '加氢总价', dataIndex: 'customerAmount', key: 'customerAmount', width: 92, align: 'right', render: function (v) { return h2FormatYuanNum(v); } } + ]; + + var refuelModalSummary = useMemo(function () { + var records = refuelModal.records || []; + var totalKg = 0; + var totalCost = 0; + var totalCustomer = 0; + var i; + for (i = 0; i < records.length; i++) { + totalKg += records[i].hydrogenKg || 0; + totalCost += records[i].costAmount || 0; + totalCustomer += records[i].customerAmount || 0; + } + return { count: records.length, totalKg: totalKg, totalCost: totalCost, totalCustomer: totalCustomer }; + }, [refuelModal.records]); + + var renderRefuelTotalsBar = useCallback(function (summary) { + var items = [ + { key: 'count', label: '加氢次数(次)', value: String(summary.count || 0) }, + { key: 'hydrogenKg', label: '加氢量(kg)', value: h2FormatKgNum(summary.totalKg) }, + { key: 'costTotal', label: '成本总价(元)', value: h2FormatYuanNum(summary.totalCost) }, + { key: 'customerAmount', label: '加氢总价(元)', value: h2FormatYuanNum(summary.totalCustomer) } + ]; + return React.createElement('div', { className: 'h2-ledger-totals-bar' }, + React.createElement('div', { className: 'h2-ledger-totals-bar__title' }, '合计'), + React.createElement('div', { className: 'h2-ledger-totals-bar__items' }, + items.map(function (item) { + return React.createElement('div', { key: item.key, className: 'h2-ledger-totals-bar__item' }, + React.createElement('div', { className: 'h2-ledger-totals-bar__label' }, item.label), + React.createElement('div', { className: 'h2-ledger-totals-bar__value' }, item.value) + ); + }) + ) + ); + }, []); + + var businessStatusLogColumns = [ + { title: '操作时间', dataIndex: 'operateTime', key: 'operateTime', width: 150 }, + { title: '操作人', dataIndex: 'operator', key: 'operator', width: 100, ellipsis: true }, + { + title: '修改前状态', + dataIndex: 'beforeStatus', + key: 'beforeStatus', + width: 110, + render: function (v) { return renderBusinessStatusText(v, 'muted'); } + }, + { + title: '修改后状态', + dataIndex: 'afterStatus', + key: 'afterStatus', + width: 110, + render: function (v) { return renderBusinessStatusText(v); } + } + ]; + + var columns = [ + { + title: '加氢站名称', + dataIndex: 'name', + key: 'name', + width: 248, + fixed: 'left', + render: function (v, r) { + return React.createElement('div', { className: 'lc-station-name-row' }, + React.createElement('div', { className: 'lc-station-name' }, v || '—'), + React.createElement(Tag, { + color: r.isSigned ? 'success' : 'default', + className: 'lc-station-signed-tag', + style: r.isSigned ? undefined : { color: '#64748b', background: '#f1f5f9', border: '1px solid #e2e8f0' } + }, r.isSigned ? '已签约' : '未签约') + ); + } + }, + { + title: '地址', + key: 'address', + width: 280, + ellipsis: true, + render: function (_, r) { + return React.createElement('div', null, + React.createElement('div', { className: 'lc-station-region' }, h2FormatRegion(r.region)), + React.createElement('div', { style: { color: '#334155' } }, r.addressDetail || '—') + ); + } + }, + { + title: '签约时间', + key: 'contractRange', + width: 220, + render: function (_, r) { + if (!r.isSigned) return React.createElement('span', { style: { color: '#94a3b8' } }, '—'); + return React.createElement('div', null, + React.createElement('div', { style: { fontSize: 12, fontWeight: 600, color: '#334155' } }, + (r.contractStart || '—') + ' 至 ' + (r.contractEnd || '—') + ), + r.contractEnd ? h2RenderContractRemainTag(r.contractEnd) : null + ); + } + }, + { + title: '营业状态', + dataIndex: 'businessStatus', + key: 'businessStatus', + width: 110, + render: renderBusinessStatusTag + }, + { title: '营业时间', dataIndex: 'businessHours', key: 'businessHours', width: 140, render: function (v) { return h2DisplayBusinessHours(v); } }, + { + title: '加氢次数', + dataIndex: 'refuelCount', + key: 'refuelCount', + width: 96, + align: 'right', + render: function (v, record) { + return renderRefuelDrillLink(String(v || 0), record, '查看加氢记录,共 ' + (v || 0) + ' 次'); + } + }, + { + title: '加氢量(kg)', + dataIndex: 'refuelTotalKg', + key: 'refuelTotalKg', + width: 148, + align: 'right', + render: function (v, record) { + return React.createElement('div', { className: 'lc-refuel-kg-row' }, + renderRefuelFreqShortTag(record.refuelFreqKey), + renderRefuelDrillLink(h2FormatKgNum(v), record, '查看加氢记录,合计 ' + h2FormatKgNum(v) + ' kg') + ); + } + }, + { + title: '预付余额(元)', + dataIndex: 'prepaidBalance', + key: 'prepaidBalance', + width: 188, + align: 'right', + className: 'h2-col-prepaid-balance', + onCell: function () { + return { className: 'h2-cell-prepaid-balance' }; + }, + render: function (v, record) { return renderPrepaidBalance(v, record); } + }, + { + title: '联系方式', + key: 'contactInfo', + width: 120, + render: function (_, r) { + var mobile = r.mobilePhone || r.phone || ''; + var landline = r.landlinePhone || ''; + return React.createElement('div', null, + React.createElement('div', { className: 'lc-station-contact-name' }, r.contact || '—'), + mobile ? React.createElement('div', { className: 'lc-station-contact-phone' }, mobile) : null, + landline ? React.createElement('div', { className: 'lc-station-contact-phone' }, landline) : null, + !mobile && !landline ? React.createElement('div', { className: 'lc-station-contact-phone' }, '—') : null + ); + } + }, + { + title: '操作', + key: 'action', + width: 108, + fixed: 'right', + render: function (_, record) { + return React.createElement('div', { className: 'h2-row-actions' }, + React.createElement(Button, { + type: 'link', + size: 'small', + className: 'lc-action-btn', + onClick: function () { openView(record); } + }, '查看'), + React.createElement(Dropdown, { + trigger: ['click'], + placement: 'bottomRight', + menu: { items: getRowMoreMenuItems(record) } + }, + React.createElement(Tooltip, { title: '更多' }, + React.createElement('span', { + className: 'h2-action-more-btn', + role: 'button', + tabIndex: 0, + 'aria-label': '更多操作', + onClick: function (e) { e.stopPropagation(); } + }, h2MoreIcon()) + ) + ) + ); + } + } + ]; + + var formItem = function (label, required, node, extra) { + return React.createElement(Form.Item, { + label: required ? React.createElement('span', null, React.createElement('span', { style: { color: '#ef4444', marginRight: 4 } }, '*'), label) : label, + extra: extra, + style: { marginBottom: 16 } + }, node); + }; + + var drawerSection = function (title, children) { + return React.createElement('div', { style: { marginBottom: 8 } }, + React.createElement('div', { className: 'h2-drawer-section-title' }, title), + children + ); + }; + + var drawerTitle = drawer.mode === 'edit' ? '编辑加氢站点' : '查看加氢站点'; + + var createViewNode = subView === 'create' ? h2BuildStationCreateView({ + station: createStation, + supplier: createSupplier, + supplierMode: createSupplierMode, + linkedSupplierId: createLinkedSupplierId, + supplierReadonly: createSupplierReadonly, + updateStation: updateCreateStation, + updateSupplier: updateCreateSupplier, + syncSupplierFromStation: syncCreateSupplierFromStation, + handleSupplierModeChange: handleCreateSupplierModeChange, + handleLinkSupplierChange: handleCreateLinkSupplierChange, + handleSupplierCityChange: handleCreateSupplierCityChange, + onCancel: handleCreateCancel, + onReset: handleCreateReset, + onSubmit: handleCreateSubmit, + submitting: createSubmitting, + onOpenRequirement: function () { setRequirementModalOpen(true); }, + reqIcon: H2_ICONS.doc, + reqBtnStyle: H2_REQ_BTN_STYLE + }) : null; + + var emptyNode = React.createElement('div', { style: { padding: '40px 0', textAlign: 'center' } }, + H2_ICONS.empty, + React.createElement('div', { style: { color: '#94a3b8', marginTop: 12 } }, '暂无符合检索条件的站点') + ); + + return React.createElement('div', { + className: 'h2-station-page lc-edit-page' + (subView === 'create' ? ' h2-station-page--create' : '') + }, + React.createElement('style', null, H2_PAGE_STYLE), + createViewNode, + subView === 'list' ? React.createElement('div', { style: { display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' } }, + React.createElement('div', { style: { marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 12 } }, + React.createElement(Breadcrumb, { items: [{ title: '加氢站管理' }, { title: '站点信息' }] }), + React.createElement(Button, { + type: 'default', + icon: H2_ICONS.doc, + style: H2_REQ_BTN_STYLE, + onClick: function () { setRequirementModalOpen(true); }, + 'aria-label': '查看需求说明' + }, '查看需求说明') + ), + + React.createElement(Card, { className: 'lc-filter-card', title: '筛选条件', bordered: false }, + React.createElement('div', { className: 'lc-filter-grid' }, + renderFilterField('加氢站名称', React.createElement(Input, { + placeholder: '请输入加氢站名称', + value: listFilters.name, + onChange: function (e) { setListFilters(function (p) { return Object.assign({}, p, { name: e.target.value }); }); }, + onPressEnter: handleListFilterQuery, + allowClear: true, + style: { borderRadius: 8 } + })), + renderFilterField('是否签约', React.createElement(Select, { + placeholder: '全部', + allowClear: true, + value: listFilters.signed, + onChange: function (v) { setListFilters(function (p) { return Object.assign({}, p, { signed: v }); }); }, + options: [{ value: 'yes', label: '已签约' }, { value: 'no', label: '未签约' }], + style: { width: '100%' }, + dropdownStyle: { borderRadius: 8 } + })), + renderFilterField('地区(省-市)', React.createElement(Cascader, { + options: H2_REGION_CASCADER_OPTIONS, + value: listFilters.region && listFilters.region.length ? listFilters.region : undefined, + onChange: function (v) { setListFilters(function (p) { return Object.assign({}, p, { region: v || undefined }); }); }, + placeholder: '请选择省 / 市', + allowClear: true, + style: { width: '100%', borderRadius: 8 }, + changeOnSelect: true + })), + renderFilterField('营业状态', React.createElement(Select, { + placeholder: '全部', + allowClear: true, + value: listFilters.businessStatus, + onChange: function (v) { setListFilters(function (p) { return Object.assign({}, p, { businessStatus: v }); }); }, + options: H2_BUSINESS_STATUS_OPTIONS, + style: { width: '100%' }, + dropdownStyle: { borderRadius: 8 } + })) + ), + React.createElement('div', { className: 'lc-filter-actions' }, + React.createElement(Button, { onClick: handleListFilterReset, style: { borderRadius: 8 } }, '重置'), + React.createElement(Button, { type: 'primary', onClick: handleListFilterQuery, style: H2_PRIMARY_BTN_STYLE }, '查询') + ) + ), + + React.createElement('div', { className: 'lc-alert-stats-row' }, + renderAlertStatCard(H2_STATION_KPI_CARDS[0], { + active: categoryTab === 'all', + count: categoryCounts.all || 0, + onClick: function () { handleKpiCardClick('all'); }, + icon: h2KpiIcon('all') + }), + H2_SIGNED_FILTER_CARDS.map(function (card) { + return renderAlertStatCard(card, { + active: appliedFilters.signed === card.key, + count: card.key === 'yes' ? signedStats.signed : signedStats.unsigned, + onClick: function () { handleSignedFilterCardClick(card.key); }, + icon: h2SignedFilterIcon(card.key) + }); + }), + H2_STATION_KPI_CARDS.slice(1).map(function (card) { + return renderAlertStatCard(card, { + active: categoryTab === card.key, + count: categoryCounts[card.key] || 0, + onClick: function () { handleKpiCardClick(card.key); }, + icon: h2KpiIcon(card.key) + }); + }) + ), + + React.createElement('div', { className: 'lc-table-section' }, + React.createElement('div', { className: 'lc-table-toolbar' }, + React.createElement('div', { className: 'lc-table-toolbar-actions' }, + React.createElement(Button, { + type: 'default', + icon: H2_ICONS.upload, + style: { borderRadius: 8, fontWeight: 600, borderColor: '#10b981', color: '#059669' }, + onClick: function () { setImportModalOpen(true); }, + 'aria-label': '批量导入加氢站点' + }, '批量导入'), + React.createElement(Button, { + type: 'primary', + style: H2_PRIMARY_BTN_STYLE, + onClick: openCreate, + 'aria-label': '新建加氢站点' + }, '新建站点') + ) + ), + React.createElement('div', { className: 'lc-table-card' }, + React.createElement(Table, { + className: 'lc-list-table', + rowKey: 'id', + columns: columns, + dataSource: displayList, + pagination: tablePagination, + size: 'middle', + scroll: { x: 1570 }, + locale: { emptyText: emptyNode } + }) + ) + ) + ) : null, + + React.createElement(Drawer, { + title: drawerTitle, + width: 580, + open: drawer.open, + onClose: closeDrawer, + destroyOnClose: true, + styles: { body: { paddingTop: 8 } }, + footer: readOnly ? React.createElement(Button, { onClick: closeDrawer, style: { borderRadius: 8 } }, '关闭') : React.createElement(Space, null, + React.createElement(Button, { onClick: closeDrawer, style: { borderRadius: 8 } }, '取消'), + React.createElement(Button, { type: 'primary', onClick: handleSave, style: H2_PRIMARY_BTN_STYLE }, '保存') + ) + }, + React.createElement(Form, { layout: 'vertical', requiredMark: false }, + drawerSection('基本信息', + React.createElement(React.Fragment, null, + formItem('加氢站名称', true, React.createElement(Input, { + value: form.name, + disabled: readOnly, + placeholder: '请输入加氢站名称', + maxLength: 80, + style: { borderRadius: 8 }, + onChange: function (e) { setForm(function (f) { return Object.assign({}, f, { name: e.target.value }); }); } + })), + formItem('地址', true, React.createElement(RegionAddressInput, { + value: form.address, + disabled: readOnly, + onChange: function (v) { setForm(function (f) { return Object.assign({}, f, { address: v }); }); } + })), + formItem('营业时间', false, React.createElement(BusinessHoursInput, { + value: form.businessHours, + disabled: readOnly, + fieldStyle: { width: '100%', borderRadius: 8 }, + onChange: function (v) { setForm(function (f) { return Object.assign({}, f, { businessHours: v }); }); } + })) + ) + ), + React.createElement(Divider, { style: { margin: '8px 0 20px' } }), + drawerSection('签约信息', + React.createElement(React.Fragment, null, + formItem('是否签约', false, React.createElement(Switch, { + checked: form.isSigned, + disabled: readOnly, + checkedChildren: '已签约', + unCheckedChildren: '未签约', + onChange: function (v) { setForm(function (f) { return Object.assign({}, f, { isSigned: v }); }); } + })), + formItem('签约开始时间', form.isSigned, React.createElement(Input, { + type: 'date', + style: { width: '100%', borderRadius: 8 }, + disabled: readOnly || !form.isSigned, + value: form.contractStart || '', + onChange: function (e) { setForm(function (f) { return Object.assign({}, f, { contractStart: e.target.value }); }); } + })), + formItem('签约结束时间', form.isSigned, React.createElement(Input, { + type: 'date', + style: { width: '100%', borderRadius: 8 }, + disabled: readOnly || !form.isSigned, + value: form.contractEnd || '', + onChange: function (e) { setForm(function (f) { return Object.assign({}, f, { contractEnd: e.target.value }); }); } + })) + ) + ), + React.createElement(Divider, { style: { margin: '8px 0 20px' } }), + drawerSection('联系信息', + React.createElement(React.Fragment, null, + formItem('联系人', true, React.createElement(Input, { + value: form.contact, + disabled: readOnly, + placeholder: '请输入联系人', + style: { borderRadius: 8 }, + onChange: function (e) { setForm(function (f) { return Object.assign({}, f, { contact: e.target.value }); }); } + })), + formItem('联系电话', true, React.createElement(Input, { + value: form.phone, + disabled: readOnly, + placeholder: '请输入联系电话', + style: { borderRadius: 8 }, + onChange: function (e) { setForm(function (f) { return Object.assign({}, f, { phone: e.target.value }); }); } + })) + ) + ) + ) + ), + + React.createElement(Modal, { + title: '确认删除', + open: deleteModal.open, + centered: true, + onCancel: function () { setDeleteModal({ open: false, record: null }); }, + onOk: function () { + if (deleteModal.record) { + setListData(function (prev) { return prev.filter(function (r) { return r.id !== deleteModal.record.id; }); }); + message.success('已删除'); + } + setDeleteModal({ open: false, record: null }); + }, + okButtonProps: { danger: true }, + okText: '删除', + cancelText: '取消' + }, '确定删除站点「' + ((deleteModal.record && deleteModal.record.name) || '') + '」吗?此操作不可撤销。'), + + React.createElement(Modal, { + className: 'h2-prd-modal', + title: React.createElement('span', { style: { fontWeight: 700 } }, '需求说明 · 站点信息'), + open: requirementModalOpen, + onCancel: function () { setRequirementModalOpen(false); }, + width: 640, + centered: true, + footer: React.createElement(Button, { onClick: function () { setRequirementModalOpen(false); }, style: { borderRadius: 8 } }, '关闭') + }, React.createElement('div', { className: 'h2-prd-content' }, getH2StationRequirementDoc())), + + React.createElement(Modal, { + title: '批量导入加氢站点', + open: importModalOpen, + onCancel: closeImportModal, + width: 560, + centered: true, + destroyOnClose: true, + okText: '确认导入', + cancelText: '取消', + okButtonProps: { + disabled: !importPreview || !importPreview.valid.length, + style: H2_PRIMARY_BTN_STYLE + }, + onOk: handleConfirmImport + }, + React.createElement('div', { className: 'h2-import-template-bar' }, + React.createElement('div', { className: 'h2-import-template-bar-text' }, + React.createElement('div', { style: { fontWeight: 700, marginBottom: 4, color: '#0f172a' } }, '第一步:下载 CSV 导入模板'), + '模板含加氢站名称、省、市、详细地址、是否签约、签约起止时间、营业状态、营业时间、联系人、联系电话;与新建表单字段一致。' + ), + React.createElement(Button, { + type: 'primary', + ghost: true, + style: { borderColor: '#10b981', color: '#059669', fontWeight: 600, flexShrink: 0, borderRadius: 8 }, + onClick: downloadImportTemplate + }, '下载模板') + ), + React.createElement(Alert, { + type: 'info', + showIcon: true, + style: { marginBottom: 14, borderRadius: 10 }, + message: '第二步:填写模板后上传文件', + description: '支持 .csv(推荐)、.xlsx、.xls;原型阶段 Excel 请另存为 CSV UTF-8 后上传。上传后系统将校验并预览可导入条数。' + }), + React.createElement(Upload.Dragger, { + accept: '.csv,.xlsx,.xls', + multiple: false, + maxCount: 1, + fileList: importFileList, + beforeUpload: function (file) { + parseImportFile(file); + return false; + }, + onRemove: function () { + setImportFileList([]); + setImportPreview(null); + return true; + } + }, + React.createElement('p', { style: { margin: '8px 0 4px', fontWeight: 600, color: '#334155' } }, '点击或拖拽文件到此处上传'), + React.createElement('p', { style: { margin: 0, fontSize: 12, color: '#94a3b8' } }, '单次上传一个文件') + ), + importPreview ? React.createElement('div', { className: 'h2-import-preview' }, + React.createElement('div', { style: { fontWeight: 700, marginBottom: 6 } }, '解析结果 · ' + (importPreview.fileName || '')), + React.createElement('div', null, + React.createElement('span', { style: { color: '#059669', fontWeight: 600 } }, '可导入 ' + importPreview.valid.length + ' 条'), + importPreview.errors.length ? React.createElement('span', { style: { marginLeft: 12, color: '#dc2626', fontWeight: 600 } }, '失败 ' + importPreview.errors.length + ' 条') : null + ), + importPreview.errors.length ? React.createElement('ul', { className: 'h2-import-error-list' }, + importPreview.errors.slice(0, 8).map(function (err, ei) { + return React.createElement('li', { key: ei }, + '第 ' + err.lineNo + ' 行「' + err.name + '」:' + err.reasons.join(';') + ); + }).concat( + importPreview.errors.length > 8 + ? [React.createElement('li', { key: 'more' }, '… 另有 ' + (importPreview.errors.length - 8) + ' 条错误未展示')] + : [] + ) + ) : null + ) : null + ), + + React.createElement(Modal, { + title: '营业状态', + open: businessModal.open, + onCancel: closeBusinessModal, + onOk: handleSaveBusinessSetting, + okText: '保存', + cancelText: '取消', + centered: true, + width: 680, + destroyOnClose: true, + okButtonProps: { style: H2_PRIMARY_BTN_STYLE } + }, + React.createElement('div', { style: { marginBottom: 12, fontSize: 13, color: '#64748b' } }, + '站点:', React.createElement('strong', { style: { color: '#0f172a' } }, (businessModal.record && businessModal.record.name) || '—') + ), + formItem('营业状态', true, React.createElement(Select, { + style: { width: '100%' }, + value: businessModal.businessStatus, + options: H2_BUSINESS_STATUS_OPTIONS, + onChange: function (v) { setBusinessModal(function (m) { return Object.assign({}, m, { businessStatus: v }); }); }, + dropdownStyle: { borderRadius: 8 } + })), + formItem('营业时间', false, React.createElement(BusinessHoursInput, { + value: businessModal.businessHours, + fieldStyle: { width: '100%', borderRadius: 8 }, + onChange: function (v) { setBusinessModal(function (m) { return Object.assign({}, m, { businessHours: v }); }); } + }), '暂停或停止营业时可将类型改为非全天并清空时间'), + React.createElement('div', { className: 'h2-status-log-section' }, + React.createElement('div', { className: 'h2-status-log-title' }, '营业状态变更记录'), + React.createElement(Table, { + className: 'h2-status-log-table', + size: 'small', + rowKey: 'id', + columns: businessStatusLogColumns, + dataSource: businessModal.statusLogs || [], + pagination: (businessModal.statusLogs || []).length > 5 ? { pageSize: 5, size: 'small' } : false, + locale: { emptyText: '暂无状态变更记录' }, + scroll: { x: 520 } + }) + ) + ), + + React.createElement(Modal, { + title: '加氢记录' + (refuelModal.station && refuelModal.station.name ? ' · ' + refuelModal.station.name : ''), + open: refuelModal.open, + onCancel: closeRefuelModal, + footer: React.createElement(Button, { onClick: closeRefuelModal, style: { borderRadius: 8 } }, '关闭'), + width: 960, + centered: true, + destroyOnClose: true, + styles: { body: { maxHeight: '72vh', overflow: 'auto', paddingTop: 8 } } + }, + renderRefuelTotalsBar(refuelModalSummary), + React.createElement(Table, { + className: 'h2-refuel-record-table', + size: 'small', + bordered: true, + rowKey: 'id', + columns: refuelRecordColumns, + dataSource: refuelModal.records || [], + pagination: (refuelModal.records || []).length > 8 ? { pageSize: 8, showSizeChanger: false, size: 'small' } : false, + locale: { emptyText: '暂无加氢记录' }, + scroll: { x: 'max-content', y: 360 } + }) + ), + + React.createElement(Modal, { + title: '余额变更明细' + (prepaidBalanceDrill.stationName ? ' · ' + prepaidBalanceDrill.stationName : ''), + open: prepaidBalanceDrill.open, + onCancel: closePrepaidBalanceDrill, + footer: React.createElement(Button, { onClick: closePrepaidBalanceDrill, style: { borderRadius: 8 } }, '关闭'), + width: 880, + centered: true, + destroyOnClose: true, + styles: { body: { maxHeight: '72vh', overflow: 'auto', paddingTop: 8 } } + }, + React.createElement(Table, { + className: 'h2-balance-record-table', + size: 'small', + bordered: true, + rowKey: 'key', + columns: prepaidBalanceDrillColumns, + dataSource: prepaidBalanceDrill.rows || [], + pagination: (prepaidBalanceDrill.rows || []).length > 10 ? { pageSize: 10, showSizeChanger: false, size: 'small' } : false, + summary: renderPrepaidBalanceDrillSummary, + locale: { emptyText: '暂无余额变更记录' }, + scroll: { x: 'max-content' } + }) + ), + + React.createElement(Modal, { + title: '生成对账单', + open: statementModal.open, + onCancel: closeStatementModal, + onOk: handleGenerateStatement, + okText: '生成对账单', + cancelText: '取消', + centered: true, + width: 520, + destroyOnClose: true, + okButtonProps: { style: H2_PRIMARY_BTN_STYLE } + }, + React.createElement('div', { style: { marginBottom: 12, fontSize: 13, color: '#64748b' } }, + '站点:', React.createElement('strong', { style: { color: '#0f172a' } }, (statementModal.record && statementModal.record.name) || '—') + ), + React.createElement(Alert, { + type: 'info', + showIcon: true, + style: { marginBottom: 14, borderRadius: 10 }, + message: '按周期汇总站点加氢明细,生成氢费对账单(对站)', + description: '对账单将汇总该周期内加氢量、成本总价等,提交后可进入审批流程(原型演示)。' + }), + formItem('对账周期', true, React.createElement(Input, { + type: 'month', + style: { width: '100%', borderRadius: 8 }, + value: statementModal.period || '', + onChange: function (e) { setStatementModal(function (m) { return Object.assign({}, m, { period: e.target.value }); }); } + })), + statementModal.record ? React.createElement('div', { style: { fontSize: 12, color: '#64748b', lineHeight: 1.6 } }, + '当前预付余额:', + renderPrepaidBalance(statementModal.record.prepaidBalance), + ';本站点累计加氢 ', + React.createElement('strong', { style: { color: '#0f172a' } }, String(statementModal.record.refuelCount != null ? statementModal.record.refuelCount : h2CalcRefuelStats(statementModal.record.name).count)), + ' 次' + ) : null + ), + + React.createElement(Modal, { + title: '价格配置', + open: priceModal.open, + onCancel: closePriceModal, + onOk: handleSavePriceConfig, + okText: '保存', + cancelText: '取消', + centered: true, + width: 480, + destroyOnClose: true, + okButtonProps: { style: H2_PRIMARY_BTN_STYLE } + }, + React.createElement('div', { style: { marginBottom: 12, fontSize: 13, color: '#64748b' } }, + '站点:', React.createElement('strong', { style: { color: '#0f172a' } }, (priceModal.record && priceModal.record.name) || '—') + ), + formItem('成本单价(元/kg)', false, React.createElement(Input, { + value: priceModal.costUnitPrice, + placeholder: '请输入成本单价', + style: { borderRadius: 8 }, + onChange: function (e) { setPriceModal(function (m) { return Object.assign({}, m, { costUnitPrice: e.target.value }); }); } + })), + formItem('对客单价(元/kg)', false, React.createElement(Input, { + value: priceModal.customerUnitPrice, + placeholder: '请输入对客单价', + style: { borderRadius: 8 }, + onChange: function (e) { setPriceModal(function (m) { return Object.assign({}, m, { customerUnitPrice: e.target.value }); }); } + }), '用于加氢订单结算参考,联调后对接价格体系') + ) + ); +}; + +if (typeof window !== 'undefined') window.Component = Component; diff --git a/web端/台账数据/保险分摊明细.jsx b/web端/台账数据/保险分摊明细.jsx new file mode 100644 index 0000000..138261e --- /dev/null +++ b/web端/台账数据/保险分摊明细.jsx @@ -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 + }) + ) + ) + ) + ); +}; diff --git a/web端/台账数据/氢费采购端汇总报表-需求文档.md b/web端/台账数据/氢费采购端汇总报表-需求文档.md new file mode 100644 index 0000000..72dffc7 --- /dev/null +++ b/web端/台账数据/氢费采购端汇总报表-需求文档.md @@ -0,0 +1,199 @@ +# 氢费(采购端)汇总报表 — 产品需求说明(PRD) + +| 项目 | 内容 | +|------|------| +| 文档版本 | v1.0(业务版) | +| 产品模块 | 台账数据 → 氢费(采购端)汇总报表 | +| 文档类型 | 产品需求说明 | +| 适用读者 | 产品、采购、财务、运营、测试、项目 | + +--- + +## 一、为什么做这件事 + +### 1.1 业务痛点 + +- 采购侧需按加氢站维度掌握氢费规模、应付与实付、开票与预付余额,现有数据分散在车辆氢费明细、打款、开票等模块,缺少一站汇总视图。 +- 财务对账需快速识别「未付金额」「站点欠费」等风险,并下钻到账单、流水、发票凭据。 + +### 1.2 产品价值 + +| 价值点 | 说明 | +|--------|------| +| 站点汇总 | 一行一站,看清加氢量、应付、已付、未付、开票、未开票、当前余额 | +| 对账效率 | 顶部合计条随筛选即时汇总,支撑采购与财务日常核对 | +| 可追溯 | 关键金额可钻取明细,付款凭据、发票支持在线预览与下载 | +| 风险可见 | 未付/未开票金额为正、当前余额为负时突出展示 | + +### 1.3 本期目标 + +建设 Web 端「氢费(采购端)汇总报表」,按地区、加氢站、结算方式查询,展示站点汇总列表及合计;支持多类钻取与 CSV 导出。 + +### 1.4 本期不做 + +- 报表内直接发起打款、开票(跳转或对接后续迭代)。 +- 跨年度历史数据全量迁移方案(本期以 2026 年起统计口径描述为准,上线以数据准备结果为准)。 + +--- + +## 二、谁在用、用来干什么 + +### 2.1 用户角色 + +| 角色 | 典型诉求 | +|------|----------| +| **采购人员** | 按站点查看氢费与支付进度,核对当前余额、欠费站点 | +| **财务人员** | 核对应付/实付/未付/已开票,查看付款证明与发票附件 | +| **运营/主管** | 按地区、结算方式浏览整体规模与异常站点 | + +### 2.2 核心使用场景 + +1. **日常巡检**:按地区或结算方式筛选,查看顶部合计与列表,关注未付、已欠费站点。 +2. **站点下钻**:点击加氢总量、应付总金额等,打开明细弹窗核对构成。 +3. **付款与发票核查**:从已付总金额、已开票金额钻取,预览付款凭据或发票。 +4. **导出报送**:按当前筛选结果导出 CSV。 + +--- + +## 三、页面功能说明 + +### 3.1 页面组成 + +路径:**台账数据 → 氢费(采购端)汇总报表** + +自上而下: + +1. **查询条件区** — 地区、加氢站全称、结算方式 +2. **汇总列表区** — 标题、导出、筛选摘要、顶部合计条、站点表格 +3. **弹窗** — 需求说明、各类钻取明细、附件预览 + +面包屑行右上角提供 **「查看需求说明」**(本文档);汇总列表标题右侧提供 **「导出」**。 + +### 3.2 查询条件 + +| 查询项 | 业务说明 | +|--------|----------| +| 地区 | 加氢站所属省/市,支持搜索;空为全部 | +| 加氢站全称 | 从加氢站主数据选择;空为全部 | +| 结算方式 | 预付 / 月结;空为全部 | + +**交互:** 修改条件后点击 **「查询」** 生效;**「重置」** 清空条件。查询成功给予简短提示。列表上方展示当前筛选摘要(地区、加氢站、结算方式)。 + +### 3.3 顶部合计条 + +表格上方独立展示(随当前列表筛选结果汇总): + +| 合计项 | 计算口径 | +|--------|----------| +| 加氢总量(kg) | 列表各行加氢总量之和 | +| 应付总金额(元) | 列表各行应付总金额之和 | +| 已付总金额(元) | 列表各行已付总金额之和 | +| 未付总金额(元) | **应付总金额合计 − 已付总金额合计**;大于 0 时红色显示 | +| 未开票总金额(元) | **应付总金额合计 − 已开票总金额合计**;大于 0 时红色显示 | +| 充值总额(元) | 列表各行充值总额之和 | +| 当前总余额(元) | 列表各行当前余额之和;合计为负时红色显示 | + +### 3.4 汇总列表字段 + +**列顺序(左→右):** +序号 → 地区 → 加氢站全称 → 结算方式 → 加氢总量(kg) → 应付总金额(元) → 已付总金额(元) → 未付总金额(元) → 已开票金额(元) → 未开票金额(元) → 充值总额(元) → 当前余额(元) + +**表头列宽:** 支持鼠标拖动表头右侧调整列宽(最小宽度限制),便于长站名与金额列阅读。 + +| 字段 | 业务口径 | 交互 | +|------|----------|------| +| 结算方式 | 预付 / 月结 | 只读 | +| 加氢总量(kg) | 自 **2026 年起** 该站全部氢费加氢量之和 | 点击钻取 **加氢量明细**(仅加氢量,不含金额) | +| 应付总金额(元) | 该站「车辆氢费明细」**加氢总价(元)** 求和 | 点击钻取明细(加氢量、成本单价、成本总价) | +| 已付总金额(元) | 加氢站打款管理中 **已支付金额** 求和 | 点击钻取支付账单明细 | +| 未付总金额(元) | **应付总金额 − 已付总金额**(行内计算) | 只读;大于 0 红色 | +| 已开票金额(元) | 该站已开票金额合计 | 点击钻取开票记录 | +| 未开票金额(元) | **应付总金额 − 已开票金额**(行内计算) | 只读;大于 0 红色 | +| 充值总额(元) | 该站预充值(打款入账)金额合计 | 点击钻取充值明细 | +| 当前余额(元) | 2026 年期初余额,扣减 2025 年及以前未扣款记录后的实时余额 | 点击钻取余额变更明细;**为负** 时金额红色 + 左侧 **「已欠费」** 标签 | + +### 3.5 钻取明细说明 + +#### (1)加氢量明细(由加氢总量进入) + +| 列 | 说明 | +|----|------| +| 序号、加氢时间、订单编号、车牌号、加氢量(kg) | 来源于车辆氢费明细,按当前加氢站筛选 | +| 合计 | 加氢量合计 | + +#### (2)应付总金额明细 + +| 列 | 说明 | +|----|------| +| 序号、加氢时间、订单编号、车牌号、加氢量(kg)、成本单价(元/kg)、成本总价(元) | 按站筛选的车辆氢费明细 | +| 合计 | 加氢量、成本总价;弹窗标题含站名 | + +#### (3)已付总金额明细(由已付总金额进入) + +| 列 | 说明 | +|----|------| +| 加氢站全称、账单开始时间、账单结束时间、应付总金额(元)、已付总金额(元)、银行付款证明 | 按账单维度展示 | +| 银行付款证明 | **查看付款凭据** — 预览付款证明图片 | +| 合计 | 应付、实付合计 | + +#### (4)已开票明细(由已开票金额进入) + +| 列 | 说明 | +|----|------| +| 加氢站全称、开票时间、开票金额(元)、发票 | | +| 发票 | **查看发票** — 在线预览 PDF/图片;**下载发票** — 下载 PDF/图片文件 | +| 合计 | 开票金额合计 | + +#### (5)充值总额明细(由充值总额进入) + +| 列 | 说明 | +|----|------| +| 加氢站全称、支付时间、预充金额(元)、付款凭证 | 支付时间为 **YYYY-MM-DD** | +| 付款凭证 | **预览** — 在线查看;**下载** — 下载凭证文件 | +| 合计 | 预充金额(充值总额)合计 | + +#### (6)余额变更明细(由当前余额进入) + +| 列 | 说明 | +|----|------| +| 加氢站全称、收入金额(元)、支出金额(元)、余额(元)、订单编号 | 收入/支出为空显示「—」;余额为负红色 | +| 合计 | 收入合计、支出合计、末行余额(与列表该行当前余额一致) | + +### 3.6 附件预览 + +- 付款凭据、发票在弹窗中预览;PDF 使用文档预览,图片直接展示。 +- 发票支持下载,文件名与开票记录一致。 + +### 3.7 导出 + +- 点击 **「导出」** 下载当前筛选结果 CSV(含列表字段及合计行)。 +- 编码 UTF-8(带 BOM),便于 Excel 打开。 + +--- + +## 四、业务规则摘要 + +| 规则 | 说明 | +|------|------| +| 统计起点 | 加氢总量等业务口径默认 **2026 年起**(与车辆氢费明细、主数据生效规则一致) | +| 未付(行) | 应付总金额 − 已付总金额 | +| 未付(合计条) | 各站未付之和,等价于应付合计 − 已付合计 | +| 未开票(行) | 应付总金额 − 已开票金额 | +| 未开票(合计条) | 各站未开票之和,等价于应付合计 − 已开票合计 | +| 已欠费 | 仅 **当前余额 < 0** 时展示标签 | +| 钻取范围 | 均限定为 **当前汇总行对应加氢站** | + +--- + +## 五、验收要点(业务) + +1. 查询、重置、筛选摘要、合计条与列表数据一致。 +2. 各可点击金额/加氢量钻取弹窗字段与上文一致,合计正确。 +3. 未付、已欠费展示规则符合第四节。 +4. 付款凭据可预览;发票可预览与下载。 +5. 列宽可拖动;导出字段完整。 +6. 「查看需求说明」可打开本 PRD 全文。 + +--- + +**文档结束** diff --git a/web端/台账数据/氢费采购端汇总报表.jsx b/web端/台账数据/氢费采购端汇总报表.jsx new file mode 100644 index 0000000..343c465 --- /dev/null +++ b/web端/台账数据/氢费采购端汇总报表.jsx @@ -0,0 +1,2329 @@ +// 氢费(采购端)汇总报表 PRD(与 氢费采购端汇总报表-需求文档.md 同步) +var H2_PURCHASE_SUMMARY_REQUIREMENT_DOC = `# 氢费(采购端)汇总报表 — 产品需求说明(PRD) + +| 项目 | 内容 | +|------|------| +| 文档版本 | v1.0(业务版) | +| 产品模块 | 台账数据 → 氢费(采购端)汇总报表 | +| 文档类型 | 产品需求说明 | +| 适用读者 | 产品、采购、财务、运营、测试、项目 | + +--- + +## 一、为什么做这件事 + +### 1.1 业务痛点 + +- 采购侧需按加氢站维度掌握氢费规模、应付与实付、开票与预付余额,现有数据分散在车辆氢费明细、打款、开票等模块,缺少一站汇总视图。 +- 财务对账需快速识别「未付金额」「站点欠费」等风险,并下钻到账单、流水、发票凭据。 + +### 1.2 产品价值 + +| 价值点 | 说明 | +|--------|------| +| 站点汇总 | 一行一站,看清加氢量、应付、已付、未付、开票、未开票、当前余额 | +| 对账效率 | 顶部合计条随筛选即时汇总,支撑采购与财务日常核对 | +| 可追溯 | 关键金额可钻取明细,付款凭据、发票支持在线预览与下载 | +| 风险可见 | 未付/未开票金额为正、当前余额为负时突出展示 | + +### 1.3 本期目标 + +建设 Web 端「氢费(采购端)汇总报表」,按地区、加氢站、结算方式查询,展示站点汇总列表及合计;支持多类钻取与 CSV 导出。 + +### 1.4 本期不做 + +- 报表内直接发起打款、开票(跳转或对接后续迭代)。 +- 跨年度历史数据全量迁移方案(本期以 2026 年起统计口径描述为准,上线以数据准备结果为准)。 + +--- + +## 二、谁在用、用来干什么 + +### 2.1 用户角色 + +| 角色 | 典型诉求 | +|------|----------| +| **采购人员** | 按站点查看氢费与支付进度,核对当前余额、欠费站点 | +| **财务人员** | 核对应付/实付/未付/已开票,查看付款证明与发票附件 | +| **运营/主管** | 按地区、结算方式浏览整体规模与异常站点 | + +### 2.2 核心使用场景 + +1. **日常巡检**:按地区或结算方式筛选,查看顶部合计与列表,关注未付、已欠费站点。 +2. **站点下钻**:点击加氢总量、应付总金额等,打开明细弹窗核对构成。 +3. **付款与发票核查**:从已付总金额、已开票金额钻取,预览付款凭据或发票。 +4. **导出报送**:按当前筛选结果导出 CSV。 + +--- + +## 三、页面功能说明 + +### 3.1 页面组成 + +路径:**台账数据 → 氢费(采购端)汇总报表** + +自上而下: + +1. **查询条件区** — 地区、加氢站全称、结算方式 +2. **汇总列表区** — 标题、导出、筛选摘要、顶部合计条、站点表格 +3. **弹窗** — 需求说明、各类钻取明细、附件预览 + +面包屑行右上角提供 **「查看需求说明」**(本文档);汇总列表标题右侧提供 **「导出」**。 + +### 3.2 查询条件 + +| 查询项 | 业务说明 | +|--------|----------| +| 地区 | 加氢站所属省/市,支持搜索;空为全部 | +| 加氢站全称 | 从加氢站主数据选择;空为全部 | +| 结算方式 | 预付 / 月结;空为全部 | + +**交互:** 修改条件后点击 **「查询」** 生效;**「重置」** 清空条件。查询成功给予简短提示。列表上方展示当前筛选摘要(地区、加氢站、结算方式)。 + +### 3.3 顶部合计条 + +表格上方独立展示(随当前列表筛选结果汇总): + +| 合计项 | 计算口径 | +|--------|----------| +| 加氢总量(kg) | 列表各行加氢总量之和 | +| 应付总金额(元) | 列表各行应付总金额之和 | +| 已付总金额(元) | 列表各行已付总金额之和 | +| 未付总金额(元) | **应付总金额合计 − 已付总金额合计**;大于 0 时红色显示 | +| 未开票总金额(元) | **应付总金额合计 − 已开票总金额合计**;大于 0 时红色显示 | +| 充值总额(元) | 列表各行充值总额之和 | +| 当前总余额(元) | 列表各行当前余额之和;合计为负时红色显示 | + +### 3.4 汇总列表字段 + +**列顺序(左→右):** +序号 → 地区 → 加氢站全称 → 结算方式 → 加氢总量(kg) → 应付总金额(元) → 已付总金额(元) → 未付总金额(元) → 已开票金额(元) → 未开票金额(元) → 充值总额(元) → 当前余额(元) + +**表头列宽:** 支持鼠标拖动表头右侧调整列宽(最小宽度限制),便于长站名与金额列阅读。 + +| 字段 | 业务口径 | 交互 | +|------|----------|------| +| 结算方式 | 预付 / 月结 | 只读 | +| 加氢总量(kg) | 自 **2026 年起** 该站全部氢费加氢量之和 | 点击钻取 **加氢量明细**(仅加氢量,不含金额) | +| 应付总金额(元) | 该站「车辆氢费明细」**加氢总价(元)** 求和 | 点击钻取明细(加氢量、成本单价、成本总价) | +| 已付总金额(元) | 加氢站打款管理中 **已支付金额** 求和 | 点击钻取支付账单明细 | +| 未付总金额(元) | **应付总金额 − 已付总金额**(行内计算) | 只读;大于 0 红色 | +| 已开票金额(元) | 该站已开票金额合计 | 点击钻取开票记录 | +| 未开票金额(元) | **应付总金额 − 已开票金额**(行内计算) | 只读;大于 0 红色 | +| 充值总额(元) | 该站预充值(打款入账)金额合计 | 点击钻取充值明细 | +| 当前余额(元) | 2026 年期初余额,扣减 2025 年及以前未扣款记录后的实时余额 | 点击钻取余额变更明细;**为负** 时金额红色 + 左侧 **「已欠费」** 标签 | + +### 3.5 钻取明细说明 + +#### (1)加氢量明细(由加氢总量进入) + +| 列 | 说明 | +|----|------| +| 序号、加氢时间、订单编号、车牌号、加氢量(kg) | 来源于车辆氢费明细,按当前加氢站筛选 | +| 合计 | 加氢量合计 | + +#### (2)应付总金额明细 + +| 列 | 说明 | +|----|------| +| 序号、加氢时间、订单编号、车牌号、加氢量(kg)、成本单价(元/kg)、成本总价(元) | 按站筛选的车辆氢费明细 | +| 合计 | 加氢量、成本总价;弹窗标题含站名 | + +#### (3)已付总金额明细(由已付总金额进入) + +| 列 | 说明 | +|----|------| +| 加氢站全称、账单开始时间、账单结束时间、应付总金额(元)、已付总金额(元)、银行付款证明 | 按账单维度展示 | +| 银行付款证明 | **查看付款凭据** — 预览付款证明图片 | +| 合计 | 应付、实付合计 | + +#### (4)已开票明细(由已开票金额进入) + +| 列 | 说明 | +|----|------| +| 加氢站全称、开票时间、开票金额(元)、发票 | | +| 发票 | **查看发票** — 在线预览 PDF/图片;**下载发票** — 下载 PDF/图片文件 | +| 合计 | 开票金额合计 | + +#### (5)充值总额明细(由充值总额进入) + +| 列 | 说明 | +|----|------| +| 加氢站全称、支付时间、预充金额(元)、付款凭证 | 支付时间为 **YYYY-MM-DD** | +| 付款凭证 | **预览** — 在线查看;**下载** — 下载凭证文件 | +| 合计 | 预充金额(充值总额)合计 | + +#### (6)余额变更明细(由当前余额进入) + +| 列 | 说明 | +|----|------| +| 加氢站全称、收入金额(元)、支出金额(元)、余额(元)、订单编号 | 收入/支出为空显示「—」;余额为负红色 | +| 合计 | 收入合计、支出合计、末行余额(与列表该行当前余额一致) | + +### 3.6 附件预览 + +- 付款凭据、发票在弹窗中预览;PDF 使用文档预览,图片直接展示。 +- 发票支持下载,文件名与开票记录一致。 + +### 3.7 导出 + +- 点击 **「导出」** 下载当前筛选结果 CSV(含列表字段及合计行)。 +- 编码 UTF-8(带 BOM),便于 Excel 打开。 + +--- + +## 四、业务规则摘要 + +| 规则 | 说明 | +|------|------| +| 统计起点 | 加氢总量等业务口径默认 **2026 年起**(与车辆氢费明细、主数据生效规则一致) | +| 未付(行) | 应付总金额 − 已付总金额 | +| 未付(合计条) | 各站未付之和,等价于应付合计 − 已付合计 | +| 未开票(行) | 应付总金额 − 已开票金额 | +| 未开票(合计条) | 各站未开票之和,等价于应付合计 − 已开票合计 | +| 已欠费 | 仅 **当前余额 < 0** 时展示标签 | +| 钻取范围 | 均限定为 **当前汇总行对应加氢站** | + +--- + +## 五、验收要点(业务) + +1. 查询、重置、筛选摘要、合计条与列表数据一致。 +2. 各可点击金额/加氢量钻取弹窗字段与上文一致,合计正确。 +3. 未付、已欠费展示规则符合第四节。 +4. 付款凭据可预览;发票可预览与下载。 +5. 列宽可拖动;导出字段完整。 +6. 「查看需求说明」可打开本 PRD 全文。 + +--- + +**文档结束** +`; + +/** PRD Markdown 渲染(氢费采购端汇总报表) */ +function parsePurchasePrdInlineText(text) { + var parts = String(text || '').split(/(\*\*[^*]+\*\*)/g); + var nodes = []; + var i; + for (i = 0; i < parts.length; i++) { + var p = parts[i]; + if (!p) continue; + if (p.indexOf('**') === 0 && p.lastIndexOf('**') === p.length - 2) { + nodes.push(React.createElement('strong', { key: i }, p.slice(2, -2))); + } else { + nodes.push(p); + } + } + return nodes.length === 1 ? nodes[0] : nodes; +} +function isPurchasePrdTableRow(line) { + return /^\|.+\|$/.test(String(line || '').trim()); +} +function isPurchasePrdTableSep(line) { + return /^\|[\s\-:|]+\|$/.test(String(line || '').trim()); +} +function renderPurchasePrdTableRow(line, rowKey, isHeader) { + var cells = String(line).trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map(function (c) { return c.trim(); }); + return React.createElement('tr', { key: rowKey }, cells.map(function (cell, ci) { + var Tag = isHeader ? 'th' : 'td'; + return React.createElement(Tag, { + key: ci, + style: { border: '1px solid #e5e7eb', padding: '8px 10px', textAlign: 'left', verticalAlign: 'top', fontWeight: isHeader ? 600 : 400, background: isHeader ? '#f8fafc' : '#fff', fontSize: 13, lineHeight: 1.5 } + }, parsePurchasePrdInlineText(cell)); + })); +} +function renderPurchasePrdMarkdown(markdown) { + var lines = String(markdown || '').split(/\r?\n/); + var nodes = []; + var i = 0; + var inCode = false; + var codeBuf = []; + while (i < lines.length) { + var line = lines[i]; + var trimmed = String(line || '').trim(); + if (trimmed.indexOf('```') === 0) { + if (inCode) { + nodes.push(React.createElement('pre', { key: 'code-' + i, style: { margin: '12px 0', padding: '12px 14px', background: '#f6f8fa', border: '1px solid #e5e7eb', borderRadius: 8, fontSize: 12, lineHeight: 1.6, overflow: 'auto', color: '#334155', whiteSpace: 'pre-wrap' } }, codeBuf.join('\n'))); + codeBuf = []; + inCode = false; + } else inCode = true; + i += 1; + continue; + } + if (inCode) { codeBuf.push(line); i += 1; continue; } + if (trimmed === '---') { + nodes.push(React.createElement('hr', { key: 'hr-' + i, style: { border: 'none', borderTop: '1px solid #e8ecf0', margin: '20px 0' } })); + i += 1; + continue; + } + if (isPurchasePrdTableRow(trimmed)) { + var tableLines = []; + while (i < lines.length && isPurchasePrdTableRow(String(lines[i]).trim())) { + tableLines.push(String(lines[i]).trim()); + i += 1; + } + var bodyRows = []; + var ti; + for (ti = 0; ti < tableLines.length; ti++) { + if (isPurchasePrdTableSep(tableLines[ti])) continue; + bodyRows.push(renderPurchasePrdTableRow(tableLines[ti], 'tr-' + i + '-' + ti, ti === 0)); + } + if (bodyRows.length) { + nodes.push(React.createElement('div', { key: 'tbl-' + i, style: { overflowX: 'auto', margin: '12px 0 16px' } }, + React.createElement('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: 13 } }, + React.createElement('tbody', null, bodyRows)))); + } + continue; + } + if (!trimmed) { i += 1; continue; } + if (trimmed.indexOf('# ') === 0) { + nodes.push(React.createElement('h1', { key: 'h1-' + i, style: { fontSize: 20, fontWeight: 700, color: '#0f172a', margin: '0 0 16px', lineHeight: 1.35 } }, parsePurchasePrdInlineText(trimmed.slice(2).trim()))); + i += 1; + continue; + } + if (trimmed.indexOf('## ') === 0) { + nodes.push(React.createElement('h2', { key: 'h2-' + i, style: { fontSize: 16, fontWeight: 700, color: '#1e293b', margin: '24px 0 12px', paddingBottom: 6, borderBottom: '2px solid #e0f2fe', lineHeight: 1.4 } }, parsePurchasePrdInlineText(trimmed.slice(3).trim()))); + i += 1; + continue; + } + if (trimmed.indexOf('### ') === 0) { + nodes.push(React.createElement('h3', { key: 'h3-' + i, style: { fontSize: 14, fontWeight: 600, color: '#334155', margin: '16px 0 8px', lineHeight: 1.45 } }, parsePurchasePrdInlineText(trimmed.slice(4).trim()))); + i += 1; + continue; + } + if (trimmed === '**文档结束**') { + nodes.push(React.createElement('div', { key: 'end-' + i, style: { marginTop: 24, paddingTop: 16, borderTop: '1px dashed #e2e8f0', color: '#94a3b8', fontSize: 13, textAlign: 'center' } }, '— 文档结束 —')); + i += 1; + continue; + } + if (/^\d+\.\s/.test(trimmed)) { + nodes.push(React.createElement('div', { key: 'ol-' + i, style: { fontSize: 13, color: '#475569', lineHeight: 1.75, margin: '6px 0 6px 4px', paddingLeft: 4 } }, parsePurchasePrdInlineText(trimmed))); + i += 1; + continue; + } + if (trimmed.indexOf('- ') === 0) { + nodes.push(React.createElement('div', { key: 'ul-' + i, style: { display: 'flex', gap: 8, fontSize: 13, color: '#475569', lineHeight: 1.75, margin: '4px 0 4px 2px' } }, + React.createElement('span', { style: { color: '#1677ff', flexShrink: 0 } }, '•'), + React.createElement('span', { style: { flex: 1 } }, parsePurchasePrdInlineText(trimmed.slice(2).trim())))); + i += 1; + continue; + } + nodes.push(React.createElement('p', { key: 'p-' + i, style: { fontSize: 13, color: '#475569', lineHeight: 1.75, margin: '6px 0' } }, parsePurchasePrdInlineText(trimmed))); + i += 1; + } + return nodes; +} +function renderPurchaseRequirementDocPanel() { + var md = typeof H2_PURCHASE_SUMMARY_REQUIREMENT_DOC !== 'undefined' && H2_PURCHASE_SUMMARY_REQUIREMENT_DOC ? H2_PURCHASE_SUMMARY_REQUIREMENT_DOC : ''; + if (!md) { + return React.createElement('div', { style: { padding: 24, color: '#64748b', textAlign: 'center' } }, '需求文档未加载'); + } + return React.createElement('div', { className: 'h2-req-doc-panel', style: { padding: '4px 4px 16px' } }, renderPurchasePrdMarkdown(md)); +} + + +// 【重要】必须使用 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 Row = antd.Row; + var Col = antd.Col; + var Space = antd.Space; + var Tag = antd.Tag; + var Tooltip = antd.Tooltip; + var Modal = antd.Modal; + var message = antd.message; + + var SETTLEMENT_OPTIONS = [ + { value: '预付', label: '预付' }, + { value: '月结', label: '月结' } + ]; + + /** 原型:加氢站主数据(联调后由接口加载) */ + var STATION_MASTER = [ + { code: 'HS000059', name: '佛山南海羚牛加氢站', region: '广东省/佛山市', project: '佛山氢能城配', settlement: '预付' }, + { code: 'HS000035', name: '广州联新氢能东晖加氢站', region: '广东省/广州市', project: '广州干线运输', settlement: '月结' }, + { code: '000069', name: '中国石化广州开泰北加油加氢站', region: '广东省/广州市', project: '广州干线运输', settlement: '月结' }, + { code: '000072', name: '中国石化广州金坑加氢站', region: '广东省/广州市', project: '广州冷链专线', settlement: '预付' }, + { code: 'HS000030', name: '大兴国际氢能示范区海珀尔加氢站', region: '北京市/大兴区', project: '北京示范运营', settlement: '预付' }, + { code: 'HS000007', name: '上海嘉氢实业加氢站(江桥重塑)', region: '上海市/嘉定区', project: '长三角城际', settlement: '月结' }, + { code: 'HS000096', name: '松江九亭加油站', region: '上海市/松江区', project: '长三角城际', settlement: '月结' }, + { code: 'HS000041', name: '宿迁沭阳开发区加氢站', region: '江苏省/宿迁市', project: '苏北物流', settlement: '预付' }, + { code: 'HS000040', name: '扬州文昌西路站', region: '江苏省/扬州市', project: '苏北物流', settlement: '月结' }, + { code: '000048', name: '南京溧水柘塘东站', region: '江苏省/南京市', project: '南京港口短驳', settlement: '预付' }, + { code: 'HS000051', name: '成都华通加氢站', region: '四川省/成都市', project: '西南干线', settlement: '月结' }, + { code: '000090', name: '成都天府机场高速北站', region: '四川省/成都市', project: '西南干线', settlement: '预付' }, + { code: 'HS000057', name: '重庆元琨双宝氢能综合能源站', region: '重庆市/渝北区', project: '西南干线', settlement: '月结' }, + { code: '000091', name: '重庆双溪加氢站', region: '重庆市/涪陵区', project: '西南干线', settlement: '预付' }, + { code: '000053', name: '武汉革新大道加油站', region: '湖北省/武汉市', project: '华中城配', settlement: '月结' }, + { code: 'HS000081', name: '武汉群力加油站', region: '湖北省/武汉市', project: '华中城配', settlement: '预付' }, + { code: '000049', name: '宁波镇海区中国石化加氢站', region: '浙江省/宁波市', project: '浙江氢能租赁', settlement: '月结' }, + { code: 'HS000086', name: '空气化工产品(浙江)有限公司海盐经济开发区加氢站', region: '浙江省/嘉兴市', project: '浙江氢能租赁', settlement: '预付' }, + { code: 'HS000039', name: '海盐JUPITER厂内加氢站', region: '浙江省/嘉兴市', project: '园区通勤', settlement: '预付' }, + { code: '000043', name: '豪汇综合能源站', region: '山东省/淄博市', project: '鲁中物流', settlement: '月结' } + ]; + + var REGION_OPTIONS = (function () { + var map = {}; + STATION_MASTER.forEach(function (s) { + map[s.region] = s.region; + }); + return Object.keys(map).sort().map(function (r) { + return { value: r, label: r }; + }); + })(); + + var STATION_OPTIONS = STATION_MASTER.map(function (s) { + return { value: s.code, label: s.name }; + }); + + 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 fmtQty(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 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 findStation(code) { + for (var i = 0; i < STATION_MASTER.length; i++) { + if (STATION_MASTER[i].code === code) return STATION_MASTER[i]; + } + return null; + } + + function formatDateTime(d) { + if (!d || !window.dayjs) return '—'; + try { + var x = window.dayjs(d); + return x.isValid() ? x.format('YYYY-MM-DD HH:mm:ss') : '—'; + } catch (eDt) { + return '—'; + } + } + + function formatDate(d) { + if (!d || !window.dayjs) return '—'; + try { + var x = window.dayjs(d); + return x.isValid() ? x.format('YYYY-MM-DD') : '—'; + } catch (eD) { + return '—'; + } + } + + var MOCK_SAMPLE_PDF_URL = + 'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf'; + + var MOCK_PLATES = ['粤A12345F', '浙AD12345F', '沪A06695F', '苏EF99887F', '京CN88771F', '渝A88888F']; + + /** + * 原型:车辆氢费明细钻取数据(联调后按加氢站 + 汇总条件查询车辆氢费明细接口) + * payable:应付总金额 = 该站车辆氢费明细「加氢总价」求和 + */ + function buildMockStationDetailRows(summaryRow, drillType) { + var seed = 0; + var code = summaryRow.stationCode || ''; + var i; + for (i = 0; i < code.length; i++) seed += code.charCodeAt(i); + var lineCount = Math.max(4, 5 + (seed % 6)); + var rows = []; + var unitPrice = numOrZero(summaryRow.unitPrice) || 28; + var baseDay = window.dayjs ? window.dayjs('2026-05-01') : null; + + if (drillType === 'hydrogenKg') { + var totalKg = numOrZero(summaryRow.hydrogenKg); + var remainKg = totalKg; + for (i = 0; i < lineCount; i++) { + var isLast = i === lineCount - 1; + var kg = isLast + ? Math.round(remainKg * 100) / 100 + : Math.round((totalKg / lineCount) * (0.82 + ((seed + i) % 4) * 0.06) * 100) / 100; + if (!isLast) remainKg -= kg; + var costTotal = Math.round(kg * unitPrice * 100) / 100; + var customerAmount = Math.round(kg * (unitPrice + 2) * 100) / 100; + var day = baseDay && baseDay.subtract ? baseDay.subtract((seed + i) % 40, 'day') : null; + rows.push({ + key: code + '-kg-' + i, + seq: i + 1, + hydrogenTime: day, + orderNo: 'JQ' + code + (day && day.format ? day.format('YYMMDD') : '') + String(i + 1).padStart(5, '0'), + plateNo: MOCK_PLATES[(seed + i) % MOCK_PLATES.length], + hydrogenKg: kg, + costUnitPrice: unitPrice, + costTotal: costTotal, + customerAmount: customerAmount + }); + } + return rows; + } + + if (drillType === 'payableAmount') { + var totalPay = numOrZero(summaryRow.payableAmount); + var remainPay = totalPay; + for (i = 0; i < lineCount; i++) { + var isLast = i === lineCount - 1; + var customerAmount = isLast + ? Math.round(remainPay * 100) / 100 + : Math.round((totalPay / lineCount) * (0.8 + ((seed + i) % 5) * 0.05) * 100) / 100; + if (!isLast) remainPay -= customerAmount; + var kg = unitPrice > 0 ? Math.round((customerAmount / (unitPrice + 2)) * 100) / 100 : 0; + var costTotal = Math.round(kg * unitPrice * 100) / 100; + var day = baseDay && baseDay.subtract ? baseDay.subtract((seed + i) % 40, 'day') : null; + rows.push({ + key: code + '-pay-' + i, + seq: i + 1, + hydrogenTime: day, + orderNo: 'JQ' + code + (day && day.format ? day.format('YYMMDD') : '') + String(100 + i).padStart(5, '0'), + plateNo: MOCK_PLATES[(seed + i + 2) % MOCK_PLATES.length], + hydrogenKg: kg, + costUnitPrice: unitPrice, + costTotal: costTotal, + customerAmount: customerAmount + }); + } + return rows; + } + + return rows; + } + + /** + * 原型:加氢站预付余额变更明细(联调后对接加氢站打款/余额流水接口) + */ + function buildMockPrepaidBalanceRows(summaryRow) { + var seed = 0; + var code = summaryRow.stationCode || ''; + var i; + for (i = 0; i < code.length; i++) seed += code.charCodeAt(i); + var stationName = summaryRow.stationName || '—'; + var finalBalance = numOrZero(summaryRow.prepaidBalance); + var lineCount = Math.max(5, 6 + (seed % 5)); + var rows = []; + var openingBalance = Math.round((finalBalance + 38000 + seed * 620) * 100) / 100; + if (openingBalance < 0) openingBalance = Math.abs(openingBalance) + 50000; + + rows.push({ + key: code + '-bal-0', + stationName: stationName, + incomeAmount: openingBalance, + expenseAmount: null, + balance: openingBalance, + orderNo: 'QC' + code + '260101' + }); + + var balance = openingBalance; + for (i = 1; i < lineCount - 1; i++) { + var isIncome = (seed + i) % 3 === 0; + var incomeAmount = null; + var expenseAmount = null; + if (isIncome) { + incomeAmount = Math.round((6000 + (seed + i) * 380) * 100) / 100; + balance = Math.round((balance + incomeAmount) * 100) / 100; + } else { + expenseAmount = Math.round((2800 + (seed + i) * 210) * 100) / 100; + balance = Math.round((balance - expenseAmount) * 100) / 100; + } + rows.push({ + key: code + '-bal-' + i, + stationName: stationName, + incomeAmount: incomeAmount, + expenseAmount: expenseAmount, + balance: balance, + orderNo: (isIncome ? 'DK' : 'JQ') + code + String(200 + i).padStart(5, '0') + }); + } + + var diff = Math.round((finalBalance - balance) * 100) / 100; + if (Math.abs(diff) < 0.01) { + rows.push({ + key: code + '-bal-last', + stationName: stationName, + incomeAmount: null, + expenseAmount: null, + balance: finalBalance, + orderNo: 'BZ' + code + String(900).padStart(5, '0') + }); + } else if (diff > 0) { + rows.push({ + key: code + '-bal-last', + stationName: stationName, + incomeAmount: diff, + expenseAmount: null, + balance: finalBalance, + orderNo: 'DK' + code + String(999).padStart(5, '0') + }); + } else { + rows.push({ + key: code + '-bal-last', + stationName: stationName, + incomeAmount: null, + expenseAmount: Math.round(Math.abs(diff) * 100) / 100, + balance: finalBalance, + orderNo: 'JQ' + code + String(999).padStart(5, '0') + }); + } + + return rows; + } + + /** 原型:充值总额钻取(联调后对接加氢站预充值记录接口) */ + function buildMockRechargeDrillRows(summaryRow) { + var seed = 0; + var code = summaryRow.stationCode || ''; + var i; + for (i = 0; i < code.length; i++) seed += code.charCodeAt(i); + var stationName = summaryRow.stationName || '—'; + var totalRecharge = numOrZero(summaryRow.rechargeTotal); + var rechargeCount = Math.max(2, 4 + (seed % 4)); + var rows = []; + var remain = totalRecharge; + var baseDay = window.dayjs ? window.dayjs('2026-01-15') : null; + + for (i = 0; i < rechargeCount; i++) { + var isLast = i === rechargeCount - 1; + var prepayAmount = isLast + ? Math.round(remain * 100) / 100 + : Math.round((totalRecharge / rechargeCount) * (0.75 + ((seed + i) % 5) * 0.05) * 100) / 100; + if (!isLast) remain -= prepayAmount; + var payDay = baseDay && baseDay.add ? baseDay.add((seed + i) * 17, 'day') : null; + var isPdf = (seed + i) % 3 === 0; + rows.push({ + key: code + '-rc-' + i, + stationName: stationName, + payTime: payDay, + prepayAmount: prepayAmount, + voucherUrl: isPdf + ? MOCK_SAMPLE_PDF_URL + : 'https://picsum.photos/seed/rc-' + code + '-' + i + '/960/640', + voucherFileName: '付款凭证_' + code + '_' + String(i + 1).padStart(2, '0') + (isPdf ? '.pdf' : '.jpg'), + voucherMime: isPdf ? 'pdf' : 'image' + }); + } + return rows; + } + + /** 原型:采购支付钻取(联调后对接加氢站打款账单接口) */ + function buildMockPaymentDrillRows(summaryRow) { + var seed = 0; + var code = summaryRow.stationCode || ''; + var i; + for (i = 0; i < code.length; i++) seed += code.charCodeAt(i); + var stationName = summaryRow.stationName || '—'; + var totalPaid = numOrZero(summaryRow.paidAmount); + var billCount = Math.max(2, 3 + (seed % 3)); + var rows = []; + var remainPaid = totalPaid; + var baseMonth = window.dayjs ? window.dayjs('2026-01-01') : null; + + for (i = 0; i < billCount; i++) { + var isLast = i === billCount - 1; + var paidPart = isLast + ? Math.round(remainPaid * 100) / 100 + : Math.round((totalPaid / billCount) * (0.78 + ((seed + i) % 4) * 0.06) * 100) / 100; + if (!isLast) remainPaid -= paidPart; + var payablePart = Math.round(paidPart * (1.04 + ((seed + i) % 3) * 0.02) * 100) / 100; + var start = baseMonth && baseMonth.add ? baseMonth.add(i * 2, 'month').startOf('month') : null; + var end = start && start.endOf ? start.endOf('month') : null; + rows.push({ + key: code + '-payment-' + i, + stationName: stationName, + billStartDate: start, + billEndDate: end, + payableAmount: payablePart, + paidAmount: paidPart, + voucherUrl: 'https://picsum.photos/seed/pay-' + code + '-' + i + '/960/640', + voucherName: '付款凭据_' + code + '_' + String(i + 1).padStart(2, '0') + '.jpg', + voucherMime: 'image' + }); + } + return rows; + } + + /** 原型:已开票金额钻取(联调后对接开票记录接口) */ + function buildMockInvoiceDrillRows(summaryRow) { + var seed = 0; + var code = summaryRow.stationCode || ''; + var i; + for (i = 0; i < code.length; i++) seed += code.charCodeAt(i); + var stationName = summaryRow.stationName || '—'; + var totalInvoiced = numOrZero(summaryRow.invoicedAmount); + var invoiceCount = Math.max(2, 3 + (seed % 3)); + var rows = []; + var remainInv = totalInvoiced; + var baseDay = window.dayjs ? window.dayjs('2026-02-01') : null; + + for (i = 0; i < invoiceCount; i++) { + var isLast = i === invoiceCount - 1; + var amount = isLast + ? Math.round(remainInv * 100) / 100 + : Math.round((totalInvoiced / invoiceCount) * (0.8 + ((seed + i) % 4) * 0.05) * 100) / 100; + if (!isLast) remainInv -= amount; + var isPdf = (seed + i) % 2 === 0; + var invDay = baseDay && baseDay.add ? baseDay.add((seed + i) * 11, 'day') : null; + rows.push({ + key: code + '-inv-' + i, + stationName: stationName, + invoiceTime: invDay, + invoiceAmount: amount, + invoiceUrl: isPdf + ? MOCK_SAMPLE_PDF_URL + : 'https://picsum.photos/seed/inv-' + code + '-' + i + '/860/1200', + invoiceFileName: '发票_' + code + '_' + String(i + 1).padStart(2, '0') + (isPdf ? '.pdf' : '.jpg'), + invoiceMime: isPdf ? 'pdf' : 'image' + }); + } + return rows; + } + + function fmtLedgerMoney(v) { + if (v === null || v === undefined || v === '' || Number(v) === 0) return '—'; + return fmtMoney(v, 2); + } + + /** 原型:按站点生成汇总行(联调后由后端聚合) */ + function buildMockSummaryRows() { + return STATION_MASTER.map(function (s, idx) { + var seed = idx + 1; + var hydrogenKg = Math.round((1200 + seed * 137.5) * 100) / 100; + var unitPrice = 28 + (seed % 5) * 0.5; + /** 应付总金额 = 车辆氢费明细「加氢总价」按站点求和(原型用加氢单价近似) */ + var customerUnitPrice = unitPrice + 2; + var payable = Math.round(hydrogenKg * customerUnitPrice * 100) / 100; + var paid = Math.round(payable * (0.55 + (seed % 4) * 0.1) * 100) / 100; + var unpaid = Math.round((payable - paid) * 100) / 100; + var invoiced = Math.round(Math.min(payable, paid * (0.72 + (seed % 5) * 0.05)) * 100) / 100; + var prepaid = Math.round((80000 - seed * 4200 + (seed % 3 === 0 ? -15000 : 0)) * 100) / 100; + /** 原型:首行演示预付余额为负(已欠费) */ + if (idx === 0) { + prepaid = -12580.5; + } + var rechargeTotal = Math.round((48000 + seed * 3200 + (idx === 0 ? 8000 : 0)) * 100) / 100; + return { + key: s.code, + stationCode: s.code, + region: s.region, + stationName: s.name, + hydrogenKg: hydrogenKg, + unitPrice: unitPrice, + payableAmount: payable, + paidAmount: paid, + unpaidAmount: unpaid, + invoicedAmount: invoiced, + rechargeTotal: rechargeTotal, + prepaidBalance: prepaid, + settlement: s.settlement + }; + }); + } + + 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 = + '.h2-purchase-summary-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)}' + + '.h2-purchase-summary-wrap .ant-table-wrapper,.h2-purchase-summary-wrap .ant-table-content{overflow-x:hidden!important}' + + '.h2-purchase-summary-wrap .h2-purchase-summary-table{width:100%!important;table-layout:fixed}' + + '.h2-purchase-summary-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}' + + '.h2-purchase-summary-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;overflow:hidden!important}' + + '.h2-purchase-summary-table td.h2-cell-prepaid-balance{overflow:hidden!important}' + + '.h2-purchase-summary-table .h2-prepaid-balance-cell{display:flex;align-items:center;justify-content:flex-end;gap:4px;max-width:100%;min-width:0;overflow:hidden;box-sizing:border-box}' + + '.h2-purchase-summary-table .h2-prepaid-balance-cell .h2-prepaid-balance-amount{flex:0 1 auto;min-width:0;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;border:none;background:none;padding:0;font:inherit;cursor:pointer;text-decoration:underline;text-underline-offset:2px}' + + '.h2-purchase-summary-table .h2-prepaid-balance-cell .ant-tag{flex-shrink:0;margin:0!important;font-size:11px;line-height:16px;padding:0 5px}' + + '.h2-purchase-summary-table .ant-table-tbody>tr.h2-purchase-row:hover>td{background:#f0f9ff!important}' + + '.h2-purchase-summary-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}' + + '.h2-ledger-totals-bar{display:flex;align-items:stretch;gap:0;margin-bottom:10px;border:1px solid #bae6fd;border-radius:10px;overflow:hidden;background:#f8fafc;box-shadow:0 1px 0 rgba(15,23,42,0.04)}' + + '.h2-ledger-totals-bar__title{display:flex;align-items:center;justify-content:center;min-width:72px;padding:10px 14px;font-size:14px;font-weight:700;color:#0f172a;background:#e8f4fc;border-right:1px solid #bae6fd}' + + '.h2-ledger-totals-bar__items{display:flex;flex:1;flex-wrap:wrap}' + + '.h2-ledger-totals-bar__item{flex:1;min-width:140px;padding:8px 16px;border-right:1px solid #e2e8f0;display:flex;flex-direction:column;justify-content:center;gap:4px}' + + '.h2-ledger-totals-bar__item:last-child{border-right:none}' + + '.h2-ledger-totals-bar__label{font-size:12px;color:rgba(15,23,42,0.55);font-weight:500;line-height:1.2}' + + '.h2-ledger-totals-bar__value{font-size:16px;font-weight:700;color:#0f172a;font-variant-numeric:tabular-nums;line-height:1.3}' + + '.h2-ledger-totals-bar__value.is-warn{color:#cf1322}' + + '.h2-purchase-summary-table .h2-col-header{position:relative;display:flex;align-items:center;width:100%;height:100%;min-height:38px;padding:0 10px 0 8px;box-sizing:border-box}' + + '.h2-purchase-summary-table .h2-col-header__text{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}' + + '.h2-purchase-summary-table .h2-col-header__resizer{position:absolute;right:0;top:0;bottom:0;width:8px;cursor:col-resize;z-index:2;touch-action:none}' + + '.h2-purchase-summary-table .h2-col-header__resizer:hover{background:rgba(22,119,255,0.12)}' + + '.h2-purchase-summary-table.ant-table-wrapper .ant-table-thead>tr>th{position:relative}' + + '.h2-req-doc-panel{max-width:100%}' + + '.h2-req-doc-panel h1:first-child{margin-top:0}'; + + /** 主表默认列宽 */ + var DEFAULT_MAIN_COLUMN_WIDTHS = { + seq: 48, + region: 96, + stationName: 148, + settlement: 72, + hydrogenKg: 96, + payableAmount: 100, + paidAmount: 100, + unpaidAmount: 100, + invoicedAmount: 100, + uninvoicedAmount: 100, + rechargeTotal: 100, + prepaidBalance: 118 + }; + var MIN_COLUMN_WIDTH = 48; + + var drillLinkStyle = { + cursor: 'pointer', + color: '#1677ff', + border: 'none', + background: 'none', + padding: 0, + font: 'inherit', + fontVariantNumeric: 'tabular-nums', + textDecoration: 'underline', + textUnderlineOffset: 2 + }; + + var regionDraftState = useState(undefined); + var regionDraft = regionDraftState[0]; + var setRegionDraft = regionDraftState[1]; + var stationDraftState = useState(undefined); + var stationDraft = stationDraftState[0]; + var setStationDraft = stationDraftState[1]; + var settlementDraftState = useState(undefined); + var settlementDraft = settlementDraftState[0]; + var setSettlementDraft = settlementDraftState[1]; + + var regionAppliedState = useState(undefined); + var regionApplied = regionAppliedState[0]; + var setRegionApplied = regionAppliedState[1]; + var stationAppliedState = useState(undefined); + var stationApplied = stationAppliedState[0]; + var setStationApplied = stationAppliedState[1]; + var settlementAppliedState = useState(undefined); + var settlementApplied = settlementAppliedState[0]; + var setSettlementApplied = settlementAppliedState[1]; + + var detailDrillModalState = useState({ + open: false, + type: '', + stationName: '', + summary: null, + rows: [] + }); + var detailDrillModal = detailDrillModalState[0]; + var setDetailDrillModal = detailDrillModalState[1]; + + var prepaidBalanceDrillModalState = useState({ + open: false, + stationName: '', + endingBalance: 0, + rows: [] + }); + var prepaidBalanceDrillModal = prepaidBalanceDrillModalState[0]; + var setPrepaidBalanceDrillModal = prepaidBalanceDrillModalState[1]; + + var paymentDrillModalState = useState({ open: false, stationName: '', rows: [] }); + var paymentDrillModal = paymentDrillModalState[0]; + var setPaymentDrillModal = paymentDrillModalState[1]; + + var invoiceDrillModalState = useState({ open: false, stationName: '', rows: [] }); + var invoiceDrillModal = invoiceDrillModalState[0]; + var setInvoiceDrillModal = invoiceDrillModalState[1]; + + var rechargeDrillModalState = useState({ open: false, stationName: '', rows: [] }); + var rechargeDrillModal = rechargeDrillModalState[0]; + var setRechargeDrillModal = rechargeDrillModalState[1]; + + var attachmentPreviewState = useState({ open: false, url: '', title: '', mime: 'image' }); + var attachmentPreview = attachmentPreviewState[0]; + var setAttachmentPreview = attachmentPreviewState[1]; + + var reqDetailOpenState = useState(false); + var reqDetailOpen = reqDetailOpenState[0]; + var setReqDetailOpen = reqDetailOpenState[1]; + + var columnWidthsState = useState(function () { + return Object.assign({}, DEFAULT_MAIN_COLUMN_WIDTHS); + }); + var columnWidths = columnWidthsState[0]; + var setColumnWidths = columnWidthsState[1]; + var columnWidthsRef = { current: columnWidths }; + columnWidthsRef.current = columnWidths; + + var handleColumnResize = useCallback(function (key, width) { + setColumnWidths(function (prev) { + var next = Object.assign({}, prev); + next[key] = Math.max(MIN_COLUMN_WIDTH, Math.round(width)); + return next; + }); + }, []); + + var makeResizableTitle = useCallback( + function (title, key) { + return function ResizableColumnTitle() { + var onResizeMouseDown = function (e) { + if (e.button !== 0) return; + e.preventDefault(); + e.stopPropagation(); + var startX = e.clientX; + var startWidth = columnWidthsRef.current[key] || DEFAULT_MAIN_COLUMN_WIDTHS[key] || 100; + var onMouseMove = function (ev) { + handleColumnResize(key, startWidth + ev.clientX - startX); + }; + var onMouseUp = function () { + document.removeEventListener('mousemove', onMouseMove); + document.removeEventListener('mouseup', onMouseUp); + document.body.style.cursor = ''; + document.body.style.userSelect = ''; + }; + document.body.style.cursor = 'col-resize'; + document.body.style.userSelect = 'none'; + document.addEventListener('mousemove', onMouseMove); + document.addEventListener('mouseup', onMouseUp); + }; + return React.createElement( + 'div', + { className: 'h2-col-header' }, + React.createElement('span', { className: 'h2-col-header__text' }, title), + React.createElement('span', { + className: 'h2-col-header__resizer', + onMouseDown: onResizeMouseDown, + onClick: function (ev) { + ev.stopPropagation(); + } + }) + ); + }; + }, + [handleColumnResize] + ); + + var allRows = useMemo(function () { + return buildMockSummaryRows(); + }, []); + + var dataSource = useMemo(function () { + var list = allRows.filter(function (r) { + if (regionApplied && r.region !== regionApplied) return false; + if (stationApplied && r.stationCode !== stationApplied) return false; + if (settlementApplied && r.settlement !== settlementApplied) return false; + return true; + }); + return list.map(function (r, idx) { + var unpaid = Math.round((numOrZero(r.payableAmount) - numOrZero(r.paidAmount)) * 100) / 100; + var uninvoiced = Math.round((numOrZero(r.payableAmount) - numOrZero(r.invoicedAmount)) * 100) / 100; + return Object.assign({}, r, { seq: idx + 1, unpaidAmount: unpaid, uninvoicedAmount: uninvoiced }); + }); + }, [allRows, regionApplied, stationApplied, settlementApplied]); + + var totals = useMemo(function () { + var acc = dataSource.reduce( + function (sum, r) { + sum.hydrogenKg += numOrZero(r.hydrogenKg); + sum.payableAmount += numOrZero(r.payableAmount); + sum.paidAmount += numOrZero(r.paidAmount); + sum.invoicedAmount += numOrZero(r.invoicedAmount); + sum.rechargeTotal += numOrZero(r.rechargeTotal); + sum.prepaidBalance += numOrZero(r.prepaidBalance); + return sum; + }, + { hydrogenKg: 0, payableAmount: 0, paidAmount: 0, invoicedAmount: 0, rechargeTotal: 0, prepaidBalance: 0 } + ); + acc.unpaidAmount = Math.round((acc.payableAmount - acc.paidAmount) * 100) / 100; + acc.uninvoicedAmount = Math.round((acc.payableAmount - acc.invoicedAmount) * 100) / 100; + return acc; + }, [dataSource]); + + var filterSummaryText = useMemo(function () { + var parts = []; + parts.push('地区:' + (regionApplied || '全部')); + var st = findStation(stationApplied); + parts.push('加氢站:' + (st ? st.name : '全部')); + parts.push('结算方式:' + (settlementApplied || '全部')); + return parts.join('  '); + }, [regionApplied, stationApplied, settlementApplied]); + + var openDetailDrill = useCallback(function (record, drillType) { + var rows = buildMockStationDetailRows(record, drillType); + var title = + drillType === 'payableAmount' + ? '应付总金额明细' + : '加氢总量明细'; + setDetailDrillModal({ + open: true, + type: drillType, + stationName: record.stationName, + summary: record, + rows: rows + }); + }, []); + + var closeDetailDrill = useCallback(function () { + setDetailDrillModal({ open: false, type: '', stationName: '', summary: null, rows: [] }); + }, []); + + var openPrepaidBalanceDrill = useCallback(function (record) { + setPrepaidBalanceDrillModal({ + open: true, + stationName: record.stationName, + endingBalance: numOrZero(record.prepaidBalance), + rows: buildMockPrepaidBalanceRows(record) + }); + }, []); + + var closePrepaidBalanceDrill = useCallback(function () { + setPrepaidBalanceDrillModal({ open: false, stationName: '', endingBalance: 0, rows: [] }); + }, []); + + var openPaymentDrill = useCallback(function (record) { + setPaymentDrillModal({ + open: true, + stationName: record.stationName, + rows: buildMockPaymentDrillRows(record) + }); + }, []); + + var closePaymentDrill = useCallback(function () { + setPaymentDrillModal({ open: false, stationName: '', rows: [] }); + }, []); + + var openInvoiceDrill = useCallback(function (record) { + setInvoiceDrillModal({ + open: true, + stationName: record.stationName, + rows: buildMockInvoiceDrillRows(record) + }); + }, []); + + var closeInvoiceDrill = useCallback(function () { + setInvoiceDrillModal({ open: false, stationName: '', rows: [] }); + }, []); + + var openRechargeDrill = useCallback(function (record) { + setRechargeDrillModal({ + open: true, + stationName: record.stationName, + rows: buildMockRechargeDrillRows(record) + }); + }, []); + + var closeRechargeDrill = useCallback(function () { + setRechargeDrillModal({ open: false, stationName: '', rows: [] }); + }, []); + + var openAttachmentPreview = useCallback(function (file) { + if (!file || !file.url) { + message.warning('暂无附件'); + return; + } + setAttachmentPreview({ + open: true, + url: file.url, + title: file.title || '预览', + mime: file.mime || 'image' + }); + }, []); + + var closeAttachmentPreview = useCallback(function () { + setAttachmentPreview({ open: false, url: '', title: '', mime: 'image' }); + }, []); + + var downloadAttachment = useCallback(function (file) { + if (!file || !file.url) { + message.warning('暂无文件'); + return; + } + var a = document.createElement('a'); + a.href = file.url; + a.download = file.fileName || 'download'; + a.target = '_blank'; + a.rel = 'noopener noreferrer'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + message.success('开始下载'); + }, []); + + var renderDrillLink = useCallback(function (display, record, drillType) { + return React.createElement( + 'button', + { + type: 'button', + style: drillLinkStyle, + title: '点击查看明细', + onClick: function (e) { + if (e && e.stopPropagation) e.stopPropagation(); + openDetailDrill(record, drillType); + } + }, + display + ); + }, [openDetailDrill]); + + var renderDrillLinkWithHandler = useCallback(function (display, record, onDrill) { + return React.createElement( + 'button', + { + type: 'button', + style: drillLinkStyle, + title: '点击查看明细', + onClick: function (e) { + if (e && e.stopPropagation) e.stopPropagation(); + onDrill(record); + } + }, + display + ); + }, []); + + var renderPrepaidBalanceCell = useCallback(function (v, record) { + var n = numOrZero(v); + var isArrears = n < 0; + var amountStyle = isArrears + ? { color: '#cf1322', fontVariantNumeric: 'tabular-nums' } + : { color: '#1677ff', fontVariantNumeric: 'tabular-nums' }; + return React.createElement( + 'div', + { className: 'h2-prepaid-balance-cell' }, + isArrears + ? React.createElement(Tag, { color: 'error' }, '已欠费') + : null, + React.createElement( + 'button', + { + type: 'button', + className: 'h2-prepaid-balance-amount', + style: amountStyle, + title: '点击查看明细', + onClick: function (e) { + if (e && e.stopPropagation) e.stopPropagation(); + openPrepaidBalanceDrill(record); + } + }, + fmtMoney(v, 2) + ) + ); + }, [openPrepaidBalanceDrill]); + + var renderUnpaidAmountCell = useCallback(function (v) { + var n = numOrZero(v); + var style = n > 0 ? { color: '#cf1322', fontWeight: 600, fontVariantNumeric: 'tabular-nums' } : { fontVariantNumeric: 'tabular-nums' }; + return React.createElement('span', { style: style }, fmtMoney(v, 2)); + }, []); + + var renderUninvoicedAmountCell = renderUnpaidAmountCell; + + var handleQuery = useCallback(function () { + setRegionApplied(regionDraft); + setStationApplied(stationDraft); + setSettlementApplied(settlementDraft); + message.success('查询成功'); + }, [regionDraft, stationDraft, settlementDraft]); + + var handleReset = useCallback(function () { + setRegionDraft(undefined); + setStationDraft(undefined); + setSettlementDraft(undefined); + setRegionApplied(undefined); + setStationApplied(undefined); + setSettlementApplied(undefined); + }, []); + + var handleExport = useCallback(function () { + var headers = [ + '序号', + '地区', + '加氢站全称', + '结算方式', + '加氢总量(kg)', + '应付总金额(元)', + '已付总金额(元)', + '未付总金额(元)', + '已开票金额(元)', + '未开票金额(元)', + '充值总额(元)', + '当前余额(元)', + '结算方式' + ]; + var body = dataSource.map(function (r) { + return [ + r.seq, + r.region, + r.stationName, + r.settlement, + r.hydrogenKg, + r.payableAmount, + r.paidAmount, + r.unpaidAmount, + r.invoicedAmount, + r.uninvoicedAmount, + r.rechargeTotal, + r.prepaidBalance + ]; + }); + body.push([ + '合计', + '', + '', + '', + '', + Math.round(totals.hydrogenKg * 100) / 100, + '', + Math.round(totals.payableAmount * 100) / 100, + Math.round(totals.paidAmount * 100) / 100, + Math.round(totals.unpaidAmount * 100) / 100, + Math.round(totals.invoicedAmount * 100) / 100, + Math.round(totals.uninvoicedAmount * 100) / 100, + Math.round(totals.rechargeTotal * 100) / 100, + Math.round(totals.prepaidBalance * 100) / 100 + ]); + downloadCsv('氢费采购端汇总报表_' + new Date().getTime() + '.csv', [headers].concat(body)); + message.success('已导出 CSV'); + }, [dataSource, totals]); + + var columns = useMemo(function () { + var w = columnWidths; + return [ + { + title: makeResizableTitle('序号', 'seq'), + dataIndex: 'seq', + key: 'seq', + width: w.seq, + align: 'center', + fixed: 'left' + }, + { + title: makeResizableTitle('地区', 'region'), + dataIndex: 'region', + key: 'region', + width: w.region, + align: 'center', + ellipsis: true + }, + { + title: makeResizableTitle('加氢站全称', 'stationName'), + dataIndex: 'stationName', + key: 'stationName', + width: w.stationName, + align: 'left', + ellipsis: true + }, + { + title: makeResizableTitle('结算方式', 'settlement'), + dataIndex: 'settlement', + key: 'settlement', + width: w.settlement, + align: 'center' + }, + { + title: makeResizableTitle('加氢总量(kg)', 'hydrogenKg'), + dataIndex: 'hydrogenKg', + key: 'hydrogenKg', + width: w.hydrogenKg, + align: 'right', + render: function (v, record) { + return renderDrillLink(fmtQty(v), record, 'hydrogenKg'); + } + }, + { + title: makeResizableTitle('应付总金额(元)', 'payableAmount'), + dataIndex: 'payableAmount', + key: 'payableAmount', + width: w.payableAmount, + align: 'right', + render: function (v, record) { + return renderDrillLink(fmtMoney(v, 2), record, 'payableAmount'); + } + }, + { + title: makeResizableTitle('已付总金额(元)', 'paidAmount'), + dataIndex: 'paidAmount', + key: 'paidAmount', + width: w.paidAmount, + align: 'right', + render: function (v, record) { + return renderDrillLinkWithHandler(fmtMoney(v, 2), record, openPaymentDrill); + } + }, + { + title: makeResizableTitle('未付总金额(元)', 'unpaidAmount'), + dataIndex: 'unpaidAmount', + key: 'unpaidAmount', + width: w.unpaidAmount, + align: 'right', + render: renderUnpaidAmountCell + }, + { + title: makeResizableTitle('已开票金额(元)', 'invoicedAmount'), + dataIndex: 'invoicedAmount', + key: 'invoicedAmount', + width: w.invoicedAmount, + align: 'right', + render: function (v, record) { + return renderDrillLinkWithHandler(fmtMoney(v, 2), record, openInvoiceDrill); + } + }, + { + title: makeResizableTitle('未开票金额(元)', 'uninvoicedAmount'), + dataIndex: 'uninvoicedAmount', + key: 'uninvoicedAmount', + width: w.uninvoicedAmount, + align: 'right', + render: renderUninvoicedAmountCell + }, + { + title: makeResizableTitle('充值总额(元)', 'rechargeTotal'), + dataIndex: 'rechargeTotal', + key: 'rechargeTotal', + width: w.rechargeTotal, + align: 'right', + render: function (v, record) { + return renderDrillLinkWithHandler(fmtMoney(v, 2), record, openRechargeDrill); + } + }, + { + title: makeResizableTitle('当前余额(元)', 'prepaidBalance'), + dataIndex: 'prepaidBalance', + key: 'prepaidBalance', + width: w.prepaidBalance, + align: 'right', + className: 'h2-col-prepaid-balance', + onCell: function () { + return { className: 'h2-cell-prepaid-balance' }; + }, + render: renderPrepaidBalanceCell + } + ]; + }, [ + columnWidths, + makeResizableTitle, + renderDrillLink, + renderDrillLinkWithHandler, + openPaymentDrill, + openInvoiceDrill, + openRechargeDrill, + renderUnpaidAmountCell, + renderUninvoicedAmountCell, + renderPrepaidBalanceCell + ]); + + var detailDrillBaseColumns = useMemo(function () { + return [ + { title: '序号', dataIndex: 'seq', key: 'seq', width: 56, align: 'center' }, + { + title: '加氢时间', + dataIndex: 'hydrogenTime', + key: 'hydrogenTime', + width: 168, + align: 'center', + render: function (v) { return formatDateTime(v); } + }, + { + title: '订单编号', + dataIndex: 'orderNo', + key: 'orderNo', + width: 168, + align: 'center', + render: function (v) { + return React.createElement('span', { style: { fontFamily: 'monospace', fontSize: 12 } }, v || '—'); + } + }, + { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 108, align: 'center' } + ]; + }, []); + + var detailDrillQtyColumn = useMemo(function () { + return { + title: '加氢量(kg)', + dataIndex: 'hydrogenKg', + key: 'hydrogenKg', + width: 100, + align: 'right', + render: function (v) { return fmtQty(v); } + }; + }, []); + + var detailDrillCostColumns = useMemo(function () { + return [ + { + title: '成本单价(元/kg)', + dataIndex: 'costUnitPrice', + key: 'costUnitPrice', + width: 120, + align: 'right', + render: function (v) { return fmtMoney(v, 2); } + }, + { + title: '成本总价(元)', + dataIndex: 'costTotal', + key: 'costTotal', + width: 110, + align: 'right', + render: function (v) { return fmtMoney(v, 2); } + } + ]; + }, []); + + var detailDrillColumns = useMemo( + function () { + if (detailDrillModal.type === 'payableAmount') { + return detailDrillBaseColumns.concat([detailDrillQtyColumn], detailDrillCostColumns); + } + return detailDrillBaseColumns.concat([detailDrillQtyColumn]); + }, + [detailDrillModal.type, detailDrillBaseColumns, detailDrillQtyColumn, detailDrillCostColumns] + ); + + var detailDrillSummary = useMemo(function () { + if (!detailDrillModal.rows || !detailDrillModal.rows.length) { + return { hydrogenKg: 0, costTotal: 0 }; + } + return detailDrillModal.rows.reduce( + function (acc, r) { + acc.hydrogenKg += numOrZero(r.hydrogenKg); + acc.costTotal += numOrZero(r.costTotal); + return acc; + }, + { hydrogenKg: 0, costTotal: 0 } + ); + }, [detailDrillModal.rows]); + + var detailDrillTableSummary = useCallback(function () { + if (detailDrillModal.type === 'payableAmount') { + return React.createElement( + Table.Summary, + null, + React.createElement( + Table.Summary.Row, + null, + React.createElement(Table.Summary.Cell, { index: 0, align: 'center' }, '合计'), + React.createElement(Table.Summary.Cell, { index: 1, colSpan: 3 }), + React.createElement(Table.Summary.Cell, { index: 4, align: 'right' }, fmtQty(detailDrillSummary.hydrogenKg)), + React.createElement(Table.Summary.Cell, { index: 5 }), + React.createElement(Table.Summary.Cell, { index: 6, align: 'right' }, fmtMoney(detailDrillSummary.costTotal, 2)) + ) + ); + } + return React.createElement( + Table.Summary, + null, + React.createElement( + Table.Summary.Row, + null, + React.createElement(Table.Summary.Cell, { index: 0, align: 'center' }, '合计'), + React.createElement(Table.Summary.Cell, { index: 1, colSpan: 3 }), + React.createElement(Table.Summary.Cell, { index: 4, align: 'right' }, fmtQty(detailDrillSummary.hydrogenKg)) + ) + ); + }, [detailDrillModal.type, detailDrillSummary]); + + var detailDrillTitle = useMemo(function () { + if (!detailDrillModal.open) return ''; + var name = detailDrillModal.stationName ? ' · ' + detailDrillModal.stationName : ''; + return (detailDrillModal.type === 'payableAmount' ? '应付总金额明细' : '加氢量明细') + name; + }, [detailDrillModal]); + + var renderTotalsBar = useCallback(function () { + var items = [ + { key: 'hydrogenKg', label: '加氢总量(kg)', value: fmtQty(totals.hydrogenKg) }, + { key: 'payableAmount', label: '应付总金额(元)', value: fmtMoney(totals.payableAmount, 2) }, + { key: 'paidAmount', label: '已付总金额(元)', value: fmtMoney(totals.paidAmount, 2) }, + { + key: 'unpaidAmount', + label: '未付总金额(元)', + value: fmtMoney(totals.unpaidAmount, 2), + warn: numOrZero(totals.unpaidAmount) > 0 + }, + { + key: 'uninvoicedAmount', + label: '未开票总金额(元)', + value: fmtMoney(totals.uninvoicedAmount, 2), + warn: numOrZero(totals.uninvoicedAmount) > 0 + }, + { key: 'rechargeTotal', label: '充值总额(元)', value: fmtMoney(totals.rechargeTotal, 2) }, + { + key: 'prepaidBalance', + label: '当前总余额(元)', + value: fmtMoney(totals.prepaidBalance, 2), + warn: numOrZero(totals.prepaidBalance) < 0 + } + ]; + return React.createElement( + 'div', + { className: 'h2-ledger-totals-bar' }, + React.createElement('div', { className: 'h2-ledger-totals-bar__title' }, '合计'), + React.createElement( + 'div', + { className: 'h2-ledger-totals-bar__items' }, + items.map(function (item) { + return React.createElement( + 'div', + { key: item.key, className: 'h2-ledger-totals-bar__item' }, + React.createElement('div', { className: 'h2-ledger-totals-bar__label' }, item.label), + React.createElement( + 'div', + { + className: + 'h2-ledger-totals-bar__value' + (item.warn ? ' is-warn' : '') + }, + item.value + ) + ); + }) + ) + ); + }, [totals]); + + var prepaidBalanceDrillColumns = useMemo(function () { + return [ + { + title: '加氢站全称', + dataIndex: 'stationName', + key: 'stationName', + width: 220, + ellipsis: true + }, + { + title: '收入金额(元)', + dataIndex: 'incomeAmount', + key: 'incomeAmount', + width: 120, + align: 'right', + render: function (v) { return fmtLedgerMoney(v); } + }, + { + title: '支出金额(元)', + dataIndex: 'expenseAmount', + key: 'expenseAmount', + width: 120, + align: 'right', + render: function (v) { return fmtLedgerMoney(v); } + }, + { + title: '余额(元)', + dataIndex: 'balance', + key: 'balance', + width: 120, + align: 'right', + render: function (v) { + var n = numOrZero(v); + var style = n < 0 ? { color: '#cf1322', fontWeight: 600 } : undefined; + return React.createElement('span', { style: style }, fmtMoney(v, 2)); + } + }, + { + title: '订单编号', + dataIndex: 'orderNo', + key: 'orderNo', + width: 168, + align: 'center', + render: function (v) { + return React.createElement('span', { style: { fontFamily: 'monospace', fontSize: 12 } }, v || '—'); + } + } + ]; + }, []); + + var prepaidBalanceDrillSummary = useMemo(function () { + if (!prepaidBalanceDrillModal.rows || !prepaidBalanceDrillModal.rows.length) { + return { incomeTotal: 0, expenseTotal: 0, endingBalance: 0 }; + } + var last = prepaidBalanceDrillModal.rows[prepaidBalanceDrillModal.rows.length - 1]; + return prepaidBalanceDrillModal.rows.reduce( + function (acc, r) { + acc.incomeTotal += numOrZero(r.incomeAmount); + acc.expenseTotal += numOrZero(r.expenseAmount); + return acc; + }, + { incomeTotal: 0, expenseTotal: 0, endingBalance: numOrZero(last.balance) } + ); + }, [prepaidBalanceDrillModal.rows]); + + var prepaidBalanceDrillTableSummary = useCallback(function () { + return React.createElement( + Table.Summary, + null, + React.createElement( + Table.Summary.Row, + null, + React.createElement(Table.Summary.Cell, { index: 0, align: 'center' }, '合计'), + React.createElement( + Table.Summary.Cell, + { index: 1, align: 'right' }, + fmtMoney(prepaidBalanceDrillSummary.incomeTotal, 2) + ), + React.createElement( + Table.Summary.Cell, + { index: 2, align: 'right' }, + fmtMoney(prepaidBalanceDrillSummary.expenseTotal, 2) + ), + React.createElement( + Table.Summary.Cell, + { index: 3, align: 'right' }, + fmtMoney(prepaidBalanceDrillSummary.endingBalance, 2) + ), + React.createElement(Table.Summary.Cell, { index: 4 }) + ) + ); + }, [prepaidBalanceDrillSummary]); + + var rechargeDrillColumns = useMemo( + function () { + return [ + { + title: '加氢站全称', + dataIndex: 'stationName', + key: 'stationName', + width: 220, + ellipsis: true + }, + { + title: '支付时间', + dataIndex: 'payTime', + key: 'payTime', + width: 120, + align: 'center', + render: function (v) { return formatDate(v); } + }, + { + title: '预充金额(元)', + dataIndex: 'prepayAmount', + key: 'prepayAmount', + width: 120, + align: 'right', + render: function (v) { return fmtMoney(v, 2); } + }, + { + title: '付款凭证', + key: 'voucher', + width: 160, + align: 'center', + render: function (_, row) { + if (!row.voucherUrl) return '—'; + var file = { + url: row.voucherUrl, + fileName: row.voucherFileName, + mime: row.voucherMime || 'image', + title: row.voucherFileName || '付款凭证' + }; + return React.createElement( + Space, + { size: 4, wrap: true, style: { justifyContent: 'center' } }, + React.createElement( + Button, + { + type: 'link', + size: 'small', + style: { padding: 0 }, + onClick: function () { + openAttachmentPreview({ + url: file.url, + title: file.title, + mime: file.mime + }); + } + }, + '预览' + ), + React.createElement( + Button, + { + type: 'link', + size: 'small', + style: { padding: 0 }, + onClick: function () { + downloadAttachment(file); + } + }, + '下载' + ) + ); + } + } + ]; + }, + [openAttachmentPreview, downloadAttachment] + ); + + var rechargeDrillSummary = useMemo(function () { + if (!rechargeDrillModal.rows || !rechargeDrillModal.rows.length) { + return { prepayAmount: 0 }; + } + return rechargeDrillModal.rows.reduce( + function (acc, r) { + acc.prepayAmount += numOrZero(r.prepayAmount); + return acc; + }, + { prepayAmount: 0 } + ); + }, [rechargeDrillModal.rows]); + + var rechargeDrillTableSummary = useCallback(function () { + return React.createElement( + Table.Summary, + null, + React.createElement( + Table.Summary.Row, + null, + React.createElement(Table.Summary.Cell, { index: 0, align: 'center' }, '合计'), + React.createElement(Table.Summary.Cell, { index: 1 }), + React.createElement( + Table.Summary.Cell, + { index: 2, align: 'right' }, + fmtMoney(rechargeDrillSummary.prepayAmount, 2) + ), + React.createElement(Table.Summary.Cell, { index: 3 }) + ) + ); + }, [rechargeDrillSummary]); + + var paymentDrillColumns = useMemo( + function () { + return [ + { + title: '加氢站全称', + dataIndex: 'stationName', + key: 'stationName', + width: 220, + ellipsis: true + }, + { + title: '账单开始时间', + dataIndex: 'billStartDate', + key: 'billStartDate', + width: 120, + align: 'center', + render: function (v) { return formatDate(v); } + }, + { + title: '账单结束时间', + dataIndex: 'billEndDate', + key: 'billEndDate', + width: 120, + align: 'center', + render: function (v) { return formatDate(v); } + }, + { + title: '应付总金额(元)', + dataIndex: 'payableAmount', + key: 'payableAmount', + width: 120, + align: 'right', + render: function (v) { return fmtMoney(v, 2); } + }, + { + title: '已付总金额(元)', + dataIndex: 'paidAmount', + key: 'paidAmount', + width: 140, + align: 'right', + render: function (v) { return fmtMoney(v, 2); } + }, + { + title: '银行付款证明', + key: 'voucher', + width: 130, + align: 'center', + render: function (_, row) { + if (!row.voucherUrl) return '—'; + return React.createElement( + Button, + { + type: 'link', + size: 'small', + style: { padding: 0 }, + onClick: function () { + openAttachmentPreview({ + url: row.voucherUrl, + title: row.voucherName || '付款凭据', + mime: row.voucherMime || 'image' + }); + } + }, + '查看付款凭据' + ); + } + } + ]; + }, + [openAttachmentPreview] + ); + + var paymentDrillSummary = useMemo(function () { + if (!paymentDrillModal.rows || !paymentDrillModal.rows.length) { + return { payableAmount: 0, paidAmount: 0 }; + } + return paymentDrillModal.rows.reduce( + function (acc, r) { + acc.payableAmount += numOrZero(r.payableAmount); + acc.paidAmount += numOrZero(r.paidAmount); + return acc; + }, + { payableAmount: 0, paidAmount: 0 } + ); + }, [paymentDrillModal.rows]); + + var paymentDrillTableSummary = useCallback(function () { + return React.createElement( + Table.Summary, + null, + React.createElement( + Table.Summary.Row, + null, + React.createElement(Table.Summary.Cell, { index: 0, align: 'center' }, '合计'), + React.createElement(Table.Summary.Cell, { index: 1, colSpan: 2 }), + React.createElement( + Table.Summary.Cell, + { index: 3, align: 'right' }, + fmtMoney(paymentDrillSummary.payableAmount, 2) + ), + React.createElement( + Table.Summary.Cell, + { index: 4, align: 'right' }, + fmtMoney(paymentDrillSummary.paidAmount, 2) + ), + React.createElement(Table.Summary.Cell, { index: 5 }) + ) + ); + }, [paymentDrillSummary]); + + var invoiceDrillColumns = useMemo( + function () { + return [ + { + title: '加氢站全称', + dataIndex: 'stationName', + key: 'stationName', + width: 220, + ellipsis: true + }, + { + title: '开票时间', + dataIndex: 'invoiceTime', + key: 'invoiceTime', + width: 168, + align: 'center', + render: function (v) { return formatDateTime(v); } + }, + { + title: '开票金额(元)', + dataIndex: 'invoiceAmount', + key: 'invoiceAmount', + width: 120, + align: 'right', + render: function (v) { return fmtMoney(v, 2); } + }, + { + title: '发票', + key: 'invoiceFile', + width: 200, + align: 'center', + render: function (_, row) { + if (!row.invoiceUrl) return '—'; + var file = { + url: row.invoiceUrl, + fileName: row.invoiceFileName, + mime: row.invoiceMime || 'image', + title: row.invoiceFileName || '发票' + }; + return React.createElement( + Space, + { size: 4, wrap: true, style: { justifyContent: 'center' } }, + React.createElement( + Button, + { + type: 'link', + size: 'small', + style: { padding: 0 }, + onClick: function () { + openAttachmentPreview({ + url: file.url, + title: file.title, + mime: file.mime + }); + } + }, + '查看发票' + ), + React.createElement( + Button, + { + type: 'link', + size: 'small', + style: { padding: 0 }, + onClick: function () { + downloadAttachment(file); + } + }, + '下载发票' + ) + ); + } + } + ]; + }, + [openAttachmentPreview, downloadAttachment] + ); + + var invoiceDrillSummary = useMemo(function () { + if (!invoiceDrillModal.rows || !invoiceDrillModal.rows.length) { + return { invoiceAmount: 0 }; + } + return invoiceDrillModal.rows.reduce( + function (acc, r) { + acc.invoiceAmount += numOrZero(r.invoiceAmount); + return acc; + }, + { invoiceAmount: 0 } + ); + }, [invoiceDrillModal.rows]); + + var invoiceDrillTableSummary = useCallback(function () { + return React.createElement( + Table.Summary, + null, + React.createElement( + Table.Summary.Row, + null, + React.createElement(Table.Summary.Cell, { index: 0, align: 'center' }, '合计'), + React.createElement(Table.Summary.Cell, { index: 1 }), + React.createElement( + Table.Summary.Cell, + { index: 2, align: 'right' }, + fmtMoney(invoiceDrillSummary.invoiceAmount, 2) + ), + React.createElement(Table.Summary.Cell, { index: 3 }) + ) + ); + }, [invoiceDrillSummary]); + + return React.createElement( + App, + null, + React.createElement('style', null, ledgerTableStyle), + React.createElement( + 'div', + { style: layoutStyle }, + React.createElement( + 'div', + { + style: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: 12, + gap: 16 + } + }, + React.createElement(Breadcrumb, { + items: [{ title: '台账数据' }, { title: '氢费(采购端)汇总报表' }] + }), + React.createElement( + Button, + { + type: 'link', + style: { padding: 0, flexShrink: 0, fontSize: 14 }, + onClick: function () { + setReqDetailOpen(true); + } + }, + '查看需求说明' + ) + ), + React.createElement( + Card, + { style: filterCardStyle, bodyStyle: { paddingBottom: 4 } }, + React.createElement( + Row, + { gutter: [16, 0], align: 'bottom' }, + React.createElement( + Col, + { xs: 24, sm: 12, lg: 8 }, + React.createElement( + 'div', + { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '地区'), + React.createElement(Select, { + allowClear: true, + showSearch: true, + placeholder: '全部', + style: filterControlStyle, + value: regionDraft, + onChange: function (v) { setRegionDraft(v); }, + options: REGION_OPTIONS, + filterOption: filterOption + }) + ) + ), + React.createElement( + Col, + { xs: 24, sm: 12, lg: 8 }, + React.createElement( + 'div', + { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '加氢站全称'), + React.createElement(Select, { + allowClear: true, + showSearch: true, + placeholder: '全部', + style: filterControlStyle, + value: stationDraft, + onChange: function (v) { setStationDraft(v); }, + options: STATION_OPTIONS, + filterOption: filterOption + }) + ) + ), + React.createElement( + Col, + { xs: 24, sm: 12, lg: 8 }, + React.createElement( + 'div', + { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '结算方式'), + React.createElement(Select, { + allowClear: true, + placeholder: '全部', + style: filterControlStyle, + value: settlementDraft, + onChange: function (v) { setSettlementDraft(v); }, + options: SETTLEMENT_OPTIONS + }) + ) + ), + React.createElement( + Col, + { xs: 24, sm: 24, lg: 24, style: Object.assign({}, filterActionsColStyle, { textAlign: 'right' }) }, + React.createElement( + 'div', + { style: Object.assign({}, filterItemStyle, { marginBottom: 4 }) }, + React.createElement( + Space, + { wrap: true, style: { justifyContent: 'flex-end', width: '100%' } }, + 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 + } + }, + filterSummaryText + ), + renderTotalsBar(), + React.createElement( + 'div', + { className: 'h2-purchase-summary-wrap' }, + React.createElement(Table, { + className: 'h2-purchase-summary-table h2-purchase-summary-table--resizable', + size: 'small', + bordered: true, + rowKey: 'key', + columns: columns, + dataSource: dataSource, + pagination: false, + rowClassName: function () { return 'h2-purchase-row'; }, + scroll: { y: 'calc(100vh - 430px)' }, + sticky: true, + tableLayout: 'fixed', + locale: { emptyText: '暂无数据,请调整筛选条件后查询' } + }) + ) + ), + React.createElement( + Modal, + { + title: detailDrillTitle, + open: detailDrillModal.open, + onCancel: closeDetailDrill, + footer: React.createElement(Button, { onClick: closeDetailDrill }, '关闭'), + width: 960, + destroyOnClose: true, + styles: { body: { maxHeight: '72vh', overflow: 'auto', paddingTop: 8 } } + }, + React.createElement(Table, { + className: 'h2-purchase-summary-table', + size: 'small', + bordered: true, + rowKey: 'key', + pagination: detailDrillModal.rows.length > 10 ? { pageSize: 10, showSizeChanger: false } : false, + columns: detailDrillColumns, + dataSource: detailDrillModal.rows, + summary: detailDrillTableSummary, + scroll: { x: 'max-content' }, + locale: { emptyText: '暂无明细数据' } + }) + ), + React.createElement( + Modal, + { + title: + '充值总额明细' + + (rechargeDrillModal.stationName ? ' · ' + rechargeDrillModal.stationName : ''), + open: rechargeDrillModal.open, + onCancel: closeRechargeDrill, + footer: React.createElement(Button, { onClick: closeRechargeDrill }, '关闭'), + width: 880, + destroyOnClose: true, + styles: { body: { maxHeight: '72vh', overflow: 'auto', paddingTop: 8 } } + }, + React.createElement(Table, { + className: 'h2-purchase-summary-table', + size: 'small', + bordered: true, + rowKey: 'key', + pagination: + rechargeDrillModal.rows.length > 10 + ? { pageSize: 10, showSizeChanger: false } + : false, + columns: rechargeDrillColumns, + dataSource: rechargeDrillModal.rows, + summary: rechargeDrillTableSummary, + scroll: { x: 'max-content' }, + locale: { emptyText: '暂无充值记录' } + }) + ), + React.createElement( + Modal, + { + title: + '余额变更明细' + + (prepaidBalanceDrillModal.stationName ? ' · ' + prepaidBalanceDrillModal.stationName : ''), + open: prepaidBalanceDrillModal.open, + onCancel: closePrepaidBalanceDrill, + footer: React.createElement(Button, { onClick: closePrepaidBalanceDrill }, '关闭'), + width: 880, + destroyOnClose: true, + styles: { body: { maxHeight: '72vh', overflow: 'auto', paddingTop: 8 } } + }, + React.createElement(Table, { + className: 'h2-purchase-summary-table', + size: 'small', + bordered: true, + rowKey: 'key', + pagination: + prepaidBalanceDrillModal.rows.length > 10 + ? { pageSize: 10, showSizeChanger: false } + : false, + columns: prepaidBalanceDrillColumns, + dataSource: prepaidBalanceDrillModal.rows, + summary: prepaidBalanceDrillTableSummary, + scroll: { x: 'max-content' }, + locale: { emptyText: '暂无余额变更记录' } + }) + ), + React.createElement( + Modal, + { + title: + '已付总金额明细' + + (paymentDrillModal.stationName ? ' · ' + paymentDrillModal.stationName : ''), + open: paymentDrillModal.open, + onCancel: closePaymentDrill, + footer: React.createElement(Button, { onClick: closePaymentDrill }, '关闭'), + width: 1020, + destroyOnClose: true, + styles: { body: { maxHeight: '72vh', overflow: 'auto', paddingTop: 8 } } + }, + React.createElement(Table, { + className: 'h2-purchase-summary-table', + size: 'small', + bordered: true, + rowKey: 'key', + pagination: + paymentDrillModal.rows.length > 10 + ? { pageSize: 10, showSizeChanger: false } + : false, + columns: paymentDrillColumns, + dataSource: paymentDrillModal.rows, + summary: paymentDrillTableSummary, + scroll: { x: 'max-content' }, + locale: { emptyText: '暂无支付明细' } + }) + ), + React.createElement( + Modal, + { + title: + '已开票明细' + (invoiceDrillModal.stationName ? ' · ' + invoiceDrillModal.stationName : ''), + open: invoiceDrillModal.open, + onCancel: closeInvoiceDrill, + footer: React.createElement(Button, { onClick: closeInvoiceDrill }, '关闭'), + width: 920, + destroyOnClose: true, + styles: { body: { maxHeight: '72vh', overflow: 'auto', paddingTop: 8 } } + }, + React.createElement(Table, { + className: 'h2-purchase-summary-table', + size: 'small', + bordered: true, + rowKey: 'key', + pagination: + invoiceDrillModal.rows.length > 10 + ? { pageSize: 10, showSizeChanger: false } + : false, + columns: invoiceDrillColumns, + dataSource: invoiceDrillModal.rows, + summary: invoiceDrillTableSummary, + scroll: { x: 'max-content' }, + locale: { emptyText: '暂无开票记录' } + }) + ), + React.createElement( + Modal, + { + title: attachmentPreview.title || '附件预览', + open: attachmentPreview.open, + onCancel: closeAttachmentPreview, + footer: React.createElement(Button, { onClick: closeAttachmentPreview }, '关闭'), + width: attachmentPreview.mime === 'pdf' ? 920 : 720, + destroyOnClose: true, + centered: true, + styles: { body: { paddingTop: 12, textAlign: 'center' } } + }, + attachmentPreview.mime === 'pdf' + ? React.createElement('iframe', { + src: attachmentPreview.url, + title: attachmentPreview.title, + style: { width: '100%', height: '70vh', border: 'none' } + }) + : React.createElement('img', { + src: attachmentPreview.url, + alt: attachmentPreview.title, + style: { maxWidth: '100%', maxHeight: '70vh', objectFit: 'contain' } + }) + ), + React.createElement( + Modal, + { + title: '氢费(采购端)汇总报表 — 需求说明', + open: reqDetailOpen, + onCancel: function () { + setReqDetailOpen(false); + }, + footer: React.createElement( + Button, + { + type: 'primary', + onClick: function () { + setReqDetailOpen(false); + } + }, + '知道了' + ), + width: 840, + centered: true, + destroyOnClose: true, + styles: { + body: { + maxHeight: '72vh', + overflow: 'auto', + paddingTop: 8, + paddingBottom: 16 + } + } + }, + renderPurchaseRequirementDocPanel() + ) + ) + ); +}; diff --git a/web端/台账数据/车辆氢费明细-需求内容.js b/web端/台账数据/车辆氢费明细-需求内容.js index d6369a0..6a6ec35 100644 --- a/web端/台账数据/车辆氢费明细-需求内容.js +++ b/web端/台账数据/车辆氢费明细-需求内容.js @@ -3,11 +3,11 @@ var H2_LEDGER_REQUIREMENT_DOC = `# 车辆氢费明细 — 产品需求说明(P | 项目 | 内容 | |------|------| -| 文档版本 | v2.0(业务版) | +| 文档版本 | v2.1(业务版) | | 产品模块 | 台账数据 → 车辆氢费明细 | | 文档类型 | 产品需求说明 | | 适用读者 | 产品、业务、运营、测试、项目 | -| 修订说明 | 从业务与使用场景出发描述需求,不展开技术实现细节 | +| 修订说明 | 同步列表字段顺序、顶部合计条、承担方式、系统带出字段等现行页面行为 | --- @@ -130,9 +130,9 @@ flowchart LR | 客户名称 | 从客户库选择 | | 加氢站名称 | 从加氢站库选择 | | 业务员 | 按客户归属业务员筛选 | -| 结算状态 | 客户承担 / 我司承担 | -| 付款状态 | 未付款 / 已付款 / 部分付款 | -| 开票公司 | 从开票主体选择 | +| 承担方式 | 客户承担 / 我司承担 / 客户自行结算 / 其他结算 | +| 客户收款状态 | 已付款 / 未付款(系统带出,用于筛选) | +| 开票公司 | 从开票主体选择(系统带出字段对应公司) | 查询成功给予简短成功提示。 @@ -154,7 +154,17 @@ flowchart LR 不同区域之间有明显分隔,已对账区背景略灰、待保存区背景略黄,降低误操作概率。 -**底部合计:** 对当前列表可见记录,合计 **加氢量、成本总价、加氢总价**(随筛选与「仅看异常」变化)。 +**顶部合计条:** 表格上方独立展示 **加氢量(kg)、成本总价(元)、加氢总价(元)** 三项合计。 + +- **统计范围:** 与当前列表可见行一致。 +- **随查询重算:** 点击「查询」应用筛选条件后,合计立即按新结果重新汇总。 +- **随「仅显示异常数据」变化:** 开关开启时,仅对当前可见的异常记录(及本人待保存行)合计。 +- **说明:** 合计条含「待保存 / 未对账 / 已对账」等当前列表中的全部可见记录,与导出范围(仅未对账+已对账)不同。 + +**列表列顺序(左→右,业务视角):** +序号 → 加氢日期 → 加氢时间 → 加氢站名称 → 客户名称 → 车牌号 → 加氢量 → 成本单价 → 成本总价 → 加氢单价 → 加氢总价 → 行驶里程 → 备注 → 业务员 → 承担方式 → 对账日期 → **收票日期** → **加氢站付款状态** → **开票日期** → **客户收款状态** → **开票公司** → 状态 → 订单编号 → 操作(操作列固定右侧)。 + +**系统带出字段(不可手工维护):** 收票日期、加氢站付款状态、开票日期、客户收款状态、开票公司;选择/变更客户等信息后由系统自动刷新,单元格为浅灰底只读样式。 **列表勾选:** 仅 **未对账** 记录可勾选,用于批量「完成对账」。 @@ -178,26 +188,28 @@ flowchart LR | 信息项 | 用户是否填写 | 业务说明 | |--------|--------------|----------| -| 状态 | 否 | 展示「未对账」「已对账」;待保存不显示标签 | -| 订单编号 | 否 | 系统按规则自动生成,不可改 | | 加氢时间 | 是* | 精确到秒,必填 | -| 加氢日期 | 否 | 由加氢时间自动得出 | +| 加氢日期 | 否 | 由加氢时间自动得出,只读 | | 加氢站名称 | 是* | 必选 | +| 客户名称 | 是* | 必选,位于加氢站右侧;决定业务员与系统带出信息 | | 车牌号 | 是* | 必选,须为公司登记车辆 | -| 行驶里程 | 否 | 选填,用于业务留痕 | | 加氢量(kg) | 是* | 必填,用于算总价 | | 成本单价 | 是* | 必填,向站点的采购成本价 | | 成本总价 | 否 | 加氢量 × 成本单价,自动计算 | -| 客户名称 | 是* | 必选,决定业务员与财务带出信息 | | 加氢单价 | 是* | 必填,对客户的销售单价 | | 加氢总价 | 否 | 加氢量 × 加氢单价,自动计算 | -| 业务员 | 否 | 选客户后自动带出 | -| 结算状态 | 是* | 客户承担 / 我司承担 | -| 开票日期 | 否 | 系统按客户等业务规则带出 | -| 对账日期 | 否 | 系统带出 | -| 付款状态 | 否 | 系统带出 | -| 开票公司 | 否 | 系统带出 | +| 行驶里程 | 否 | 选填,位于加氢总价之后 | | 备注 | 否 | 自由文本 | +| 业务员 | 否 | 选客户后自动带出 | +| 承担方式 | 是* | 客户承担 / 我司承担 / 客户自行结算 / 其他结算 | +| 对账日期 | 否 | 完成对账等流程带出,只读 | +| 收票日期 | 否 | 由财务/收票模块自动带出,不可编辑 | +| 加氢站付款状态 | 否 | 已付款 / 未付款,由加氢站打款管理带出 | +| 开票日期 | 否 | 由开票模块自动带出 | +| 客户收款状态 | 否 | 已付款 / 未付款,由客户收款模块带出 | +| 开票公司 | 否 | 显示开票公司名称,系统自动带出 | +| 状态 | 否 | 展示「未对账」「已对账」;待保存不显示标签;列在列表末尾 | +| 订单编号 | 否 | 系统按规则自动生成,不可改;列在列表末尾 | \\* 保存时校验,缺失则标红提示。 @@ -267,7 +279,7 @@ flowchart LR ### 8.1 表头批量改价 / 改结算 - 在 **成本单价**、**加氢单价** 列表头可批量填入统一单价,作用于当前可编辑的所有记录(待保存 + 未对账)。 -- 在 **结算状态** 列表头可批量改为「客户承担」或「我司承担」。 +- 在 **承担方式** 列表头可批量改为四种承担方式之一。 - **已对账** 记录不参与批量修改。 ### 8.2 批量导入 @@ -277,11 +289,11 @@ flowchart LR **流程:** 1. 点击「批量导入」→ 下载标准模板 -2. 按模板填写(无需填开票日期、对账日期、付款状态、开票公司) +2. 按模板填写(无需填对账日期及收票/开票/付款等系统带出字段) 3. 上传文件 → 系统生成多条 **待保存** 记录 4. 业务核对列表 → 点击 **保存** 进入未对账 -**模板包含列:** 加氢时间、加氢站名称、车牌号、行驶里程、加氢量、成本单价、客户名称、加氢单价、结算状态、备注。 +**模板包含列:** 加氢时间、加氢站名称、车牌号、加氢量、成本单价、客户名称、加氢单价、承担方式、备注(行驶里程可在备注前按业务需要填写,导入模板列顺序以页面下载为准)。 **导入后状态:** 均为待保存,与手工新增一致,须保存后才参与对账与导出。 @@ -297,7 +309,7 @@ flowchart LR - 弹窗文案:「请选择导出的列」;支持全选/取消全选;默认勾选全部可导出列。 - 无符合条件数据时提示:暂无未对账或已对账数据可导出。 -**可导出列(业务名称):** 序号、状态、订单编号、加氢时间、加氢日期、加氢站名称、车牌号、行驶里程、加氢量、成本单价、成本总价、客户名称、加氢单价、加氢总价、业务员、结算状态、开票日期、对账日期、付款状态、开票公司、备注。 +**可导出列(业务名称):** 序号、状态、订单编号、加氢时间、加氢日期、加氢站名称、车牌号、加氢量、成本单价、成本总价、客户名称、加氢单价、加氢总价、行驶里程、业务员、承担方式、对账日期、备注、收票日期、加氢站付款状态、开票日期、客户收款状态、开票公司。 --- @@ -326,7 +338,7 @@ flowchart LR ### 10.1 保存 - 一次保存处理页面上**全部**待保存记录。 -- 必填:加氢时间、加氢站、车牌(须为登记车辆)、客户、加氢量、成本单价、加氢单价、结算状态。 +- 必填:加氢时间、加氢站、车牌(须为登记车辆)、客户、加氢量、成本单价、加氢单价、承担方式。 - 校验失败:仅格子标红,无「保存成功」类提示。 ### 10.2 完成对账 @@ -338,8 +350,8 @@ flowchart LR ### 10.3 筛选与列表 - 查询点击后生效;重置恢复。 -- 筛选后合计与所见列表一致。 -- 本人待保存新增行在筛选后仍可见。 +- **顶部合计条** 对当前列表可见行实时汇总;修改筛选并点击「查询」后重新计算;「仅显示异常数据」开启时仅统计可见行。 +- 本人待保存新增行在筛选后仍可见(合计亦包含这些行,若其在列表中展示)。 ### 10.4 导出 @@ -375,9 +387,13 @@ flowchart LR | 待保存 | 草稿,未进入对账流程 | | 未对账 | 已保存,等待业务确认并完成对账 | | 已对账 | 对账完成,业务员不可随意改动 | -| 客户承担 | 氢费由客户结算 | -| 我司承担 | 氢费由公司承担 | -| 未付款 / 已付款 / 部分付款 | 系统带出的付款进度,业务只读 | +| 客户承担 | 氢费由客户承担 | +| 我司承担 | 氢费由我司承担 | +| 客户自行结算 | 由客户自行与加氢站等方结算 | +| 其他结算 | 其他结算方式 | +| 加氢站付款状态 | 加氢站侧是否已付款,已付款 / 未付款 | +| 客户收款状态 | 客户侧是否已收款,已付款 / 未付款 | +| 收票日期 | 财务收票日期,系统带出 | | 标准价 | 公司维护的、按加氢站与生效时段确定的参考单价 | --- diff --git a/web端/台账数据/车辆氢费明细-需求文档.md b/web端/台账数据/车辆氢费明细-需求文档.md index 5df2e3d..82c47d3 100644 --- a/web端/台账数据/车辆氢费明细-需求文档.md +++ b/web端/台账数据/车辆氢费明细-需求文档.md @@ -2,11 +2,11 @@ | 项目 | 内容 | |------|------| -| 文档版本 | v2.0(业务版) | +| 文档版本 | v2.1(业务版) | | 产品模块 | 台账数据 → 车辆氢费明细 | | 文档类型 | 产品需求说明 | | 适用读者 | 产品、业务、运营、测试、项目 | -| 修订说明 | 从业务与使用场景出发描述需求,不展开技术实现细节 | +| 修订说明 | 同步列表字段顺序、顶部合计条、承担方式、系统带出字段等现行页面行为 | --- @@ -75,14 +75,14 @@ ### 3.1 端到端流程(推荐操作顺序) -```mermaid +\`\`\`mermaid flowchart LR A[录入/复制] --> B[待保存] B --> C[点击保存] C[保存/导入] --> D[未对账] D --> E[勾选并完成对账] E --> F[已对账] -``` +\`\`\` **说明:** @@ -129,9 +129,9 @@ flowchart LR | 客户名称 | 从客户库选择 | | 加氢站名称 | 从加氢站库选择 | | 业务员 | 按客户归属业务员筛选 | -| 结算状态 | 客户承担 / 我司承担 | -| 付款状态 | 未付款 / 已付款 / 部分付款 | -| 开票公司 | 从开票主体选择 | +| 承担方式 | 客户承担 / 我司承担 / 客户自行结算 / 其他结算 | +| 客户收款状态 | 已付款 / 未付款(系统带出,用于筛选) | +| 开票公司 | 从开票主体选择(系统带出字段对应公司) | 查询成功给予简短成功提示。 @@ -153,7 +153,17 @@ flowchart LR 不同区域之间有明显分隔,已对账区背景略灰、待保存区背景略黄,降低误操作概率。 -**底部合计:** 对当前列表可见记录,合计 **加氢量、成本总价、加氢总价**(随筛选与「仅看异常」变化)。 +**顶部合计条:** 表格上方独立展示 **加氢量(kg)、成本总价(元)、加氢总价(元)** 三项合计。 + +- **统计范围:** 与当前列表可见行一致。 +- **随查询重算:** 点击「查询」应用筛选条件后,合计立即按新结果重新汇总。 +- **随「仅显示异常数据」变化:** 开关开启时,仅对当前可见的异常记录(及本人待保存行)合计。 +- **说明:** 合计条含「待保存 / 未对账 / 已对账」等当前列表中的全部可见记录,与导出范围(仅未对账+已对账)不同。 + +**列表列顺序(左→右,业务视角):** +序号 → 加氢日期 → 加氢时间 → 加氢站名称 → 客户名称 → 车牌号 → 加氢量 → 成本单价 → 成本总价 → 加氢单价 → 加氢总价 → 行驶里程 → 备注 → 业务员 → 承担方式 → 对账日期 → **收票日期** → **加氢站付款状态** → **开票日期** → **客户收款状态** → **开票公司** → 状态 → 订单编号 → 操作(操作列固定右侧)。 + +**系统带出字段(不可手工维护):** 收票日期、加氢站付款状态、开票日期、客户收款状态、开票公司;选择/变更客户等信息后由系统自动刷新,单元格为浅灰底只读样式。 **列表勾选:** 仅 **未对账** 记录可勾选,用于批量「完成对账」。 @@ -177,28 +187,30 @@ flowchart LR | 信息项 | 用户是否填写 | 业务说明 | |--------|--------------|----------| -| 状态 | 否 | 展示「未对账」「已对账」;待保存不显示标签 | -| 订单编号 | 否 | 系统按规则自动生成,不可改 | | 加氢时间 | 是* | 精确到秒,必填 | -| 加氢日期 | 否 | 由加氢时间自动得出 | +| 加氢日期 | 否 | 由加氢时间自动得出,只读 | | 加氢站名称 | 是* | 必选 | +| 客户名称 | 是* | 必选,位于加氢站右侧;决定业务员与系统带出信息 | | 车牌号 | 是* | 必选,须为公司登记车辆 | -| 行驶里程 | 否 | 选填,用于业务留痕 | | 加氢量(kg) | 是* | 必填,用于算总价 | | 成本单价 | 是* | 必填,向站点的采购成本价 | | 成本总价 | 否 | 加氢量 × 成本单价,自动计算 | -| 客户名称 | 是* | 必选,决定业务员与财务带出信息 | | 加氢单价 | 是* | 必填,对客户的销售单价 | | 加氢总价 | 否 | 加氢量 × 加氢单价,自动计算 | -| 业务员 | 否 | 选客户后自动带出 | -| 结算状态 | 是* | 客户承担 / 我司承担 | -| 开票日期 | 否 | 系统按客户等业务规则带出 | -| 对账日期 | 否 | 系统带出 | -| 付款状态 | 否 | 系统带出 | -| 开票公司 | 否 | 系统带出 | +| 行驶里程 | 否 | 选填,位于加氢总价之后 | | 备注 | 否 | 自由文本 | +| 业务员 | 否 | 选客户后自动带出 | +| 承担方式 | 是* | 客户承担 / 我司承担 / 客户自行结算 / 其他结算 | +| 对账日期 | 否 | 完成对账等流程带出,只读 | +| 收票日期 | 否 | 由财务/收票模块自动带出,不可编辑 | +| 加氢站付款状态 | 否 | 已付款 / 未付款,由加氢站打款管理带出 | +| 开票日期 | 否 | 由开票模块自动带出 | +| 客户收款状态 | 否 | 已付款 / 未付款,由客户收款模块带出 | +| 开票公司 | 否 | 显示开票公司名称,系统自动带出 | +| 状态 | 否 | 展示「未对账」「已对账」;待保存不显示标签;列在列表末尾 | +| 订单编号 | 否 | 系统按规则自动生成,不可改;列在列表末尾 | -\* 保存时校验,缺失则标红提示。 +\\* 保存时校验,缺失则标红提示。 ### 5.2 订单编号规则(业务口径) @@ -266,7 +278,7 @@ flowchart LR ### 8.1 表头批量改价 / 改结算 - 在 **成本单价**、**加氢单价** 列表头可批量填入统一单价,作用于当前可编辑的所有记录(待保存 + 未对账)。 -- 在 **结算状态** 列表头可批量改为「客户承担」或「我司承担」。 +- 在 **承担方式** 列表头可批量改为四种承担方式之一。 - **已对账** 记录不参与批量修改。 ### 8.2 批量导入 @@ -276,11 +288,11 @@ flowchart LR **流程:** 1. 点击「批量导入」→ 下载标准模板 -2. 按模板填写(无需填开票日期、对账日期、付款状态、开票公司) +2. 按模板填写(无需填对账日期及收票/开票/付款等系统带出字段) 3. 上传文件 → 系统生成多条 **待保存** 记录 4. 业务核对列表 → 点击 **保存** 进入未对账 -**模板包含列:** 加氢时间、加氢站名称、车牌号、行驶里程、加氢量、成本单价、客户名称、加氢单价、结算状态、备注。 +**模板包含列:** 加氢时间、加氢站名称、车牌号、加氢量、成本单价、客户名称、加氢单价、承担方式、备注(行驶里程可在备注前按业务需要填写,导入模板列顺序以页面下载为准)。 **导入后状态:** 均为待保存,与手工新增一致,须保存后才参与对账与导出。 @@ -296,7 +308,7 @@ flowchart LR - 弹窗文案:「请选择导出的列」;支持全选/取消全选;默认勾选全部可导出列。 - 无符合条件数据时提示:暂无未对账或已对账数据可导出。 -**可导出列(业务名称):** 序号、状态、订单编号、加氢时间、加氢日期、加氢站名称、车牌号、行驶里程、加氢量、成本单价、成本总价、客户名称、加氢单价、加氢总价、业务员、结算状态、开票日期、对账日期、付款状态、开票公司、备注。 +**可导出列(业务名称):** 序号、状态、订单编号、加氢时间、加氢日期、加氢站名称、车牌号、加氢量、成本单价、成本总价、客户名称、加氢单价、加氢总价、行驶里程、业务员、承担方式、对账日期、备注、收票日期、加氢站付款状态、开票日期、客户收款状态、开票公司。 --- @@ -325,7 +337,7 @@ flowchart LR ### 10.1 保存 - 一次保存处理页面上**全部**待保存记录。 -- 必填:加氢时间、加氢站、车牌(须为登记车辆)、客户、加氢量、成本单价、加氢单价、结算状态。 +- 必填:加氢时间、加氢站、车牌(须为登记车辆)、客户、加氢量、成本单价、加氢单价、承担方式。 - 校验失败:仅格子标红,无「保存成功」类提示。 ### 10.2 完成对账 @@ -337,8 +349,8 @@ flowchart LR ### 10.3 筛选与列表 - 查询点击后生效;重置恢复。 -- 筛选后合计与所见列表一致。 -- 本人待保存新增行在筛选后仍可见。 +- **顶部合计条** 对当前列表可见行实时汇总;修改筛选并点击「查询」后重新计算;「仅显示异常数据」开启时仅统计可见行。 +- 本人待保存新增行在筛选后仍可见(合计亦包含这些行,若其在列表中展示)。 ### 10.4 导出 @@ -374,9 +386,13 @@ flowchart LR | 待保存 | 草稿,未进入对账流程 | | 未对账 | 已保存,等待业务确认并完成对账 | | 已对账 | 对账完成,业务员不可随意改动 | -| 客户承担 | 氢费由客户结算 | -| 我司承担 | 氢费由公司承担 | -| 未付款 / 已付款 / 部分付款 | 系统带出的付款进度,业务只读 | +| 客户承担 | 氢费由客户承担 | +| 我司承担 | 氢费由我司承担 | +| 客户自行结算 | 由客户自行与加氢站等方结算 | +| 其他结算 | 其他结算方式 | +| 加氢站付款状态 | 加氢站侧是否已付款,已付款 / 未付款 | +| 客户收款状态 | 客户侧是否已收款,已付款 / 未付款 | +| 收票日期 | 财务收票日期,系统带出 | | 标准价 | 公司维护的、按加氢站与生效时段确定的参考单价 | --- diff --git a/web端/台账数据/车辆维修明细.jsx b/web端/台账数据/车辆维修明细.jsx new file mode 100644 index 0000000..143f4d2 --- /dev/null +++ b/web端/台账数据/车辆维修明细.jsx @@ -0,0 +1,1823 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 台账数据 - 车辆维修/保养明细(运维手动录入;提报人/类型多选筛选;确认提交后锁定,主管可编辑) +// 原型:保存/确认提交、表格内联编辑;联调后对接登录用户、角色与车辆清单接口 + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + var useCallback = React.useCallback; + var useRef = React.useRef; + + 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 Input = antd.Input; + var InputNumber = antd.InputNumber; + var Row = antd.Row; + var Col = antd.Col; + var Space = antd.Space; + var Tag = antd.Tag; + var Popconfirm = antd.Popconfirm; + var Popover = antd.Popover; + var Tooltip = antd.Tooltip; + var Dropdown = antd.Dropdown; + var Modal = antd.Modal; + var message = antd.message; + + var CURRENT_USER = { id: 'u_zhang', name: '张运维', role: 'staff' }; + + var SUBMIT_CONFIRM_TEXT = '提交后数据将无法修改,如需修改请联系运维主管进行修改'; + + var RECORD_TYPE_OPTIONS = [ + { value: '维修', label: '维修' }, + { value: '保养', label: '保养' } + ]; + + var CHANGE_LOG_FIELD_LABELS = { + repairDate: '进修日期', + plateNo: '车牌号', + recordType: '类型', + itemDesc: '项目/材料', + laborUnit: '人工费单位', + laborQty: '人工费数量', + laborUnitPrice: '人工费单价(含税)', + partsUnit: '配件费单位', + partsQty: '配件费数量', + partsUnitPrice: '配件费单价(含税)', + submitStatus: '提交状态', + saveStatus: '保存状态', + _copy: '复制新增', + _create: '新增记录' + }; + + /** + * 系统全部车辆车牌(原型枚举,与车辆管理模块一致;联调后由车辆清单接口加载) + */ + var SYSTEM_VEHICLE_PLATES = [ + '京C12345', + '京CN88771F', + '京E88888', + '京F33333', + '京G12345', + '京H88888', + '川AL55602F', + '沪A12345', + '沪AD12345F', + '沪B99999', + '沪C11111', + '沪D66666', + '浙AD12345F', + '浙AH55660F', + '浙BK33210F', + '苏EF99887F', + '粤A11B22', + '粤A12345', + '粤A66D66', + '粤A77F77', + '粤A88K88', + '粤A99A99', + '粤B12345', + '粤B22C33', + '粤B44E44', + '粤B55B55', + '粤B67890', + '粤BK33210F' + ]; + + function normPlateKey(plate) { + return String(plate == null ? '' : plate) + .replace(/[\s\-·]/g, '') + .toUpperCase(); + } + + function isPlateInVehicleTable(plate) { + if (!plate) return false; + var key = normPlateKey(plate); + for (var i = 0; i < SYSTEM_VEHICLE_PLATES.length; i++) { + if (normPlateKey(SYSTEM_VEHICLE_PLATES[i]) === key) return true; + } + return false; + } + + function isEmptyStr(v) { + return !String(v == null ? '' : v).trim(); + } + + function isEmptyNum(v) { + return v === null || v === undefined || v === ''; + } + + function getRowInvalidMap(row) { + var inv = {}; + if (!row.repairDate) inv.repairDate = true; + if (!row.plateNo) inv.plateNo = 'empty'; + else if (!isPlateInVehicleTable(row.plateNo)) inv.plateNo = 'not_exist'; + if (isEmptyStr(row.recordType)) inv.recordType = true; + if (isEmptyStr(row.itemDesc)) inv.itemDesc = true; + if (isEmptyStr(row.laborUnit)) inv.laborUnit = true; + if (isEmptyNum(row.laborQty)) inv.laborQty = true; + if (isEmptyNum(row.laborUnitPrice)) inv.laborUnitPrice = true; + if (isEmptyStr(row.partsUnit)) inv.partsUnit = true; + if (isEmptyNum(row.partsQty)) inv.partsQty = true; + if (isEmptyNum(row.partsUnitPrice)) inv.partsUnitPrice = true; + return inv; + } + + /** 列表展示顺序:0 已确认提交 → 1 已保存 → 2 新增未保存 */ + function rowDisplaySortTier(row) { + if (row && row.submitStatus === 'submitted') return 0; + if (row && row.saveStatus === 'saved') return 1; + return 2; + } + + function rowStatusLabel(row) { + if (row && row.submitStatus === 'submitted') return '已提交'; + if (row && row.saveStatus === 'saved') return '已保存'; + return '待保存'; + } + + function isSavedDraftRow(row) { + return row && row.submitStatus !== 'submitted' && row.saveStatus === 'saved'; + } + + /** 状态为「已保存」的行均在操作列展示「编辑」「删除」 */ + function canShowSavedActions(row) { + return isSavedDraftRow(row); + } + + /** 状态为「已提交」:仅运维主管在操作列展示「编辑」「删除」 */ + function canShowSubmittedActions(row, isSupervisorRole) { + return !!isSupervisorRole && row && row.submitStatus === 'submitted'; + } + + function canDeleteRow(row, isSupervisorRole) { + if (row && row.submitStatus === 'submitted') { + return !!isSupervisorRole; + } + if (isSupervisorRole) return true; + return row && row.createdBy === CURRENT_USER.id; + } + + /** 已提交不可复制;其余本人或主管可复制 */ + function canCopyRow(row, isSupervisorRole) { + if (!row || row.submitStatus === 'submitted') return false; + if (isSupervisorRole) return true; + return row.createdBy === CURRENT_USER.id; + } + + function cloneRowTimeValue(t) { + if (!t) return t; + try { + if (window.dayjs && window.dayjs.isDayjs && window.dayjs.isDayjs(t)) return t.clone(); + if (window.dayjs) { + var d = window.dayjs(t); + return d.isValid() ? d : t; + } + } catch (eClone) {} + return t; + } + + function buildCopiedRow(sourceRow) { + var now = nowDayjs(); + return recalcRow({ + key: nextRowKey(), + createdBy: CURRENT_USER.id, + reporterName: CURRENT_USER.name, + repairDate: cloneRowTimeValue(sourceRow.repairDate), + plateNo: sourceRow.plateNo, + reportTime: now, + recordType: sourceRow.recordType, + itemDesc: sourceRow.itemDesc || '', + laborUnit: sourceRow.laborUnit || '', + laborQty: sourceRow.laborQty, + laborUnitPrice: sourceRow.laborUnitPrice, + laborAmount: 0, + partsUnit: sourceRow.partsUnit || '', + partsQty: sourceRow.partsQty, + partsUnitPrice: sourceRow.partsUnitPrice, + partsAmount: 0, + totalCost: 0, + submitStatus: 'draft', + saveStatus: 'unsaved', + submittedAt: null + }); + } + + function valuesEqualForLog(field, a, b) { + if (a === b) return true; + if ((a == null || a === '') && (b == null || b === '')) return true; + if (field === 'repairDate' && window.dayjs) { + try { + return window.dayjs(a).valueOf() === window.dayjs(b).valueOf(); + } catch (eLog) { + return false; + } + } + if ( + field === 'laborQty' || + field === 'laborUnitPrice' || + field === 'partsQty' || + field === 'partsUnitPrice' + ) { + if (isEmptyNum(a) && isEmptyNum(b)) return true; + if (field === 'laborQty' || field === 'partsQty') return toIntQty(a) === toIntQty(b); + return roundMoney(a) === roundMoney(b); + } + return String(a) === String(b); + } + + function formatLogValue(field, value, rowCtx) { + if (value == null || value === '') return '—'; + if (field === 'repairDate') return formatDate(value) || formatDateTime(value) || String(value); + if (field === 'submitStatus') { + if (value === 'submitted') return '已提交'; + return '未提交'; + } + if (field === 'saveStatus') { + if (value === 'saved') return '已保存'; + return '待保存'; + } + if ( + field === 'laborQty' || + field === 'partsQty' + ) { + return isEmptyNum(value) ? '—' : String(toIntQty(value)); + } + if (field === 'laborUnitPrice' || field === 'partsUnitPrice') { + return isEmptyNum(value) ? '—' : fmtMoney(value); + } + return String(value); + } + + function rowTimeValue(row, field) { + var v = row && row[field]; + if (!v || !window.dayjs) return 0; + try { + return window.dayjs(v).valueOf(); + } catch (e1) { + return 0; + } + } + + function sortRowsWithinTier(rows) { + return rows.slice().sort(function (a, b) { + var ta = rowTimeValue(a, 'submittedAt') || rowTimeValue(a, 'reportTime'); + var tb = rowTimeValue(b, 'submittedAt') || rowTimeValue(b, 'reportTime'); + if (tb !== ta) return tb - ta; + return String(a.key || '').localeCompare(String(b.key || '')); + }); + } + + /** 分区合并,避免同层数据按日期与另一层穿插 */ + function sortRowsByDisplayTier(rows) { + var submitted = []; + var saved = []; + var unsaved = []; + (rows || []).forEach(function (r) { + var tier = rowDisplaySortTier(r); + if (tier === 0) submitted.push(r); + else if (tier === 1) saved.push(r); + else unsaved.push(r); + }); + return sortRowsWithinTier(submitted).concat(sortRowsWithinTier(saved)).concat(sortRowsWithinTier(unsaved)); + } + + function firstInvalidMessage(row, inv, seq) { + var prefix = '第' + seq + '行:'; + if (inv.repairDate) return prefix + '请填写进修日期'; + if (inv.plateNo === 'empty') return prefix + '请填写车牌号'; + if (inv.plateNo === 'not_exist') return prefix + '车辆不存在'; + if (inv.recordType) return prefix + '请选择类型'; + if (inv.itemDesc) return prefix + '请填写项目/材料'; + if (inv.laborUnit) return prefix + '请填写人工费单位'; + if (inv.laborQty) return prefix + '请填写人工费数量'; + if (inv.laborUnitPrice) return prefix + '请填写人工费单价(含税)'; + if (inv.partsUnit) return prefix + '请填写配件费单位'; + if (inv.partsQty) return prefix + '请填写配件费数量'; + if (inv.partsUnitPrice) return prefix + '请填写配件费单价(含税)'; + return prefix + '请完善必填项'; + } + + var OTHER_REPORTERS = [ + { id: 'u_zhou', name: '周敏' }, + { id: 'u_li_h', name: '李航' }, + { id: 'u_wang', name: '王磊' } + ]; + + var rowIdSeed = 0; + function nextRowKey() { + rowIdSeed += 1; + return 'mr-' + Date.now() + '-' + rowIdSeed; + } + + function filterOption(input, option) { + var label = (option && (option.label || option.children)) || ''; + return String(label).toLowerCase().indexOf(String(input || '').toLowerCase()) >= 0; + } + + /** 车牌号下拉:支持输入模糊匹配(忽略空格、大小写) */ + function filterPlateOption(input, option) { + var q = normPlateKey(input); + if (!q) return true; + var label = normPlateKey(option && (option.label != null ? option.label : option.value)); + return label.indexOf(q) >= 0; + } + + function numOrZero(v) { + if (v === null || v === undefined || v === '') return 0; + var n = Number(v); + return isNaN(n) ? 0 : n; + } + + function roundMoney(n) { + return Math.round(numOrZero(n) * 100) / 100; + } + + function toIntQty(v) { + if (v === null || v === undefined || v === '') return 0; + var n = Math.floor(Number(v)); + return isNaN(n) || n < 0 ? 0 : n; + } + + function fmtMoney(n) { + if (n === null || n === undefined || n === '') return '0.00'; + return roundMoney(n).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + } + + function nowDayjs() { + try { + if (window.dayjs) return window.dayjs(); + } catch (e1) {} + return null; + } + + function formatDate(d) { + if (!d || !window.dayjs) return ''; + try { + return window.dayjs(d).format('YYYY-MM-DD'); + } catch (e2) { + return ''; + } + } + + function formatDateTime(d) { + if (!d || !window.dayjs) return ''; + try { + return window.dayjs(d).format('YYYY-MM-DD HH:mm'); + } catch (e3) { + return ''; + } + } + + function recalcRow(row) { + var laborQty = toIntQty(row.laborQty); + var partsQty = toIntQty(row.partsQty); + var laborAmt = roundMoney(laborQty * numOrZero(row.laborUnitPrice)); + var partsAmt = roundMoney(partsQty * numOrZero(row.partsUnitPrice)); + var total = roundMoney(laborAmt + partsAmt); + return Object.assign({}, row, { + laborQty: laborQty, + partsQty: partsQty, + laborAmount: laborAmt, + partsAmount: partsAmt, + totalCost: total + }); + } + + function buildEmptyRow(user) { + var now = nowDayjs(); + return recalcRow({ + key: nextRowKey(), + createdBy: user.id, + reporterName: user.name, + repairDate: now, + plateNo: undefined, + reportTime: now, + recordType: undefined, + itemDesc: '', + laborUnit: '', + laborQty: null, + laborUnitPrice: null, + laborAmount: 0, + partsUnit: '', + partsQty: null, + partsUnitPrice: null, + partsAmount: 0, + totalCost: 0, + submitStatus: 'draft', + saveStatus: 'unsaved' + }); + } + + function buildMockAllRows() { + var plates = SYSTEM_VEHICLE_PLATES; + var items = ['更换空压机滤芯', '制动片检修', '冷却液补充', '高压线束检查', '轮胎换位']; + var rows = []; + var reporters = [{ id: CURRENT_USER.id, name: CURRENT_USER.name }].concat(OTHER_REPORTERS); + var i; + for (i = 0; i < 8; i++) { + var rep = reporters[i % reporters.length]; + var submitStatus = 'draft'; + var saveStatus = 'unsaved'; + if (rep.id === CURRENT_USER.id) { + if (i < 3) { + submitStatus = 'submitted'; + saveStatus = 'saved'; + } else if (i === 3) { + saveStatus = 'saved'; + } + } else { + saveStatus = 'saved'; + } + var laborQty = 1 + (i % 4); + var laborPrice = 120 + i * 15; + var partsQty = i % 2 === 0 ? 2 : 0; + var partsPrice = 80 + i * 20; + var d = nowDayjs(); + var repairDay = d && d.subtract ? d.subtract(i, 'day') : d; + rows.push( + recalcRow({ + key: 'mock-' + (i + 1), + createdBy: rep.id, + reporterName: rep.name, + repairDate: repairDay, + plateNo: plates[i % plates.length], + reportTime: repairDay, + recordType: i % 3 === 0 ? '保养' : '维修', + itemDesc: items[i % items.length], + laborUnit: '工时', + laborQty: laborQty, + laborUnitPrice: laborPrice, + laborAmount: 0, + partsUnit: '件', + partsQty: partsQty, + partsUnitPrice: partsPrice, + partsAmount: 0, + totalCost: 0, + submitStatus: submitStatus, + saveStatus: saveStatus, + submittedAt: submitStatus === 'submitted' ? repairDay : null + }) + ); + } + 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 = + '.maint-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)}' + + '.maint-ledger-table .ant-table-thead>tr>th,.maint-ledger-table .ant-table-thead .ant-table-cell{white-space:nowrap;color:#0f172a!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;text-align:center!important}' + + '.maint-ledger-table .ant-table-thead>tr:not(:last-child)>th{border-bottom:1px solid #bae6fd!important}' + + '.maint-repair-date-picker,.maint-repair-date-picker .ant-picker-input{width:100%;min-width:132px}' + + '.maint-repair-date-picker .ant-picker-input>input{min-width:118px}' + + '.maint-ledger-table .ant-table-tbody>tr:not(.ant-table-measure-row)>td{padding:4px 6px!important;vertical-align:middle!important}' + + '.maint-ledger-table .ant-table-tbody>tr.maint-row-data:hover>td{background:#f0f9ff!important}' + + '.maint-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}' + + '.maint-cell-readonly{color:#64748b;background:#f8fafc}' + + '.maint-inline-input{width:100%}' + + '.maint-row-tier-0>td{background:#f8fafc!important}' + + '.maint-row-tier-1>td{background:#fff!important}' + + '.maint-row-tier-2>td{background:#fffbeb!important}' + + '.maint-row-tier-boundary>td{border-top:2px solid #7dd3fc!important}' + + '.maint-action-icon-btn{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:4px;cursor:pointer;color:rgba(15,23,42,0.55);transition:background .15s,color .15s}' + + '.maint-action-icon-btn:hover{background:#f0f9ff;color:#1677ff}' + + '.maint-action-icon-btn.maint-action-icon-danger:hover{background:#fff1f0;color:#ff4d4f}' + + '.maint-action-icon-btn.is-disabled{color:rgba(15,23,42,0.25);cursor:not-allowed;pointer-events:none}' + + '.maint-row-more-btn.maint-action-icon-btn:hover{background:#f5f5f5;color:rgba(15,23,42,0.75)}'; + + var rowsState = useState(buildMockAllRows); + var allRows = rowsState[0]; + var setAllRows = rowsState[1]; + + var vehiclePlateOptions = useMemo(function () { + return SYSTEM_VEHICLE_PLATES.map(function (p) { + return { value: p, label: p }; + }); + }, []); + + var plateDraftState = useState(undefined); + var plateDraft = plateDraftState[0]; + var setPlateDraft = plateDraftState[1]; + + /** 提报人多选:空数组表示全部 */ + var reporterDraftState = useState([]); + var reporterDraft = reporterDraftState[0]; + var setReporterDraft = reporterDraftState[1]; + + var dateRangeDraftState = useState(null); + var dateRangeDraft = dateRangeDraftState[0]; + var setDateRangeDraft = dateRangeDraftState[1]; + + /** 类型多选:空数组表示全部(保养、维修) */ + var typeDraftState = useState([]); + var typeDraft = typeDraftState[0]; + var setTypeDraft = typeDraftState[1]; + + var plateAppliedState = useState(undefined); + var plateApplied = plateAppliedState[0]; + var setPlateApplied = plateAppliedState[1]; + + var reporterAppliedState = useState([]); + var reporterApplied = reporterAppliedState[0]; + var setReporterApplied = reporterAppliedState[1]; + + var dateRangeAppliedState = useState(null); + var dateRangeApplied = dateRangeAppliedState[0]; + var setDateRangeApplied = dateRangeAppliedState[1]; + + var typeAppliedState = useState([]); + var typeApplied = typeAppliedState[0]; + var setTypeApplied = typeAppliedState[1]; + + /** 联调后由账号权限判断是否运维主管 */ + var isSupervisor = false; + var editingKeysState = useState([]); + var editingKeys = editingKeysState[0]; + var setEditingKeys = editingKeysState[1]; + + var rowInvalidState = useState({}); + var rowInvalid = rowInvalidState[0]; + var setRowInvalid = rowInvalidState[1]; + + var changeLogsByKeyState = useState({}); + var changeLogsByKey = changeLogsByKeyState[0]; + var setChangeLogsByKey = changeLogsByKeyState[1]; + + var changeLogModalState = useState({ open: false, rowKey: null, rowLabel: '' }); + var changeLogModal = changeLogModalState[0]; + var setChangeLogModal = changeLogModalState[1]; + + var changeLogsSeededRef = useRef(false); + + var appendChangeLog = useCallback(function (rowKey, field, before, after, rowCtx, meta) { + if (!rowKey) return; + meta = meta || {}; + var fieldLabel = meta.fieldLabel || CHANGE_LOG_FIELD_LABELS[field] || field; + var beforeText = meta.beforeText != null ? meta.beforeText : formatLogValue(field, before, rowCtx); + var afterText = meta.afterText != null ? meta.afterText : formatLogValue(field, after, rowCtx); + if (!meta.force && beforeText === afterText) return; + setChangeLogsByKey(function (prev) { + var list = (prev[rowKey] || []).slice(); + list.unshift({ + id: 'clog-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8), + at: nowDayjs(), + userId: CURRENT_USER.id, + userName: CURRENT_USER.name, + field: field, + fieldLabel: fieldLabel, + before: beforeText, + after: afterText + }); + var next = Object.assign({}, prev); + next[rowKey] = list; + return next; + }); + }, []); + + var openChangeLogModal = useCallback(function (record) { + if (!record) return; + var label = (record.plateNo || '') + (record.repairDate ? ' · ' + formatDate(record.repairDate) : ''); + setChangeLogModal({ + open: true, + rowKey: record.key, + rowLabel: label.trim() + }); + }, []); + + var closeChangeLogModal = useCallback(function () { + setChangeLogModal({ open: false, rowKey: null, rowLabel: '' }); + }, []); + + var copyPopoverKeyState = useState(null); + var copyPopoverKey = copyPopoverKeyState[0]; + var setCopyPopoverKey = copyPopoverKeyState[1]; + var copyRowCountState = useState(1); + var copyRowCount = copyRowCountState[0]; + var setCopyRowCount = copyRowCountState[1]; + + var applyCopyRows = useCallback(function (sourceKey, count) { + var n = Math.floor(Number(count)); + if (!isFinite(n) || n < 1) { + message.warning('请输入大于0的复制行数'); + return; + } + if (n > 100) { + message.warning('单次最多复制100行'); + return; + } + var didCopy = false; + setAllRows(function (prev) { + var source = null; + var i; + for (i = 0; i < prev.length; i++) { + if (prev[i].key === sourceKey) { + source = prev[i]; + break; + } + } + if (!source || !canCopyRow(source, isSupervisor)) return prev; + var copies = []; + for (i = 0; i < n; i++) { + copies.push(buildCopiedRow(source)); + } + var ci; + var sourceLabel = (source.plateNo || '当前行') + (source.itemDesc ? ' · ' + source.itemDesc : ''); + for (ci = 0; ci < copies.length; ci++) { + appendChangeLog(copies[ci].key, '_copy', null, null, source, { + fieldLabel: '复制新增', + beforeText: '—', + afterText: '复制自「' + sourceLabel + '」', + force: true + }); + } + didCopy = true; + return prev.concat(copies); + }); + setCopyPopoverKey(null); + if (didCopy) message.success('已复制 ' + n + ' 行'); + else message.warning('无法复制该条记录'); + }, [appendChangeLog, isSupervisor]); + + var latestRefs = useRef({}); + latestRefs.current = { + setAllRows: setAllRows, + setEditingKeys: setEditingKeys, + setRowInvalid: setRowInvalid, + editingKeys: editingKeys, + rowInvalid: rowInvalid, + isSupervisor: isSupervisor, + vehicleOptions: vehiclePlateOptions, + appendChangeLog: appendChangeLog + }; + + React.useEffect(function () { + if (changeLogsSeededRef.current) return; + changeLogsSeededRef.current = true; + var demoAt = nowDayjs(); + var demoAt2 = demoAt && demoAt.subtract ? demoAt.subtract(2, 'hour') : demoAt; + setChangeLogsByKey({ + 'mock-1': [ + { + id: 'clog-demo-2', + at: demoAt, + userId: CURRENT_USER.id, + userName: CURRENT_USER.name, + field: 'itemDesc', + fieldLabel: '项目/材料', + before: '更换空压机滤芯', + after: '制动片检修' + }, + { + id: 'clog-demo-1', + at: demoAt2, + userId: CURRENT_USER.id, + userName: CURRENT_USER.name, + field: 'recordType', + fieldLabel: '类型', + before: '维修', + after: '保养' + } + ] + }); + }, []); + + var isRowSubmitted = useCallback(function (row) { + return row && row.submitStatus === 'submitted'; + }, []); + + var isRowSaved = useCallback(function (row) { + return row && row.saveStatus === 'saved'; + }, []); + + /** 是否可编辑单元格:未保存草稿直接编辑;已保存需点「编辑」;已提交仅运维主管点「编辑」后可改 */ + var canEditRow = useCallback( + function (row) { + if (!row) return false; + if (isRowSubmitted(row)) { + if (!isSupervisor) return false; + return editingKeys.indexOf(row.key) >= 0; + } + if (isSavedDraftRow(row)) { + if (isSupervisor) return editingKeys.indexOf(row.key) >= 0; + if (row.createdBy !== CURRENT_USER.id) return false; + return editingKeys.indexOf(row.key) >= 0; + } + if (isSupervisor) return false; + if (row.createdBy !== CURRENT_USER.id) return false; + return true; + }, + [isSupervisor, editingKeys, isRowSubmitted] + ); + + var visibleRows = useMemo(function () { + var list = allRows.slice(); + if (plateApplied) { + list = list.filter(function (r) { return r.plateNo === plateApplied; }); + } + if (reporterApplied && reporterApplied.length > 0) { + list = list.filter(function (r) { return reporterApplied.indexOf(r.reporterName) >= 0; }); + } + if (typeApplied && typeApplied.length > 0) { + list = list.filter(function (r) { return typeApplied.indexOf(r.recordType) >= 0; }); + } + if (dateRangeApplied && dateRangeApplied[0] && dateRangeApplied[1] && window.dayjs) { + var start = window.dayjs(dateRangeApplied[0]).startOf('day'); + var end = window.dayjs(dateRangeApplied[1]).endOf('day'); + list = list.filter(function (r) { + if (!r.repairDate) return false; + var d = window.dayjs(r.repairDate); + return d.isAfter(start.subtract(1, 'ms')) && d.isBefore(end.add(1, 'ms')); + }); + } + list = sortRowsByDisplayTier(list); + return list.map(function (r, idx) { + return Object.assign({}, r, { seq: idx + 1, displayTier: rowDisplaySortTier(r) }); + }); + }, [allRows, plateApplied, reporterApplied, typeApplied, dateRangeApplied]); + + /** 合计行仅汇总三个人含税金额字段 */ + var totals = useMemo(function () { + return visibleRows.reduce( + function (acc, r) { + acc.laborAmount += numOrZero(r.laborAmount); + acc.partsAmount += numOrZero(r.partsAmount); + acc.totalCost += numOrZero(r.totalCost); + return acc; + }, + { laborAmount: 0, partsAmount: 0, totalCost: 0 } + ); + }, [visibleRows]); + + var reporterOptions = useMemo(function () { + var names = {}; + names[CURRENT_USER.name] = true; + OTHER_REPORTERS.forEach(function (rep) { + if (rep.name) names[rep.name] = true; + }); + allRows.forEach(function (r) { + if (r.reporterName) names[r.reporterName] = true; + }); + return Object.keys(names).map(function (n) { return { value: n, label: n }; }); + }, [allRows]); + + var updateRow = useCallback(function (key, patch) { + setAllRows(function (prev) { + return prev.map(function (r) { + if (r.key !== key) return r; + return recalcRow(Object.assign({}, r, patch)); + }); + }); + }, []); + + var addRow = useCallback(function () { + var row = buildEmptyRow(CURRENT_USER); + appendChangeLog(row.key, '_create', null, null, row, { + fieldLabel: '新增记录', + beforeText: '—', + afterText: '新增空白行', + force: true + }); + setAllRows(function (prev) { + return prev.concat([row]); + }); + message.success('已新增一行,请填写维修/保养信息'); + }, [appendChangeLog]); + + var handleQuery = useCallback(function () { + setPlateApplied(plateDraft); + setReporterApplied(reporterDraft); + setTypeApplied(typeDraft); + setDateRangeApplied(dateRangeDraft); + message.success('查询成功'); + }, [plateDraft, reporterDraft, typeDraft, dateRangeDraft]); + + var handleReset = useCallback(function () { + setPlateDraft(undefined); + setReporterDraft([]); + setTypeDraft([]); + setDateRangeDraft(null); + setPlateApplied(undefined); + setReporterApplied([]); + setTypeApplied([]); + setDateRangeApplied(null); + }, []); + + function validateMaintainableRows(rows) { + var allInvalid = {}; + var firstMsg = ''; + (rows || []).forEach(function (r, idx) { + var inv = getRowInvalidMap(r); + if (Object.keys(inv).length > 0) { + allInvalid[r.key] = inv; + if (!firstMsg) firstMsg = firstInvalidMessage(r, inv, r.seq || idx + 1); + } + }); + if (firstMsg) { + setRowInvalid(allInvalid); + message.warning(firstMsg); + return false; + } + setRowInvalid({}); + return true; + } + + var pendingSubmitRows = useMemo(function () { + return allRows.filter(function (r) { + return r.createdBy === CURRENT_USER.id && r.submitStatus !== 'submitted' && r.saveStatus === 'saved'; + }); + }, [allRows]); + + var handleSave = useCallback(function () { + var toSave = allRows.filter(function (r) { + return r.createdBy === CURRENT_USER.id && r.submitStatus !== 'submitted'; + }); + if (!toSave.length) { + message.info('暂无待保存的草稿记录'); + return; + } + if (!validateMaintainableRows(toSave)) return; + setAllRows(function (prev) { + return prev.map(function (r) { + if (r.createdBy === CURRENT_USER.id && r.submitStatus !== 'submitted') { + return Object.assign({}, r, { saveStatus: 'saved' }); + } + return r; + }); + }); + setEditingKeys([]); + message.success('保存成功(原型)'); + }, [allRows]); + + var handleConfirmSubmit = useCallback(function () { + if (isSupervisor) { + message.info('运维主管无需提交,请使用编辑修改已提交记录'); + return; + } + if (!pendingSubmitRows.length) { + var hasUnsaved = allRows.some(function (r) { + return r.createdBy === CURRENT_USER.id && r.submitStatus !== 'submitted' && r.saveStatus !== 'saved'; + }); + message.info(hasUnsaved ? '请先保存后再确认提交' : '暂无待提交的已保存记录'); + return; + } + if (!validateMaintainableRows(pendingSubmitRows)) return; + Modal.confirm({ + title: '确认提交', + content: SUBMIT_CONFIRM_TEXT, + okText: '确认', + cancelText: '取消', + onOk: function () { + setAllRows(function (prev) { + return prev.map(function (r) { + if (r.createdBy === CURRENT_USER.id && r.submitStatus !== 'submitted' && r.saveStatus === 'saved') { + if (!valuesEqualForLog('submitStatus', r.submitStatus, 'submitted')) { + appendChangeLog(r.key, 'submitStatus', r.submitStatus, 'submitted', r, { force: true }); + } + return Object.assign({}, r, { + submitStatus: 'submitted', + saveStatus: 'saved', + submittedAt: nowDayjs() + }); + } + return r; + }); + }); + setEditingKeys([]); + message.success('提交成功,数据已锁定'); + } + }); + }, [isSupervisor, pendingSubmitRows, allRows, appendChangeLog]); + + var cellInputStyle = { width: '100%' }; + var cellNumStyle = { width: '100%' }; + + var columns = useMemo(function () { + function patch(key, field, val) { + var ref = latestRefs.current; + var patchObj = {}; + patchObj[field] = val; + ref.setAllRows(function (prev) { + return prev.map(function (r) { + if (r.key !== key) return r; + var next = recalcRow(Object.assign({}, r, patchObj)); + if (ref.appendChangeLog && !valuesEqualForLog(field, r[field], val)) { + ref.appendChangeLog(key, field, r[field], val, next); + } + return next; + }); + }); + ref.setRowInvalid(function (prev) { + if (!prev || !prev[key] || !prev[key][field]) return prev; + var next = Object.assign({}, prev); + var rowInv = Object.assign({}, next[key]); + delete rowInv[field]; + if (Object.keys(rowInv).length === 0) { + delete next[key]; + } else { + next[key] = rowInv; + } + return next; + }); + } + + function readOnlyCell(v, align) { + return React.createElement( + 'div', + { + className: 'maint-cell-readonly', + style: { padding: '4px 8px', textAlign: align || 'center' } + }, + v === 0 || v === '0' ? v : v || '-' + ); + } + + function fieldInvalid(record, field) { + var ref = latestRefs.current; + var rowInv = (ref.rowInvalid && ref.rowInvalid[record.key]) || {}; + return rowInv[field]; + } + + function reqTitle(text) { + return React.createElement( + 'span', + null, + React.createElement('span', { style: { color: '#ff4d4f', marginRight: 2 } }, '*'), + text + ); + } + + return [ + { + title: '序号', + dataIndex: 'seq', + key: 'seq', + width: 56, + align: 'center', + fixed: 'left' + }, + { + title: '状态', + dataIndex: 'displayTier', + key: 'recordStatus', + width: 76, + align: 'center', + fixed: 'left', + render: function (_, record) { + var label = rowStatusLabel(record); + var color = label === '已提交' ? 'blue' : label === '已保存' ? 'orange' : 'default'; + return React.createElement(Tag, { color: color, style: { margin: 0 } }, label); + } + }, + { + title: reqTitle('进修日期'), + dataIndex: 'repairDate', + key: 'repairDate', + width: 152, + align: 'center', + render: function (v, record) { + if (!canEditRow(record)) return readOnlyCell(formatDate(v)); + return React.createElement(DatePicker, { + className: 'maint-repair-date-picker', + style: { width: '100%', minWidth: 132 }, + format: 'YYYY-MM-DD', + placeholder: '请选择进修日期', + allowClear: true, + value: v, + status: fieldInvalid(record, 'repairDate') ? 'error' : undefined, + onChange: function (d) { patch(record.key, 'repairDate', d); } + }); + } + }, + { + title: reqTitle('车牌号'), + dataIndex: 'plateNo', + key: 'plateNo', + width: 140, + align: 'center', + render: function (v, record) { + var ref = latestRefs.current; + if (!canEditRow(record)) return readOnlyCell(v); + var plateInv = fieldInvalid(record, 'plateNo'); + return React.createElement(Select, { + style: cellInputStyle, + showSearch: true, + allowClear: true, + placeholder: '请输入车牌号搜索', + value: v, + status: plateInv ? 'error' : undefined, + options: ref.vehicleOptions, + optionFilterProp: 'label', + filterOption: filterPlateOption, + notFoundContent: '未找到匹配车辆', + onChange: function (val) { patch(record.key, 'plateNo', val); } + }); + } + }, + { + title: '提报人', + dataIndex: 'reporterName', + key: 'reporterName', + width: 88, + align: 'center', + render: function (v) { return readOnlyCell(v); } + }, + { + title: '提报时间', + dataIndex: 'reportTime', + key: 'reportTime', + width: 168, + align: 'center', + render: function (v) { + return React.createElement( + 'div', + { className: 'maint-cell-readonly', style: { padding: '4px 8px', textAlign: 'center', whiteSpace: 'nowrap', minWidth: 152 } }, + formatDateTime(v) || '-' + ); + } + }, + { + title: reqTitle('类型'), + dataIndex: 'recordType', + key: 'recordType', + width: 100, + align: 'center', + render: function (v, record) { + if (!canEditRow(record)) return readOnlyCell(v); + return React.createElement(Select, { + style: cellInputStyle, + allowClear: true, + placeholder: '请选择类型', + value: v, + status: fieldInvalid(record, 'recordType') ? 'error' : undefined, + options: RECORD_TYPE_OPTIONS, + onChange: function (val) { patch(record.key, 'recordType', val); } + }); + } + }, + { + title: reqTitle('项目/材料'), + dataIndex: 'itemDesc', + key: 'itemDesc', + width: 160, + align: 'center', + render: function (v, record) { + if (!canEditRow(record)) return readOnlyCell(v); + return React.createElement(Input, { + className: 'maint-inline-input', + value: v, + placeholder: '请输入项目/材料', + status: fieldInvalid(record, 'itemDesc') ? 'error' : undefined, + onChange: function (e) { patch(record.key, 'itemDesc', e.target.value); } + }); + } + }, + { + title: '人工费', + key: 'laborGroup', + children: [ + { + title: reqTitle('单位'), + dataIndex: 'laborUnit', + key: 'laborUnit', + width: 72, + align: 'center', + render: function (v, record) { + if (!canEditRow(record)) return readOnlyCell(v); + return React.createElement(Input, { + className: 'maint-inline-input', + value: v, + placeholder: '单位', + status: fieldInvalid(record, 'laborUnit') ? 'error' : undefined, + onChange: function (e) { patch(record.key, 'laborUnit', e.target.value); } + }); + } + }, + { + title: reqTitle('数量'), + dataIndex: 'laborQty', + key: 'laborQty', + width: 88, + align: 'right', + render: function (v, record) { + if (!canEditRow(record)) return readOnlyCell(toIntQty(v), 'right'); + return React.createElement(InputNumber, { + style: cellNumStyle, + min: 0, + step: 1, + precision: 0, + placeholder: '数量', + value: isEmptyNum(v) ? null : toIntQty(v), + status: fieldInvalid(record, 'laborQty') ? 'error' : undefined, + onChange: function (val) { patch(record.key, 'laborQty', val == null ? null : toIntQty(val)); } + }); + } + }, + { + title: reqTitle('单价(含税)'), + dataIndex: 'laborUnitPrice', + key: 'laborUnitPrice', + width: 100, + align: 'right', + render: function (v, record) { + if (!canEditRow(record)) return readOnlyCell(fmtMoney(v), 'right'); + return React.createElement(InputNumber, { + style: cellNumStyle, + min: 0, + precision: 2, + placeholder: '单价', + value: isEmptyNum(v) ? null : v, + status: fieldInvalid(record, 'laborUnitPrice') ? 'error' : undefined, + onChange: function (val) { patch(record.key, 'laborUnitPrice', val == null ? null : val); } + }); + } + }, + { + title: '金额(含税)', + dataIndex: 'laborAmount', + key: 'laborAmount', + width: 100, + align: 'right', + render: function (v) { return React.createElement('span', { style: { paddingRight: 8 } }, fmtMoney(v)); } + } + ] + }, + { + title: '配件费', + key: 'partsGroup', + children: [ + { + title: reqTitle('单位'), + dataIndex: 'partsUnit', + key: 'partsUnit', + width: 72, + align: 'center', + render: function (v, record) { + if (!canEditRow(record)) return readOnlyCell(v); + return React.createElement(Input, { + className: 'maint-inline-input', + value: v, + placeholder: '单位', + status: fieldInvalid(record, 'partsUnit') ? 'error' : undefined, + onChange: function (e) { patch(record.key, 'partsUnit', e.target.value); } + }); + } + }, + { + title: reqTitle('数量'), + dataIndex: 'partsQty', + key: 'partsQty', + width: 88, + align: 'right', + render: function (v, record) { + if (!canEditRow(record)) return readOnlyCell(toIntQty(v), 'right'); + return React.createElement(InputNumber, { + style: cellNumStyle, + min: 0, + step: 1, + precision: 0, + placeholder: '数量', + value: isEmptyNum(v) ? null : toIntQty(v), + status: fieldInvalid(record, 'partsQty') ? 'error' : undefined, + onChange: function (val) { patch(record.key, 'partsQty', val == null ? null : toIntQty(val)); } + }); + } + }, + { + title: reqTitle('单价(含税)'), + dataIndex: 'partsUnitPrice', + key: 'partsUnitPrice', + width: 100, + align: 'right', + render: function (v, record) { + if (!canEditRow(record)) return readOnlyCell(fmtMoney(v), 'right'); + return React.createElement(InputNumber, { + style: cellNumStyle, + min: 0, + precision: 2, + placeholder: '单价', + value: isEmptyNum(v) ? null : v, + status: fieldInvalid(record, 'partsUnitPrice') ? 'error' : undefined, + onChange: function (val) { patch(record.key, 'partsUnitPrice', val == null ? null : val); } + }); + } + }, + { + title: '金额(含税)', + dataIndex: 'partsAmount', + key: 'partsAmount', + width: 100, + align: 'right', + render: function (v) { return React.createElement('span', { style: { paddingRight: 8 } }, fmtMoney(v)); } + } + ] + }, + { + title: '费用总计(含税)', + dataIndex: 'totalCost', + key: 'totalCost', + width: 120, + align: 'right', + fixed: 'right', + render: function (v) { + return React.createElement('span', { style: { paddingRight: 8, fontWeight: 600, color: '#0f172a' } }, fmtMoney(v)); + } + }, + { + title: '操作', + key: 'action', + width: 128, + align: 'center', + fixed: 'right', + render: function (_, record) { + var ref = latestRefs.current; + var submitted = record.submitStatus === 'submitted'; + var isOwn = record.createdBy === CURRENT_USER.id; + var allowCopy = canCopyRow(record, ref.isSupervisor); + + function renderEditIcon() { + return React.createElement( + 'svg', + { viewBox: '64 64 896 896', width: 14, height: 14, fill: 'currentColor', 'aria-hidden': true }, + React.createElement('path', { + d: 'M257.7 752c2 0 4-.2 6-.5L431.9 722c2-.4 3.9-1.3 5.3-2.8l423.9-423.9a9.96 9.96 0 0 0 0-14.1L694.9 114.9c-1.9-1.9-4.4-2.9-7.1-2.9s-5.2 1-7.1 2.9L256.8 538.8c-1.5 1.5-2.4 3.3-2.8 5.3l-29.5 168.2a33.5 33.5 0 0 0 9.4 29.8c6.6 6.4 14.9 9.9 23.8 9.9z' + }) + ); + } + + function renderCopyIcon() { + return React.createElement( + 'svg', + { viewBox: '64 64 896 896', width: 14, height: 14, fill: 'currentColor', 'aria-hidden': true }, + React.createElement('path', { + d: 'M832 64H296c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h496v688c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8V96c0-17.7-14.3-32-32-32zM704 192H192c-17.7 0-32 14.3-32 32v530c0 17.7 14.3 32 32 32h512c17.7 0 32-14.3 32-32V224c0-17.7-14.3-32-32-32z' + }) + ); + } + + function renderDeleteIcon() { + return React.createElement( + 'svg', + { viewBox: '64 64 896 896', width: 14, height: 14, fill: 'currentColor', 'aria-hidden': true }, + React.createElement('path', { + d: 'M360 184h-8c4.4 0 8-3.6 8-8v8h304v-8c0 4.4 3.6 8 8 8h-8v72h72v-80c0-35.3-28.7-64-64-64H352c-35.3 0-64 28.7-64 64v80h72v-72zm504 72H160c-17.7 0-32 14.3-32 32v32c0 4.4 3.6 8 8 8h60v520c0 17.7 14.3 32 32 32h632c17.7 0 32-14.3 32-32V336h60c4.4 0 8-3.6 8-8v-32c0-17.7-14.3-32-32-32zM512 464c-17.7 0-32 14.3-32 32v288c0 17.7 14.3 32 32 32h64c17.7 0 32-14.3 32-32V496c0-17.7-14.3-32-32-32h-64zm-192 0c-17.7 0-32 14.3-32 32v288c0 17.7 14.3 32 32 32h64c17.7 0 32-14.3 32-32V496c0-17.7-14.3-32-32-32h-64zm-192 0c-17.7 0-32 14.3-32 32v288c0 17.7 14.3 32 32 32h64c17.7 0 32-14.3 32-32V496c0-17.7-14.3-32-32-32h-64z' + }) + ); + } + + function renderMoreIcon() { + return React.createElement( + 'svg', + { viewBox: '0 0 16 16', width: 16, height: 16, fill: 'currentColor', 'aria-hidden': true }, + React.createElement('circle', { cx: '8', cy: '3', r: '1.5' }), + React.createElement('circle', { cx: '8', cy: '8', r: '1.5' }), + React.createElement('circle', { cx: '8', cy: '13', r: '1.5' }) + ); + } + + function renderIconAction(title, iconNode, onClick, extraClass, disabled) { + var cls = + 'maint-action-icon-btn' + (extraClass ? ' ' + extraClass : '') + (disabled ? ' is-disabled' : ''); + var node = React.createElement( + 'span', + { + className: cls, + role: 'button', + tabIndex: disabled ? -1 : 0, + 'aria-label': title, + onClick: + disabled || !onClick + ? undefined + : function (e) { + e.stopPropagation(); + onClick(e); + } + }, + iconNode + ); + return React.createElement(Tooltip, { title: title }, node); + } + + function renderMoreAction() { + return React.createElement( + Dropdown, + { + trigger: ['hover'], + placement: 'bottomRight', + menu: { + items: [ + { + key: 'changelog', + label: '变更日志', + onClick: function () { + openChangeLogModal(record); + } + } + ] + } + }, + renderIconAction('更多', renderMoreIcon(), null, 'maint-row-more-btn') + ); + } + + function renderCopyButton() { + if (!allowCopy) return null; + var popOpen = copyPopoverKey === record.key; + var popContent = React.createElement( + 'div', + { style: { width: 220 } }, + React.createElement( + 'div', + { style: { marginBottom: 8, fontSize: 13, color: 'rgba(15,23,42,0.65)' } }, + '复制当前行已填写的全部数据' + ), + React.createElement( + 'div', + { style: { marginBottom: 6, fontSize: 13, color: 'rgba(15,23,42,0.55)' } }, + '复制行数' + ), + React.createElement(InputNumber, { + style: { width: '100%', marginBottom: 10 }, + min: 1, + max: 100, + precision: 0, + value: popOpen ? copyRowCount : 1, + onChange: function (val) { + setCopyRowCount(val == null ? 1 : val); + } + }), + React.createElement( + Button, + { + type: 'primary', + block: true, + size: 'small', + onClick: function () { + applyCopyRows(record.key, copyRowCount); + } + }, + '确认复制' + ) + ); + return React.createElement( + Popover, + { + trigger: 'click', + placement: 'leftTop', + open: popOpen, + content: popContent, + onOpenChange: function (open) { + if (open) { + setCopyPopoverKey(record.key); + setCopyRowCount(1); + } else if (copyPopoverKey === record.key) { + setCopyPopoverKey(null); + } + } + }, + renderIconAction('复制', renderCopyIcon()) + ); + } + + function toggleEditKeys() { + ref.setEditingKeys(function (prev) { + if (prev.indexOf(record.key) >= 0) { + return prev.filter(function (k) { return k !== record.key; }); + } + return prev.concat([record.key]); + }); + } + + function renderEditDeletePair(onEditClick, opts) { + opts = opts || {}; + var showCopy = opts.showCopy !== false && allowCopy; + var allowEdit = opts.allowEdit !== false; + var allowDelete = + opts.allowDelete !== false && canDeleteRow(record, ref.isSupervisor); + var isEditing = ref.editingKeys.indexOf(record.key) >= 0; + var editTitle = opts.editDisabledTitle && !allowEdit + ? opts.editDisabledTitle + : isEditing + ? '完成编辑' + : '编辑'; + var deleteTitle = opts.deleteDisabledTitle && !allowDelete + ? opts.deleteDisabledTitle + : '删除'; + var deleteBtn = React.createElement( + Popconfirm, + { + title: '确认删除该条维修/保养记录?', + disabled: !allowDelete, + onConfirm: function () { + if (!allowDelete) { + if (record.submitStatus === 'submitted') { + message.info('已提交记录仅运维主管可删除'); + } + return; + } + ref.setAllRows(function (prev) { + return prev.filter(function (r) { return r.key !== record.key; }); + }); + ref.setEditingKeys(function (prev) { + return prev.filter(function (k) { return k !== record.key; }); + }); + message.success('已删除'); + } + }, + renderIconAction( + deleteTitle, + renderDeleteIcon(), + null, + 'maint-action-icon-danger', + !allowDelete + ) + ); + var nodes = [ + renderIconAction( + editTitle, + renderEditIcon(), + allowEdit ? onEditClick : null, + '', + !allowEdit + ) + ]; + if (showCopy) nodes.push(renderCopyButton()); + nodes.push(deleteBtn, renderMoreAction()); + return React.createElement( + 'div', + { style: { display: 'inline-flex', alignItems: 'center', gap: 2, flexWrap: 'nowrap' } }, + nodes + ); + } + + if (submitted) { + if (!canShowSubmittedActions(record, ref.isSupervisor)) { + return renderMoreAction(); + } + return renderEditDeletePair( + function () { + if (!ref.isSupervisor) { + message.info('已提交记录仅运维主管可编辑'); + return; + } + toggleEditKeys(); + }, + { showCopy: false } + ); + } + + if (isSavedDraftRow(record)) { + if (!canShowSavedActions(record)) return renderMoreAction(); + return renderEditDeletePair(function () { + if (!ref.isSupervisor && record.createdBy !== CURRENT_USER.id) { + message.info('仅可编辑本人提报的维修/保养记录'); + return; + } + toggleEditKeys(); + }); + } + + if (ref.isSupervisor || !isOwn) return renderMoreAction(); + + return React.createElement( + 'div', + { style: { display: 'inline-flex', alignItems: 'center', gap: 2, flexWrap: 'nowrap' } }, + renderCopyButton(), + React.createElement( + Popconfirm, + { + title: '确认删除该条维修/保养记录?', + onConfirm: function () { + ref.setAllRows(function (prev) { + return prev.filter(function (r) { return r.key !== record.key; }); + }); + message.success('已删除'); + } + }, + renderIconAction('删除', renderDeleteIcon(), null, 'maint-action-icon-danger') + ), + renderMoreAction() + ); + } + } + ]; + }, [ + canEditRow, + editingKeys, + isSupervisor, + rowInvalid, + copyPopoverKey, + copyRowCount, + applyCopyRows, + openChangeLogModal + ]); + + var changeLogRows = useMemo( + function () { + if (!changeLogModal.rowKey) return []; + return changeLogsByKey[changeLogModal.rowKey] || []; + }, + [changeLogModal.rowKey, changeLogsByKey] + ); + + var changeLogColumns = useMemo( + function () { + return [ + { + title: '修改时间', + dataIndex: 'at', + key: 'at', + width: 168, + render: function (v) { + return formatDateTime(v) || '—'; + } + }, + { + title: '修改人', + dataIndex: 'userName', + key: 'userName', + width: 88, + align: 'center' + }, + { + title: '修改字段', + dataIndex: 'fieldLabel', + key: 'fieldLabel', + width: 140, + align: 'center' + }, + { + title: '修改前', + dataIndex: 'before', + key: 'before', + ellipsis: true, + render: function (v) { + return React.createElement('span', { style: { color: '#cf1322' } }, v == null || v === '' ? '—' : v); + } + }, + { + title: '修改后', + dataIndex: 'after', + key: 'after', + ellipsis: true, + render: function (v) { + return React.createElement('span', { style: { color: '#389e0d' } }, v == null || v === '' ? '—' : v); + } + } + ]; + }, + [] + ); + + 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' }, '合计'), + React.createElement(Table.Summary.Cell, { index: 1, colSpan: 7 }), + React.createElement(Table.Summary.Cell, { index: 8, colSpan: 3 }), + React.createElement(Table.Summary.Cell, { index: 11, align: 'right' }, fmtMoney(totals.laborAmount)), + React.createElement(Table.Summary.Cell, { index: 12, colSpan: 3 }), + React.createElement(Table.Summary.Cell, { index: 15, align: 'right' }, fmtMoney(totals.partsAmount)), + React.createElement(Table.Summary.Cell, { index: 16, align: 'right' }, fmtMoney(totals.totalCost)), + React.createElement(Table.Summary.Cell, { index: 17 }) + ) + ); + }, [totals]); + + 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: 5 }, + React.createElement( + 'div', + { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '进修日期'), + React.createElement(DatePicker.RangePicker, { + style: filterControlStyle, + format: 'YYYY-MM-DD', + value: dateRangeDraft, + onChange: function (v) { setDateRangeDraft(v); } + }) + ) + ), + React.createElement( + Col, + { xs: 24, sm: 12, md: 8, lg: 4 }, + React.createElement( + 'div', + { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '车牌号'), + React.createElement(Select, { + allowClear: true, + showSearch: true, + placeholder: '全部,可输入车牌搜索', + style: filterControlStyle, + value: plateDraft, + onChange: function (v) { setPlateDraft(v); }, + options: vehiclePlateOptions, + optionFilterProp: 'label', + filterOption: filterPlateOption + }) + ) + ), + React.createElement( + Col, + { xs: 24, sm: 12, md: 8, lg: 4 }, + React.createElement( + 'div', + { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '类型'), + React.createElement(Select, { + mode: 'multiple', + allowClear: true, + placeholder: '全部', + style: filterControlStyle, + value: typeDraft, + onChange: function (v) { setTypeDraft(v || []); }, + options: RECORD_TYPE_OPTIONS, + maxTagCount: 2 + }) + ) + ), + React.createElement( + Col, + { xs: 24, sm: 12, md: 8, lg: 5 }, + React.createElement( + 'div', + { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '提报人'), + React.createElement(Select, { + mode: 'multiple', + allowClear: true, + showSearch: true, + placeholder: '全部', + style: filterControlStyle, + value: reporterDraft, + onChange: function (v) { setReporterDraft(v || []); }, + options: reporterOptions, + filterOption: filterOption, + maxTagCount: 2 + }) + ) + ), + 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 200px' + } + }, + '车辆维修/保养明细' + ), + React.createElement( + 'div', + { style: { position: 'absolute', right: 0, top: '50%', transform: 'translateY(-50%)' } }, + React.createElement( + Space, + null, + React.createElement(Button, { onClick: handleSave }, '保存'), + !isSupervisor + ? React.createElement(Button, { type: 'primary', onClick: handleConfirmSubmit }, '确认提交') + : null + ) + ) + ), + React.createElement( + 'div', + { className: 'maint-ledger-table-wrap' }, + React.createElement(Table, { + className: 'maint-ledger-table', + size: 'small', + bordered: true, + rowKey: 'key', + columns: columns, + dataSource: visibleRows, + pagination: false, + rowClassName: function (record, index) { + var tier = record.displayTier != null ? record.displayTier : rowDisplaySortTier(record); + var prev = visibleRows[index - 1]; + var prevTier = prev ? (prev.displayTier != null ? prev.displayTier : rowDisplaySortTier(prev)) : tier; + var cls = 'maint-row-data maint-row-tier-' + tier; + if (index > 0 && prevTier !== tier) cls += ' maint-row-tier-boundary'; + return cls; + }, + scroll: { x: 'max-content', y: 'calc(100vh - 360px)' }, + sticky: true, + summary: tableSummary + }) + ), + !isSupervisor + ? React.createElement( + Button, + { + type: 'dashed', + block: true, + style: { marginTop: 12 }, + onClick: addRow + }, + '新增一行' + ) + : null + ), + React.createElement( + Modal, + { + title: '变更日志' + (changeLogModal.rowLabel ? ' · ' + changeLogModal.rowLabel : ''), + open: changeLogModal.open, + onCancel: closeChangeLogModal, + footer: React.createElement(Button, { onClick: closeChangeLogModal }, '关闭'), + width: 880, + destroyOnClose: true + }, + React.createElement(Table, { + size: 'small', + bordered: true, + rowKey: 'id', + pagination: changeLogRows.length > 8 ? { pageSize: 8, showSizeChanger: false } : false, + columns: changeLogColumns, + dataSource: changeLogRows, + locale: { emptyText: '暂无变更记录' }, + scroll: { x: 'max-content', y: 360 } + }) + ) + ) + ); +}; diff --git a/web端/工作台.jsx b/web端/工作台.jsx index 8e8cc9c..a57455a 100644 --- a/web端/工作台.jsx +++ b/web端/工作台.jsx @@ -27,9 +27,12 @@ const Component = function () { var Typography = antd.Typography; var message = antd.message; var Tooltip = antd.Tooltip; + var Input = antd.Input; + var Checkbox = antd.Checkbox; var Text = Typography.Text; var Title = Typography.Title; + var RangePicker = DatePicker.RangePicker; var pageBg = '#f5f7fa'; var cardRadius = 12; @@ -94,6 +97,51 @@ const Component = function () { var overdueReturnModalOpen = overdueReturnModalState[0]; var setOverdueReturnModalOpen = overdueReturnModalState[1]; + var inspectModalState = useState(false); + var inspectModalOpen = inspectModalState[0]; + var setInspectModalOpen = inspectModalState[1]; + + var genDeliveryPickOpenState = useState(false); + var genDeliveryPickOpen = genDeliveryPickOpenState[0]; + var setGenDeliveryPickOpen = genDeliveryPickOpenState[1]; + var genDeliveryConfigOpenState = useState(false); + var genDeliveryConfigOpen = genDeliveryConfigOpenState[0]; + var setGenDeliveryConfigOpen = genDeliveryConfigOpenState[1]; + var genDeliveryCustomerState = useState(''); + var genDeliveryCustomer = genDeliveryCustomerState[0]; + var setGenDeliveryCustomer = genDeliveryCustomerState[1]; + var genDeliveryProjectIdState = useState(undefined); + var genDeliveryProjectId = genDeliveryProjectIdState[0]; + var setGenDeliveryProjectId = genDeliveryProjectIdState[1]; + var genDeliveryExpectedState = useState(null); + var genDeliveryExpected = genDeliveryExpectedState[0]; + var setGenDeliveryExpected = genDeliveryExpectedState[1]; + var genDeliveryBillingState = useState(null); + var genDeliveryBilling = genDeliveryBillingState[0]; + var setGenDeliveryBilling = genDeliveryBillingState[1]; + var genDeliverySelectedKeysState = useState([]); + var genDeliverySelectedKeys = genDeliverySelectedKeysState[0]; + var setGenDeliverySelectedKeys = genDeliverySelectedKeysState[1]; + + var genReturnPickOpenState = useState(false); + var genReturnPickOpen = genReturnPickOpenState[0]; + var setGenReturnPickOpen = genReturnPickOpenState[1]; + var genReturnConfigOpenState = useState(false); + var genReturnConfigOpen = genReturnConfigOpenState[0]; + var setGenReturnConfigOpen = genReturnConfigOpenState[1]; + var genReturnCustomerState = useState(''); + var genReturnCustomer = genReturnCustomerState[0]; + var setGenReturnCustomer = genReturnCustomerState[1]; + var genReturnProjectIdState = useState(undefined); + var genReturnProjectId = genReturnProjectIdState[0]; + var setGenReturnProjectId = genReturnProjectIdState[1]; + var genReturnDateState = useState(null); + var genReturnDate = genReturnDateState[0]; + var setGenReturnDate = genReturnDateState[1]; + var genReturnSelectedKeysState = useState([]); + var genReturnSelectedKeys = genReturnSelectedKeysState[0]; + var setGenReturnSelectedKeys = genReturnSelectedKeysState[1]; + // 业管-能源部 · 独立卡片「本日导入加氢明细条数」:0 条时卡片内显示提示文案(联调接接口) var energyH2ImportTodayState = useState(0); var energyH2ImportTodayCount = energyH2ImportTodayState[0]; @@ -285,6 +333,26 @@ const Component = function () { setOverdueReturnModalOpen(true); }, []); + // 工作台-年审/等级评定:根据提供的列表要求展示示意数据 + var inspectMockRows = useMemo(function () { + var pad = function (n) { return n < 10 ? '0' + n : '' + n; }; + var fmtD = function (d) { + return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()); + }; + var base = new Date(); + base.setHours(12, 0, 0, 0); + var addDays = function (days) { + var x = new Date(base.getTime()); + x.setDate(x.getDate() + days); + return x; + }; + return [ + { id: 'wb_insp_1', plateNo: '沪A12345', vehicleType: '重型厢式货车', brand: '东风', model: 'DFH1180', nextDate: fmtD(addDays(2)), remainDays: 2 }, + { id: 'wb_insp_2', plateNo: '苏B88888', vehicleType: '厢式货车', brand: '福田', model: 'BJ1180', nextDate: fmtD(addDays(5)), remainDays: 5 }, + { id: 'wb_insp_3', plateNo: '浙C66666', vehicleType: '轻型厢式货车', brand: '江淮', model: 'HFC1180', nextDate: fmtD(addDays(-1)), remainDays: -1 } + ]; + }, []); + // 待办任务表(原型:任务类型、任务名称、状态、任务时间、操作) var dashboardTodoRows = useMemo(function () { return [ @@ -832,13 +900,295 @@ const Component = function () { ); } + var deliveryTaskProjectList = useMemo(function () { + return [ + { + id: 'p1', + name: '嘉兴某某物流氢能运输项目', + contractCode: 'JXZL20260216YW101235A', + customerName: '嘉兴某某物流有限公司', + deliveryRegion: '浙江省 / 嘉兴市', + deliveryLocation: '浙江省嘉兴市南湖区科技大道1号', + vehicles: [ + { key: 'v1', seq: 1, brand: '东风', model: '氢燃料电池重卡 H31', plateNo: '浙A10001', vin: 'LFV2BJCH8K3123456', monthRent: '12800', serviceFee: '800', deposit: '30000', remark: '首车', deliveryStatus: 'submitted' }, + { key: 'v1c', seq: 1, brand: '东风', model: '氢燃料电池重卡 H31', plateNo: '浙A10000', vin: 'LFV2BJCH8K3000001', monthRent: '12800', serviceFee: '800', deposit: '30000', remark: '已交车样例', deliveryStatus: 'completed' }, + { key: 'v2', seq: 2, brand: '东风', model: '氢燃料电池重卡 H31', plateNo: '浙A10002', vin: 'LFV2BJCH8K3123457', monthRent: '12800', serviceFee: '800', deposit: '30000', remark: '', deliveryStatus: 'submitted' }, + { key: 'v3', seq: 3, brand: '福田', model: '智蓝氢能轻卡', plateNo: '', vin: 'LZYTBACR2M1234567', monthRent: '8500', serviceFee: '500', deposit: '20000', remark: '待上牌', deliveryStatus: 'none' }, + { key: 'v4', seq: 4, brand: '重汽', model: '豪沃氢能牵引车', plateNo: '浙F20001', vin: 'ZZ4257N386FZ12345', monthRent: '15000', serviceFee: '1000', deposit: '35000', remark: '', deliveryStatus: 'none' }, + { key: 'v5', seq: 5, brand: '陕汽', model: '德龙氢能自卸', plateNo: '浙F20002', vin: 'SX1313GR456123456', monthRent: '13200', serviceFee: '880', deposit: '32000', remark: '固定线路', deliveryStatus: 'none' } + ] + }, + { + id: 'p2', + name: '上海某某运输氢能租赁项目', + contractCode: 'SHZL20260201YW200123A', + customerName: '上海某某运输公司', + deliveryRegion: '上海市 / 上海市', + deliveryLocation: '上海市浦东新区张江高科技园区', + vehicles: [ + { key: 'v7', seq: 2, brand: '宇通', model: '氢能公交 ZK6126', plateNo: '沪B40001', vin: 'LZYTAGCF8K4567890', monthRent: '22000', serviceFee: '1200', deposit: '50000', remark: '示范线路', deliveryStatus: 'none' }, + { key: 'v8', seq: 3, brand: '福田', model: '欧辉氢能大巴', plateNo: '', vin: 'LZYTBACR2M2345678', monthRent: '19800', serviceFee: '1100', deposit: '45000', remark: '', deliveryStatus: 'none' } + ] + }, + { + id: 'p3', + name: '杭州某某租赁氢能项目', + contractCode: 'HZZL20260115YW100089A', + customerName: '杭州某某租赁有限公司', + deliveryRegion: '浙江省 / 杭州市', + deliveryLocation: '浙江省杭州市余杭区未来科技城', + vehicles: [ + { key: 'v11', seq: 3, brand: '东风', model: '氢燃料电池厢货', plateNo: '浙A50001', vin: 'LFV2BJCH8K5678901', monthRent: '9200', serviceFee: '520', deposit: '22000', remark: '城配', deliveryStatus: 'none' }, + { key: 'v12', seq: 4, brand: '开沃', model: '创源氢能轻卡', plateNo: '浙A50002', vin: 'LJXTBACR9N6789012', monthRent: '8800', serviceFee: '480', deposit: '21000', remark: '', deliveryStatus: 'none' } + ] + } + ]; + }, []); + + function filterProjectsByCustomer(customerName) { + var kw = String(customerName || '').trim().toLowerCase(); + if (!kw) return deliveryTaskProjectList; + return deliveryTaskProjectList.filter(function (p) { + return (p.customerName || '').toLowerCase().indexOf(kw) !== -1; + }); + } + + var genDeliveryCustomerOptions = useMemo(function () { + var seen = {}; + var options = []; + deliveryTaskProjectList.forEach(function (p) { + var name = p.customerName; + if (name && !seen[name]) { + seen[name] = true; + options.push({ value: name, label: name }); + } + }); + return options; + }, [deliveryTaskProjectList]); + + var genDeliveryProjectOptions = useMemo(function () { + if (!genDeliveryCustomer) return []; + return deliveryTaskProjectList.filter(function (p) { + return p.customerName === genDeliveryCustomer; + }).map(function (p) { + return { value: p.id, label: p.name }; + }); + }, [genDeliveryCustomer, deliveryTaskProjectList]); + + var genReturnProjectOptions = useMemo(function () { + return filterProjectsByCustomer(genReturnCustomer).map(function (p) { + return { value: p.id, label: p.name }; + }); + }, [genReturnCustomer, deliveryTaskProjectList]); + + var selectedGenDeliveryProject = useMemo(function () { + return deliveryTaskProjectList.find(function (p) { return p.id === genDeliveryProjectId; }) || null; + }, [genDeliveryProjectId, deliveryTaskProjectList]); + + var selectedGenReturnProject = useMemo(function () { + return deliveryTaskProjectList.find(function (p) { return p.id === genReturnProjectId; }) || null; + }, [genReturnProjectId, deliveryTaskProjectList]); + + var genDeliveryVehicleList = useMemo(function () { + if (!selectedGenDeliveryProject) return []; + return (selectedGenDeliveryProject.vehicles || []).filter(function (v) { + return v.deliveryStatus !== 'completed'; + }); + }, [selectedGenDeliveryProject]); + + var genDeliverySelectableVehicles = useMemo(function () { + return genDeliveryVehicleList.filter(function (v) { + return v.deliveryStatus === 'none'; + }); + }, [genDeliveryVehicleList]); + + var genReturnVehicleList = useMemo(function () { + if (!selectedGenReturnProject) return []; + return (selectedGenReturnProject.vehicles || []).filter(function (v) { + return v.plateNo && v.deliveryStatus === 'completed'; + }); + }, [selectedGenReturnProject]); + + var genReturnSelectableVehicles = genReturnVehicleList; + + var genDeliveryRowSelection = useMemo(function () { + return { + selectedRowKeys: genDeliverySelectedKeys, + onChange: function (keys) { setGenDeliverySelectedKeys(keys); }, + getCheckboxProps: function (record) { + var disabled = record.deliveryStatus !== 'none'; + return { + disabled: disabled, + title: disabled ? (record.deliveryStatus === 'submitted' ? '该车辆已创建交车任务' : '该车辆已完成交车') : undefined + }; + }, + selections: [ + { + key: 'all', + text: '全选可选车辆', + onSelect: function () { + setGenDeliverySelectedKeys(genDeliverySelectableVehicles.map(function (v) { return v.key; })); + } + }, + { key: 'none', text: '清空选择', onSelect: function () { setGenDeliverySelectedKeys([]); } } + ] + }; + }, [genDeliverySelectedKeys, genDeliverySelectableVehicles]); + + var genReturnRowSelection = useMemo(function () { + return { + selectedRowKeys: genReturnSelectedKeys, + onChange: function (keys) { setGenReturnSelectedKeys(keys); }, + selections: [ + { + key: 'all', + text: '全选', + onSelect: function () { + setGenReturnSelectedKeys(genReturnSelectableVehicles.map(function (v) { return v.key; })); + } + }, + { key: 'none', text: '清空选择', onSelect: function () { setGenReturnSelectedKeys([]); } } + ] + }; + }, [genReturnSelectedKeys, genReturnSelectableVehicles]); + + function renderGenTaskLabel(text, required) { + return React.createElement('label', { className: 'workbench-gen-task-form-label' }, + required ? React.createElement('span', { className: 'req' }, '*') : null, + text + ); + } + + function resetGenDeliveryFlow() { + setGenDeliveryCustomer(''); + setGenDeliveryProjectId(undefined); + setGenDeliveryExpected(null); + setGenDeliveryBilling(null); + setGenDeliverySelectedKeys([]); + } + + function resetGenReturnFlow() { + setGenReturnCustomer(''); + setGenReturnProjectId(undefined); + setGenReturnDate(null); + setGenReturnSelectedKeys([]); + } + + function openGenDeliveryPick() { + resetGenDeliveryFlow(); + setGenDeliveryPickOpen(true); + } + + function openGenReturnPick() { + resetGenReturnFlow(); + setGenReturnPickOpen(true); + } + + function handleGenDeliveryNext() { + if (!genDeliveryCustomer) { + message.warning('请选择客户名称'); + return; + } + if (!genDeliveryProjectId) { + message.warning('请选择项目名称'); + return; + } + setGenDeliveryExpected(null); + setGenDeliveryBilling(null); + setGenDeliverySelectedKeys([]); + setGenDeliveryPickOpen(false); + setGenDeliveryConfigOpen(true); + } + + function handleGenDeliveryConfirm() { + if (!genDeliveryExpected || !genDeliveryExpected.length) { + message.warning('请选择预计交车日期'); + return; + } + if (!genDeliveryBilling) { + message.warning('请选择开始计费日期'); + return; + } + if (!genDeliverySelectedKeys.length) { + message.warning('请至少勾选一辆车辆'); + return; + } + setGenDeliveryConfigOpen(false); + resetGenDeliveryFlow(); + message.success('已生成交车任务(原型)'); + } + + function handleGenReturnNext() { + if (!genReturnCustomer.trim()) { + message.warning('请输入客户名称'); + return; + } + if (!genReturnProjectId) { + message.warning('请选择项目名称'); + return; + } + setGenReturnDate(null); + setGenReturnSelectedKeys([]); + setGenReturnPickOpen(false); + setGenReturnConfigOpen(true); + } + + function handleGenReturnConfirm() { + if (!genReturnDate) { + message.warning('请选择预计还车日期'); + return; + } + if (!genReturnSelectedKeys.length) { + message.warning('请至少勾选一辆车辆'); + return; + } + setGenReturnConfigOpen(false); + resetGenReturnFlow(); + message.success('已生成还车任务(原型)'); + } + + function handleQuickItemClick(it) { + if (it.action === 'genDelivery') { + openGenDeliveryPick(); + return; + } + if (it.action === 'genReturn') { + openGenReturnPick(); + return; + } + if (it.p) protoNav(it.p); + } + + var genDeliveryVehicleColumns = useMemo(function () { + return [ + { title: '序号', dataIndex: 'seq', key: 'seq', width: 56 }, + { title: '品牌', dataIndex: 'brand', key: 'brand', width: 88 }, + { title: '型号', dataIndex: 'model', key: 'model', width: 140, ellipsis: true }, + { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 100, render: function (v) { return v || '—'; } }, + { title: '车辆识别代码', dataIndex: 'vin', key: 'vin', width: 168, ellipsis: true }, + { title: '月租金', dataIndex: 'monthRent', key: 'monthRent', width: 88, render: function (v) { return v ? v + ' 元' : '—'; } }, + { title: '备注', dataIndex: 'remark', key: 'remark', width: 100, ellipsis: true, render: function (v) { return v || '—'; } } + ]; + }, []); + + var genReturnVehicleColumns = useMemo(function () { + return [ + { title: '序号', dataIndex: 'seq', key: 'seq', width: 56 }, + { title: '品牌', dataIndex: 'brand', key: 'brand', width: 88 }, + { title: '型号', dataIndex: 'model', key: 'model', width: 140, ellipsis: true }, + { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 100 }, + { title: '车辆识别代码', dataIndex: 'vin', key: 'vin', width: 168, ellipsis: true } + ]; + }, []); + var quickByRole = useMemo(function () { return { ye: { label: '业管', items: [ + { t: '生成交车任务', action: 'genDelivery', accent: 'primary' }, + { t: '生成还车任务', action: 'genReturn', accent: 'orange' }, { t: '租赁合同', p: 'web端/车辆租赁合同/车辆租赁合同.jsx' }, - { t: '交车任务', p: 'web端/业务管理/交车任务.jsx' }, { t: '提车应收款', p: 'web端/财务管理/提车应收款.jsx' }, { t: '租赁账单', p: 'web端/业务管理/租赁账单.jsx' }, { t: '还车应结款', p: 'web端/财务管理/还车应结款.jsx' }, @@ -846,7 +1196,7 @@ const Component = function () { { t: '氢费账单', p: 'web端/财务管理/氢费账单.jsx' }, { t: '电费账单', p: 'web端/财务管理/电费账单.jsx' }, { t: 'ETC账单', p: 'web端/业务管理/ETC管理.jsx' }, - { t: '保险管理', p: 'web端/业务管理/保险管理.jsx' }, + { t: '保险采购', p: 'web端/业务管理/保险采购.jsx' }, { t: '审批中心', p: 'web端/审批中心.jsx' }, { t: '意见建议', p: 'web端/意见建议.jsx' } ] @@ -854,6 +1204,7 @@ const Component = function () { yeEnergy: { label: '业管-能源部', items: [ + { t: '站点信息', p: 'web端/加氢站管理/站点信息.jsx' }, { t: '加氢订单管理', p: 'web端/加氢站管理/加氢订单.jsx' }, { t: '意见建议', p: 'web端/意见建议.jsx' } ] @@ -863,6 +1214,7 @@ const Component = function () { items: [ { t: '车辆管理', p: 'web端/车辆管理.jsx' }, { t: '证照管理', p: 'web端/运维管理/车辆业务/证照管理.jsx' }, + { t: '证照管理-编辑', p: 'web端/运维管理/车辆业务/证照管理-编辑.jsx' }, { t: '备车管理', p: 'web端/运维管理/车辆业务/备车管理.jsx' }, { t: '交车管理', p: 'web端/运维管理/车辆业务/交车管理.jsx' }, { t: '还车管理', p: 'web端/运维管理/车辆业务/还车管理.jsx' }, @@ -1005,6 +1357,26 @@ const Component = function () { } ]; + var inspectModalColumns = [ + { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 120 }, + { title: '车辆类型', dataIndex: 'vehicleType', key: 'vehicleType', width: 140 }, + { title: '品牌', dataIndex: 'brand', key: 'brand', width: 100 }, + { title: '型号', dataIndex: 'model', key: 'model', width: 120 }, + { title: '下次检验/等评日期', dataIndex: 'nextDate', key: 'nextDate', width: 160 }, + { title: '剩余天数', dataIndex: 'remainDays', key: 'remainDays', width: 120, render: function(v) { + return React.createElement(Text, { type: v < 0 ? 'danger' : undefined }, v + ' 天'); + } }, + { + title: '操作', + key: 'action', + width: 80, + fixed: 'right', + render: function (_, record) { + return React.createElement(Button, { type: 'link', size: 'small', style: { padding: 0 }, onClick: function () { message.info('跳转年审详情(原型)'); } }, '年审'); + } + } + ]; + var overdueDeliveryModalColumns = [ { title: '预计交车时间', dataIndex: 'expectedDate', key: 'expectedDate', width: 220, ellipsis: true }, { title: '合同编码', dataIndex: 'contractCode', key: 'contractCode', width: 150, ellipsis: true }, @@ -1334,6 +1706,14 @@ const Component = function () { openOverdueReturnModal(); return; } + if (s.key === 'w_ops_inspect') { + setInspectModalOpen(true); + return; + } + if (s.key === 'w_ops_maint') { + message.info('未来上线,敬请期待'); + return; + } message.info('「' + s.title + '」明细(原型,联调接口后打开列表)'); } }; @@ -1532,6 +1912,22 @@ const Component = function () { '.workbench-quick-item:focus-visible{box-shadow:0 0 0 2px rgba(22,119,255,0.2)}' + '.workbench-quick-item-icon{flex-shrink:0;width:26px;height:26px;border-radius:6px;background:linear-gradient(135deg,#f0f5ff,#d6e4ff);line-height:26px;text-align:center;font-size:12px;font-weight:600;color:#1677ff}' + '.workbench-quick-item-label{flex:1;min-width:0;font-size:11px;line-height:1.3;color:rgba(0,0,0,0.78);word-break:break-word;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden}' + + '.workbench-quick-item--action{padding:7px 10px;min-height:38px}' + + '.workbench-quick-item--primary{border-color:rgba(22,119,255,0.2);background:linear-gradient(135deg,#f8fbff 0%,#eef5ff 100%)}' + + '.workbench-quick-item--primary:hover{border-color:rgba(22,119,255,0.35);background:linear-gradient(135deg,#f0f7ff 0%,#e6f0ff 100%)}' + + '.workbench-quick-item--primary .workbench-quick-item-icon{background:linear-gradient(135deg,#1677ff,#4096ff);color:#fff;box-shadow:0 2px 6px rgba(22,119,255,0.25)}' + + '.workbench-quick-item--orange{border-color:rgba(250,140,22,0.22);background:linear-gradient(135deg,#fffaf5 0%,#fff3e6 100%)}' + + '.workbench-quick-item--orange:hover{border-color:rgba(250,140,22,0.38);background:linear-gradient(135deg,#fff7ef 0%,#ffebd6 100%)}' + + '.workbench-quick-item--orange .workbench-quick-item-icon{background:linear-gradient(135deg,#fa8c16,#ffa940);color:#fff;box-shadow:0 2px 6px rgba(250,140,22,0.22)}' + + '.workbench-quick-section-hint{font-size:11px;line-height:1.35;color:rgba(0,0,0,0.45);padding:0 4px 6px;font-weight:500;letter-spacing:0.02em}' + + '.workbench-gen-task-modal .ant-modal-body{padding-top:14px}' + + '.workbench-gen-task-form-label{display:block;margin-bottom:6px;font-size:13px;font-weight:500;color:rgba(0,0,0,0.88);line-height:1.4}' + + '.workbench-gen-task-form-label .req{color:#ff4d4f;margin-right:4px}' + + '.workbench-gen-task-section{background:#fafafa;border:1px solid rgba(0,0,0,0.06);border-radius:10px;padding:14px 16px;margin-bottom:14px}' + + '.workbench-gen-task-readonly{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px 16px;margin-bottom:4px}' + + '.workbench-gen-task-readonly-item{font-size:12px;line-height:1.45;color:rgba(0,0,0,0.65)}' + + '.workbench-gen-task-readonly-item strong{color:rgba(0,0,0,0.88);font-weight:500;margin-right:6px}' + + '.workbench-gen-task-table-hint{font-size:12px;color:rgba(0,0,0,0.45);margin:4px 0 10px;line-height:1.45}' + '.workbench-header-warning-dept-tabs{flex-shrink:0;max-width:100%}' + '.workbench-header-warning-dept-tabs.ant-tabs{min-width:0}' + '.workbench-header-warning-dept-tabs .ant-tabs-nav{margin:0!important;min-height:32px}' + @@ -1730,18 +2126,46 @@ const Component = function () { ) }, React.createElement('div', { className: 'workbench-quick-scroll', style: { padding: '6px 6px 4px' } }, + quickItems.some(function (it) { return it.action; }) + ? React.createElement('div', { className: 'workbench-quick-section-hint' }, '常用操作') + : null, + React.createElement(Row, { gutter: [6, 6], style: { marginBottom: quickItems.some(function (it) { return !it.action; }) ? 8 : 0 } }, + quickItems.filter(function (it) { return it.action; }).map(function (it, idx) { + var accentClass = it.accent === 'orange' ? ' workbench-quick-item--orange' : ' workbench-quick-item--primary'; + return React.createElement(Col, { span: 24, key: it.t + '-action-' + idx }, + React.createElement('div', { + className: 'workbench-quick-item workbench-quick-item--action' + accentClass, + role: 'button', + tabIndex: 0, + onClick: function () { handleQuickItemClick(it); }, + onKeyDown: function (e) { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleQuickItemClick(it); + } + } + }, + React.createElement('div', { className: 'workbench-quick-item-icon', 'aria-hidden': true }, it.t.charAt(0)), + React.createElement('div', { className: 'workbench-quick-item-label' }, it.t) + ) + ); + }) + ), + quickItems.some(function (it) { return !it.action; }) + ? React.createElement('div', { className: 'workbench-quick-section-hint' }, '功能入口') + : null, React.createElement(Row, { gutter: [6, 6], style: { marginBottom: 0 } }, - quickItems.map(function (it, idx) { + quickItems.filter(function (it) { return !it.action; }).map(function (it, idx) { return React.createElement(Col, { span: 12, key: it.t + '-' + idx }, React.createElement('div', { className: 'workbench-quick-item', role: 'button', tabIndex: 0, - onClick: function () { protoNav(it.p); }, + onClick: function () { handleQuickItemClick(it); }, onKeyDown: function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); - protoNav(it.p); + handleQuickItemClick(it); } } }, @@ -1854,7 +2278,7 @@ const Component = function () { destroyOnClose: true }, React.createElement(Text, { type: 'secondary', style: { fontSize: 12, display: 'block', marginBottom: 10 } }, - '以下任务当前日期已超过预计交车结束日期(含区间结束日),示意数据标红;列表字段与交车管理-待处理一致,联调接接口。' + '以下任务已超过预计交车结束日期,请与业管人员沟通交车是否延误,修改交车时间或及时处理' ), React.createElement(Table, { className: 'workbench-overdue-delivery-modal', @@ -1887,6 +2311,258 @@ const Component = function () { pagination: false, scroll: { x: 1780, y: 360 } }) + ), + + React.createElement(Modal, { + title: '年审/等级评定', + open: inspectModalOpen, + width: 1000, + onCancel: function () { setInspectModalOpen(false); }, + footer: React.createElement(Button, { onClick: function () { setInspectModalOpen(false); } }, '关闭'), + destroyOnClose: true + }, + React.createElement(Table, { + size: 'small', + rowKey: 'id', + columns: inspectModalColumns, + dataSource: inspectMockRows, + pagination: false, + scroll: { x: 800, y: 360 } + }) + ), + + React.createElement(Modal, { + className: 'workbench-gen-task-modal', + title: '生成交车任务', + open: genDeliveryPickOpen, + width: 520, + destroyOnClose: true, + onCancel: function () { + setGenDeliveryPickOpen(false); + resetGenDeliveryFlow(); + }, + footer: React.createElement(Space, null, + React.createElement(Button, { + onClick: function () { + setGenDeliveryPickOpen(false); + resetGenDeliveryFlow(); + } + }, '取消'), + React.createElement(Button, { type: 'primary', onClick: handleGenDeliveryNext }, '生成') + ) + }, + React.createElement('div', { className: 'workbench-gen-task-section' }, + React.createElement('div', { style: { marginBottom: 14 } }, + renderGenTaskLabel('客户名称', true), + React.createElement(Select, { + showSearch: true, + allowClear: true, + placeholder: '请选择客户名称', + style: { width: '100%' }, + value: genDeliveryCustomer || undefined, + options: genDeliveryCustomerOptions, + optionFilterProp: 'label', + onChange: function (val) { + setGenDeliveryCustomer(val || ''); + setGenDeliveryProjectId(undefined); + } + }) + ), + React.createElement('div', null, + renderGenTaskLabel('项目名称', true), + React.createElement(Select, { + showSearch: true, + allowClear: true, + placeholder: genDeliveryCustomer + ? (genDeliveryProjectOptions.length ? '请选择或搜索项目名称' : '该客户暂无可用项目') + : '请先选择客户名称', + style: { width: '100%' }, + value: genDeliveryProjectId, + options: genDeliveryProjectOptions, + optionFilterProp: 'label', + disabled: !genDeliveryCustomer, + onChange: function (val) { setGenDeliveryProjectId(val); } + }) + ) + ), + React.createElement(Text, { type: 'secondary', style: { fontSize: 12 } }, '选择客户与项目后,将配置预计交车日期并勾选待交车辆。') + ), + + React.createElement(Modal, { + className: 'workbench-gen-task-modal', + title: '配置交车任务', + open: genDeliveryConfigOpen, + width: 980, + destroyOnClose: true, + onCancel: function () { + setGenDeliveryConfigOpen(false); + resetGenDeliveryFlow(); + }, + footer: React.createElement(Space, null, + React.createElement(Button, { + onClick: function () { + setGenDeliveryConfigOpen(false); + setGenDeliveryPickOpen(true); + } + }, '上一步'), + React.createElement(Button, { + onClick: function () { + setGenDeliveryConfigOpen(false); + resetGenDeliveryFlow(); + } + }, '取消'), + React.createElement(Button, { type: 'primary', onClick: handleGenDeliveryConfirm }, '确认生成') + ) + }, + selectedGenDeliveryProject + ? React.createElement('div', { className: 'workbench-gen-task-readonly' }, + React.createElement('div', { className: 'workbench-gen-task-readonly-item' }, React.createElement('strong', null, '项目名称'), selectedGenDeliveryProject.name), + React.createElement('div', { className: 'workbench-gen-task-readonly-item' }, React.createElement('strong', null, '合同编码'), selectedGenDeliveryProject.contractCode), + React.createElement('div', { className: 'workbench-gen-task-readonly-item' }, React.createElement('strong', null, '客户名称'), selectedGenDeliveryProject.customerName), + React.createElement('div', { className: 'workbench-gen-task-readonly-item' }, React.createElement('strong', null, '交车区域'), selectedGenDeliveryProject.deliveryRegion) + ) + : null, + React.createElement(Row, { gutter: [16, 12], style: { marginBottom: 12 } }, + React.createElement(Col, { xs: 24, md: 12 }, + renderGenTaskLabel('预计交车日期', true), + React.createElement(RangePicker, { + style: { width: '100%' }, + value: genDeliveryExpected, + onChange: function (val) { setGenDeliveryExpected(val); }, + placeholder: ['开始日期', '结束日期'] + }) + ), + React.createElement(Col, { xs: 24, md: 12 }, + renderGenTaskLabel('开始计费日期', true), + React.createElement(DatePicker, { + style: { width: '100%' }, + value: genDeliveryBilling, + onChange: function (val) { setGenDeliveryBilling(val); }, + placeholder: '请选择开始计费日期' + }) + ) + ), + React.createElement('div', { className: 'workbench-gen-task-table-hint' }, + '勾选待交车辆;已创建交车任务或已完成交车的车辆不可选。' + ), + React.createElement(Table, { + size: 'small', + rowKey: 'key', + columns: genDeliveryVehicleColumns, + dataSource: genDeliveryVehicleList, + rowSelection: genDeliveryRowSelection, + pagination: false, + scroll: { x: 860, y: 280 }, + locale: { emptyText: '当前项目暂无可选车辆' } + }) + ), + + React.createElement(Modal, { + className: 'workbench-gen-task-modal', + title: '生成还车任务', + open: genReturnPickOpen, + width: 520, + destroyOnClose: true, + onCancel: function () { + setGenReturnPickOpen(false); + resetGenReturnFlow(); + }, + footer: React.createElement(Space, null, + React.createElement(Button, { + onClick: function () { + setGenReturnPickOpen(false); + resetGenReturnFlow(); + } + }, '取消'), + React.createElement(Button, { type: 'primary', onClick: handleGenReturnNext }, '生成') + ) + }, + React.createElement('div', { className: 'workbench-gen-task-section' }, + React.createElement('div', { style: { marginBottom: 14 } }, + renderGenTaskLabel('客户名称', true), + React.createElement(Input, { + placeholder: '请输入客户名称', + value: genReturnCustomer, + onChange: function (e) { + setGenReturnCustomer(e.target.value); + setGenReturnProjectId(undefined); + }, + allowClear: true + }) + ), + React.createElement('div', null, + renderGenTaskLabel('项目名称', true), + React.createElement(Select, { + showSearch: true, + allowClear: true, + placeholder: genReturnProjectOptions.length ? '请选择或搜索项目名称' : '暂无匹配项目,请调整客户名称', + style: { width: '100%' }, + value: genReturnProjectId, + options: genReturnProjectOptions, + optionFilterProp: 'label', + onChange: function (val) { setGenReturnProjectId(val); } + }) + ) + ), + React.createElement(Text, { type: 'secondary', style: { fontSize: 12 } }, '选择客户与项目后,将配置预计还车日期并勾选待还车辆。') + ), + + React.createElement(Modal, { + className: 'workbench-gen-task-modal', + title: '配置还车任务', + open: genReturnConfigOpen, + width: 860, + destroyOnClose: true, + onCancel: function () { + setGenReturnConfigOpen(false); + resetGenReturnFlow(); + }, + footer: React.createElement(Space, null, + React.createElement(Button, { + onClick: function () { + setGenReturnConfigOpen(false); + setGenReturnPickOpen(true); + } + }, '上一步'), + React.createElement(Button, { + onClick: function () { + setGenReturnConfigOpen(false); + resetGenReturnFlow(); + } + }, '取消'), + React.createElement(Button, { type: 'primary', onClick: handleGenReturnConfirm }, '确认生成') + ) + }, + selectedGenReturnProject + ? React.createElement('div', { className: 'workbench-gen-task-readonly' }, + React.createElement('div', { className: 'workbench-gen-task-readonly-item' }, React.createElement('strong', null, '项目名称'), selectedGenReturnProject.name), + React.createElement('div', { className: 'workbench-gen-task-readonly-item' }, React.createElement('strong', null, '合同编码'), selectedGenReturnProject.contractCode), + React.createElement('div', { className: 'workbench-gen-task-readonly-item' }, React.createElement('strong', null, '客户名称'), selectedGenReturnProject.customerName), + React.createElement('div', { className: 'workbench-gen-task-readonly-item' }, React.createElement('strong', null, '交车区域'), selectedGenReturnProject.deliveryRegion) + ) + : null, + React.createElement('div', { style: { marginBottom: 12, maxWidth: 320 } }, + renderGenTaskLabel('预计还车日期', true), + React.createElement(DatePicker, { + style: { width: '100%' }, + value: genReturnDate, + onChange: function (val) { setGenReturnDate(val); }, + placeholder: '请选择预计还车日期' + }) + ), + React.createElement('div', { className: 'workbench-gen-task-table-hint' }, + '勾选已交车、待还车辆;仅展示可发起还车流程的车辆。' + ), + React.createElement(Table, { + size: 'small', + rowKey: 'key', + columns: genReturnVehicleColumns, + dataSource: genReturnVehicleList, + rowSelection: genReturnRowSelection, + pagination: false, + scroll: { x: 620, y: 280 }, + locale: { emptyText: '当前项目暂无已交车车辆' } + }) ) ) ); diff --git a/web端/帮助中心/功能说明书.jsx b/web端/帮助中心/功能说明书.jsx new file mode 100644 index 0000000..b276119 --- /dev/null +++ b/web端/帮助中心/功能说明书.jsx @@ -0,0 +1,232 @@ +// 【重要】必须使用 const Component 作为组件变量名 - Axhub 产品原型 +// 帮助中心 - 功能说明书 + +const Component = function () { + var antd = window.antd; + var Layout = antd.Layout; + var Menu = antd.Menu; + var Typography = antd.Typography; + var Button = antd.Button; + var Space = antd.Space; + var Divider = antd.Divider; + + var Title = Typography.Title; + var Paragraph = Typography.Paragraph; + var Text = Typography.Text; + + var Sider = Layout.Sider; + var Content = Layout.Content; + + var React = window.React; + var useState = React.useState; + var useMemo = React.useMemo; + + // 菜单数据源(依据系统最新结构) + var menuItems = [ + { + key: '1', + label: '工作台' + }, + { + key: '2', + label: '业务管理组', + children: [ + { key: '2-1', label: '客户管理' }, + { key: '2-2', label: '合同创建' }, + { key: '2-3', label: '提车应收款' }, + { key: '2-4', label: '交还车任务' }, + { key: '2-5', label: '租赁账单' }, + { key: '2-6', label: '还车应结款' }, + { key: '2-7', label: '替换车' }, + { key: '2-8', label: '调拨' } + ] + }, + { + key: '3', + label: '运维服务组', + children: [ + { key: '3-1', label: '车辆管理' }, + { key: '3-2', label: '备件库管理' }, + { key: '3-3', label: '备车' }, + { key: '3-4', label: '交车' }, + { key: '3-5', label: '还车' }, + { key: '3-6', label: '异动' } + ] + }, + { + key: '4', + label: '业务管理组-能源部', + children: [ + { key: '4-1', label: '加氢记录' }, + { key: '4-2', label: 'ETC记录' }, + { key: '4-3', label: '充电记录' }, + { key: '4-4', label: '能源账单' }, + { key: '4-5', label: '能源账户' }, + { key: '4-6', label: '充值管理' } + ] + }, + { + key: '5', + label: '安全部', + children: [ + { key: '5-1', label: '事故管理' }, + { key: '5-2', label: '违章管理' }, + { key: '5-3', label: '司机管理' }, + { key: '5-4', label: '安全培训资料' }, + { key: '5-5', label: '安全培训记录' } + ] + }, + { + key: '6', + label: '审批中心' + } + ]; + + // 扁平化数据用于获取上下文和翻页 + var flattenMenu = useMemo(function () { + var list = []; + var traverse = function (items) { + items.forEach(function (item) { + if (!item.children) { + list.push(item); + } else { + list.push(item); + traverse(item.children); + } + }); + }; + traverse(menuItems); + return list; + }, []); + + var ms1 = useState('1'); + var selectedKey = ms1[0]; + var setSelectedKey = ms1[1]; + + var currentIndex = flattenMenu.findIndex(function (item) { + return item.key === selectedKey; + }); + var currentItem = flattenMenu[currentIndex]; + var prevItem = currentIndex > 0 ? flattenMenu[currentIndex - 1] : null; + var nextItem = currentIndex < flattenMenu.length - 1 ? flattenMenu[currentIndex + 1] : null; + + var handleMenuClick = function (e) { + setSelectedKey(e.key); + }; + + // 渲染正文内容 + var renderContent = function () { + return React.createElement( + Typography, + null, + React.createElement(Title, { level: 2, className: 'acro-text-xxl' }, currentItem ? currentItem.label : '内容建设中'), + React.createElement( + Paragraph, + { className: 'acro-text-base', style: { color: 'rgba(0,0,0,0.45)' } }, + '该章节(' + (currentItem ? currentItem.label : '') + ')的功能说明正在建设中,您可以随时在这里修改补充...' + ) + ); + }; + + var styles = { + layout: { + minHeight: '100vh', + backgroundColor: '#fff', + }, + sider: { + backgroundColor: '#fafafa', + borderRight: '1px solid #f0f0f0', + overflowY: 'auto', + height: '100vh', + position: 'fixed', + left: 0, + top: 0, + bottom: 0, + }, + siderHeader: { + padding: '24px 20px 12px', + fontSize: 18, + fontWeight: 600, + color: 'rgba(0,0,0,0.85)', + borderBottom: '1px solid #f0f0f0' + }, + contentWrapper: { + marginLeft: 280, + padding: '40px 60px', + maxWidth: 1000, + display: 'flex', + flexDirection: 'column', + minHeight: '100vh' + }, + contentInner: { + flex: 1, + marginBottom: 40 + }, + footerNav: { + display: 'flex', + justifyContent: 'space-between', + borderTop: '1px solid #f0f0f0', + paddingTop: 24, + marginTop: 'auto' + } + }; + + return React.createElement( + Layout, + { style: styles.layout }, + React.createElement( + Sider, + { width: 280, style: styles.sider, theme: 'light' }, + React.createElement('div', { style: styles.siderHeader }, '《 功能说明书 》'), + React.createElement(Menu, { + mode: 'inline', + selectedKeys: [selectedKey], + defaultOpenKeys: ['2', '3', '4', '5'], + style: { borderRight: 0, padding: '12px 0' }, + items: menuItems, + onClick: handleMenuClick + }) + ), + React.createElement( + Layout, + { style: { backgroundColor: '#fff' } }, + React.createElement( + Content, + { style: styles.contentWrapper }, + React.createElement( + 'div', + { style: styles.contentInner }, + renderContent() + ), + React.createElement( + 'div', + { style: styles.footerNav }, + prevItem + ? React.createElement( + Button, + { + type: 'link', + size: 'large', + onClick: function () { setSelectedKey(prevItem.key); }, + style: { padding: 0 } + }, + '← 上一页:' + prevItem.label + ) + : React.createElement('div', null), + nextItem + ? React.createElement( + Button, + { + type: 'link', + size: 'large', + onClick: function () { setSelectedKey(nextItem.key); }, + style: { padding: 0 } + }, + '下一页:' + nextItem.label + ' →' + ) + : React.createElement('div', null) + ) + ) + ) + ); +}; diff --git a/web端/数据分析/业务台账.jsx b/web端/数据分析/业务台账.jsx new file mode 100644 index 0000000..7244635 --- /dev/null +++ b/web端/数据分析/业务台账.jsx @@ -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 + ) + ); +}; diff --git a/web端/数据分析/业务部台账.jsx b/web端/数据分析/业务部台账.jsx new file mode 100644 index 0000000..38ad7f0 --- /dev/null +++ b/web端/数据分析/业务部台账.jsx @@ -0,0 +1,2468 @@ +// 【重要】必须使用 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 Card = antd.Card; + var Button = antd.Button; + var Table = antd.Table; + var Select = antd.Select; + var DatePicker = antd.DatePicker; + var RangePicker = DatePicker.RangePicker; + var Radio = antd.Radio; + var Row = antd.Row; + var Col = antd.Col; + var Space = antd.Space; + var Form = antd.Form; + var Divider = antd.Divider; + var Typography = antd.Typography; + var message = antd.message; + var Input = antd.Input; + var InputNumber = antd.InputNumber; + var Popover = antd.Popover; + + var Title = Typography.Title; + + /** 业务板块列(每列下挂:业绩、成本、利润) */ + var SECTOR_COLUMNS = [ + { key: 'logistics', label: '物流业务' }, + { key: 'lease', label: '租赁业务' }, + { key: 'sales', label: '销售业务' }, + { key: 'hydrogen', label: '氢费业务' }, + { key: 'elec', label: '电费业务' }, + { key: 'etc', label: 'ETC业务' }, + { key: 'other', label: '其他' } + ]; + + var ALL_DATA_KEYS = SECTOR_COLUMNS.map(function (s) { return s.key; }); + + var SearchIcon = function () { return React.createElement('svg', { viewBox: '0 0 48 48', width: 14, height: 14, fill: 'none', stroke: 'currentColor', strokeWidth: 4 }, React.createElement('path', { d: 'M33.072 33.071c6.248-6.248 6.248-16.379 0-22.627-6.249-6.249-16.38-6.249-22.628 0-6.248 6.248-6.248 16.379 0 22.627 6.248 6.248 16.38 6.248 22.628 0Zm0 0 8.485 8.485' })); }; + var ResetIcon = function () { return React.createElement('svg', { viewBox: '0 0 48 48', width: 14, height: 14, fill: 'none', stroke: 'currentColor', strokeWidth: 4 }, React.createElement('path', { d: 'M38.837 18C36.463 12.136 30.715 8 24 8 15.163 8 8 15.163 8 24s7.163 16 16 16c7.455 0 13.72-5.1 15.496-12M40 8v10H30' })); }; + var DownloadIcon = function () { return React.createElement('svg', { viewBox: '0 0 48 48', width: 14, height: 14, fill: 'none', stroke: 'currentColor', strokeWidth: 4 }, React.createElement('path', { d: 'm33.072 22.071-9.07 9.071-9.072-9.07M24 5v26m16 4v6H8v-6' })); }; + var EditIcon = function () { return React.createElement('svg', { viewBox: '0 0 48 48', width: 14, height: 14, fill: 'none', stroke: 'currentColor', strokeWidth: 4, strokeLinecap: 'butt', strokeLinejoin: 'miter' }, React.createElement('path', { d: 'M29 10l9 9L17 40H8v-9L29 10z' })); }; + + function filterOption(input, option) { + var label = (option && (option.label || option.children)) || ''; + return String(label).toLowerCase().indexOf(String(input || '').toLowerCase()) >= 0; + } + + /** 主表金额:含 0 与负数,与设计稿一致 */ + function fmtLedgerCell(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 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); + } + + function mockHydrogenCustomerPerf(baseMonth, baseAmount, yearApplied, filters) { + var year = yearApplied && yearApplied.format ? yearApplied.format('YYYY') : '2026'; + var data = []; + var customers = [ + '嘉兴古道物流有限公司', '杭州张建丽信息咨询有限公司', '嘉兴益顺冷链物流有限公司', + '嘉兴港区众通快递有限公司', '嘉兴港区韵达快递有限公司新仓分理部', '嘉兴港区韵达快递有限公司' + ]; + + var targetMonths = []; + if (filters && filters.month) { + targetMonths = [filters.month.month() + 1]; + } else if (baseMonth <= 12) { + targetMonths = [baseMonth]; + } else { + targetMonths = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; + } + + var targetCustomer = filters && filters.customerName; + + targetMonths.forEach(function(m) { + var monthLabel = year + '年' + m + '月'; + var monthCustomers = targetCustomer ? [targetCustomer] : customers; + + var monthData = []; + monthCustomers.forEach(function(c) { + var count = 50 + Math.floor(Math.random() * 50); + var amountKg = count * 1.5 + Math.random() * 10; + var totalAmount = amountKg * 30; + + monthData.push({ + month: m, + monthLabel: monthLabel, + customerName: c, + count: count, + amountKg: Math.round(amountKg * 100) / 100, + totalAmount: Math.round(totalAmount * 100) / 100 + }); + }); + + if (targetMonths.length === 1 && !targetCustomer && baseAmount) { + var currentSum = monthData.reduce(function(sum, r) { return sum + r.totalAmount; }, 0); + if (currentSum > 0) { + var ratio = numOrZero(baseAmount) / currentSum; + monthData.forEach(function(r) { + r.totalAmount = Math.round(r.totalAmount * ratio * 100) / 100; + r.amountKg = Math.round(r.amountKg * ratio * 100) / 100; + }); + } + } + + if (monthData.length > 0) { + monthData[0].monthSpan = monthData.length; + for (var i = 1; i < monthData.length; i++) { + monthData[i].monthSpan = 0; + } + } + + data = data.concat(monthData); + }); + + data.forEach(function(item, index) { + item.key = 'h2-perf-' + index; + }); + + return { + rows: data, + options: { + customers: customers.map(function(c) { return { label: c, value: c }; }) + } + }; + } + + var HYDROGEN_STATION_NAMES = [ + '嘉兴港区羚牛加氢站', + '杭州余杭加氢示范站', + '宁波北仑港加氢站', + '上海临港加氢站', + '绍兴滨海新区加氢站' + ]; + + function hashStr(s) { + var h = 0; + for (var hi = 0; hi < s.length; hi++) { + h = ((h << 5) - h) + s.charCodeAt(hi); + h |= 0; + } + return Math.abs(h); + } + + function splitCustomerRowToStations(stationNames, totalCount, totalKg, totalAmount, seedStr) { + var n = stationNames.length; + if (n === 0) return []; + var tc = Math.round(numOrZero(totalCount)); + var tkg = numOrZero(totalKg); + var tm = numOrZero(totalAmount); + if (tc === 0 && tkg === 0 && tm === 0) return []; + + var weights = []; + var sumW = 0; + for (var wi = 0; wi < n; wi++) { + var w = 0.32 + ((hashStr(seedStr + '|st|' + wi) % 680) / 1000); + weights.push(w); + sumW += w; + } + var counts = []; + var acc = 0; + for (var ci = 0; ci < n; ci++) { + if (ci === n - 1) { + counts.push(Math.max(0, tc - acc)); + } else if (tc > 0) { + var c0 = Math.max(0, Math.floor(tc * (weights[ci] / sumW))); + counts.push(c0); + acc += c0; + } else { + counts.push(0); + } + } + var rows = []; + var remK = tkg; + var remM = tm; + for (var ki = 0; ki < n; ki++) { + var ratio = tc > 0 ? (counts[ki] / tc) : (weights[ki] / sumW); + var isLast = ki === n - 1; + var kg = isLast ? remK : Math.round(tkg * ratio * 100) / 100; + var money = isLast ? remM : Math.round(tm * ratio * 100) / 100; + if (!isLast) { + remK = Math.round((remK - kg) * 100) / 100; + remM = Math.round((remM - money) * 100) / 100; + } + rows.push({ + stationName: stationNames[ki], + count: counts[ki], + amountKg: kg, + totalAmount: money + }); + } + return rows; + } + + function mockHydrogenStationBreakdown(customerRows, drillMode, drillCustomerName) { + var filtered = (customerRows || []).filter(function (r) { + if (drillMode === 'customer' && r.customerName !== drillCustomerName) return false; + return true; + }); + if (filtered.length === 0) { + return { + rows: [], + monthLabel: '—', + drillScopeLabel: drillMode === 'customer' ? (drillCustomerName || '—') : '全部客户' + }; + } + var stationOrder = {}; + HYDROGEN_STATION_NAMES.forEach(function (sn, ord) { + stationOrder[sn] = ord; + }); + var rawPairs = []; + filtered.forEach(function (r) { + var parts = splitCustomerRowToStations( + HYDROGEN_STATION_NAMES, + r.count, + r.amountKg, + r.totalAmount, + r.customerName + '|' + r.monthLabel + ); + parts.forEach(function (p) { + if (numOrZero(p.count) === 0 && numOrZero(p.amountKg) === 0 && numOrZero(p.totalAmount) === 0) return; + rawPairs.push({ + monthLabel: r.monthLabel, + customerName: r.customerName, + stationName: p.stationName, + count: p.count, + amountKg: Math.round(numOrZero(p.amountKg) * 100) / 100, + totalAmount: Math.round(numOrZero(p.totalAmount) * 100) / 100 + }); + }); + }); + rawPairs.sort(function (a, b) { + var m = String(a.monthLabel).localeCompare(String(b.monthLabel), 'zh'); + if (m !== 0) return m; + var c = String(a.customerName).localeCompare(String(b.customerName), 'zh'); + if (c !== 0) return c; + return (stationOrder[a.stationName] || 0) - (stationOrder[b.stationName] || 0); + }); + var n = rawPairs.length; + var i = 0; + while (i < n) { + var ml = rawPairs[i].monthLabel; + var j = i + 1; + while (j < n && rawPairs[j].monthLabel === ml) j++; + var mlen = j - i; + var p = i; + while (p < j) { + var cn = rawPairs[p].customerName; + var q = p + 1; + while (q < j && rawPairs[q].customerName === cn) q++; + var clen = q - p; + for (var t = p; t < q; t++) { + rawPairs[t].monthSpan = t === i ? mlen : 0; + rawPairs[t].customerSpan = t === p ? clen : 0; + } + p = q; + } + i = j; + } + var dataRows = rawPairs.map(function (row, idx) { + return { + key: 'h2-sta-' + idx + '-' + String(hashStr(row.customerName + '|' + row.stationName)), + monthLabel: row.monthLabel, + customerName: row.customerName, + stationName: row.stationName, + count: row.count, + amountKg: row.amountKg, + totalAmount: row.totalAmount, + monthSpan: row.monthSpan, + customerSpan: row.customerSpan + }; + }); + return { + rows: dataRows, + monthLabel: filtered[0].monthLabel, + drillScopeLabel: drillMode === 'customer' ? drillCustomerName : '全部客户' + }; + } + + function mockHydrogenRefuelDetailRows(ctx, yearApplied, hydroFilterApplied, salesModal) { + var customerName = ctx && ctx.customerName; + var stationFilter = ctx && ctx.stationName; + var year = yearApplied && yearApplied.format ? yearApplied.format('YYYY') : '2026'; + var monthNum = 1; + if (hydroFilterApplied && hydroFilterApplied.month && hydroFilterApplied.month.format) { + year = hydroFilterApplied.month.format('YYYY'); + monthNum = hydroFilterApplied.month.month() + 1; + } else if (salesModal && salesModal.month != null && salesModal.month <= 12) { + monthNum = salesModal.month; + } + if (!customerName) { + return { rows: [] }; + } + var daysInMonth = new Date(Number(year), monthNum, 0).getDate(); + if (daysInMonth < 1) daysInMonth = 28; + var stationsPool = HYDROGEN_STATION_NAMES.concat(['嘉兴嘉锦亭桥北综合供能服务站']); + var projects = ['宁波港49T*5租赁', '嘉兴大森租赁49T*3', '市区配送项目', '干线运输项目']; + var plates = ['沪A68122F', '粤AGP3513', '浙A12345F', '沪A69864F', '苏E88888F']; + var salespeople = ['刘念念', '尚建华', '谯云', '董剑煜']; + var n = 10 + (hashStr(String(customerName) + '|' + String(stationFilter || '')) % 18); + var rows = []; + for (var i = 0; i < n; i++) { + var day = 1 + (hashStr(String(customerName) + '|d|' + i) % daysInMonth); + var stName = stationFilter + ? stationFilter + : stationsPool[(hashStr(String(customerName) + '|s|' + i) % stationsPool.length)]; + var amountKg = Math.round((8 + (hashStr(String(customerName) + '|kg|' + i) % 190) / 10) * 100) / 100; + var unitPrice = 30; + var fee = Math.round(amountKg * unitPrice * 100) / 100; + rows.push({ + key: 'h2-det-' + i + '-' + hashStr(String(customerName) + stName + i), + refuelDateLabel: year + '年' + monthNum + '月' + day + '日', + customerName: customerName, + projectName: projects[hashStr(String(customerName) + '|p|' + i) % projects.length], + plateNo: plates[hashStr(String(customerName) + '|pl|' + i) % plates.length], + stationName: stName, + amountKg: amountKg, + unitPrice: unitPrice, + customerFee: fee, + salesperson: salespeople[hashStr(String(customerName) + '|sp|' + i) % salespeople.length] + }); + } + rows.sort(function (a, b) { + var da = a.refuelDateLabel; + var db = b.refuelDateLabel; + if (da !== db) return String(da).localeCompare(String(db), 'zh'); + return String(a.stationName).localeCompare(String(b.stationName), 'zh'); + }); + return { rows: rows }; + } + + function mockCustomerSummary(month, amount, deptApplied, filterApplied, isCost, overrides) { + var data = [ + { dept: '业务二部', salesperson: '刘念念', customerName: '安徽驰远供应链管理有限公司', receivable: 10000, received: 8000, unreceived: 2000, vehicleCost: 5000, brokerageFee: 1000, totalCost: 6000 }, + { dept: '业务二部', salesperson: '刘念念', customerName: '嘉兴港区韵达快递有限公司', receivable: 5000, received: 5000, unreceived: 0, vehicleCost: 2500, brokerageFee: 500, totalCost: 3000 }, + { dept: '业务二部', salesperson: '谯云', customerName: '无锡双庙运输有限公司', receivable: 8000, received: 0, unreceived: 8000, vehicleCost: 4000, brokerageFee: 800, totalCost: 4800 }, + { dept: '业务二部', salesperson: '尚建华', customerName: '新疆佳淇信息科技有限公司', receivable: 12000, received: 10000, unreceived: 2000, vehicleCost: 6000, brokerageFee: 1200, totalCost: 7200 }, + { dept: '业务二部', salesperson: '尚建华', customerName: '嘉兴益顺冷链物流有限公司', receivable: 6000, received: 6000, unreceived: 0, vehicleCost: 3000, brokerageFee: 600, totalCost: 3600 }, + { dept: '业务二部', salesperson: '尚建华', customerName: '新疆中铁黑豹物流有限公司', receivable: 9000, received: 4000, unreceived: 5000, vehicleCost: 4500, brokerageFee: 900, totalCost: 5400 }, + { dept: '业务二部', salesperson: '董剑煜', customerName: '嘉兴必出彩供应链有限公司', receivable: 7000, received: 7000, unreceived: 0, vehicleCost: 3500, brokerageFee: 700, totalCost: 4200 } + ]; + + var currentSum = 0; + data.forEach(function(r) { currentSum += isCost ? r.totalCost : r.receivable; }); + var targetTotal = numOrZero(amount); + + if (targetTotal > 0 && currentSum > 0) { + var ratio = targetTotal / currentSum; + data.forEach(function(r) { + if (isCost) { + r.vehicleCost = Math.round(r.vehicleCost * ratio * 100) / 100; + r.baseBrokerageFee = Math.round(r.brokerageFee * ratio * 100) / 100; + r.brokerageFee = r.baseBrokerageFee; + r.totalCost = Math.round((r.vehicleCost + r.brokerageFee) * 100) / 100; + } else { + r.receivable = Math.round(r.receivable * ratio * 100) / 100; + r.received = Math.round(r.received * ratio * 100) / 100; + r.unreceived = Math.round((r.receivable - r.received) * 100) / 100; + } + }); + } else { + data.forEach(function(r) { + if (isCost) { + r.baseBrokerageFee = r.brokerageFee; + } + }); + } + + if (isCost && overrides) { + data.forEach(function(r) { + var p1BaseBF = Math.round(r.baseBrokerageFee * 0.4 * 100) / 100; + var p2BaseBF = r.baseBrokerageFee - p1BaseBF; + + var sum = 0; + var plates = ['沪A34641F', '沪A69864F', '沪A89546F', '沪A99357F']; + var projects = [ + { name: '市区配送项目', base: p1BaseBF }, + { name: '干线运输项目', base: p2BaseBF } + ]; + + projects.forEach(function(proj) { + var projHasOverride = false; + var projSum = 0; + for (var i = 0; i < plates.length; i++) { + var key = r.customerName + '|' + proj.name + '|' + plates[i]; + if (overrides[key] !== undefined) { + projHasOverride = true; + projSum += numOrZero(overrides[key]); + } else { + var count = 4; + var plateRatio = (i === count - 1) ? 1 - (Math.floor(100/count)/100)*(count-1) : Math.floor(100/count)/100; + projSum += Math.round(proj.base * plateRatio * 100) / 100; + } + } + sum += projHasOverride ? projSum : proj.base; + }); + + r.brokerageFee = sum; + r.totalCost = Math.round((r.vehicleCost + r.brokerageFee) * 100) / 100; + }); + } + + var uniqueCustomers = []; + data.forEach(function(r) { + if (uniqueCustomers.indexOf(r.customerName) === -1) uniqueCustomers.push(r.customerName); + }); + var options = { + customers: uniqueCustomers.map(function(v) { return { label: v, value: v }; }) + }; + + var filteredData = data.filter(function(r) { + if (filterApplied && filterApplied.customerName && r.customerName !== filterApplied.customerName) return false; + return true; + }); + + var deptSpan = {}; + var spSpan = {}; + + filteredData.forEach(function(item, index) { + if (isCost) { + var p1Key = item.customerName + '|市区配送项目'; + var p2Key = item.customerName + '|干线运输项目'; + var p1BF = Math.round(item.baseBrokerageFee * 0.4 * 100) / 100; + var p2BF = item.baseBrokerageFee - p1BF; + + if (overrides) { + if (overrides[p1Key] !== undefined) p1BF = overrides[p1Key]; + if (overrides[p2Key] !== undefined) p2BF = overrides[p2Key]; + } + + item.brokerageFee = p1BF + p2BF; + item.totalCost = item.vehicleCost + item.brokerageFee; + } + + if (index === 0 || filteredData[index - 1].dept !== item.dept) { + var count = 0; + for (var i = index; i < filteredData.length; i++) { + if (filteredData[i].dept === item.dept) count++; + else break; + } + deptSpan[index] = count; + } else { + deptSpan[index] = 0; + } + + if (index === 0 || filteredData[index - 1].salesperson !== item.salesperson) { + var count = 0; + for (var i = index; i < filteredData.length; i++) { + if (filteredData[i].salesperson === item.salesperson) count++; + else break; + } + spSpan[index] = count; + } else { + spSpan[index] = 0; + } + + item.deptSpan = deptSpan[index]; + item.spSpan = spSpan[index]; + item.key = 'cust-sum-' + index; + }); + + return { rows: filteredData, options: options }; + } + + function mockProjectSummary(customerRecord, isCost, overrides) { + function getProjectBrokerage(cName, pName, baseBF) { + if (!isCost || !overrides) return baseBF; + var plates = ['沪A34641F', '沪A69864F', '沪A89546F', '沪A99357F']; + var hasOverride = false; + var sum = 0; + for (var i = 0; i < plates.length; i++) { + var key = cName + '|' + pName + '|' + plates[i]; + if (overrides[key] !== undefined) { + hasOverride = true; + sum += numOrZero(overrides[key]); + } else { + var count = 4; + var ratio = (i === count - 1) ? 1 - (Math.floor(100/count)/100)*(count-1) : Math.floor(100/count)/100; + sum += Math.round(baseBF * ratio * 100) / 100; + } + } + return hasOverride ? sum : baseBF; + } + + function generateProjects(cRecord) { + var targetReceivable = numOrZero(cRecord.receivable); + var targetVehicleCost = numOrZero(cRecord.vehicleCost); + var baseBrokerageFee = numOrZero(cRecord.baseBrokerageFee || cRecord.brokerageFee); + + var p1VC = Math.round(targetVehicleCost * 0.4 * 100) / 100; + var p2VC = targetVehicleCost - p1VC; + + var p1BaseBF = Math.round(baseBrokerageFee * 0.4 * 100) / 100; + var p2BaseBF = baseBrokerageFee - p1BaseBF; + + var p1BF = getProjectBrokerage(cRecord.customerName, '市区配送项目', p1BaseBF); + var p2BF = getProjectBrokerage(cRecord.customerName, '干线运输项目', p2BaseBF); + + var p1Rec = Math.round(targetReceivable * 0.4 * 100) / 100; + var p2Rec = targetReceivable - p1Rec; + + return [ + { + customerName: cRecord.customerName, + salesperson: cRecord.salesperson, + projectName: '市区配送项目', + receivable: p1Rec, + received: Math.round(p1Rec * 0.8 * 100) / 100, + unreceived: Math.round(p1Rec * 0.2 * 100) / 100, + vehicleCost: p1VC, + brokerageFee: p1BF, + totalCost: Math.round((p1VC + p1BF) * 100) / 100 + }, + { + customerName: cRecord.customerName, + salesperson: cRecord.salesperson, + projectName: '干线运输项目', + receivable: p2Rec, + received: Math.round(p2Rec * 0.5 * 100) / 100, + unreceived: Math.round(p2Rec * 0.5 * 100) / 100, + vehicleCost: p2VC, + brokerageFee: p2BF, + totalCost: Math.round((p2VC + p2BF) * 100) / 100 + } + ]; + } + + if (customerRecord.isAll) { + var customerMap = {}; + customerRecord.sourceRows.forEach(function(r) { + if (!customerMap[r.customerName]) { + customerMap[r.customerName] = { + customerName: r.customerName, + salesperson: r.salesperson, + receivable: 0, received: 0, unreceived: 0, + vehicleCost: 0, brokerageFee: 0, baseBrokerageFee: 0, totalCost: 0 + }; + } + var c = customerMap[r.customerName]; + c.receivable += numOrZero(r.receivable); + c.received += numOrZero(r.received); + c.unreceived += numOrZero(r.unreceived); + c.vehicleCost += numOrZero(r.vehicleCost); + c.brokerageFee += numOrZero(r.brokerageFee); + c.baseBrokerageFee += numOrZero(r.baseBrokerageFee || r.brokerageFee); + c.totalCost += numOrZero(r.totalCost); + }); + + var allData = []; + Object.keys(customerMap).forEach(function(cName) { + allData = allData.concat(generateProjects(customerMap[cName])); + }); + + var currentCust = null; + var currentCustStartIndex = -1; + allData.forEach(function(r, idx) { + r.key = 'proj-sum-all-' + idx; + if (r.customerName !== currentCust) { + if (currentCustStartIndex !== -1) { + allData[currentCustStartIndex].custSpan = idx - currentCustStartIndex; + } + currentCust = r.customerName; + currentCustStartIndex = idx; + r.custSpan = 1; + } else { + r.custSpan = 0; + } + }); + if (currentCustStartIndex !== -1) { + allData[currentCustStartIndex].custSpan = allData.length - currentCustStartIndex; + } + return allData; + } else { + var data = generateProjects(customerRecord); + data.forEach(function(item, index) { + item.custSpan = index === 0 ? data.length : 0; + item.key = 'proj-sum-' + index; + }); + return data; + } + } + + function mockLeaseOrderDrill(month, amount, deptApplied, deptOptions, yearApplied, drillFilterApplied, detailProject, isCost, overrides) { + var year = yearApplied && yearApplied.format ? yearApplied.format('YYYY') : '2026'; + var billCycle; + if (month === 13) { + billCycle = year + '.01.01-' + year + '.12.31'; + } else { + var mStr = pad2(month); + var daysInMonth = new Date(year, month, 0).getDate(); + billCycle = year + '.' + mStr + '.01-' + year + '.' + mStr + '.' + daysInMonth; + } + + var rows = []; + var plates = ['沪A34641F', '沪A69864F', '沪A89546F', '沪A99357F']; + var count = 4; + + function generateRowsForProject(proj, idxOffset) { + var targetTotal = proj ? numOrZero(isCost ? proj.totalCost : proj.receivable) : numOrZero(amount); + var targetVehicleCost = proj ? numOrZero(proj.vehicleCost) : targetTotal * 0.9; + var targetBrokerageFee = proj ? numOrZero(proj.baseBrokerageFee || proj.brokerageFee) : targetTotal * 0.1; + + for (var i = 0; i < count; i++) { + var ratio = (i === count - 1) ? 1 - (Math.floor(100/count)/100)*(count-1) : Math.floor(100/count)/100; + var rReceivable = Math.round(targetTotal * ratio * 100) / 100; + var rVehicleCost = Math.round(targetVehicleCost * ratio * 100) / 100; + var rBrokerageFee = Math.round(targetBrokerageFee * ratio * 100) / 100; + + var plateNo = plates[i] || ('沪A' + Math.floor(10000 + Math.random() * 90000) + 'F'); + var custName = proj ? proj.customerName : '测试客户' + (i + 1); + var projName = proj ? proj.projectName : '测试项目' + (i + 1); + var orderKey = custName + '|' + projName + '|' + plateNo; + if (isCost && overrides && overrides[orderKey] !== undefined) { + rBrokerageFee = numOrZero(overrides[orderKey]); + } + + rows.push({ + key: 'drill-' + idxOffset + '-' + i, + billCycle: billCycle, + billInstallment: '第' + (i+1) + '期/共12期', + plateNo: plateNo, + brand: '苏龙', + vehicleModel: '海格牌18吨双飞翼货车', + salesperson: proj ? proj.salesperson : '业务员' + (i + 1), + nature: i % 2 === 0 ? '租赁' : '试用', + customerName: custName, + projectName: projName, + contractDate: '2025.12.1-2026.11.30', + pickupDate: '2026.01.01', + deposit: '/', + receivable: rReceivable, + received: null, + unreceived: rReceivable, + naturalMonthIncome: rReceivable, + vehicleCost: rVehicleCost, + brokerageFee: rBrokerageFee, + totalCost: Math.round((rVehicleCost + rBrokerageFee) * 100) / 100, + paymentDate: '2026.01.01', + payMethod: '预付', + payCycle: '1个月' + }); + } + } + + if (detailProject && detailProject.isAll) { + detailProject.sourceRows.forEach(function(proj, idx) { + generateRowsForProject(proj, idx); + }); + } else { + generateRowsForProject(detailProject, 0); + } + + var uniqueSalespersons = []; + var uniqueCustomers = []; + var uniquePlateNos = []; + rows.forEach(function(r) { + if (uniqueSalespersons.indexOf(r.salesperson) === -1) uniqueSalespersons.push(r.salesperson); + if (uniqueCustomers.indexOf(r.customerName) === -1) uniqueCustomers.push(r.customerName); + if (uniquePlateNos.indexOf(r.plateNo) === -1) uniquePlateNos.push(r.plateNo); + }); + var options = { + salespersons: uniqueSalespersons.map(function(v) { return { label: v, value: v }; }), + customers: uniqueCustomers.map(function(v) { return { label: v, value: v }; }), + plateNos: uniquePlateNos.map(function(v) { return { label: v, value: v }; }) + }; + + var sumReceivable = 0, sumReceived = 0, sumUnreceived = 0, sumNatural = 0, sumVehicleCost = 0, sumBrokerageFee = 0, sumTotalCost = 0; + + // 应用筛选条件 + var filteredRows = rows.filter(function(r) { + if (drillFilterApplied.salesperson && r.salesperson !== drillFilterApplied.salesperson) return false; + if (drillFilterApplied.customerName && r.customerName !== drillFilterApplied.customerName) return false; + if (drillFilterApplied.nature && r.nature !== drillFilterApplied.nature) return false; + if (drillFilterApplied.plateNo && r.plateNo !== drillFilterApplied.plateNo) return false; + if (drillFilterApplied.paymentDate && drillFilterApplied.paymentDate.length === 2) { + var start = drillFilterApplied.paymentDate[0].format('YYYY.MM.DD'); + var end = drillFilterApplied.paymentDate[1].format('YYYY.MM.DD'); + if (r.paymentDate < start || r.paymentDate > end) return false; + } + return true; + }); + + filteredRows.forEach(function(r) { + sumReceivable += r.receivable; + sumReceived += numOrZero(r.received); + sumUnreceived += r.unreceived; + sumNatural += r.naturalMonthIncome; + sumVehicleCost += numOrZero(r.vehicleCost); + sumBrokerageFee += numOrZero(r.brokerageFee); + sumTotalCost += numOrZero(r.totalCost); + }); + + return { + rows: filteredRows, + options: options, + totals: { + receivable: sumReceivable, + received: sumReceived, + unreceived: sumUnreceived, + naturalMonthIncome: sumNatural, + vehicleCost: sumVehicleCost, + brokerageFee: sumBrokerageFee, + totalCost: sumTotalCost + } + }; + } + + function mockLeaseOrderDetail(ym, deptName, salesperson, amount) { + var totalAmount = numOrZero(amount); + var rows = []; + var isAll = deptName === '全部'; + var depts = ['业务二部', '华东业务部', '华南业务部']; + var names = ['尚建华', '刘念念', '谯云']; + var n = isAll ? 6 : 3; + var sumDeposit = 0, sumVehicle = 0, sumAdditional = 0, sumTotal = 0, sumReceived = 0, sumUnreceived = 0; + + for (var i = 0; i < n; i++) { + var ratio = (i === n - 1) ? 1 - (Math.floor(100/n)/100)*(n-1) : Math.floor(100/n)/100; + var rTotal = Math.round(totalAmount * ratio * 100) / 100; + var rDeposit = Math.round(rTotal * 0.1 * 100) / 100; + var rVehicle = Math.round(rTotal * 0.8 * 100) / 100; + var rAdditional = Math.round((rTotal - rDeposit - rVehicle) * 100) / 100; + var rReceived = Math.round(rTotal * 0.6 * 100) / 100; + var rUnreceived = Math.round((rTotal - rReceived) * 100) / 100; + + var curDept = isAll ? depts[Math.floor(i / 2) % depts.length] : deptName; + var curName = isAll ? names[Math.floor(i / 2) % names.length] : salesperson; + + // For "全部", we span 2 rows for each person. For specific person, we span n rows for the first one. + var rowSpan = 0; + if (isAll) { + rowSpan = (i % 2 === 0) ? 2 : 0; + } else { + rowSpan = (i === 0) ? n : 0; + } + + rows.push({ + key: 'detail-' + i, + ym: ym, + deptName: curDept, + salesperson: curName, + billDate: ym + '-15', + plateNo: '浙A' + (10000 + i), + vehicleModel: 'XL-100', + customerName: '测试客户' + (i + 1), + contractDate: '2026-01-01至2027-01-01', + pickupDate: '2026-01-05', + payMethod: i % 2 === 0 ? '预付' : '后付', + payCycle: '1个月', + billCalcMethod: '自然月', + actualDays: '30天', + depositAmount: rDeposit, + vehicleAmount: rVehicle, + additionalAmount: rAdditional, + totalAmount: rTotal, + receivedAmount: rReceived, + unreceivedAmount: rUnreceived, + groupRowSpan: rowSpan, + monthRowSpan: i === 0 ? n : 0 + }); + sumDeposit += rDeposit; + sumVehicle += rVehicle; + sumAdditional += rAdditional; + sumTotal += rTotal; + sumReceived += rReceived; + sumUnreceived += rUnreceived; + } + return { + rows: rows, + totals: { + deposit: sumDeposit, + vehicle: sumVehicle, + additional: sumAdditional, + total: sumTotal, + received: sumReceived, + unreceived: sumUnreceived + } + }; + } + + /** 计算单行横向合计:业绩合计 / 成本合计 / 利润合计 */ + function calcRowTotals(r) { + var metrics = ['Perf', 'Cost', 'Profit']; + metrics.forEach(function (m) { + var rowSum = ALL_DATA_KEYS.reduce(function (acc, k) { return acc + numOrZero(r[k + m]); }, 0); + r['rowTotal' + m] = rowSum === 0 ? null : rowSum; + }); + return r; + } + + /** 纵向(全年)合计行 */ + function sumLedgerRows(monthRows) { + var acc = { month: 13, monthLabel: '合计', rowType: 'total', key: 'total' }; + var metrics = ['Perf', 'Cost', 'Profit']; + var allKeys = ALL_DATA_KEYS.concat(['rowTotal']); + + allKeys.forEach(function (k) { + metrics.forEach(function (m) { + acc[k + m] = 0; + }); + }); + + (monthRows || []).forEach(function (r) { + allKeys.forEach(function (k) { + metrics.forEach(function (m) { + acc[k + m] += numOrZero(r[k + m]); + }); + }); + }); + + allKeys.forEach(function (k) { + metrics.forEach(function (m) { + if (acc[k + m] === 0) acc[k + m] = null; + }); + }); + return acc; + } + + function buildMockYear2026() { + var rows = []; + var i; + for (i = 1; i <= 12; i++) { + var logisticsPerf = 800000 + Math.random() * 400000; + var logisticsCost = logisticsPerf * (0.9 + Math.random() * 0.2); + + var leasePerf = 200000 + Math.random() * 100000; + var leaseCost = leasePerf * (0.8 + Math.random() * 0.15); + + var salesPerf = 40000 + Math.random() * 60000; + var salesCost = salesPerf * (0.7 + Math.random() * 0.2); + + var hydrogenPerf = 100000 + Math.random() * 50000; + var hydrogenCost = hydrogenPerf * (0.85 + Math.random() * 0.1); + + var elecPerf = 3000 + Math.random() * 2000; + var elecCost = elecPerf * (0.4 + Math.random() * 0.3); + + var etcPerf = 60000 + Math.random() * 30000; + var etcCost = etcPerf; + + var otherPerf = 5000 + Math.random() * 10000; + var otherCost = otherPerf * (0.6 + Math.random() * 0.3); + + var src = { + logisticsPerf: Math.round(logisticsPerf * 100) / 100, + logisticsCost: Math.round(logisticsCost * 100) / 100, + logisticsProfit: Math.round((logisticsPerf - logisticsCost) * 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, + + hydrogenPerf: Math.round(hydrogenPerf * 100) / 100, + hydrogenCost: Math.round(hydrogenCost * 100) / 100, + hydrogenProfit: Math.round((hydrogenPerf - hydrogenCost) * 100) / 100, + + elecPerf: Math.round(elecPerf * 100) / 100, + elecCost: Math.round(elecCost * 100) / 100, + elecProfit: Math.round((elecPerf - elecCost) * 100) / 100, + + etcPerf: Math.round(etcPerf * 100) / 100, + etcCost: Math.round(etcCost * 100) / 100, + etcProfit: 0, + + otherPerf: Math.round(otherPerf * 100) / 100, + otherCost: Math.round(otherCost * 100) / 100, + otherProfit: Math.round((otherPerf - otherCost) * 100) / 100 + }; + + var row = { key: 'm' + i, month: i, monthLabel: i, rowType: 'month' }; + ALL_DATA_KEYS.forEach(function (k) { + ['Perf', 'Cost', 'Profit'].forEach(function (m) { + var v = src[k + m]; + row[k + m] = v !== undefined && v !== null ? v : null; + }); + }); + calcRowTotals(row); + rows.push(row); + } + rows.push(sumLedgerRows(rows)); + 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 = + '.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:0 8px!important;height:38px!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:0 8px!important;height:38px!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-table .ant-table-summary>tr>td{font-weight:700;background:#f8fafc!important;color:#0f172a!important;border-top:2px solid #cbd5e1!important;border-bottom:none!important;border-inline-end:1px solid #f1f5f9!important;padding:0 8px!important;height:38px!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 metricState = useState('Perf'); + var metricType = metricState[0]; + var setMetricType = metricState[1]; + + var viewState = useState('main'); + var view = viewState[0]; + var setView = viewState[1]; + + var salesModalState = useState({ month: null, monthLabel: '', amount: null, cellType: 'Perf' }); + var salesModal = salesModalState[0]; + var setSalesModal = salesModalState[1]; + + var salesDetailModalState = useState({ ym: '', deptName: '', salesperson: '', amount: null }); + var salesDetailModal = salesDetailModalState[0]; + var setSalesDetailModal = salesDetailModalState[1]; + + var drillFilterDraftState = useState({ salesperson: undefined, customerName: undefined, nature: undefined, plateNo: undefined, paymentDate: null }); + var drillFilterDraft = drillFilterDraftState[0]; + var setDrillFilterDraft = drillFilterDraftState[1]; + + var drillFilterAppliedState = useState({ salesperson: undefined, customerName: undefined, nature: undefined, plateNo: undefined, paymentDate: null }); + var drillFilterApplied = drillFilterAppliedState[0]; + var setDrillFilterApplied = drillFilterAppliedState[1]; + + var hydroFilterDraftState = useState({ month: null, customerName: undefined }); + var hydroFilterDraft = hydroFilterDraftState[0]; + var setHydroFilterDraft = hydroFilterDraftState[1]; + + var hydroFilterAppliedState = useState({ month: null, customerName: undefined }); + var hydroFilterApplied = hydroFilterAppliedState[0]; + var setHydroFilterApplied = hydroFilterAppliedState[1]; + + var hydroStationDrillState = useState({ mode: 'all', customerName: null }); + var hydroStationDrill = hydroStationDrillState[0]; + var setHydroStationDrill = hydroStationDrillState[1]; + + var hydroRefuelDetailCtxState = useState({ customerName: '', stationName: null }); + var hydroRefuelDetailCtx = hydroRefuelDetailCtxState[0]; + var setHydroRefuelDetailCtx = hydroRefuelDetailCtxState[1]; + + var custSumFilterDraftState = useState({ customerName: undefined }); + var custSumFilterDraft = custSumFilterDraftState[0]; + var setCustSumFilterDraft = custSumFilterDraftState[1]; + + var custSumFilterAppliedState = useState({ customerName: undefined }); + var custSumFilterApplied = custSumFilterAppliedState[0]; + var setCustSumFilterApplied = custSumFilterAppliedState[1]; + + var detailCustomerState = useState(null); + var detailCustomer = detailCustomerState[0]; + var setDetailCustomer = detailCustomerState[1]; + + var detailProjectState = useState(null); + var detailProject = detailProjectState[0]; + var setDetailProject = detailProjectState[1]; + + var editingBrokerageState = useState(null); + var editingBrokerage = editingBrokerageState[0]; + var setEditingBrokerage = editingBrokerageState[1]; + + var brokerageFeeOverridesState = useState({}); + var brokerageFeeOverrides = brokerageFeeOverridesState[0]; + var setBrokerageFeeOverrides = brokerageFeeOverridesState[1]; + + var brokerageBatchPopoverOpenState = useState(false); + var brokerageBatchPopoverOpen = brokerageBatchPopoverOpenState[0]; + var setBrokerageBatchPopoverOpen = brokerageBatchPopoverOpenState[1]; + + var brokerageBatchInputState = useState(null); + var brokerageBatchInput = brokerageBatchInputState[0]; + var setBrokerageBatchInput = brokerageBatchInputState[1]; + + var dataSource = useMemo(function () { + var y = yearApplied && yearApplied.format ? yearApplied.format('YYYY') : ''; + if (y === '2026') return buildMockYear2026(); + return []; + }, [yearApplied]); + + var tableTitle = useMemo(function () { + return '浙江羚牛氢能业务部汇总台账'; + }, []); + + 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 timeDisplayLabel = useMemo(function () { + return yearApplied && yearApplied.format ? yearApplied.format('YYYY年') : '—'; + }, [yearApplied]); + + var handleQuery = useCallback(function () { + setYearApplied(yearDraft); + setDeptApplied(deptDraft); + }, [yearDraft, deptDraft]); + + var handleReset = useCallback(function () { + var y0 = initialYear(); + setYearDraft(y0); + setYearApplied(y0); + setDeptDraft([]); + setDeptApplied([]); + }, []); + + var handleDrillQuery = useCallback(function () { + setDrillFilterApplied(drillFilterDraft); + }, [drillFilterDraft]); + + var handleDrillReset = useCallback(function () { + var empty = { salesperson: undefined, customerName: undefined, nature: undefined, plateNo: undefined, paymentDate: null }; + setDrillFilterDraft(empty); + setDrillFilterApplied(empty); + }, []); + + var handleCustSumQuery = useCallback(function () { + setCustSumFilterApplied(custSumFilterDraft); + }, [custSumFilterDraft]); + + var handleCustSumReset = useCallback(function () { + var empty = { customerName: undefined }; + setCustSumFilterDraft(empty); + setCustSumFilterApplied(empty); + }, []); + + var openProjectSummary = useCallback(function (record) { + setDetailCustomer(record); + setView('projectSummary'); + }, []); + + var openProjectDetail = useCallback(function (record) { + setDetailProject(record); + setView('sales'); + }, []); + + var openHydrogenDrill = useCallback(function (row, amount, hydrogenAsCost) { + if (amount === null || amount === undefined || amount === '' || numOrZero(amount) === 0) { + message.warning('该单元格无数据'); + return; + } + setSalesModal({ + month: row.month, + monthLabel: row.monthLabel, + amount: amount, + cellType: hydrogenAsCost ? 'HydrogenCost' : 'HydrogenPerf' + }); + var initialMonth = row.month <= 12 && yearApplied && yearApplied.format ? dayjs(yearApplied.format('YYYY') + '-' + pad2(row.month) + '-01') : null; + var resetState = { month: initialMonth, customerName: undefined }; + setHydroFilterDraft(resetState); + setHydroFilterApplied(resetState); + setView('hydrogenCustomerPerf'); + }, [yearApplied]); + + var openHydroStationDrillByCustomer = useCallback(function (record) { + setHydroStationDrill({ mode: 'customer', customerName: record.customerName }); + setView('hydrogenStationDrill'); + }, []); + + var openHydroStationDrillAllStations = useCallback(function () { + setHydroStationDrill({ mode: 'all', customerName: null }); + setView('hydrogenStationDrill'); + }, []); + + var openHydroRefuelDetailByCustomer = useCallback(function (customerName) { + if (!customerName) return; + setHydroRefuelDetailCtx({ customerName: customerName, stationName: null }); + setView('hydrogenRefuelDetail'); + }, []); + + var openHydroRefuelDetailByStationRow = useCallback(function (record) { + if (!record || !record.customerName || !record.stationName) return; + setHydroRefuelDetailCtx({ customerName: record.customerName, stationName: record.stationName }); + setView('hydrogenRefuelDetail'); + }, []); + + var openLeaseDrill = useCallback(function (row, cellKey, amount) { + if (cellKey !== 'leasePerf' && cellKey !== 'leaseCost') return; + if (amount === null || amount === undefined || amount === '' || numOrZero(amount) === 0) { + message.warning('该单元格无数据'); + return; + } + setSalesModal({ + month: row.month, + monthLabel: row.monthLabel, + amount: amount, + cellType: cellKey === 'leasePerf' ? 'Perf' : 'Cost' + }); + setDetailCustomer(null); + setDetailProject(null); + setView('customerSummary'); + }, []); + + var openDetailDrill = useCallback(function (row, amount) { + setSalesDetailModal({ + ym: row.ym, + deptName: row.deptName, + salesperson: row.salesperson, + amount: amount + }); + setView('sales_detail'); + }, []); + + var drillPayload = useMemo(function () { + if (view !== 'sales' || salesModal.month == null) { + return { rows: [], options: { salespersons: [], customers: [], plateNos: [] }, totals: { receivable: 0, received: 0, unreceived: 0, naturalMonthIncome: 0, vehicleCost: 0, brokerageFee: 0, totalCost: 0 } }; + } + return mockLeaseOrderDrill(salesModal.month, salesModal.amount, deptApplied, deptOptions, yearApplied, drillFilterApplied, detailProject, salesModal.cellType === 'Cost', brokerageFeeOverrides); + }, [view, salesModal, deptApplied, deptOptions, yearApplied, drillFilterApplied, detailProject, brokerageFeeOverrides]); + + var applyBrokerageBatch = useCallback(function () { + if (brokerageBatchInput === null || brokerageBatchInput === undefined || brokerageBatchInput === '') { + message.warning('请填写居间费批量设置'); + return; + } + var num = Number(brokerageBatchInput); + if (isNaN(num)) { + message.warning('请输入有效数字'); + return; + } + var val = Math.round(num * 100) / 100; + if (view !== 'sales' || salesModal.cellType !== 'Cost') { + setBrokerageBatchPopoverOpen(false); + setBrokerageBatchInput(null); + return; + } + var rows = drillPayload.rows || []; + if (rows.length === 0) { + message.warning('当前无数据'); + return; + } + var next = Object.assign({}, brokerageFeeOverrides); + rows.forEach(function (r) { + var compositeKey = r.customerName + '|' + r.projectName + '|' + r.plateNo; + next[compositeKey] = val; + }); + setBrokerageFeeOverrides(next); + setEditingBrokerage(null); + setBrokerageBatchPopoverOpen(false); + setBrokerageBatchInput(null); + message.success('已批量设置居间费'); + }, [brokerageBatchInput, view, salesModal, drillPayload.rows, brokerageFeeOverrides]); + + var hydrogenCustomerPerfPayload = useMemo(function () { + if (view !== 'hydrogenCustomerPerf' || salesModal.month == null) { + return { rows: [], options: { customers: [] } }; + } + return mockHydrogenCustomerPerf(salesModal.month, salesModal.amount, yearApplied, hydroFilterApplied); + }, [view, salesModal, yearApplied, hydroFilterApplied]); + + var hydrogenStationDrillPayload = useMemo(function () { + if (view !== 'hydrogenStationDrill' || salesModal.month == null) { + return { rows: [], monthLabel: '—', drillScopeLabel: '全部客户' }; + } + var customerRows = mockHydrogenCustomerPerf(salesModal.month, salesModal.amount, yearApplied, hydroFilterApplied).rows; + return mockHydrogenStationBreakdown(customerRows, hydroStationDrill.mode, hydroStationDrill.customerName); + }, [view, salesModal, yearApplied, hydroFilterApplied, hydroStationDrill]); + + var hydrogenRefuelDetailPayload = useMemo(function () { + if (view !== 'hydrogenRefuelDetail' || salesModal.month == null) { + return { rows: [] }; + } + return mockHydrogenRefuelDetailRows(hydroRefuelDetailCtx, yearApplied, hydroFilterApplied, salesModal); + }, [view, salesModal, yearApplied, hydroFilterApplied, hydroRefuelDetailCtx]); + + var customerSummaryPayload = useMemo(function () { + if (view !== 'customerSummary' || salesModal.month == null) { + return { rows: [], options: { customers: [] } }; + } + return mockCustomerSummary(salesModal.month, salesModal.amount, deptApplied, custSumFilterApplied, salesModal.cellType === 'Cost', brokerageFeeOverrides); + }, [view, salesModal, deptApplied, custSumFilterApplied, brokerageFeeOverrides]); + + var projectSummaryPayload = useMemo(function () { + if (view !== 'projectSummary' || !detailCustomer) { + return []; + } + return mockProjectSummary(detailCustomer, salesModal.cellType === 'Cost', brokerageFeeOverrides); + }, [view, detailCustomer, salesModal, brokerageFeeOverrides]); + + var drillColumns = useMemo(function () { + var isCost = salesModal.cellType === 'Cost'; + var cols = [ + { title: '账单周期', dataIndex: 'billCycle', key: 'billCycle', width: 160, align: 'center' }, + { title: '账单期数', dataIndex: 'billInstallment', key: 'billInstallment', width: 120, align: 'center' }, + { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 100, align: 'center' }, + { title: '品牌', dataIndex: 'brand', key: 'brand', width: 80, align: 'center' }, + { title: '型号', dataIndex: 'vehicleModel', key: 'vehicleModel', width: 180, align: 'center' }, + { title: '业务员', dataIndex: 'salesperson', key: 'salesperson', width: 80, align: 'center' }, + { title: '性质', dataIndex: 'nature', key: 'nature', width: 80, align: 'center' }, + { title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 220, align: 'center' }, + { title: '项目名称', dataIndex: 'projectName', key: 'projectName', width: 160, align: 'center' }, + { title: '合同有效期', dataIndex: 'contractDate', key: 'contractDate', width: 160, align: 'center' }, + { title: '提车日期', dataIndex: 'pickupDate', key: 'pickupDate', width: 100, align: 'center' } + ]; + + if (isCost) { + var handleBrokerageBatchPopoverChange = function (nextOpen) { + setBrokerageBatchPopoverOpen(nextOpen); + if (!nextOpen) setBrokerageBatchInput(null); + }; + var brokerageBatchPopoverContent = React.createElement('div', { style: { minWidth: 268 } }, + React.createElement('div', { style: { marginBottom: 12, display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: 8 } }, + React.createElement('span', { style: { color: '#ff4d4f' } }, '*'), + React.createElement('span', { style: { fontSize: 14 } }, '居间费批量设置'), + React.createElement(InputNumber, { + min: 0, + precision: 2, + step: 0.01, + controls: true, + value: brokerageBatchInput, + onChange: function (val) { setBrokerageBatchInput(val); }, + placeholder: '请输入', + style: { width: 150 } + }) + ), + React.createElement(Space, { size: 'small' }, + React.createElement(Button, { type: 'primary', size: 'small', onClick: applyBrokerageBatch }, '确认'), + React.createElement(Button, { size: 'small', onClick: function () { setBrokerageBatchPopoverOpen(false); setBrokerageBatchInput(null); } }, '关闭') + ) + ); + var brokerageColumnTitle = React.createElement('div', { style: { display: 'inline-flex', alignItems: 'center', justifyContent: 'flex-end', gap: 6 } }, + React.createElement('span', null, '居间费'), + React.createElement(Popover, { + trigger: 'click', + placement: 'bottomRight', + content: brokerageBatchPopoverContent, + open: brokerageBatchPopoverOpen, + visible: brokerageBatchPopoverOpen, + onOpenChange: handleBrokerageBatchPopoverChange, + onVisibleChange: handleBrokerageBatchPopoverChange + }, React.createElement('span', { + role: 'button', + tabIndex: 0, + style: { cursor: 'pointer', color: '#0ea5e9', display: 'inline-flex', alignItems: 'center' }, + 'aria-label': '批量设置居间费' + }, React.createElement(EditIcon, null))) + ); + cols.push( + { title: '车辆成本', dataIndex: 'vehicleCost', key: 'vehicleCost', width: 100, align: 'right', render: function(v){ return fmtLedgerCell(v); } }, + { + title: brokerageColumnTitle, + dataIndex: 'brokerageFee', + key: 'brokerageFee', + width: 120, + align: 'right', + render: function (v, record) { + if (record.rowType === 'total') return fmtLedgerCell(v); + var compositeKey = record.customerName + '|' + record.projectName + '|' + record.plateNo; + var isEditing = editingBrokerage && editingBrokerage.key === compositeKey; + if (isEditing) { + return React.createElement(InputNumber, { + autoFocus: true, + value: editingBrokerage.value, + onChange: function(val) { setEditingBrokerage({ key: compositeKey, value: val }); }, + onBlur: function() { + setBrokerageFeeOverrides(Object.assign({}, brokerageFeeOverrides, { [compositeKey]: editingBrokerage.value })); + setEditingBrokerage(null); + }, + onPressEnter: function() { + setBrokerageFeeOverrides(Object.assign({}, brokerageFeeOverrides, { [compositeKey]: editingBrokerage.value })); + setEditingBrokerage(null); + }, + style: { width: 100 } + }); + } + return React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 6 } }, + fmtLedgerCell(v), + React.createElement('span', { + style: { cursor: 'pointer', color: '#165dff', display: 'flex', alignItems: 'center' }, + onClick: function() { setEditingBrokerage({ key: compositeKey, value: v }); } + }, React.createElement(EditIcon, null)) + ); + } + }, + { title: '总成本', dataIndex: 'totalCost', key: 'totalCost', width: 100, align: 'right', render: function(v){ return fmtLedgerCell(v); } } + ); + } else { + cols.push( + { title: '应收', dataIndex: 'receivable', key: 'receivable', width: 100, align: 'right', render: function(v){ return fmtLedgerCell(v); } }, + { title: '实收', dataIndex: 'received', key: 'received', width: 100, align: 'right', render: function(v){ return fmtLedgerCell(v); } }, + { title: '未收', dataIndex: 'unreceived', key: 'unreceived', width: 100, align: 'right', render: function(v){ return React.createElement('span', { style: { color: '#f53f3f' } }, fmtLedgerCell(v)); } }, + { title: '自然月收入', dataIndex: 'naturalMonthIncome', key: 'naturalMonthIncome', width: 100, align: 'right', render: function(v){ return fmtLedgerCell(v); } }, + { title: '付款日期', dataIndex: 'paymentDate', key: 'paymentDate', width: 100, align: 'center' }, + { title: '付款方式', dataIndex: 'payMethod', key: 'payMethod', width: 80, align: 'center' }, + { title: '付款周期', dataIndex: 'payCycle', key: 'payCycle', width: 80, align: 'center' } + ); + } + return cols; + }, [salesModal.cellType, detailProject, editingBrokerage, brokerageFeeOverrides, brokerageBatchPopoverOpen, brokerageBatchInput, applyBrokerageBatch]); + + var detailPayload = useMemo(function () { + if (view !== 'sales_detail' || salesDetailModal.ym == null) { + return { rows: [], totals: { deposit: 0, vehicle: 0, additional: 0, total: 0 } }; + } + return mockLeaseOrderDetail(salesDetailModal.ym, salesDetailModal.deptName, salesDetailModal.salesperson, salesDetailModal.amount); + }, [view, salesDetailModal]); + + var detailColumns = useMemo(function () { + return [ + { title: '月份', dataIndex: 'ym', key: 'ym', width: 104, align: 'center', onCell: function(r){ return { rowSpan: r.monthRowSpan }; } }, + { title: '业务部门', dataIndex: 'deptName', key: 'deptName', width: 124, align: 'center', onCell: function(r){ return { rowSpan: r.groupRowSpan }; } }, + { title: '业务人员', dataIndex: 'salesperson', key: 'salesperson', width: 100, align: 'center', onCell: function(r){ return { rowSpan: r.groupRowSpan }; } }, + { title: '账单日期', dataIndex: 'billDate', key: 'billDate', width: 110, align: 'center', onCell: function(r){ return { rowSpan: r.groupRowSpan }; } }, + { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 120, align: 'center' }, + { title: '车辆型号', dataIndex: 'vehicleModel', key: 'vehicleModel', width: 120, align: 'center' }, + { title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 180, align: 'center' }, + { title: '合同日期', dataIndex: 'contractDate', key: 'contractDate', width: 200, align: 'center' }, + { title: '提车日期', dataIndex: 'pickupDate', key: 'pickupDate', width: 110, align: 'center' }, + { title: '付款方式', dataIndex: 'payMethod', key: 'payMethod', width: 90, align: 'center' }, + { title: '付款周期', dataIndex: 'payCycle', key: 'payCycle', width: 90, align: 'center' }, + { title: '账单计算方式', dataIndex: 'billCalcMethod', key: 'billCalcMethod', width: 110, align: 'center' }, + { title: '实际计费天数', dataIndex: 'actualDays', key: 'actualDays', width: 110, align: 'center' }, + { title: '保证金应收', dataIndex: 'depositAmount', key: 'depositAmount', width: 120, align: 'right', render: function(v){ return fmtLedgerCell(v); } }, + { title: '车辆应收', dataIndex: 'vehicleAmount', key: 'vehicleAmount', width: 120, align: 'right', render: function(v){ return fmtLedgerCell(v); } }, + { title: '附加费用应收', dataIndex: 'additionalAmount', key: 'additionalAmount', width: 140, align: 'right', render: function(v){ return fmtLedgerCell(v); } }, + { title: '合计应收', dataIndex: 'totalAmount', key: 'totalAmount', width: 120, align: 'right', render: function(v){ return fmtLedgerCell(v); } } + ]; + }, []); + + var handleExport = useCallback(function () { + if (!dataSource || dataSource.length === 0) { + message.warning('当前无数据可导出,请先查询'); + return; + } + var y = yearApplied && yearApplied.format ? yearApplied.format('YYYY') : 'ledger'; + + var header = ['月份']; + SECTOR_COLUMNS.forEach(function (s) { + header.push(s.label + '-业绩', s.label + '-成本', s.label + '-利润'); + }); + header.push('业绩合计', '成本合计', '利润合计'); + var body = [header]; + + dataSource.forEach(function (r) { + var line = [r.monthLabel]; + SECTOR_COLUMNS.forEach(function (s) { + line.push(fmtLedgerCell(r[s.key + 'Perf']), fmtLedgerCell(r[s.key + 'Cost']), fmtLedgerCell(r[s.key + 'Profit'])); + }); + line.push(fmtLedgerCell(r.rowTotalPerf), fmtLedgerCell(r.rowTotalCost), fmtLedgerCell(r.rowTotalProfit)); + 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: 56, + align: 'center', + render: function (t, r) { + if (r.rowType === 'total') return React.createElement('span', { style: { fontWeight: 700 } }, t); + return t; + } + } + ]; + + SECTOR_COLUMNS.forEach(function (sec) { + cols.push({ + title: sec.label, + key: 'grp-' + sec.key, + align: 'center', + children: [ + { + title: '业绩', + dataIndex: sec.key + 'Perf', + key: sec.key + 'Perf', + width: 108, + align: 'center', + render: function (v, r) { + if (sec.key === 'lease' && numOrZero(v) !== 0) { + return React.createElement('button', { + type: 'button', + className: 'biz-standbook-perf-link', + onClick: function () { openLeaseDrill(r, sec.key + 'Perf', v); } + }, fmtLedgerCell(v)); + } + if (sec.key === 'hydrogen' && numOrZero(v) !== 0) { + return React.createElement('button', { + type: 'button', + className: 'biz-standbook-perf-link', + onClick: function () { openHydrogenDrill(r, v, false); } + }, fmtLedgerCell(v)); + } + return fmtLedgerCell(v); + } + }, + { + title: '成本', + dataIndex: sec.key + 'Cost', + key: sec.key + 'Cost', + width: 108, + align: 'center', + render: function (v, r) { + if (sec.key === 'lease' && numOrZero(v) !== 0) { + return React.createElement('button', { + type: 'button', + className: 'biz-standbook-perf-link', + onClick: function () { openLeaseDrill(r, sec.key + 'Cost', v); } + }, fmtLedgerCell(v)); + } + if (sec.key === 'hydrogen' && numOrZero(v) !== 0) { + return React.createElement('button', { + type: 'button', + className: 'biz-standbook-perf-link', + onClick: function () { openHydrogenDrill(r, v, true); } + }, fmtLedgerCell(v)); + } + return fmtLedgerCell(v); + } + }, + { + title: '利润', + dataIndex: sec.key + 'Profit', + key: sec.key + 'Profit', + width: 108, + align: 'center', + render: function (v) { return fmtLedgerCell(v); } + } + ] + }); + }); + + cols.push( + { + title: '业绩合计', + dataIndex: 'rowTotalPerf', + key: 'rowTotalPerf', + width: 112, + align: 'center', + fixed: 'right', + className: 'col-summary', + render: function (v) { return fmtLedgerCell(v); } + }, + { + title: '成本合计', + dataIndex: 'rowTotalCost', + key: 'rowTotalCost', + width: 112, + align: 'center', + fixed: 'right', + className: 'col-summary', + render: function (v) { return fmtLedgerCell(v); } + }, + { + title: '利润合计', + dataIndex: 'rowTotalProfit', + key: 'rowTotalProfit', + width: 112, + align: 'center', + fixed: 'right', + className: 'col-summary', + render: function (v) { return fmtLedgerCell(v); } + } + ); + + return cols; + }, [openLeaseDrill, openHydrogenDrill]); + + 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 } }, + '时间:', + timeDisplayLabel, + '\u00A0\u00A0\u00A0\u00A0业务部:', + 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: 'calc(100vh - 250px)' }, + sticky: true + }) + ) + ) + ); + }; + + var renderHydrogenCustomerPerfView = function () { + var hydrogenIsCost = salesModal.cellType === 'HydrogenCost'; + var hydroAmountColTitle = hydrogenIsCost ? '成本金额(元)' : '对客总额(元)'; + var hydroCustomerPageTitle = hydrogenIsCost ? '氢费业务客户成本汇总' : '氢费业务客户业绩汇总'; + var columns = [ + { + title: '月份', + dataIndex: 'monthLabel', + key: 'monthLabel', + align: 'center', + onCell: function (record) { return { rowSpan: record.monthSpan }; } + }, + { + title: '客户名称', + dataIndex: 'customerName', + key: 'customerName', + align: 'center', + render: function (v, record) { + return React.createElement('button', { + type: 'button', + className: 'biz-standbook-perf-link', + onClick: function () { openHydroStationDrillByCustomer(record); } + }, v); + } + }, + { + title: '加氢次数', + dataIndex: 'count', + key: 'count', + align: 'center' + }, + { + title: '加氢量(kg)', + dataIndex: 'amountKg', + key: 'amountKg', + align: 'right', + render: function(v) { return fmtLedgerCell(v); } + }, + { + title: hydroAmountColTitle, + dataIndex: 'totalAmount', + key: 'totalAmount', + align: 'right', + render: function(v) { return fmtLedgerCell(v); } + } + ]; + + var sumCount = 0, sumAmountKg = 0, sumTotalAmount = 0; + var uniqueCustomers = []; + hydrogenCustomerPerfPayload.rows.forEach(function(r) { + if (uniqueCustomers.indexOf(r.customerName) === -1) uniqueCustomers.push(r.customerName); + sumCount += numOrZero(r.count); + sumAmountKg += numOrZero(r.amountKg); + sumTotalAmount += numOrZero(r.totalAmount); + }); + + var handleQuery = function() { + setHydroFilterApplied(hydroFilterDraft); + }; + var handleReset = function() { + var resetState = { month: null, customerName: undefined }; + setHydroFilterDraft(resetState); + setHydroFilterApplied(resetState); + }; + + var hydroStatMonthStr = hydroFilterApplied.month && hydroFilterApplied.month.format ? hydroFilterApplied.month.format('YYYY-MM') : '—'; + var hydroPerfSubline = hydroFilterApplied.customerName + ? hydroFilterApplied.customerName + '\u00A0\u00A0\u00A0\u00A0统计时间:' + hydroStatMonthStr + : '统计时间:' + hydroStatMonthStr; + + 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: 'month', + placeholder: '日期选择器,精确至月', + style: filterControlStyle, + value: hydroFilterDraft.month, + onChange: function (v) { setHydroFilterDraft(Object.assign({}, hydroFilterDraft, { month: 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, { + placeholder: '选择器,枚举所有客户', + style: filterControlStyle, + value: hydroFilterDraft.customerName, + onChange: function (v) { setHydroFilterDraft(Object.assign({}, hydroFilterDraft, { customerName: v })); }, + allowClear: true, + showSearch: true, + filterOption: filterOption, + options: hydrogenCustomerPerfPayload.options ? hydrogenCustomerPerfPayload.options.customers : [] + }) + ) + ), + 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: '16px 24px' } }, + React.createElement('div', { style: { position: 'relative', marginBottom: 16, minHeight: 52 } }, + React.createElement('div', { style: { position: 'absolute', left: 0, top: 0, zIndex: 1 } }, + React.createElement(Button, { onClick: function () { setView('main'); } }, '返回上一步') + ), + React.createElement('div', { style: { textAlign: 'center', padding: '0 128px' } }, + React.createElement('div', { style: { fontSize: 18, fontWeight: 700, color: 'rgba(15,23,42,0.92)', letterSpacing: '0.02em' } }, hydroCustomerPageTitle), + React.createElement('div', { style: { marginTop: 6, fontSize: 13, color: 'rgba(15,23,42,0.55)', fontWeight: 500 } }, hydroPerfSubline) + ) + ), + React.createElement('div', { className: 'biz-standbook-table-wrap' }, + React.createElement(Table, { + className: 'biz-standbook-table', bordered: true, rowKey: 'key', columns: columns, dataSource: hydrogenCustomerPerfPayload.rows, pagination: false, scroll: { x: 'max-content', y: 'calc(100vh - 250px)' }, sticky: true, + summary: function () { + return React.createElement(Table.Summary, { fixed: 'bottom' }, + React.createElement(Table.Summary.Row, { className: 'row-total' }, + React.createElement(Table.Summary.Cell, { index: 0, align: 'center' }, '合计'), + React.createElement(Table.Summary.Cell, { index: 1, align: 'center' }, + React.createElement('button', { + type: 'button', + className: 'biz-standbook-perf-link', + onClick: openHydroStationDrillAllStations + }, uniqueCustomers.length) + ), + React.createElement(Table.Summary.Cell, { index: 2, align: 'center' }, sumCount), + React.createElement(Table.Summary.Cell, { index: 3, align: 'right' }, fmtLedgerCell(sumAmountKg)), + React.createElement(Table.Summary.Cell, { index: 4, align: 'right' }, fmtLedgerCell(sumTotalAmount)) + ) + ); + } + }) + ) + ) + ); + }; + + var renderHydrogenStationDrillView = function () { + var hydrogenIsCost = salesModal.cellType === 'HydrogenCost'; + var stAmountColTitle = hydrogenIsCost ? '成本金额(元)' : '对客总额(元)'; + var stPageTitle = hydrogenIsCost ? '客户各加氢站加氢成本' : '客户各加氢站加氢业绩'; + var stRows = hydrogenStationDrillPayload.rows || []; + var stSumCount = 0; + var stSumKg = 0; + var stSumAmt = 0; + stRows.forEach(function (r) { + stSumCount += numOrZero(r.count); + stSumKg += numOrZero(r.amountKg); + stSumAmt += numOrZero(r.totalAmount); + }); + var hydroStatMonthStr2 = hydroFilterApplied.month && hydroFilterApplied.month.format ? hydroFilterApplied.month.format('YYYY-MM') : '—'; + var scopeLbl = hydrogenStationDrillPayload.drillScopeLabel; + var drillPrefix = scopeLbl && scopeLbl !== '全部客户' ? scopeLbl + '\u00A0\u00A0\u00A0\u00A0' : ''; + var drillSubLabel = drillPrefix + '统计时间:' + hydroStatMonthStr2; + + var stColumns = [ + { + title: '月份', + dataIndex: 'monthLabel', + key: 'monthLabel', + align: 'center', + onCell: function (record) { return { rowSpan: record.monthSpan }; } + }, + { + title: '客户名称', + dataIndex: 'customerName', + key: 'customerName', + align: 'center', + onCell: function (record) { return { rowSpan: record.customerSpan }; }, + render: function (v, record) { + return React.createElement('button', { + type: 'button', + className: 'biz-standbook-perf-link', + onClick: function () { openHydroRefuelDetailByCustomer(v); } + }, v); + } + }, + { + title: '加氢站名称', + dataIndex: 'stationName', + key: 'stationName', + align: 'center', + render: function (v, record) { + return React.createElement('button', { + type: 'button', + className: 'biz-standbook-perf-link', + onClick: function () { openHydroRefuelDetailByStationRow(record); } + }, v); + } + }, + { + title: '加氢次数', + dataIndex: 'count', + key: 'count', + align: 'center' + }, + { + title: '加氢量(kg)', + dataIndex: 'amountKg', + key: 'amountKg', + align: 'right', + render: function (v) { return fmtLedgerCell(v); } + }, + { + title: stAmountColTitle, + dataIndex: 'totalAmount', + key: 'totalAmount', + align: 'right', + render: function (v) { return fmtLedgerCell(v); } + } + ]; + + return React.createElement(Card, { style: tableCardStyle, bodyStyle: { padding: '16px 24px' } }, + React.createElement('div', { style: { position: 'relative', marginBottom: 16, minHeight: 52 } }, + React.createElement('div', { style: { position: 'absolute', left: 0, top: 0, zIndex: 1 } }, + React.createElement(Button, { onClick: function () { setView('hydrogenCustomerPerf'); } }, '返回上一步') + ), + React.createElement('div', { style: { textAlign: 'center', padding: '0 128px' } }, + React.createElement('div', { style: { fontSize: 18, fontWeight: 700, color: 'rgba(15,23,42,0.92)', letterSpacing: '0.02em' } }, stPageTitle), + React.createElement('div', { style: { marginTop: 6, fontSize: 13, color: 'rgba(15,23,42,0.55)', fontWeight: 500 } }, drillSubLabel) + ) + ), + React.createElement('div', { className: 'biz-standbook-table-wrap' }, + React.createElement(Table, { + className: 'biz-standbook-table', + bordered: true, + rowKey: 'key', + columns: stColumns, + dataSource: stRows, + pagination: false, + scroll: { x: 'max-content', y: 'calc(100vh - 250px)' }, + sticky: true, + locale: { emptyText: '暂无加氢站明细' }, + summary: function () { + return React.createElement(Table.Summary, { fixed: 'bottom' }, + React.createElement(Table.Summary.Row, { className: 'row-total' }, + React.createElement(Table.Summary.Cell, { index: 0, colSpan: 3, align: 'center' }, '合计'), + React.createElement(Table.Summary.Cell, { index: 3, align: 'center' }, stSumCount), + React.createElement(Table.Summary.Cell, { index: 4, align: 'right' }, fmtLedgerCell(stSumKg)), + React.createElement(Table.Summary.Cell, { index: 5, align: 'right' }, fmtLedgerCell(stSumAmt)) + ) + ); + } + }) + ) + ); + }; + + var renderHydrogenRefuelDetailView = function () { + var hydrogenIsCost = salesModal.cellType === 'HydrogenCost'; + var refuelUnitTitle = hydrogenIsCost ? '成本单价(元/kg)' : '对客单价(元/kg)'; + var refuelFeeTitle = hydrogenIsCost ? '成本费用(元)' : '对客费用(元)'; + var dRows = hydrogenRefuelDetailPayload.rows || []; + var sumKg = 0; + var sumFee = 0; + dRows.forEach(function (r) { + sumKg += numOrZero(r.amountKg); + sumFee += numOrZero(r.customerFee); + }); + var avgUnit = sumKg > 0 ? Math.round((sumFee / sumKg) * 100) / 100 : 0; + var hydroStatMonthStr3 = hydroFilterApplied.month && hydroFilterApplied.month.format ? hydroFilterApplied.month.format('YYYY-MM') : '—'; + var ctx = hydroRefuelDetailCtx; + var detailSubParts = ['客户:' + (ctx.customerName || '—')]; + if (ctx.stationName) { + detailSubParts.push('加氢站:' + ctx.stationName); + } else { + detailSubParts.push('加氢站:全部'); + } + detailSubParts.push('统计时间:' + hydroStatMonthStr3); + var detailSubline = detailSubParts.join('\u00A0\u00A0\u00A0\u00A0'); + + var refuelCols = [ + { title: '加氢日期', dataIndex: 'refuelDateLabel', key: 'refuelDateLabel', align: 'center' }, + { title: '客户名称', dataIndex: 'customerName', key: 'customerName', align: 'center' }, + { title: '项目名称', dataIndex: 'projectName', key: 'projectName', align: 'center' }, + { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', align: 'center' }, + { title: '加氢站名称', dataIndex: 'stationName', key: 'stationName', align: 'center' }, + { title: '加氢量(kg)', dataIndex: 'amountKg', key: 'amountKg', align: 'right', render: function (v) { return fmtLedgerCell(v); } }, + { title: refuelUnitTitle, dataIndex: 'unitPrice', key: 'unitPrice', align: 'right', render: function (v) { return fmtLedgerCell(v); } }, + { title: refuelFeeTitle, dataIndex: 'customerFee', key: 'customerFee', align: 'right', render: function (v) { return fmtLedgerCell(v); } }, + { title: '业务员', dataIndex: 'salesperson', key: 'salesperson', align: 'center' } + ]; + + return React.createElement(Card, { style: tableCardStyle, bodyStyle: { padding: '16px 24px' } }, + React.createElement('div', { style: { position: 'relative', marginBottom: 16, minHeight: 52 } }, + React.createElement('div', { style: { position: 'absolute', left: 0, top: 0, zIndex: 1 } }, + React.createElement(Button, { onClick: function () { setView('hydrogenStationDrill'); } }, '返回上一步') + ), + React.createElement('div', { style: { textAlign: 'center', padding: '0 128px' } }, + React.createElement('div', { style: { fontSize: 18, fontWeight: 700, color: 'rgba(15,23,42,0.92)', letterSpacing: '0.02em' } }, '加氢明细'), + React.createElement('div', { style: { marginTop: 6, fontSize: 13, color: 'rgba(15,23,42,0.55)', fontWeight: 500 } }, detailSubline) + ) + ), + React.createElement('div', { className: 'biz-standbook-table-wrap' }, + React.createElement(Table, { + className: 'biz-standbook-table', + bordered: true, + rowKey: 'key', + columns: refuelCols, + dataSource: dRows, + pagination: false, + scroll: { x: 'max-content', y: 'calc(100vh - 250px)' }, + sticky: true, + locale: { emptyText: '暂无加氢明细' }, + summary: function () { + return React.createElement(Table.Summary, { fixed: 'bottom' }, + React.createElement(Table.Summary.Row, { className: 'row-total' }, + React.createElement(Table.Summary.Cell, { index: 0, colSpan: 5, align: 'center' }, '合计'), + React.createElement(Table.Summary.Cell, { index: 5, align: 'right' }, fmtLedgerCell(sumKg)), + React.createElement(Table.Summary.Cell, { index: 6, align: 'right' }, sumKg > 0 ? fmtLedgerCell(avgUnit) : '—'), + React.createElement(Table.Summary.Cell, { index: 7, align: 'right' }, fmtLedgerCell(sumFee)), + React.createElement(Table.Summary.Cell, { index: 8, align: 'center' }, '\u00a0') + ) + ); + } + }) + ) + ); + }; + + var renderCustomerSummaryView = function () { + var isCost = salesModal.cellType === 'Cost'; + var leaseSummaryMainTitle = isCost ? '租赁业务业务员成本汇总' : '租赁业务业务员业绩汇总'; + var yearStr = yearApplied && yearApplied.format ? yearApplied.format('YYYY') : 'YYYY'; + var leaseStatTimeStr; + if (salesModal.month != null && salesModal.month <= 12) { + leaseStatTimeStr = yearStr + '-' + pad2(salesModal.month); + } else if (salesModal.month === 13) { + leaseStatTimeStr = yearStr + '(全年)'; + } else { + leaseStatTimeStr = '—'; + } + var leaseSumSubline = custSumFilterApplied.customerName + ? custSumFilterApplied.customerName + '\u00A0\u00A0\u00A0\u00A0统计时间:' + leaseStatTimeStr + : '统计时间:' + leaseStatTimeStr; + + var columns = [ + { + title: '业务部门', + dataIndex: 'dept', + key: 'dept', + align: 'center', + onCell: function (record) { + return { rowSpan: record.deptSpan }; + } + }, + { + title: '业务员', + dataIndex: 'salesperson', + key: 'salesperson', + align: 'center', + onCell: function (record) { + return { rowSpan: record.spSpan }; + } + }, + { + title: '客户名称', + dataIndex: 'customerName', + key: 'customerName', + align: 'center', + render: function (v, record) { + return React.createElement('button', { + className: 'biz-standbook-perf-link', + onClick: function () { openProjectSummary(record); } + }, v); + } + } + ]; + + if (isCost) { + columns.push( + { title: '车辆成本', dataIndex: 'vehicleCost', key: 'vehicleCost', align: 'right', render: function (v) { return fmtLedgerCell(v); } }, + { title: '居间费', dataIndex: 'brokerageFee', key: 'brokerageFee', align: 'right', render: function (v) { return fmtLedgerCell(v); } }, + { title: '成本总计', dataIndex: 'totalCost', key: 'totalCost', align: 'right', render: function (v) { return fmtLedgerCell(v); } } + ); + } else { + columns.push( + { title: '应收', dataIndex: 'receivable', key: 'receivable', align: 'right', render: function (v) { return fmtLedgerCell(v); } }, + { title: '实收', dataIndex: 'received', key: 'received', align: 'right', render: function (v) { return fmtLedgerCell(v); } }, + { title: '未收', dataIndex: 'unreceived', key: 'unreceived', align: 'right', render: function (v) { return fmtLedgerCell(v); } } + ); + } + + var sum1 = 0, sum2 = 0, sum3 = 0; + var uniqueCustomers = []; + customerSummaryPayload.rows.forEach(function(r) { + if (uniqueCustomers.indexOf(r.customerName) === -1) { + uniqueCustomers.push(r.customerName); + } + if (isCost) { + sum1 += numOrZero(r.vehicleCost); + sum2 += numOrZero(r.brokerageFee); + sum3 += numOrZero(r.totalCost); + } else { + sum1 += numOrZero(r.receivable); + sum2 += numOrZero(r.received); + sum3 += numOrZero(r.unreceived); + } + }); + var customerCount = uniqueCustomers.length; + + 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(Select, { + placeholder: '请选择客户名称', + style: filterControlStyle, + value: custSumFilterDraft.customerName, + onChange: function (v) { setCustSumFilterDraft(Object.assign({}, custSumFilterDraft, { customerName: v })); }, + allowClear: true, + showSearch: true, + filterOption: filterOption, + options: customerSummaryPayload.options ? customerSummaryPayload.options.customers : [] + }) + ) + ), + 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: handleCustSumReset }, '重置'), + React.createElement(Button, { type: 'primary', onClick: handleCustSumQuery }, '查询') + ) + ) + ) + ) + ), + React.createElement(Card, { style: tableCardStyle, bodyStyle: { padding: '16px 24px' } }, + React.createElement('div', { style: { position: 'relative', marginBottom: 16, minHeight: 52 } }, + React.createElement('div', { style: { position: 'absolute', left: 0, top: 0, zIndex: 1 } }, + React.createElement(Button, { onClick: function () { setView('main'); } }, '返回上一步') + ), + React.createElement('div', { style: { textAlign: 'center', padding: '0 128px' } }, + React.createElement('div', { style: { fontSize: 18, fontWeight: 700, color: 'rgba(15,23,42,0.92)', letterSpacing: '0.02em' } }, leaseSummaryMainTitle), + React.createElement('div', { style: { marginTop: 6, fontSize: 13, color: 'rgba(15,23,42,0.55)', fontWeight: 500 } }, leaseSumSubline) + ) + ), + React.createElement('div', { className: 'biz-standbook-table-wrap' }, + React.createElement(Table, { + className: 'biz-standbook-table', + size: 'small', + bordered: true, + rowKey: 'key', + columns: columns, + dataSource: customerSummaryPayload.rows, + pagination: false, + scroll: { x: 'max-content', y: 'calc(100vh - 250px)' }, + sticky: true, + summary: function () { + return React.createElement(Table.Summary, { fixed: 'bottom' }, + React.createElement(Table.Summary.Row, { className: 'row-total' }, + React.createElement(Table.Summary.Cell, { index: 0, colSpan: 2, align: 'center' }, '合计'), + React.createElement(Table.Summary.Cell, { index: 1, align: 'center' }, + React.createElement('button', { + className: 'biz-standbook-perf-link', + onClick: function () { + openProjectSummary({ isAll: true, sourceRows: customerSummaryPayload.rows }); + } + }, customerCount) + ), + React.createElement(Table.Summary.Cell, { index: 2, align: 'right' }, fmtLedgerCell(sum1)), + React.createElement(Table.Summary.Cell, { index: 3, align: 'right' }, fmtLedgerCell(sum2)), + React.createElement(Table.Summary.Cell, { index: 4, align: 'right' }, fmtLedgerCell(sum3)) + ) + ); + } + }) + ) + ) + ); + }; + + var renderProjectSummaryView = function () { + var isCost = salesModal.cellType === 'Cost'; + var projectPageMainTitle = isCost ? '租赁业务客户各项目成本汇总' : '租赁业务客户各项目业绩汇总'; + var yearStr = yearApplied && yearApplied.format ? yearApplied.format('YYYY') : 'YYYY'; + var projStatTimeStr; + if (salesModal.month != null && salesModal.month <= 12) { + projStatTimeStr = yearStr + '-' + pad2(salesModal.month); + } else if (salesModal.month === 13) { + projStatTimeStr = yearStr + '(全年)'; + } else { + projStatTimeStr = '—'; + } + var projectCustomerScope = detailCustomer + ? (detailCustomer.isAll ? '全部客户' : (detailCustomer.customerName || '—')) + : '—'; + var projectSumSubline = projectCustomerScope + '\u00A0\u00A0\u00A0\u00A0统计时间:' + projStatTimeStr; + + var columns = [ + { + title: '客户名称', + dataIndex: 'customerName', + key: 'customerName', + align: 'center', + onCell: function (record) { return { rowSpan: record.custSpan }; } + }, + { + title: '项目名称', + dataIndex: 'projectName', + key: 'projectName', + align: 'center', + render: function (text, record) { + return React.createElement('button', { + className: 'biz-standbook-perf-link', + onClick: function () { return openProjectDetail(record); } + }, text); + } + } + ]; + + if (isCost) { + columns.push( + { title: '车辆成本', dataIndex: 'vehicleCost', key: 'vehicleCost', align: 'right', render: function (v) { return fmtLedgerCell(v); } }, + { + title: '居间费', + dataIndex: 'brokerageFee', + key: 'brokerageFee', + align: 'right', + render: function (v) { return fmtLedgerCell(v); } + }, + { title: '成本总计', dataIndex: 'totalCost', key: 'totalCost', align: 'right', render: function (v) { return fmtLedgerCell(v); } } + ); + } else { + columns.push( + { title: '应收', dataIndex: 'receivable', key: 'receivable', align: 'right', render: function (v) { return fmtLedgerCell(v); } }, + { title: '实收', dataIndex: 'received', key: 'received', align: 'right', render: function (v) { return fmtLedgerCell(v); } }, + { title: '未收', dataIndex: 'unreceived', key: 'unreceived', align: 'right', render: function (v) { return fmtLedgerCell(v); } } + ); + } + + var sum1 = 0, sum2 = 0, sum3 = 0; + var uniqueProjects = []; + projectSummaryPayload.forEach(function(r) { + var projKey = r.customerName + '|' + r.projectName; + if (uniqueProjects.indexOf(projKey) === -1) { + uniqueProjects.push(projKey); + } + if (isCost) { + sum1 += numOrZero(r.vehicleCost); + sum2 += numOrZero(r.brokerageFee); + sum3 += numOrZero(r.totalCost); + } else { + sum1 += numOrZero(r.receivable); + sum2 += numOrZero(r.received); + sum3 += numOrZero(r.unreceived); + } + }); + var projectCount = uniqueProjects.length; + + return React.createElement(Card, { style: tableCardStyle, bodyStyle: { padding: '16px 24px' } }, + React.createElement('div', { style: { position: 'relative', marginBottom: 16, minHeight: 52 } }, + React.createElement('div', { style: { position: 'absolute', left: 0, top: 0, zIndex: 1 } }, + React.createElement(Button, { onClick: function () { setView('customerSummary'); } }, '返回上一步') + ), + React.createElement('div', { style: { textAlign: 'center', padding: '0 128px' } }, + React.createElement('div', { style: { fontSize: 18, fontWeight: 700, color: 'rgba(15,23,42,0.92)', letterSpacing: '0.02em' } }, projectPageMainTitle), + React.createElement('div', { style: { marginTop: 6, fontSize: 13, color: 'rgba(15,23,42,0.55)', fontWeight: 500 } }, projectSumSubline) + ) + ), + React.createElement('div', { className: 'biz-standbook-table-wrap' }, + React.createElement(Table, { + className: 'biz-standbook-table', bordered: true, rowKey: 'key', columns: columns, dataSource: projectSummaryPayload, pagination: false, scroll: { x: 'max-content', y: 'calc(100vh - 250px)' }, sticky: true, + summary: function () { + return React.createElement(Table.Summary, { fixed: 'bottom' }, + React.createElement(Table.Summary.Row, { className: 'row-total' }, + React.createElement(Table.Summary.Cell, { index: 0, align: 'center' }, '合计'), + React.createElement(Table.Summary.Cell, { index: 1, align: 'center' }, + React.createElement('button', { + className: 'biz-standbook-perf-link', + onClick: function () { + openProjectDetail({ isAll: true, sourceRows: projectSummaryPayload }); + } + }, projectCount) + ), + React.createElement(Table.Summary.Cell, { index: 2, align: 'right' }, fmtLedgerCell(sum1)), + React.createElement(Table.Summary.Cell, { index: 3, align: 'right' }, fmtLedgerCell(sum2)), + React.createElement(Table.Summary.Cell, { index: 4, align: 'right' }, fmtLedgerCell(sum3)) + ) + ); + } + }) + ) + ); + }; + + var renderSalesView = function () { + var isCost = salesModal.cellType === 'Cost'; + var typeLabel = '业绩'; + if (isCost) typeLabel = '成本'; + else if (salesModal.cellType === 'Profit') typeLabel = '利润'; + var yearStr = yearApplied && yearApplied.format ? yearApplied.format('YYYY') : 'YYYY'; + var monthStr = salesModal.month === 13 ? '全年' : '-' + pad2(salesModal.month); + var titleText = '租赁业务' + typeLabel + '钻取明细(' + yearStr + monthStr + ')'; + if (detailProject) { + if (detailProject.isAll) { + titleText += ' - 全部项目'; + } else { + titleText += ' - ' + detailProject.customerName + ' - ' + detailProject.projectName; + } + } + 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(Select, { + placeholder: '请选择业务员', + style: filterControlStyle, + value: drillFilterDraft.salesperson, + onChange: function (v) { setDrillFilterDraft(Object.assign({}, drillFilterDraft, { salesperson: v })); }, + allowClear: true, + showSearch: true, + filterOption: filterOption, + options: drillPayload.options ? drillPayload.options.salespersons : [] + }) + ) + ), + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 6 }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '客户名称'), + React.createElement(Select, { + placeholder: '请选择客户名称', + style: filterControlStyle, + value: drillFilterDraft.customerName, + onChange: function (v) { setDrillFilterDraft(Object.assign({}, drillFilterDraft, { customerName: v })); }, + allowClear: true, + showSearch: true, + filterOption: filterOption, + options: drillPayload.options ? drillPayload.options.customers : [] + }) + ) + ), + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 6 }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '性质'), + React.createElement(Select, { + placeholder: '请选择性质', + style: filterControlStyle, + value: drillFilterDraft.nature, + onChange: function (v) { setDrillFilterDraft(Object.assign({}, drillFilterDraft, { nature: v })); }, + allowClear: true, + options: [ + { value: '租赁', label: '租赁' }, + { value: '试用', label: '试用' } + ] + }) + ) + ), + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 6 }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '车牌号'), + React.createElement(Select, { + placeholder: '请选择或输入车牌号', + style: filterControlStyle, + value: drillFilterDraft.plateNo, + onChange: function (v) { setDrillFilterDraft(Object.assign({}, drillFilterDraft, { plateNo: v })); }, + allowClear: true, + showSearch: true, + filterOption: filterOption, + options: drillPayload.options ? drillPayload.options.plateNos : [] + }) + ) + ), + React.createElement(Col, { xs: 24, sm: 12, md: 8, lg: 6 }, + React.createElement('div', { style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '付款日期'), + React.createElement(RangePicker, { + placeholder: ['开始日期', '结束日期'], + style: filterControlStyle, + value: drillFilterDraft.paymentDate, + onChange: function (v) { setDrillFilterDraft(Object.assign({}, drillFilterDraft, { paymentDate: 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: handleDrillReset }, '重置'), + React.createElement(Button, { type: 'primary', onClick: handleDrillQuery }, '查询') + ) + ) + ) + ) + ), + React.createElement(Card, { style: tableCardStyle, bodyStyle: { padding: '16px 24px' } }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', marginBottom: 20 } }, + React.createElement(Button, { + onClick: function () { setView('projectSummary'); }, + style: { marginRight: 16 } + }, '返回上一步'), + React.createElement('div', { style: { fontSize: 18, fontWeight: 700, color: '#0f172a' } }, titleText) + ), + React.createElement('div', { className: 'biz-standbook-table-wrap' }, + React.createElement(Table, { + className: 'biz-standbook-table', + size: 'small', + bordered: true, + rowKey: 'key', + columns: drillColumns, + dataSource: drillPayload.rows, + pagination: false, + scroll: { x: 'max-content', y: 'calc(100vh - 250px)' }, + sticky: true, + summary: function () { + var totals = drillPayload.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: 11, align: 'center' }, '合计'), + isCost ? React.createElement( + React.Fragment, + null, + React.createElement(Cell, { index: 1, align: 'right' }, fmtLedgerCell(totals.vehicleCost)), + React.createElement(Cell, { index: 2, align: 'right' }, fmtLedgerCell(totals.brokerageFee)), + React.createElement(Cell, { index: 3, align: 'right' }, fmtLedgerCell(totals.totalCost)) + ) : React.createElement( + React.Fragment, + null, + React.createElement(Cell, { index: 1, align: 'right' }, fmtLedgerCell(totals.receivable)), + React.createElement(Cell, { index: 2, align: 'right' }, fmtLedgerCell(totals.received)), + React.createElement(Cell, { index: 3, align: 'right' }, React.createElement('span', { style: { color: '#f53f3f' } }, fmtLedgerCell(totals.unreceived))), + React.createElement(Cell, { index: 4, align: 'right' }, fmtLedgerCell(totals.naturalMonthIncome)), + React.createElement(Cell, { index: 5, colSpan: 3 }) + ) + ) + ); + } + }) + ) + ) + ); + }; + + var renderSalesDetailView = function () { + var deptLabel = salesDetailModal.deptName === '全部' ? '全部业务部门' : salesDetailModal.deptName; + var personLabel = salesDetailModal.salesperson === '全部' ? '全部业务人员' : salesDetailModal.salesperson; + var titleText = '应收明细(' + salesDetailModal.ym + ' ' + deptLabel + ' ' + personLabel + ')'; + return React.createElement(Card, { style: tableCardStyle, bodyStyle: { padding: '16px 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', { className: 'biz-standbook-table-wrap' }, + React.createElement(Table, { + className: 'biz-standbook-table', + size: 'small', + bordered: true, + rowKey: 'key', + columns: detailColumns, + dataSource: detailPayload.rows, + pagination: false, + rowClassName: rowClassName, + scroll: { x: 'max-content', y: 'calc(100vh - 250px)' }, + summary: function () { + var totals = detailPayload.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: 13, align: 'center' }, '合计'), + React.createElement(Cell, { index: 13, align: 'right' }, fmtLedgerCell(totals.deposit)), + React.createElement(Cell, { index: 14, align: 'right' }, fmtLedgerCell(totals.vehicle)), + React.createElement(Cell, { index: 15, align: 'right' }, fmtLedgerCell(totals.additional)), + React.createElement(Cell, { index: 16, align: 'right' }, fmtLedgerCell(totals.total)) + ) + ); + } + }) + ) + ); + }; + + var renderContent = function () { + if (view === 'main') return renderMainView(); + if (view === 'hydrogenCustomerPerf') return renderHydrogenCustomerPerfView(); + if (view === 'hydrogenStationDrill') return renderHydrogenStationDrillView(); + if (view === 'hydrogenRefuelDetail') return renderHydrogenRefuelDetailView(); + if (view === 'customerSummary') return renderCustomerSummaryView(); + if (view === 'projectSummary') return renderProjectSummaryView(); + if (view === 'sales') return renderSalesView(); + if (view === 'sales_detail') return renderSalesDetailView(); + return null; + }; + + var bodyEl = renderContent(); + + return React.createElement(App, null, + React.createElement('style', null, ledgerTableStyle), + React.createElement('div', { style: layoutStyle }, + bodyEl + ) + ); +}; diff --git a/web端/数据分析/业务部汇总台账.jsx b/web端/数据分析/业务部汇总台账.jsx index c916513..c1c314b 100644 --- a/web端/数据分析/业务部汇总台账.jsx +++ b/web端/数据分析/业务部汇总台账.jsx @@ -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, diff --git a/web端/数据分析/客户服务部业务统计报表.jsx b/web端/数据分析/客户服务部业务统计报表.jsx new file mode 100644 index 0000000..51be715 --- /dev/null +++ b/web端/数据分析/客户服务部业务统计报表.jsx @@ -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 + }) + ) + ) + ); +}; diff --git a/web端/财务管理/文档/_build_还车应结款手册.py b/web端/财务管理/文档/_build_还车应结款手册.py new file mode 100644 index 0000000..5376b84 --- /dev/null +++ b/web端/财务管理/文档/_build_还车应结款手册.py @@ -0,0 +1,271 @@ +# -*- coding: utf-8 -*- +"""生成「还车应结款」用户操作说明 Word 文档及原型配图(脚本可重复运行)""" +import os +from pathlib import Path + +from docx import Document +from docx.shared import Pt, Inches, Cm +from docx.enum.text import WD_ALIGN_PARAGRAPH +from docx.oxml.ns import qn +from docx.oxml import OxmlElement + +try: + from PIL import Image, ImageDraw, ImageFont +except ImportError: + Image = None + + +ROOT = Path(__file__).resolve().parent +OUT_DOC = ROOT / "还车应结款-用户操作说明.docx" +LIST_SCREENSHOT = Path( + "/Users/sylvawong/.cursor/projects/Users-sylvawong-Desktop-CURSOR-ONE-OS/assets/" + "2230B49A-9740-471C-ADE4-FC7723DC4CA9_4_5005_c-70f13b01-98b2-42e3-a4f5-6824f02f1838.png" +) + + +def set_cell_shading(cell, fill_hex): + """Word 表格单元格背景色""" + shading = OxmlElement("w:shd") + shading.set(qn("w:fill"), fill_hex) + cell._tc.get_or_add_tcPr().append(shading) + + +def add_heading_cn(doc, text, level): + h = doc.add_heading(text, level=level) + for r in h.runs: + r.font.name = "PingFang SC" + r._element.rPr.rFonts.set(qn("w:eastAsia"), "PingFang SC") + return h + + +def add_para_cn(doc, text, bold=False): + p = doc.add_paragraph() + run = p.add_run(text) + run.font.size = Pt(11) + run.font.name = "PingFang SC" + run._element.rPr.rFonts.set(qn("w:eastAsia"), "PingFang SC") + run.bold = bold + return p + + +def make_prototype_figure(path: Path, title: str, lines: list): + """根据原型结构生成示意配图(非系统实拍,便于培训对照 JSX页面区块)""" + if Image is None: + return False + w, h = 900, 520 + img = Image.new("RGB", (w, h), (250, 250, 250)) + draw = ImageDraw.Draw(img) + draw.rectangle([0, 0, w - 1, h - 1], outline=(200, 200, 200), width=2) + try: + font_title = ImageFont.truetype("/System/Library/Fonts/PingFang.ttc", 22, index=1) + font_body = ImageFont.truetype("/System/Library/Fonts/PingFang.ttc", 17, index=1) + font_small = ImageFont.truetype("/System/Library/Fonts/PingFang.ttc", 14, index=1) + except OSError: + font_title = font_body = font_small = ImageFont.load_default() + + y = 20 + draw.text((24, y), title, fill=(17, 24, 39), font=font_title) + y += 44 + draw.line([(24, y), (w - 24, y)], fill=(217, 217, 217), width=1) + y += 16 + for line in lines: + draw.text((32, y), line, fill=(51, 51, 51), font=font_body) + y += 28 + note = "说明:本图为原型页面结构示意,正式文档可替换为系统实拍截图。" + draw.text((24, h - 36), note, fill=(120, 120, 120), font=font_small) + img.save(path, "PNG") + return True + + +def build(): + ROOT.mkdir(parents=True, exist_ok=True) + fig_view = ROOT / "配图-还车应结款-查看页-原型结构.png" + fig_fee = ROOT / "配图-还车应结款-费用明细-原型结构.png" + + make_prototype_figure( + fig_view, + "还车应结款 · 查看页(原型 还车应结款-查看.jsx)", + [ + "面包屑:财务管理 / 还车应结款 / 查看", + "卡片① 还车车辆明细表(车牌、合同、项目、客户、交还车时间)", + " · 易损保 / 轮胎保 / 养护保(是/否 + 提示图标说明)", + "卡片② 还车费用明细", + " · 统计:保证金、待结算、应退还、应补缴(可点开分项)", + " · 业务服务组 / 能源组 / 运维部 / 安全组(只读展示)", + "底部:返回列表", + ], + ) + make_prototype_figure( + fig_fee, + "还车应结款 · 费用明细页(原型 还车应结款-费用明细.jsx)", + [ + "面包屑:财务管理 / 还车应结款(含「查看需求说明」)", + "卡片 还车车辆明细(同上)", + "卡片 还车费用明细:顶部四统计 + 可折叠四组", + " · 业务服务组:固定费用行 + 可增删行;车辆租金三块", + " · 能源采购组:氢量差/交还车氢量/单价/氢电费/预付款退费", + " · 运维部:清洗保养维修等;轮胎磨损说明气泡;无忧包减免", + " · 安全组:违章清单 + 事故清单;保存/提交/撤回", + "底栏:提交审核(15天倒计时+四组已提交) / 取消", + ], + ) + + doc = Document() + sect = doc.sections[0] + sect.page_height = Cm(29.7) + sect.page_width = Cm(21.0) + sect.left_margin = Cm(2.2) + sect.right_margin = Cm(2.2) + + title = doc.add_paragraph() + title.alignment = WD_ALIGN_PARAGRAPH.CENTER + r = title.add_run("数字化资产 ONEOS 运管平台\n还车应结款 · 用户操作说明(培训版)") + r.bold = True + r.font.size = Pt(18) + r.font.name = "PingFang SC" + r._element.rPr.rFonts.set(qn("w:eastAsia"), "PingFang SC") + + add_para_cn( + doc, + "适用范围:财务管理模块下的「还车应结款」列表、只读查看页、费用明细协作页。" + "配图说明:图1 为同类财务列表界面实拍(布局与还车应结款列表一致);" + "图 2、图 3 为依据原型 JSX 页面结构整理的示意配图,可在定稿前替换为系统截图。", + ) + + add_heading_cn(doc, "一、模块作用与入口", 1) + add_para_cn( + doc, + "用于车辆还车后汇总安全、业务服务、运维、能源等数据,形成待结算金额并进入审批/账单流程。" + "入口:左侧菜单 财务管理 → 还车应结款。", + ) + + add_heading_cn(doc, "二、列表页:筛选与查询", 1) + add_para_cn(doc, "筛选区支持:合同编号、客户名称、项目名称(可展开后:车牌号、还车时间范围、审批状态)。按钮:重置、查询。") + tbl = doc.add_table(rows=8, cols=2) + tbl.style = "Table Grid" + hdr = ["条件", "说明"] + for j, t in enumerate(hdr): + tbl.rows[0].cells[j].text = t + set_cell_shading(tbl.rows[0].cells[j], "E6F4EA") + rows_data = [ + ("合同编号", "下拉 + 输入模糊匹配"), + ("客户名称", "同上"), + ("项目名称", "同上"), + ("车牌号", "展开后,同上"), + ("还车时间", "起止日期,精确到日"), + ("审批状态", "待提交/待审批/审批中/审批完成/审批驳回/撤回"), + ("导出", "列表卡片右上角导出当前结果"), + ] + for i, (a, b) in enumerate(rows_data, start=1): + tbl.rows[i].cells[0].text = a + tbl.rows[i].cells[1].text = b + + add_heading_cn(doc, "三、列表页:表格字段与审批状态", 1) + add_para_cn(doc, "提交情况四列:安全组、业务服务组、运维组、能源组。绿点=已提交,灰点=未提交,后接提交人姓名。") + add_para_cn(doc, "审批状态含义:待提交=四组未齐且未提审批;待审批=已提审无人处理;审批中=有节点已通过未完;审批完成=流程结束;审批驳回=任一节点的驳回;撤回=终审前主动撤回。") + + add_heading_cn(doc, "四、列表页:操作按钮", 1) + ops = [ + "查看:进入只读「查看」页。", + "生成账单:仅「待审批」显示;弹窗账单并可打印(需允许浏览器弹窗)。", + "费用明细:待审批/审批中/审批完成 时隐藏;其余可编辑阶段进入费用明细。", + "撤回:仅待审批、审批中显示;确认后状态改为撤回。", + ] + for o in ops: + p = doc.add_paragraph(style="List Bullet") + r = p.add_run(o) + r.font.name = "PingFang SC" + r._element.rPr.rFonts.set(qn("w:eastAsia"), "PingFang SC") + r.font.size = Pt(11) + + add_heading_cn(doc, "五、配图", 1) + + p_cap1 = doc.add_paragraph() + p_cap1.alignment = WD_ALIGN_PARAGRAPH.CENTER + r1 = p_cap1.add_run( + "图 1 财务管理 · 列表页布局参考(实拍;与「还车应结款」列表同一套:侧栏、筛选、表格、分页)" + ) + r1.font.size = Pt(10) + r1.font.name = "PingFang SC" + r1._element.rPr.rFonts.set(qn("w:eastAsia"), "PingFang SC") + r1.italic = True + + if LIST_SCREENSHOT.is_file(): + doc.add_picture(str(LIST_SCREENSHOT), width=Inches(6.2)) + doc.paragraphs[-1].alignment = WD_ALIGN_PARAGRAPH.CENTER + else: + add_para_cn(doc, "(未找到图1源文件,请将财务列表截图置于文档此处)", bold=True) + + p_cap2 = doc.add_paragraph() + p_cap2.alignment = WD_ALIGN_PARAGRAPH.CENTER + r2 = p_cap2.add_run("图 2 还车应结款 · 查看页(原型结构示意)") + r2.font.size = Pt(10) + r2.font.name = "PingFang SC" + r2._element.rPr.rFonts.set(qn("w:eastAsia"), "PingFang SC") + r2.italic = True + if fig_view.is_file(): + doc.add_picture(str(fig_view), width=Inches(6.2)) + doc.paragraphs[-1].alignment = WD_ALIGN_PARAGRAPH.CENTER + + p_cap3 = doc.add_paragraph() + p_cap3.alignment = WD_ALIGN_PARAGRAPH.CENTER + r3 = p_cap3.add_run("图 3 还车应结款 · 费用明细页(原型结构示意)") + r3.font.size = Pt(10) + r3.font.name = "PingFang SC" + r3._element.rPr.rFonts.set(qn("w:eastAsia"), "PingFang SC") + r3.italic = True + if fig_fee.is_file(): + doc.add_picture(str(fig_fee), width=Inches(6.2)) + doc.paragraphs[-1].alignment = WD_ALIGN_PARAGRAPH.CENTER + + add_heading_cn(doc, "六、查看页(只读)要点", 1) + add_para_cn( + doc, + "车辆明细含三项保险及提示文案。费用区四张统计卡可点击展开分项。" + "业务服务组、能源组、运维部、安全组为只读表格;轮胎磨损可悬停查看逐胎明细。底部返回列表。", + ) + + add_heading_cn(doc, "七、费用明细页:各组分工", 1) + add_para_cn( + doc, + "业务服务组:5 项固定费用 + 可新增行;车辆租金(已收/实际/应退,实际租金默认按日折算可手改)。保存/提交/撤回;已提交后锁定至撤回。", + ) + add_para_cn( + doc, + "能源采购组:交还车氢量、单价只读;交车氢量大于还车时氢量差补缴自动算;氢费/电费/预付款退费可填;最后一辆车红色提示。", + ) + add_para_cn( + doc, + "运维部:清洗、保养、维修、车损等;证件丢失按规则自动计价;送/接车服务费禁用反写;轮胎磨损与胎纹合计规则见需求说明;无忧包减免与三项保险挂钩。", + ) + add_para_cn( + doc, + "安全组:违章清单、事故清单;确认后提交。", + ) + + add_heading_cn(doc, "八、金额计算(口径)", 1) + add_para_cn( + doc, + "待结算总额 = 业务服务组费用合计 + 车辆应退租金 + 氢量差补缴 + 氢费补缴 + 电费补缴 − 预付款退费 + 运维部费用合计。", + ) + add_para_cn( + doc, + "应退还总额:保证金 − 待结算 为正时取该值。应补缴总额:保证金 − 待结算 为负时取绝对值。", + ) + + add_heading_cn(doc, "九、提交审核条件", 1) + add_para_cn( + doc, + "原型规则:单据生成后满 15 天倒计时结束,且业务服务组、能源采购组、运维部、安全组均为「已提交」," + "底部「提交审核」才可点;否则按钮禁用并显示剩余天/小时。提交前系统校验各必填金额与无忧包减免等。", + ) + + add_heading_cn(doc, "十、修订记录", 1) + add_para_cn(doc, "文档版本:V1.0 生成依据:web端/财务管理/还车应结款.jsx、还车应结款-查看.jsx、还车应结款-费用明细.jsx 内嵌需求说明。") + + doc.save(OUT_DOC) + print("Wrote:", OUT_DOC) + + +if __name__ == "__main__": + build() diff --git a/web端/财务管理/文档/还车应结款-用户操作说明.docx b/web端/财务管理/文档/还车应结款-用户操作说明.docx new file mode 100644 index 0000000..f582e35 Binary files /dev/null and b/web端/财务管理/文档/还车应结款-用户操作说明.docx differ diff --git a/web端/财务管理/文档/配图-还车应结款-查看页-原型结构.png b/web端/财务管理/文档/配图-还车应结款-查看页-原型结构.png new file mode 100644 index 0000000..91e02a9 Binary files /dev/null and b/web端/财务管理/文档/配图-还车应结款-查看页-原型结构.png differ diff --git a/web端/财务管理/文档/配图-还车应结款-费用明细-原型结构.png b/web端/财务管理/文档/配图-还车应结款-费用明细-原型结构.png new file mode 100644 index 0000000..c17b805 Binary files /dev/null and b/web端/财务管理/文档/配图-还车应结款-费用明细-原型结构.png differ diff --git a/web端/车辆租赁合同/车辆租赁合同-新增.jsx b/web端/车辆租赁合同/车辆租赁合同-新增.jsx index 5493da6..ff477a2 100644 --- a/web端/车辆租赁合同/车辆租赁合同-新增.jsx +++ b/web端/车辆租赁合同/车辆租赁合同-新增.jsx @@ -113,6 +113,12 @@ const Component = function() { var os1j = React.useState(''); var serviceItemSearch = os1j[0]; var setServiceItemSearch = os1j[1]; + var copyPopState = React.useState(null); + var copyPopover = copyPopState[0]; + var setCopyPopover = copyPopState[1]; + var copyQtyState = React.useState('1'); + var copyQuantity = copyQtyState[0]; + var setCopyQuantity = copyQtyState[1]; var os1k = React.useState(null); var plateNoDropdownRect = os1k[0]; var setPlateNoDropdownRect = os1k[1]; @@ -296,16 +302,57 @@ const Component = function() { next.splice(index, 1); setRentalOrders(next.length ? next : [Object.assign({}, emptyRentalRow)]); }; - var copyRentalRow = function(index) { - var row = rentalOrders[index]; - if (!row) return; - var serviceItemsCopy = (row.serviceItems || []).map(function(si) { return { project: si.project || '', fee: si.fee || '', effectiveDate: si.effectiveDate || '' }; }); + var buildDuplicateRentalRow = function(sourceRow) { + var serviceItemsCopy = (sourceRow.serviceItems || []).map(function(si) { return { project: si.project || '', fee: si.fee || '', effectiveDate: si.effectiveDate || '' }; }); if (serviceItemsCopy.length === 0) serviceItemsCopy = [{ project: '', fee: '', effectiveDate: '' }]; - var newRow = { id: nextRentalRowIdRef.current++, brand: row.brand || '', model: row.model || '', plateNo: '', vin: '', monthRent: row.monthRent || '', serviceItems: serviceItemsCopy, deposit: row.deposit || '', remark: row.remark || '' }; + return { id: nextRentalRowIdRef.current++, brand: sourceRow.brand || '', model: sourceRow.model || '', plateNo: '', vin: '', monthRent: sourceRow.monthRent || '', serviceItems: serviceItemsCopy, deposit: sourceRow.deposit || '', remark: sourceRow.remark || '' }; + }; + var applyRentalRowCopies = function(rowIndex, count) { + var row = rentalOrders[rowIndex]; + if (!row) return; + var n = typeof count === 'number' ? count : parseInt(String(count), 10); + if (isNaN(n) || n < 0) { + if (message.warning) message.warning('请输入非负整数'); + return; + } + n = Math.floor(n); + if (n === 0) { + setCopyPopover(null); + if (message.info) message.info('复制数量为 0,未新增行'); + return; + } var next = rentalOrders.slice(0); - next.splice(index + 1, 0, newRow); + var insertAt = rowIndex + 1; + for (var i = 0; i < n; i++) { + next.splice(insertAt + i, 0, buildDuplicateRentalRow(row)); + } setRentalOrders(next); - if (typeof message !== 'undefined' && message.success) message.success('已复制该行(车牌号已清空)'); + setCopyPopover(null); + if (message.success) message.success('已复制 ' + n + ' 条记录(车牌号已清空)'); + }; + var confirmRentalCopyPopover = function() { + if (!copyPopover) return; + if (copyQuantity === '') { + if (message.warning) message.warning('请输入复制数量'); + return; + } + if (!/^\d+$/.test(copyQuantity)) { + if (message.warning) message.warning('请输入非负整数'); + return; + } + applyRentalRowCopies(copyPopover.index, parseInt(copyQuantity, 10)); + }; + var openRentalCopyPopover = function(rowIndex, anchorEl) { + if (!anchorEl || !anchorEl.getBoundingClientRect) return; + var r = anchorEl.getBoundingClientRect(); + var cardH = 168; + var top = r.bottom + 8; + if (top + cardH > window.innerHeight - 8) top = Math.max(8, r.top - cardH - 8); + var left = r.left; + var cardW = 240; + if (left + cardW > window.innerWidth - 8) left = Math.max(8, window.innerWidth - cardW - 8); + setCopyPopover({ index: rowIndex, top: top, left: left }); + setCopyQuantity('1'); }; var updateRentalOrder = function(index, field, value) { var next = rentalOrders.slice(0); @@ -384,6 +431,22 @@ const Component = function() { return function() { document.removeEventListener('mousedown', handler); }; }, [deliveryRegionOpen]); + React.useEffect(function() { + if (!copyPopover) return; + var handler = function(e) { + var pop = document.getElementById('rental-copy-popover'); + if (pop && pop.contains(e.target)) return; + setCopyPopover(null); + }; + var t = window.setTimeout(function() { + document.addEventListener('mousedown', handler); + }, 0); + return function() { + window.clearTimeout(t); + document.removeEventListener('mousedown', handler); + }; + }, [copyPopover]); + React.useEffect(function() { if (plateNoFocusRow === null) { setPlateNoDropdownRect(null); return; } var timer = setTimeout(function() { @@ -451,7 +514,10 @@ const Component = function() { btnGroupItemActive: { backgroundColor: '#1677ff', color: '#fff', borderColor: '#1677ff', borderRightColor: '#1677ff' }, feeSectionTitle: { fontSize: 16, fontWeight: 600, color: '#333', marginTop: 20, marginBottom: 10 }, feeSectionTitleFirst: { marginTop: 0 }, - modalFormInput: { width: '100%', padding: '8px 12px', border: '1px solid #d9d9d9', borderRadius: 4, fontSize: 14, height: 36, boxSizing: 'border-box' } + modalFormInput: { width: '100%', padding: '8px 12px', border: '1px solid #d9d9d9', borderRadius: 4, fontSize: 14, height: 36, boxSizing: 'border-box' }, + copyPopoverCard: { position: 'fixed', zIndex: 2000, backgroundColor: '#fff', borderRadius: 8, boxShadow: '0 4px 20px rgba(0,0,0,0.12), 0 0 1px rgba(0,0,0,0.08)', border: '1px solid #f0f0f0', padding: '14px 16px', minWidth: 232, maxWidth: 'calc(100vw - 24px)' }, + copyPopoverTitle: { fontSize: 14, fontWeight: 600, color: 'rgba(0,0,0,0.88)', marginBottom: 12 }, + copyPopoverFooter: { display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 14 } }; var CardBlock = function(props) { @@ -582,7 +648,7 @@ const Component = function() { React.createElement('td', { style: styles.rentalTdCenter }, calcRowServiceFee(row) + ' 元'), React.createElement('td', { style: styles.rentalTd }, React.createElement('div', { style: { display: 'flex', alignItems: 'center' } }, React.createElement(Input, { placeholder: '0.00', value: row.deposit || '', onChange: function(e) { updateRentalOrder(idx, 'deposit', e.target.value); }, style: styles.rentalInput }), React.createElement('span', { style: { marginLeft: 4, whiteSpace: 'nowrap' } }, '元'))), React.createElement('td', { style: styles.rentalTd }, React.createElement(Input, { placeholder: '备注', value: row.remark || '', onChange: function(e) { updateRentalOrder(idx, 'remark', e.target.value); }, style: Object.assign({}, styles.rentalInput, { width: '100%' }) })), - React.createElement('td', { style: styles.rentalTdCenter }, React.createElement('div', { style: { display: 'inline-flex', gap: 4, alignItems: 'center' } }, React.createElement(Button, { type: 'link', size: 'small', onClick: function() { copyRentalRow(idx); } }, '复制'), React.createElement(Button, { type: 'link', size: 'small', danger: true, onClick: function() { removeRentalRow(idx); } }, '删除'))) + React.createElement('td', { style: styles.rentalTdCenter }, React.createElement('div', { style: { display: 'inline-flex', gap: 4, alignItems: 'center' } }, React.createElement(Button, { type: 'link', size: 'small', 'aria-haspopup': 'dialog', 'aria-expanded': copyPopover && copyPopover.index === idx, onClick: function(e) { e.preventDefault(); e.stopPropagation(); openRentalCopyPopover(idx, e.currentTarget); } }, '复制'), React.createElement(Button, { type: 'link', size: 'small', danger: true, onClick: function() { removeRentalRow(idx); } }, '删除'))) ); }); @@ -609,6 +675,41 @@ const Component = function() { var rentalTableTbody = React.createElement('tbody', null, rentalTableBody); var rentalTableEl = React.createElement('table', { style: styles.rentalTable }, rentalTableThead, rentalTableTbody); var rentalTableWrap = React.createElement('div', { style: { overflowX: 'auto', marginBottom: 16 } }, rentalTableEl); + var rentalCopyPopoverEl = copyPopover + ? React.createElement('div', { + id: 'rental-copy-popover', + role: 'dialog', + 'aria-modal': 'false', + 'aria-label': '复制租赁订单行', + style: Object.assign({}, styles.copyPopoverCard, { top: copyPopover.top, left: copyPopover.left }), + onMouseDown: function(e) { e.stopPropagation(); } + }, + React.createElement('div', { style: styles.copyPopoverTitle }, '复制本行'), + React.createElement('div', null, + React.createElement('label', { style: { display: 'block', fontSize: 13, color: 'rgba(0,0,0,0.65)', marginBottom: 6 } }, '复制数量'), + React.createElement(Input, { + value: copyQuantity, + placeholder: '非负整数,如 3', + inputMode: 'numeric', + pattern: '[0-9]*', + autoComplete: 'off', + 'aria-label': '复制数量,非负整数', + onChange: function(e) { + var raw = (e.target && e.target.value) || ''; + var digits = raw.replace(/\D/g, ''); + setCopyQuantity(digits); + }, + onPressEnter: confirmRentalCopyPopover, + style: { width: '100%' } + }) + ), + React.createElement('div', { style: styles.copyPopoverFooter }, + React.createElement(Button, { size: 'small', onClick: function() { setCopyPopover(null); } }, '取消'), + React.createElement(Button, { type: 'primary', size: 'small', onClick: confirmRentalCopyPopover }, '确认') + ) + ) + : null; + var rentalContent = React.createElement('div', null, rentalSummary, formErrors.rentalOrders ? React.createElement('div', { style: styles.errMsg }, formErrors.rentalOrders) : null, rentalTableWrap, React.createElement(Button, { type: 'dashed', style: { marginTop: 12, width: '100%' }, onClick: addRentalRow }, '添加一行'), hydrogenFormRow); var feeTableHeader3 = React.createElement('tr', null, React.createElement('th', { style: styles.rentalTh }, '项目'), React.createElement('th', { style: styles.rentalTh }, '收费标准'), React.createElement('th', { style: styles.rentalTh }, '服务费')); @@ -692,6 +793,7 @@ const Component = function() { React.createElement(CardBlock, { id: 'card-fee', cardStyle: { marginTop: 16 }, title: '其他费用信息', collapsed: cc5, setCollapsed: setCc5 }, feeContent), React.createElement(CardBlock, { id: 'card-billing', cardStyle: { marginTop: 16 }, title: '账单计算方式', collapsed: cc6, setCollapsed: setCc6 }, billingContent), React.createElement('div', { style: { height: 60 } }), + rentalCopyPopoverEl, serviceModalContent, reqSpecModalContent, React.createElement('div', { style: styles.footer }, React.createElement(Button, { type: 'primary', size: 'large', onClick: function() { if (validateSubmitAndReview()) { message.success('租赁合同已提交审核。'); } } }, '提交并审核'), React.createElement(Button, { size: 'large', onClick: function() { message.info('保存,加入租赁合同列表(仅操作人可查看编辑)'); } }, '保存'), React.createElement(Button, { size: 'large', onClick: function() { message.info('取消'); } }, '取消')) diff --git a/web端/车辆租赁合同/车辆租赁合同.jsx b/web端/车辆租赁合同/车辆租赁合同.jsx index 962b681..9ca812e 100644 --- a/web端/车辆租赁合同/车辆租赁合同.jsx +++ b/web端/车辆租赁合同/车辆租赁合同.jsx @@ -15,6 +15,8 @@ const Component = function() { var Card = antd.Card; var DatePicker = antd.DatePicker; var Popover = antd.Popover; + var Tag = antd.Tag; + var Avatar = antd.Avatar; var Dropdown = antd.Dropdown; var Modal = antd.Modal; var Upload = antd.Upload; @@ -258,7 +260,12 @@ const Component = function() { updater: '李专员', updateTime: '2025-02-15 16:00', remark: '-', - legalStampedContractUploaded: undefined + legalStampedContractUploaded: undefined, + approvalFlowNodes: [ + { nodeTitle: '业务服务主管审批', result: 'passed', operatorName: '姚守涛', operatorTime: '2026-04-29 17:57:15' }, + { nodeTitle: '发起审批', result: 'passed', operatorName: '超级用户', operatorTime: '2026-04-28 17:44:45' }, + { nodeTitle: '业务负责人审批', result: 'pending', pendingApprovers: ['超级用户', '金可鹏'] } + ] }, // 5. 审批中 + 变更(已通过审批后做了变更并重新提交) { @@ -285,7 +292,12 @@ const Component = function() { updater: '李专员', updateTime: '2025-02-22 14:00', remark: '变更车辆数量', - legalStampedContractUploaded: undefined + legalStampedContractUploaded: undefined, + approvalFlowNodes: [ + { nodeTitle: '法务审核', result: 'passed', operatorName: '李法务', operatorTime: '2025-02-21 18:00:00' }, + { nodeTitle: '发起审批', result: 'passed', operatorName: '李专员', operatorTime: '2025-02-22 10:00:00' }, + { nodeTitle: '业务负责人审批', result: 'pending', pendingApprovers: ['张经理', '赵总监'] } + ] }, // 6. 审批通过 + 合同进行中(正式合同) { @@ -664,6 +676,131 @@ const Component = function() { { title: '交车人', dataIndex: 'deliveryPerson', key: 'deliveryPerson', width: 100 } ]; + function approvalAvatarText(name) { + if (!name) return '?'; + var s = String(name); + return s.length <= 2 ? s : s.slice(-2); + } + + function renderApprovalFlowContent(nodes) { + var list = nodes && nodes.length ? nodes : []; + if (!list.length) { + return React.createElement('div', { style: { color: 'rgba(0,0,0,0.45)', fontSize: 13, padding: '4px 0' } }, '暂无审批节点明细'); + } + return React.createElement( + 'div', + { style: { maxWidth: 360, paddingTop: 4 } }, + list.map(function(node, index) { + var isLast = index === list.length - 1; + var isPending = node.result === 'pending'; + var dot = React.createElement( + Avatar, + { + size: 28, + style: { + backgroundColor: isPending ? '#1890ff' : '#f0f0f0', + color: isPending ? '#fff' : 'rgba(0,0,0,0.45)', + fontSize: 11, + lineHeight: '28px', + flexShrink: 0 + }, + children: isPending + ? React.createElement( + 'svg', + { viewBox: '0 0 24 24', width: 14, height: 14, fill: 'currentColor', style: { verticalAlign: 'middle' } }, + React.createElement('path', { + d: 'M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4zm5-9h2v2h-2V5zm0 4h2v2h-2V9z' + }) + ) + : approvalAvatarText(node.operatorName) + } + ); + var body = React.createElement( + 'div', + { style: { flex: 1, paddingLeft: 12, paddingBottom: isLast ? 0 : 14, minWidth: 0 } }, + React.createElement( + 'div', + { style: { display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: 8, marginBottom: 6 } }, + React.createElement('span', { style: { fontSize: 14, color: 'rgba(0,0,0,0.88)', fontWeight: 500 } }, node.nodeTitle || '-'), + React.createElement(Tag, { color: isPending ? 'processing' : 'success', style: { margin: 0 } }, isPending ? '待审核' : '通过') + ), + isPending && node.pendingApprovers && node.pendingApprovers.length + ? React.createElement( + Space, + { size: [8, 8], wrap: true }, + node.pendingApprovers.map(function(apName, i) { + return React.createElement( + Tag, + { + key: i, + style: { + margin: 0, + color: '#1890ff', + background: '#e6f7ff', + borderColor: '#91d5ff' + } + }, + React.createElement( + 'span', + null, + React.createElement( + 'svg', + { + viewBox: '0 0 24 24', + width: 12, + height: 12, + fill: '#1890ff', + style: { marginRight: 4, verticalAlign: '-2px' } + }, + React.createElement('path', { + d: 'M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z' + }) + ), + apName + ) + ); + }) + ) + : React.createElement( + 'div', + { style: { fontSize: 13, color: 'rgba(0,0,0,0.45)' } }, + (node.operatorName || '') + (node.operatorTime ? ' ' + node.operatorTime : '') + ) + ); + return React.createElement( + 'div', + { key: index, style: { display: 'flex', alignItems: 'stretch' } }, + React.createElement( + 'div', + { + style: { + width: 36, + flexShrink: 0, + display: 'flex', + flexDirection: 'column', + alignItems: 'center' + } + }, + React.createElement('div', { style: { lineHeight: 0 } }, dot), + isLast + ? null + : React.createElement('div', { + style: { + flex: 1, + width: 2, + minHeight: 12, + marginTop: 4, + background: '#f0f0f0', + borderRadius: 1 + } + }) + ), + body + ); + }) + ); + } + function getMoreMenuItems(record) { var status = record.contractStatus; var type = record.contractType; @@ -818,7 +955,37 @@ const Component = function() { ); } }, - { title: '审批状态', dataIndex: 'approvalStatus', key: 'approvalStatus', width: 100 }, + { + title: '审批状态', + dataIndex: 'approvalStatus', + key: 'approvalStatus', + width: 100, + render: function(text, record) { + if (text !== '审批中') return text; + var nodes = record.approvalFlowNodes; + return React.createElement( + Popover, + { + content: renderApprovalFlowContent(nodes), + trigger: 'hover', + placement: 'rightTop', + overlayInnerStyle: { maxWidth: 400 }, + mouseEnterDelay: 0.15 + }, + React.createElement( + 'span', + { + style: { + cursor: 'help', + borderBottom: '1px dashed rgba(0,0,0,0.35)', + color: 'rgba(0,0,0,0.88)' + } + }, + text + ) + ); + } + }, { title: '合同状态', dataIndex: 'contractStatus', key: 'contractStatus', width: 110 }, { title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 140 }, { title: '签约公司', dataIndex: 'signingCompany', key: 'signingCompany', width: 100 }, diff --git a/web端/车辆管理-查看.jsx b/web端/车辆管理-查看.jsx index 893fde3..11195d3 100644 --- a/web端/车辆管理-查看.jsx +++ b/web端/车辆管理-查看.jsx @@ -10,6 +10,8 @@ const Component = function () { var Tabs = antd.Tabs; var Table = antd.Table; var Tag = antd.Tag; + var Badge = antd.Badge; + var Alert = antd.Alert; var Button = antd.Button; var Tooltip = antd.Tooltip; var Select = antd.Select; @@ -75,31 +77,59 @@ const Component = function () { stackVendor: 'XXXXXXXX企业' }; - // 证照管理 Tab:证件照片占位(可替换为真实 URL,支持 Image 预览) - var licensePhotoPlaceholder = 'data:image/svg+xml,' + encodeURIComponent('行驶证/证照示意图'); - function certPhotoPlaceholder(captionText) { - return 'data:image/svg+xml,' + encodeURIComponent('' + String(captionText || '') + ''); - } - /** 证照管理 Tab mock:字段与录入页设计稿一致(本页仅查看) */ - var certificateDetail = { - drivingLicense: { img: licensePhotoPlaceholder, regDate: '2025-07-01', scrapDate: '2038-12-31', inspectExpire: '2027-07-31' }, - operationPermit: { img: licensePhotoPlaceholder, permitNo: '营330482001234', regDate: '2025-07-15', inspectExpire: '2026-07-14' }, - passPermit: { img: licensePhotoPlaceholder, code: 'TX-ZJ-2025-0088', area: '浙江省嘉兴市平湖市行政辖区' }, - registrationCert: { img: licensePhotoPlaceholder }, - h2Permit: { - photos: [ - { label: '特种设备使用登记证', src: certPhotoPlaceholder('特种设备使用登记证') }, - { label: '特种设备使用标志', src: certPhotoPlaceholder('特种设备使用标志') } - ], - cylinderVendor: '某某高压气瓶制造有限公司', - cylinderInspectDate: '2025-04-01', - cylinderCycleMonth: '36', - cylinderValidUntil: '2028-03-31' + /** 证照管理 Tab:八类证照只读展示(字段口径对齐「证照管理-编辑」) */ + var LICENSE_VIEW_ANCHOR_DATE = '2026-06-01'; + var licensePhotoPlaceholder = 'data:image/svg+xml,' + encodeURIComponent('暂无影像'); + var licenseViewBundle = { + driverLicense: { + photos: ['https://picsum.photos/seed/vd-zjf-lic/600/400', 'https://picsum.photos/seed/vd-zjf-lic2/600/400'], + regDate: '2025-07-01', + issueDate: '2025-07-01', + scrapDate: '2038-12-31', + expireDate: '2027-07-31', + shNextEvaluation: '' }, - h2Card: { img: licensePhotoPlaceholder, stationName: '嘉兴港区某某加氢站' }, - safetyValve: { img: licensePhotoPlaceholder, inspectDate: '2025-05-10', cycleMonth: '12', validUntil: '2026-05-09' }, - pressureGauge: { img: licensePhotoPlaceholder, inspectDate: '2025-05-10', cycleMonth: '12', valveInspectValidUntil: '2026-05-09' } + transportLicense: { + photos: ['https://picsum.photos/seed/vd-zjf-trans/600/400'], + licenseNo: '浙字330482001234号', + issueDate: '2025-07-15', + expireDate: '2026-07-14', + inspectValidUntil: '2026-07-20' + }, + registrationCert: { photos: ['https://picsum.photos/seed/vd-zjf-reg/600/400'] }, + specialEquipCert: { photos: ['https://picsum.photos/seed/vd-zjf-spec/600/400'] }, + specialEquipDecal: { + photos: ['https://picsum.photos/seed/vd-zjf-decal/600/400'], + nextInspectDate: '2026-07-20' + }, + hydrogenCard: { + cardNo: 'H2-3304-0690-0088', + cardType: '中石化加氢卡', + balance: 8650.5, + issueDate: '2025-03-10 10:15', + issueUser: '能源管理部-张晓' + }, + safetyValve: { + photos: ['https://picsum.photos/seed/vd-zjf-valve/600/400'], + inspectDate: '2025-05-10', + nextInspectDate: '2026-05-09' + }, + pressureGauge: { + photos: ['https://picsum.photos/seed/vd-zjf-gauge/600/400'], + inspectDate: '2025-12-15', + nextInspectDate: '2026-06-14' + } }; + var CERT_NAV_ITEMS = [ + { key: 'driverLicense', label: '行驶证' }, + { key: 'transportLicense', label: '道路运输证' }, + { key: 'registrationCert', label: '登记证' }, + { key: 'specialEquipCert', label: '特种设备使用登记证' }, + { key: 'specialEquipDecal', label: '特种设备使用标识' }, + { key: 'hydrogenCard', label: '加氢卡' }, + { key: 'safetyValve', label: '安全阀' }, + { key: 'pressureGauge', label: '压力表' } + ]; var maintenanceList = [ { key: '1', item: '变速器油', kmCycle: '60000', monthCycle: '24', laborCost: '0', materialCost: '571', total: '571', lastKm: '' }, @@ -110,6 +140,7 @@ const Component = function () { ]; var activeTab = useState('model'); + var licenseActiveNav = useState('driverLicense'); var rearFilterDraft = useState({ installRange: null, deviceType: undefined }); var rearFilterApplied = useState({ installRange: null, deviceType: undefined }); var leaseFilterDraft = useState({ contractCode: undefined, projectName: undefined, customerName: undefined }); @@ -539,122 +570,334 @@ const Component = function () { ); } - function CertFieldRow(label, value) { - var show = value != null && String(value).trim() !== ''; + function LicViewFieldRow(label, value) { + var show = value != null && String(value).trim() !== '' && String(value).trim() !== '—'; return React.createElement('div', { style: { display: 'flex', gap: 8, marginBottom: 10, fontSize: 14, lineHeight: '22px' } }, React.createElement('span', { style: { color: 'rgba(0,0,0,0.45)', width: 168, flexShrink: 0 } }, label + ':'), React.createElement('span', { style: { color: 'rgba(0,0,0,0.85)' } }, show ? value : '—') ); } - /** photoCaption:左侧附图说明,与设计稿上「行驶证」「营运证」等一致 */ - function CertSectionCard(title, imageSrc, fieldNodes, photoCaption) { - var cap = photoCaption != null && String(photoCaption).trim() !== '' ? String(photoCaption) : '证件照片'; - var right = fieldNodes && fieldNodes.length - ? React.createElement('div', { - style: { - flex: 1, - minWidth: 260, - display: 'flex', - alignItems: 'center', - alignSelf: 'stretch' - } - }, React.createElement('div', { style: { width: '100%' } }, fieldNodes)) - : null; - return React.createElement(Card, { key: title, size: 'small', style: { marginBottom: 16 }, title: title }, - React.createElement('div', { style: { display: 'flex', gap: 24, flexWrap: 'wrap', alignItems: 'stretch' } }, - React.createElement('div', { style: { flexShrink: 0, display: 'flex', flexDirection: 'column' } }, - React.createElement('div', { style: { marginBottom: 8, color: 'rgba(0,0,0,0.45)', fontSize: 13 } }, cap), - React.createElement(Image, { + + function mergeLicViewStatus(a, b) { + var rank = { error: 0, warning: 1, default: 2, success: 3, processing: 2 }; + var ra = rank[a] != null ? rank[a] : 9; + var rb = rank[b] != null ? rank[b] : 9; + return ra <= rb ? a : b; + } + + function diffDaysFromAnchor(dateStr) { + if (!dateStr) return null; + var exp = new Date(dateStr); + var today = new Date(LICENSE_VIEW_ANCHOR_DATE); + exp.setHours(0, 0, 0, 0); + today.setHours(0, 0, 0, 0); + return Math.ceil((exp.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)); + } + + function getLicViewBadgeStatus(key) { + var item = licenseViewBundle[key]; + if (!item) return 'default'; + if (key === 'hydrogenCard') { + return item.cardNo ? 'success' : 'default'; + } + if (!item.photos || !item.photos.length) return 'default'; + if (key === 'driverLicense') { + var dd = diffDaysFromAnchor(item.expireDate); + if (dd == null) return 'success'; + if (dd <= 0) return 'error'; + if (dd <= 90) return 'warning'; + return 'success'; + } + if (key === 'transportLicense') { + var fromDate = function (d) { + var days = diffDaysFromAnchor(d); + if (days == null) return 'success'; + if (days <= 0) return 'error'; + if (days <= 60) return 'warning'; + return 'success'; + }; + return mergeLicViewStatus(fromDate(item.expireDate), fromDate(item.inspectValidUntil)); + } + if (key === 'specialEquipDecal' || key === 'safetyValve' || key === 'pressureGauge') { + var nd = diffDaysFromAnchor(item.nextInspectDate); + if (nd == null) return 'success'; + if (nd <= 0) return 'error'; + if (nd <= 60) return 'warning'; + return 'success'; + } + return 'success'; + } + + function scrollToLicenseAnchor(key, setActive) { + setActive(key); + var el = document.getElementById('vd-lic-' + key); + if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + + function LicViewPhotoGrid(photos, photoLabel) { + var list = photos && photos.length ? photos : []; + if (!list.length) { + return React.createElement('div', { style: { flexShrink: 0 } }, + React.createElement('div', { style: { marginBottom: 8, color: 'rgba(0,0,0,0.45)', fontSize: 13 } }, photoLabel || '证件照片'), + React.createElement(Image, { + width: 168, + height: 112, + style: { objectFit: 'cover', borderRadius: 4, border: '1px solid #f0f0f0', background: '#fafafa' }, + src: licensePhotoPlaceholder, + alt: '暂无影像', + preview: false + }) + ); + } + return React.createElement('div', { style: { flexShrink: 0 } }, + React.createElement('div', { style: { marginBottom: 8, color: 'rgba(0,0,0,0.45)', fontSize: 13 } }, + (photoLabel || '证件照片') + (list.length > 1 ? '(' + list.length + '张)' : '') + ), + React.createElement('div', { style: { display: 'flex', gap: 12, flexWrap: 'wrap' } }, + list.map(function (url, idx) { + return React.createElement(Image, { + key: idx, width: 168, height: 112, style: { objectFit: 'cover', borderRadius: 4, border: '1px solid #f0f0f0', background: '#fafafa' }, - src: imageSrc, - alt: title, + src: url, + alt: (photoLabel || '证照') + '-' + (idx + 1), preview: true - }) - ), - right + }); + }) ) ); } - /** 证照多图:每项 { label, src },如加氢证下登记证 + 使用标志 */ - function CertSectionCardMulti(title, photoSlots, fieldNodes) { - var right = fieldNodes && fieldNodes.length + + function LicViewCertCard(cfg) { + var fields = cfg.fields || []; + var right = fields.length ? React.createElement('div', { - style: { - flex: 1, - minWidth: 260, - display: 'flex', - alignItems: 'center', - alignSelf: 'stretch' - } - }, React.createElement('div', { style: { width: '100%' } }, fieldNodes)) + style: { flex: 1, minWidth: 260, display: 'flex', alignItems: 'center', alignSelf: 'stretch' } + }, React.createElement('div', { style: { width: '100%' } }, + fields.map(function (f, i) { return React.createElement('div', { key: i }, LicViewFieldRow(f.label, f.value)); }) + )) : null; - var slots = photoSlots || []; - var photosRow = React.createElement('div', { style: { display: 'flex', gap: 16, flexWrap: 'wrap', flexShrink: 0, alignItems: 'flex-start' } }, - slots.map(function (slot, idx) { - return React.createElement('div', { key: idx, style: { display: 'flex', flexDirection: 'column' } }, - React.createElement('div', { style: { marginBottom: 8, color: 'rgba(0,0,0,0.45)', fontSize: 13 } }, slot.label || '证件照片'), - React.createElement(Image, { - width: 168, - height: 112, - style: { objectFit: 'cover', borderRadius: 4, border: '1px solid #f0f0f0', background: '#fafafa' }, - src: slot.src, - alt: (slot.label || title) + '', - preview: true - }) + return React.createElement(Card, { + id: 'vd-lic-' + cfg.id, + size: 'small', + style: { marginBottom: 16, scrollMarginTop: 72 }, + title: cfg.title + }, + cfg.alertTop || null, + React.createElement('div', { style: { display: 'flex', gap: 24, flexWrap: 'wrap', alignItems: 'stretch' } }, + LicViewPhotoGrid(cfg.photos, cfg.photoLabel), + right + ), + cfg.extraBottom || null + ); + } + + function fmtH2Balance(n) { + var num = Number(n); + if (!Number.isFinite(num)) return '—'; + return '¥ ' + num.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + } + + var lic = licenseViewBundle; + var licNavActive = licenseActiveNav[0]; + var setLicNavActive = licenseActiveNav[1]; + var isShPlate = String(overview.plateNo || '').indexOf('沪') === 0; + + var driverDays = diffDaysFromAnchor(lic.driverLicense.expireDate); + var transportExpDays = diffDaysFromAnchor(lic.transportLicense.expireDate); + var decalDays = diffDaysFromAnchor(lic.specialEquipDecal.nextInspectDate); + + var licenseIndexBar = React.createElement(Card, { + size: 'small', + style: { marginBottom: 16, borderRadius: 12, border: '1px solid #e2e8f0' }, + bodyStyle: { padding: '14px 16px' } + }, + React.createElement('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', justifyContent: 'space-between', gap: 12, marginBottom: 12 } }, + React.createElement('span', { style: { fontSize: 14, fontWeight: 700, color: '#0f172a' } }, '证照分类索引'), + React.createElement('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 10, fontSize: 11, color: '#64748b' } }, + React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 4 } }, React.createElement(Badge, { status: 'success' }), '正常'), + React.createElement(Tooltip, { title: '行驶证≤90天 / 道路运输证≤60天 / 特种设备标识≤60天 / 安全阀≤60天 / 压力表≤60天' }, + React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 4, cursor: 'help' } }, React.createElement(Badge, { status: 'warning' }), '临期') + ), + React.createElement(Tooltip, { title: '对应证件检验/有效期已到期' }, + React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 4, cursor: 'help' } }, React.createElement(Badge, { status: 'error' }), '已到期') + ), + React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 4 } }, React.createElement(Badge, { status: 'default' }), '未上传') + ) + ), + React.createElement('div', { + className: 'vd-lic-nav-grid', + style: { display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: 8 }, + role: 'navigation', + 'aria-label': '证照索引' + }, + CERT_NAV_ITEMS.map(function (nav) { + var active = licNavActive === nav.key; + return React.createElement('button', { + key: nav.key, + type: 'button', + onClick: function () { scrollToLicenseAnchor(nav.key, setLicNavActive); }, + style: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + gap: 8, + padding: '8px 12px', + borderRadius: 8, + fontSize: 13, + fontWeight: active ? 600 : 500, + color: active ? '#065f46' : '#475569', + background: active ? '#ecfdf5' : '#f8fafc', + border: active ? '1px solid #a7f3d0' : '1px solid #f1f5f9', + cursor: 'pointer', + textAlign: 'left' + } + }, + React.createElement('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, nav.label), + React.createElement(Badge, { status: getLicViewBadgeStatus(nav.key) }) ); }) - ); - return React.createElement(Card, { key: title, size: 'small', style: { marginBottom: 16 }, title: title }, - React.createElement('div', { style: { display: 'flex', gap: 24, flexWrap: 'wrap', alignItems: 'stretch' } }, - photosRow, - right - ) - ); - } + ) + ); - function fmtCycleMonth(m) { - var s = m == null ? '' : String(m).trim(); - return s ? s + ' 月' : '—'; - } - - var cd = certificateDetail; var licenseManagementTabContent = React.createElement('div', { style: { padding: '0 4px' } }, - CertSectionCard('行驶证信息', cd.drivingLicense.img, [ - CertFieldRow('注册日期', cd.drivingLicense.regDate), - CertFieldRow('强制报废日期', cd.drivingLicense.scrapDate), - CertFieldRow('行驶证审验有效期', cd.drivingLicense.inspectExpire) - ], '行驶证'), - CertSectionCard('营运证信息', cd.operationPermit.img, [ - CertFieldRow('营运证编号', cd.operationPermit.permitNo), - CertFieldRow('注册日期', cd.operationPermit.regDate), - CertFieldRow('审验有效期', cd.operationPermit.inspectExpire) - ], '营运证'), - CertSectionCard('通行证信息', cd.passPermit.img, [ - CertFieldRow('通行证编码', cd.passPermit.code), - CertFieldRow('通行区域', cd.passPermit.area) - ], '通行证'), - CertSectionCard('登记证信息', cd.registrationCert.img, null, '登记证'), - CertSectionCardMulti('加氢证信息', cd.h2Permit.photos, [ - CertFieldRow('氢气瓶厂家', cd.h2Permit.cylinderVendor), - CertFieldRow('氢气瓶检验日期', cd.h2Permit.cylinderInspectDate), - CertFieldRow('氢气瓶检验周期', fmtCycleMonth(cd.h2Permit.cylinderCycleMonth)), - CertFieldRow('氢气瓶检测有效期', cd.h2Permit.cylinderValidUntil) - ]), - CertSectionCard('加氢卡信息', cd.h2Card.img, [ - CertFieldRow('加氢卡对应加氢站', cd.h2Card.stationName) - ], '加氢卡'), - CertSectionCard('安全阀信息', cd.safetyValve.img, [ - CertFieldRow('检验日期', cd.safetyValve.inspectDate), - CertFieldRow('检验周期', fmtCycleMonth(cd.safetyValve.cycleMonth)), - CertFieldRow('安全阀检测有效期', cd.safetyValve.validUntil) - ], '安全阀'), - CertSectionCard('压力表信息', cd.pressureGauge.img, [ - CertFieldRow('检验日期', cd.pressureGauge.inspectDate), - CertFieldRow('检验周期', fmtCycleMonth(cd.pressureGauge.cycleMonth)), - CertFieldRow('压力阀检测有效期', cd.pressureGauge.valveInspectValidUntil) - ], '压力表') + React.createElement('style', null, '@media (max-width:992px){.vd-lic-nav-grid{grid-template-columns:repeat(2,minmax(0,1fr))!important}}'), + licenseIndexBar, + LicViewCertCard({ + id: 'driverLicense', + title: '行驶证', + photoLabel: '行驶证照片', + photos: lic.driverLicense.photos, + alertTop: driverDays != null && driverDays <= 90 + ? React.createElement(Alert, { + type: driverDays <= 0 ? 'error' : driverDays <= 30 ? 'error' : 'warning', + showIcon: true, + style: { marginBottom: 16, borderRadius: 8 }, + message: '行驶证检验临期提醒', + description: driverDays <= 0 + ? '检验有效期已到期,请尽快安排年检。' + : ('距离检验有效期至还剩 ' + driverDays + ' 天(系统提前 90 天感知)。') + }) + : null, + fields: [ + { label: '注册日期', value: lic.driverLicense.regDate }, + { label: '发证日期', value: lic.driverLicense.issueDate }, + { label: '强制报废日期', value: lic.driverLicense.scrapDate }, + { label: '检验有效期至', value: lic.driverLicense.expireDate } + ], + extraBottom: isShPlate && lic.driverLicense.shNextEvaluation + ? React.createElement('div', { style: { marginTop: 16, padding: 12, background: '#f5f3ff', borderRadius: 8, border: '1px solid #ddd6fe' } }, + LicViewFieldRow('下次等级评定时间', lic.driverLicense.shNextEvaluation) + ) + : null + }), + LicViewCertCard({ + id: 'transportLicense', + title: '道路运输证', + photoLabel: '道路运输证照片', + photos: lic.transportLicense.photos, + alertTop: transportExpDays != null && transportExpDays <= 60 + ? React.createElement(Alert, { + type: transportExpDays <= 0 ? 'error' : transportExpDays <= 15 ? 'error' : 'warning', + showIcon: true, + style: { marginBottom: 16, borderRadius: 8 }, + message: '道路运输证临期提醒', + description: '证件有效期距今日 ' + transportExpDays + ' 天(系统提前 60 天列入年审任务)。' + }) + : null, + fields: [ + { label: '经营许可证号', value: lic.transportLicense.licenseNo }, + { label: '核发时间', value: lic.transportLicense.issueDate }, + { label: '证件有效期', value: lic.transportLicense.expireDate }, + { label: '审验有效期', value: lic.transportLicense.inspectValidUntil } + ] + }), + LicViewCertCard({ + id: 'registrationCert', + title: '登记证', + photoLabel: '登记证照片', + photos: lic.registrationCert.photos, + fields: [] + }), + LicViewCertCard({ + id: 'specialEquipCert', + title: '特种设备使用登记证', + photoLabel: '使用登记证照片', + photos: lic.specialEquipCert.photos, + fields: [] + }), + LicViewCertCard({ + id: 'specialEquipDecal', + title: '特种设备使用标识', + photoLabel: '使用安全标识照片', + photos: lic.specialEquipDecal.photos, + alertTop: decalDays != null && decalDays <= 60 + ? React.createElement(Alert, { + type: decalDays <= 0 || decalDays <= 15 ? 'error' : 'warning', + showIcon: true, + style: { marginBottom: 16, borderRadius: 8 }, + message: '特种设备使用标识检验提醒', + description: decalDays <= 0 + ? ('下次检验日期已逾期 ' + Math.abs(decalDays) + ' 天。') + : ('距离下次检验日期还剩 ' + decalDays + ' 天(提前 60 天感知)。') + }) + : null, + fields: [{ label: '下次检验日期', value: lic.specialEquipDecal.nextInspectDate }] + }), + React.createElement(Card, { + id: 'vd-lic-hydrogenCard', + size: 'small', + style: { marginBottom: 16, scrollMarginTop: 72 }, + title: '加氢卡' + }, + React.createElement('div', { style: { display: 'flex', gap: 24, flexWrap: 'wrap' } }, + React.createElement('div', { + style: { + flex: '0 0 280px', + maxWidth: 320, + padding: 20, + borderRadius: 12, + background: 'linear-gradient(135deg, #0f172a 0%, #1e293b 100%)', + color: '#e2e8f0', + boxShadow: '0 8px 24px rgba(15,23,42,0.15)' + } + }, + React.createElement('div', { style: { fontSize: 11, color: '#94a3b8', marginBottom: 8 } }, lic.hydrogenCard.cardType || '加氢卡'), + React.createElement('div', { style: { fontSize: 18, fontWeight: 700, letterSpacing: 2, marginBottom: 16, fontFamily: 'monospace' } }, + lic.hydrogenCard.cardNo || '—' + ), + React.createElement('div', { style: { fontSize: 22, fontWeight: 800, color: '#34d399' } }, fmtH2Balance(lic.hydrogenCard.balance)) + ), + React.createElement('div', { style: { flex: 1, minWidth: 240 } }, + LicViewFieldRow('加氢卡号', lic.hydrogenCard.cardNo), + LicViewFieldRow('卡类型', lic.hydrogenCard.cardType), + LicViewFieldRow('实时余额', fmtH2Balance(lic.hydrogenCard.balance)), + LicViewFieldRow('配发时间', lic.hydrogenCard.issueDate), + LicViewFieldRow('配发经办人', lic.hydrogenCard.issueUser) + ) + ) + ), + LicViewCertCard({ + id: 'safetyValve', + title: '安全阀', + photoLabel: '安全阀校验报告照片', + photos: lic.safetyValve.photos, + fields: [ + { label: '检验日期', value: lic.safetyValve.inspectDate }, + { label: '下次检验日期', value: lic.safetyValve.nextInspectDate } + ] + }), + LicViewCertCard({ + id: 'pressureGauge', + title: '压力表', + photoLabel: '压力表校验报告照片', + photos: lic.pressureGauge.photos, + fields: [ + { label: '检验日期', value: lic.pressureGauge.inspectDate }, + { label: '下次检验日期', value: lic.pressureGauge.nextInspectDate } + ] + }) ); var tabItems = [ diff --git a/web端/车辆管理.jsx b/web端/车辆管理.jsx index 2acd4b5..e1ff9b7 100644 --- a/web端/车辆管理.jsx +++ b/web端/车辆管理.jsx @@ -18,9 +18,16 @@ const Component = function () { var Upload = antd.Upload; var Card = antd.Card; var Tabs = antd.Tabs; + var Form = antd.Form; + var Row = antd.Row; + var Col = antd.Col; var message = antd.message; var App = antd.App; + // 联调后对接权限中心;原型默认 admin 可编辑,改为 'staff' 可验证无权限隐藏 + var CURRENT_USER = { id: 'u_admin', name: '系统管理员', role: 'admin' }; + var isAdmin = CURRENT_USER.role === 'admin'; + // 筛选项状态 var _region = useState([]); var _vehicleType = useState(undefined); @@ -49,6 +56,9 @@ const Component = function () { var _requirementModalVisible = useState(false); var _inspectImportModalVisible = useState(false); var _inspectImportResult = useState(null); + var _editModalVisible = useState(false); + var _editRecord = useState(null); + var _editForm = useState({}); // 省-市 地区数据(示例) var regionOptions = [ @@ -81,6 +91,49 @@ const Component = function () { { label: '第三方融资租赁有限公司', value: '第三方融资租赁有限公司' }, { label: '无', value: '-' } ]; + var parkingOptions = [ + { label: '天河停车场', value: '天河停车场' }, + { label: '黄埔停车场', value: '黄埔停车场' }, + { label: '朝阳停车场', value: '朝阳停车场' }, + { label: '福田停车场', value: '福田停车场' }, + { label: '浦东停车场', value: '浦东停车场' }, + { label: '南山停车场', value: '南山停车场' }, + { label: '番禺停车场', value: '番禺停车场' }, + { label: '大兴停车场', value: '大兴停车场' }, + { label: '龙岗停车场', value: '龙岗停车场' }, + { label: '白云停车场', value: '白云停车场' }, + { label: '西城停车场', value: '西城停车场' }, + { label: '虹口停车场', value: '虹口停车场' }, + { label: '昌平停车场', value: '昌平停车场' }, + { label: '-', value: '-' } + ]; + var operateStatusOptions = [ + { label: '租赁', value: '租赁' }, + { label: '自营', value: '自营' }, + { label: '可运营', value: '可运营' }, + { label: '待运营', value: '待运营' }, + { label: '退出运营', value: '退出运营' } + ]; + var vehicleStatusOptions = [ + { label: '待验车', value: '待验车' }, + { label: '未备车', value: '未备车' }, + { label: '已备车', value: '已备车' }, + { label: '待交车', value: '待交车' }, + { label: '已交车', value: '已交车' }, + { label: '待还车', value: '待还车' }, + { label: '销售中', value: '销售中' }, + { label: '替换中', value: '替换中' }, + { label: '调拨中', value: '调拨中' }, + { label: '异动中', value: '异动中' }, + { label: '三方退租中', value: '三方退租中' }, + { label: '无', value: '无' } + ]; + var yearOptions = (function () { + var y = new Date().getFullYear(); + var opts = []; + for (var i = y; i >= y - 12; i--) opts.push({ label: String(i), value: String(i) }); + return opts; + })(); // 表格数据(模拟 20 条)— 状态字段按《车辆状态》脑图(2026-03): // 运营状态:租赁、自营、可运营、待运营、退出运营(原「库存」并入「可运营」) @@ -111,13 +164,16 @@ const Component = function () { { id: '20', region: '北京市/北京市', vin: 'LSJA24U70PS999000', plateNo: '京H88888', vehicleNo: '-', vehicleType: '小型轿车', brand: '蔚来', model: 'ET5', color: '白色', parking: '昌平停车场', customer: '客户D', department: '华北区', manager: '孙七', operateStatus: '退出运营', vehicleStatus: '无', outStatus: '报废出库', licenseStatus: '无', insuranceStatus: '正常', ownership: '某某科技有限公司', operateCompany: '羚牛运营(上海)', vehicleSource: '外租', leaseCompany: '-', onlineStatus: '离线', year: '2022', mileage: '15600.00', purchaseDate: '2022-06-20', regDate: '2022-07-10', inspectExpire: '2024-07', lastDeliveryTime: '2024-01-10', lastDeliveryMile: '15300.00', lastReturnTime: '2024-01-28', lastReturnMile: '15600.00', scrapDate: '2037-07-31', contractNo: '-', location: '北京市昌平区回龙观西大街100号', gpsTime: '2024-02-10 12:30' } ]; + var _tableData = useState(rawData); + var dataSource = useMemo(function () { var plate = _plateFilter[0]; - if (!plate || plate.trim() === '') return rawData; - return rawData.filter(function (row) { + var list = _tableData[0]; + if (!plate || plate.trim() === '') return list; + return list.filter(function (row) { return row.plateNo && row.plateNo.indexOf(plate) !== -1; }); - }, [rawData, _plateFilter[0]]); + }, [_tableData[0], _plateFilter[0]]); var onPlateFilterChange = useCallback(function (e) { _plateFilter[1](e.target.value); @@ -268,6 +324,92 @@ const Component = function () { closeConfirmModal(); }, []); + var openEditModal = useCallback(function (record) { + if (!isAdmin) { + message.warning('仅 Admin 可编辑车辆信息'); + return; + } + _editRecord[1](record); + _editForm[1]({ + parking: record.parking === '-' ? '-' : (record.parking || undefined), + operateStatus: record.operateStatus || undefined, + vehicleStatus: record.vehicleStatus || undefined, + ownership: record.ownership && record.ownership !== '-' ? record.ownership : '', + operateCompany: record.operateCompany || undefined, + vehicleSource: record.vehicleSource || undefined, + leaseCompany: record.leaseCompany && record.leaseCompany !== '-' ? record.leaseCompany : '', + year: record.year || undefined, + purchaseDate: record.purchaseDate || '' + }); + _editModalVisible[1](true); + }, []); + + var closeEditModal = useCallback(function () { + _editModalVisible[1](false); + _editRecord[1](null); + _editForm[1]({}); + }, []); + + var onEditFormChange = useCallback(function (field, value) { + _editForm[1](function (prev) { + var next = Object.assign({}, prev); + next[field] = value; + return next; + }); + }, []); + + var saveEdit = useCallback(function () { + if (!isAdmin) { + message.warning('仅 Admin 可保存'); + return; + } + var record = _editRecord[0]; + var form = _editForm[0]; + if (!record) return; + if (!form.operateStatus || !form.vehicleStatus) { + message.warning('请选择运营状态与车辆状态'); + return; + } + _tableData[1](function (prev) { + return prev.map(function (row) { + if (row.id !== record.id) return row; + var ownershipVal = (form.ownership || '').trim(); + var leaseVal = (form.leaseCompany || '').trim(); + return Object.assign({}, row, { + parking: form.parking || '-', + operateStatus: form.operateStatus, + vehicleStatus: form.vehicleStatus, + ownership: ownershipVal || '-', + operateCompany: form.operateCompany || row.operateCompany, + vehicleSource: form.vehicleSource || row.vehicleSource, + leaseCompany: leaseVal || '-', + year: form.year || row.year, + purchaseDate: form.purchaseDate || row.purchaseDate + }); + }); + }); + if (_detailRecord[0] && _detailRecord[0].id === record.id) { + _detailRecord[1](function (prev) { + if (!prev || prev.id !== record.id) return prev; + var ownershipVal = (form.ownership || '').trim(); + var leaseVal = (form.leaseCompany || '').trim(); + return Object.assign({}, prev, { + parking: form.parking || '-', + operateStatus: form.operateStatus, + vehicleStatus: form.vehicleStatus, + ownership: ownershipVal || '-', + operateCompany: form.operateCompany || prev.operateCompany, + vehicleSource: form.vehicleSource || prev.vehicleSource, + leaseCompany: leaseVal || '-', + year: form.year || prev.year, + purchaseDate: form.purchaseDate || prev.purchaseDate + }); + }); + } + message.success('车辆信息已保存(原型演示)'); + closeEditModal(); + }, []); + // 状态类字段:枚举为「无」时界面显示为「-」 var formatStatusDisplay = function (val) { if (val === '无') return '-'; @@ -326,15 +468,24 @@ const Component = function () { { title: '操作', key: 'action', - width: 140, + width: isAdmin ? 180 : 140, fixed: 'right', render: function (_, record) { - return React.createElement(Space, null, - React.createElement(Button, { type: 'link', size: 'small', onClick: function () { goDetail(record); } }, '查看'), - React.createElement(Dropdown, { menu: { items: getMoreMenuItems(record) }, trigger: ['click'] }, - React.createElement(Button, { type: 'link', size: 'small' }, '更多') - ) - ); + var actions = [ + React.createElement(Button, { key: 'view', type: 'link', size: 'small', onClick: function () { goDetail(record); } }, '查看') + ]; + if (isAdmin) { + actions.push(React.createElement(Button, { + key: 'edit', + type: 'link', + size: 'small', + onClick: function () { openEditModal(record); } + }, '编辑')); + } + actions.push(React.createElement(Dropdown, { key: 'more', menu: { items: getMoreMenuItems(record) }, trigger: ['click'] }, + React.createElement(Button, { type: 'link', size: 'small' }, '更多') + )); + return React.createElement(Space, { size: 0 }, actions); } } ]; @@ -680,6 +831,131 @@ const Component = function () { footer: React.createElement(Button, { onClick: function () { _requirementModalVisible[1](false); } }, '关闭') }, React.createElement('div', { style: { maxHeight: 560, overflow: 'auto', whiteSpace: 'pre-wrap', fontSize: 13, lineHeight: 1.6, color: 'rgba(0,0,0,0.85)' } }, requirementText)), + React.createElement(Modal, { + title: '编辑车辆信息', + open: _editModalVisible[0], + onCancel: closeEditModal, + width: 720, + destroyOnClose: true, + footer: [ + React.createElement(Button, { key: 'cancel', onClick: closeEditModal }, '取消'), + React.createElement(Button, { key: 'save', type: 'primary', onClick: saveEdit }, '保存') + ] + }, _editRecord[0] ? React.createElement('div', null, + React.createElement('div', { style: { marginBottom: 16, padding: '8px 12px', background: '#f5f5f5', borderRadius: 4, fontSize: 13, color: 'rgba(0,0,0,0.65)' } }, + '车牌号:', React.createElement('span', { style: { fontWeight: 600, color: 'rgba(0,0,0,0.85)' } }, _editRecord[0].plateNo || '-'), + ' VIN:', React.createElement('span', { style: { color: 'rgba(0,0,0,0.85)' } }, _editRecord[0].vin || '-') + ), + React.createElement(Form, { layout: 'vertical' }, + React.createElement(Row, { gutter: 16 }, + React.createElement(Col, { span: 12 }, + React.createElement(Form.Item, { label: '车辆归属停车场', required: true }, + React.createElement(Select, { + placeholder: '请选择停车场', + style: { width: '100%' }, + options: parkingOptions, + value: _editForm[0].parking, + onChange: function (v) { onEditFormChange('parking', v); }, + allowClear: true, + showSearch: true, + filterOption: function (input, opt) { return (opt.label || '').toString().toLowerCase().indexOf((input || '').toLowerCase()) !== -1; } + }) + ) + ), + React.createElement(Col, { span: 12 }, + React.createElement(Form.Item, { label: '运营状态', required: true }, + React.createElement(Select, { + placeholder: '请选择运营状态', + style: { width: '100%' }, + options: operateStatusOptions, + value: _editForm[0].operateStatus, + onChange: function (v) { onEditFormChange('operateStatus', v); } + }) + ) + ), + React.createElement(Col, { span: 12 }, + React.createElement(Form.Item, { label: '车辆状态', required: true }, + React.createElement(Select, { + placeholder: '请选择车辆状态', + style: { width: '100%' }, + options: vehicleStatusOptions, + value: _editForm[0].vehicleStatus, + onChange: function (v) { onEditFormChange('vehicleStatus', v); } + }) + ) + ), + React.createElement(Col, { span: 12 }, + React.createElement(Form.Item, { label: '登记所有权' }, + React.createElement(Input, { + placeholder: '请输入登记所有权', + value: _editForm[0].ownership || '', + onChange: function (e) { onEditFormChange('ownership', e.target.value); }, + allowClear: true + }) + ) + ), + React.createElement(Col, { span: 12 }, + React.createElement(Form.Item, { label: '运营公司' }, + React.createElement(Select, { + placeholder: '请选择运营公司', + style: { width: '100%' }, + options: operateCompanyOptions, + value: _editForm[0].operateCompany, + onChange: function (v) { onEditFormChange('operateCompany', v); }, + allowClear: true, + showSearch: true, + filterOption: function (input, opt) { return (opt.label || '').toString().toLowerCase().indexOf((input || '').toLowerCase()) !== -1; } + }) + ) + ), + React.createElement(Col, { span: 12 }, + React.createElement(Form.Item, { label: '车辆来源' }, + React.createElement(Select, { + placeholder: '请选择车辆来源', + style: { width: '100%' }, + options: vehicleSourceOptions, + value: _editForm[0].vehicleSource, + onChange: function (v) { onEditFormChange('vehicleSource', v); }, + allowClear: true + }) + ) + ), + React.createElement(Col, { span: 12 }, + React.createElement(Form.Item, { label: '租赁公司' }, + React.createElement(Input, { + placeholder: '请输入租赁公司,无则留空', + value: _editForm[0].leaseCompany || '', + onChange: function (e) { onEditFormChange('leaseCompany', e.target.value); }, + allowClear: true + }) + ) + ), + React.createElement(Col, { span: 12 }, + React.createElement(Form.Item, { label: '出厂年份' }, + React.createElement(Select, { + placeholder: '请选择出厂年份', + style: { width: '100%' }, + options: yearOptions, + value: _editForm[0].year, + onChange: function (v) { onEditFormChange('year', v); }, + allowClear: true + }) + ) + ), + React.createElement(Col, { span: 12 }, + React.createElement(Form.Item, { label: '采购入库时间', extra: '格式:YYYY-MM-DD' }, + React.createElement(Input, { + placeholder: '请输入采购入库时间', + value: _editForm[0].purchaseDate || '', + onChange: function (e) { onEditFormChange('purchaseDate', e.target.value); }, + allowClear: true + }) + ) + ) + ) + ) + ) : null), + React.createElement(Modal, { title: '批量导入等评时间', open: _inspectImportModalVisible[0], diff --git a/web端/运维管理/车辆业务/交车管理-交车单.jsx b/web端/运维管理/车辆业务/交车管理-交车单.jsx index 945da5f..b8209d0 100644 --- a/web端/运维管理/车辆业务/交车管理-交车单.jsx +++ b/web端/运维管理/车辆业务/交车管理-交车单.jsx @@ -48,22 +48,36 @@ const Component = function () { // 交车明细列表(序号、品牌、型号、车牌号可编辑、实际交车日期、交车人、交车状态、操作) var detailListState = useState([ - { key: 1, seq: 1, brand: '东风', model: 'DFH1180', plateNo: '京A12345', actualDate: '2025-02-28', deliveryPerson: '张三', status: '已签章' }, - { key: 2, seq: 2, brand: '东风', model: 'DFH1250', plateNo: '京C11111', actualDate: '2025-02-28', deliveryPerson: '张三', status: '已签章' }, - { key: 3, seq: 3, brand: '福田', model: 'BJ1180', plateNo: '浙A10001', actualDate: '2025-03-01', deliveryPerson: '李四', status: '已签章' }, - { key: 4, seq: 4, brand: '福田', model: 'BJ1250', plateNo: '浙F80088', actualDate: '2025-03-01', deliveryPerson: '李四', status: '已签章' }, - { key: 5, seq: 5, brand: '重汽', model: 'HOWO-T5G', plateNo: '沪A30003', actualDate: '2025-03-02', deliveryPerson: '王五', status: '已签章' }, - { key: 6, seq: 6, brand: '陕汽', model: '德龙X3000', plateNo: '京D22222', actualDate: '2025-03-02', deliveryPerson: '王五', status: '已完成' }, - { key: 7, seq: 7, brand: '解放', model: 'J6P', plateNo: '', actualDate: '', deliveryPerson: '', status: '待提交' }, - { key: 8, seq: 8, brand: '欧曼', model: 'EST-A', plateNo: '', actualDate: '', deliveryPerson: '', status: '待提交' }, - { key: 9, seq: 9, brand: '江淮', model: '格尔发K7', plateNo: '', actualDate: '', deliveryPerson: '', status: '待提交' }, - { key: 10, seq: 10, brand: '红岩', model: '杰狮C6', plateNo: '', actualDate: '', deliveryPerson: '', status: '待提交' } + { key: 1, seq: 1, brand: '东风', model: 'DFH1180', plateNo: '京A12345', actualDate: '2025-02-28', deliveryPerson: '张三', status: '客户已签章' }, + { key: 2, seq: 2, brand: '东风', model: 'DFH1250', plateNo: '京C11111', actualDate: '2025-02-28', deliveryPerson: '张三', status: '客户已签章' }, + { key: 3, seq: 3, brand: '福田', model: 'BJ1180', plateNo: '浙A10001', actualDate: '2025-03-01', deliveryPerson: '李四', status: '待客户签章' }, + { key: 4, seq: 4, brand: '福田', model: 'BJ1250', plateNo: '浙F80088', actualDate: '2025-03-01', deliveryPerson: '李四', status: '待客户签章' }, + { key: 5, seq: 5, brand: '重汽', model: 'HOWO-T5G', plateNo: '沪A30003', actualDate: '2025-03-02', deliveryPerson: '王五', status: '已保存' }, + { key: 6, seq: 6, brand: '陕汽', model: '德龙X3000', plateNo: '京D22222', actualDate: '2025-03-02', deliveryPerson: '王五', status: '已保存' }, + { key: 7, seq: 7, brand: '解放', model: 'J6P', plateNo: '', actualDate: '', deliveryPerson: '', status: '未开始' }, + { key: 8, seq: 8, brand: '欧曼', model: 'EST-A', plateNo: '', actualDate: '', deliveryPerson: '', status: '未开始' }, + { key: 9, seq: 9, brand: '江淮', model: '格尔发K7', plateNo: '', actualDate: '', deliveryPerson: '', status: '未开始' }, + { key: 10, seq: 10, brand: '红岩', model: '杰狮C6', plateNo: '', actualDate: '', deliveryPerson: '', status: '未开始' } ]); var detailList = detailListState[0]; var setDetailList = detailListState[1]; - var allSigned = useMemo(function () { - return detailList.length > 0 && detailList.every(function (row) { return row.status === '已签章'; }); + function isDetailHistoryStatus(status) { + return status === '客户已签章' || status === '已签章'; + } + + function canEditDetailRow(record) { + var s = record.status; + return s === '未开始' || s === '已保存'; + } + + function hasPlateSelected(record) { + var p = record.plateNo; + return p && String(p).trim() !== '' && p !== '-'; + } + + var allCustomerSigned = useMemo(function () { + return detailList.length > 0 && detailList.every(function (row) { return isDetailHistoryStatus(row.status); }); }, [detailList]); var updateDetailRow = useCallback(function (index, field, value) { @@ -76,10 +90,10 @@ const Component = function () { }, []); var handleSubmit = useCallback(function () { - if (!allSigned) return; + if (!allCustomerSigned) return; message.success('提交成功(原型)'); if (typeof window !== 'undefined' && window.history) window.history.back(); - }, [allSigned]); + }, [allCustomerSigned]); var handleCancel = useCallback(function () { if (typeof window !== 'undefined' && window.history) window.history.back(); @@ -140,12 +154,11 @@ const Component = function () { dataIndex: 'plateNo', key: 'plateNo', width: 120, - render: function (v, record, index) { - var isDelivered = record.status === '已签章' || record.status === '已完成'; - if (isDelivered) { + render: function (v, record) { + if (hasPlateSelected(record)) { return React.createElement(Input, { value: v || '', disabled: true, style: { width: '100%', background: '#f5f5f5' } }); } - return React.createElement(Input, { value: '-', disabled: true, style: { width: '100%', background: '#f5f5f5' } }); + return React.createElement(Input, { value: '车牌待选', disabled: true, style: { width: '100%', background: '#f5f5f5', color: '#d48806' } }); } }, { @@ -162,7 +175,7 @@ const Component = function () { width: 90, render: function (v) { return React.createElement(Input, { value: v || '', disabled: true, style: { width: '100%', background: '#f5f5f5' } }); } }, - { title: '交车状态', dataIndex: 'status', key: 'status', width: 90, render: function (v) { return v || '-'; } }, + { title: '交车状态', dataIndex: 'status', key: 'status', width: 108, render: function (v) { return v || '-'; } }, { title: '操作', key: 'action', @@ -171,8 +184,8 @@ const Component = function () { render: function (_, record, index) { return React.createElement(React.Fragment, null, React.createElement(Button, { type: 'link', size: 'small', onClick: function () { handleViewDetail(record); } }, '查看'), - record.status === '待提交' ? React.createElement(Button, { type: 'link', size: 'small', onClick: function () { handleEditDetail(record, index); } }, '编辑') : null, - record.status === '已签章' ? React.createElement(Button, { type: 'link', size: 'small', onClick: function () { handleDownloadSign(record); } }, '下载签章文件') : null + canEditDetailRow(record) ? React.createElement(Button, { type: 'link', size: 'small', onClick: function () { handleEditDetail(record, index); } }, '编辑') : null, + isDetailHistoryStatus(record.status) ? React.createElement(Button, { type: 'link', size: 'small', onClick: function () { handleDownloadSign(record); } }, '下载签章文件') : null ); } } @@ -214,7 +227,7 @@ const Component = function () { React.createElement('div', { style: { height: 60 } }), React.createElement('div', { style: styles.footer }, - React.createElement(Button, { type: 'primary', disabled: !allSigned, onClick: handleSubmit }, '提交'), + React.createElement(Button, { type: 'primary', disabled: !allCustomerSigned, onClick: handleSubmit }, '提交'), React.createElement(Button, { onClick: handleCancel }, '取消') ), diff --git a/web端/运维管理/车辆业务/交车管理-编辑抽屉.jsx b/web端/运维管理/车辆业务/交车管理-编辑抽屉.jsx new file mode 100644 index 0000000..0abf893 --- /dev/null +++ b/web端/运维管理/车辆业务/交车管理-编辑抽屉.jsx @@ -0,0 +1,503 @@ +// 交车管理 - 列表内编辑抽屉(参照 交车管理-交车单-编辑.jsx) +// 使用方式:window.DeliveryEditDrawer(props) + +function DeliveryEditDrawer(props) { + var useState = React.useState; + var useMemo = React.useMemo; + var useCallback = React.useCallback; + var useRef = React.useRef; + var useEffect = React.useEffect; + + var open = props.open; + var record = props.record; + var onClose = props.onClose; + var onSave = props.onSave; + var onSubmit = props.onSubmit; + + var antd = window.antd; + var Drawer = antd.Drawer; + var Button = antd.Button; + var Input = antd.Input; + var Select = antd.Select; + var Switch = antd.Switch; + var Modal = antd.Modal; + var Table = antd.Table; + var Tag = antd.Tag; + var message = antd.message; + + function RequiredLabel(text) { + return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 4 } }, + React.createElement('span', { style: { color: '#ef4444', fontWeight: 600 } }, '*'), + React.createElement('span', null, text) + ); + } + + function isEmpty(v) { + return v === null || v === undefined || String(v).trim() === ''; + } + + function filterOption(input, option) { + var label = (option && (option.label || option.children)) || ''; + return String(label).toLowerCase().indexOf(String(input || '').toLowerCase()) >= 0; + } + + function fileToDataUrl(file, cb) { + try { + var reader = new FileReader(); + reader.onload = function (e) { cb(null, (e && e.target && e.target.result) || ''); }; + reader.onerror = function () { cb(new Error('read error')); }; + reader.readAsDataURL(file); + } catch (e) { cb(e); } + } + + var reserveVehicles = useMemo(function () { + return [ + { plateNo: '京A12345', vehicleType: '牵引车', brand: '东风', model: 'DFH1180', vin: 'LJNAU1A2XK1234567', hasAd: true, adPhoto: [{ uid: 'ad1', name: '广告照片.jpg', url: 'https://dummyimage.com/600x400/e2e8f0/475569&text=Ad' }], bigWordPhoto: [{ uid: 'bw1', name: '放大字.jpg', url: 'https://dummyimage.com/600x400/e2e8f0/475569&text=BigWord' }], hasTailboard: true }, + { plateNo: '浙F80088', vehicleType: '厢式车', brand: '福田', model: 'BJ1180', vin: 'LJNAU1A2XK7654321', hasAd: false, adPhoto: [], bigWordPhoto: [], hasTailboard: false }, + { plateNo: '沪A30003', vehicleType: '厢式车', brand: '重汽', model: 'HOWO-T5G', vin: 'LJNAU1A2XK9999000', hasAd: true, adPhoto: [{ uid: 'ad2', name: '广告2.jpg', url: 'https://dummyimage.com/600x400/e2e8f0/475569&text=Ad2' }], bigWordPhoto: [{ uid: 'bw2', name: '放大字2.jpg', url: 'https://dummyimage.com/600x400/e2e8f0/475569&text=BW2' }], hasTailboard: true }, + { plateNo: '粤AGP4598', vehicleType: '4.5吨冷链车', brand: '现代', model: '帕力安牌4.5吨冷链车', vin: 'LNBSCPKB9RR223402', hasAd: false, adPhoto: [], bigWordPhoto: [], hasTailboard: false } + ]; + }, []); + + var plateOptions = useMemo(function () { + return reserveVehicles.map(function (v) { return { value: v.plateNo, label: v.plateNo }; }); + }, [reserveVehicles]); + + var vehicleByPlate = useMemo(function () { + var map = {}; + reserveVehicles.forEach(function (v) { map[v.plateNo] = v; }); + return map; + }, [reserveVehicles]); + + function buildInitialForm(rec) { + var plate = (rec && rec.plateNo && String(rec.plateNo).trim()) || undefined; + var veh = plate ? vehicleByPlate[plate] : null; + if (!veh && rec && rec.brand) { + veh = { plateNo: plate, vehicleType: rec.vehicleType || '', brand: rec.brand || '', model: rec.model || '', vin: rec.vin || '', hasAd: false, adPhoto: [], bigWordPhoto: [], hasTailboard: false }; + } + return { + plateNo: plate, + vehicleType: (veh && veh.vehicleType) || (rec && rec.vehicleType) || '', + brand: (veh && veh.brand) || (rec && rec.brand) || '', + model: (veh && veh.model) || (rec && rec.model) || '', + vin: (veh && veh.vin) || (rec && rec.vin) || '', + hasAd: !!(veh && veh.hasAd), + adPhoto: (veh && veh.adPhoto) || [], + bigWordPhoto: (veh && veh.bigWordPhoto) || [], + hasTailboard: !!(veh && veh.hasTailboard), + spareTirePhoto: [], + spareTireDepth: '', + trainingRecognized: false, + driverLicenses: [], + mileageKm: rec && rec.deliveryMileage != null ? String(rec.deliveryMileage) : '', + batteryPct: rec && rec.deliveryElec != null ? String(rec.deliveryElec) : '', + hydrogenAmount: rec && rec.deliveryH2 != null ? String(rec.deliveryH2) : '', + hydrogenUnit: (rec && rec.deliveryH2Unit === 'MPa') ? 'MPa' : '%', + serviceFee: '' + }; + } + + var formState = useState(buildInitialForm(null)); + var form = formState[0]; + var setForm = formState[1]; + var activeSectionState = useState('basic'); + var activeSection = activeSectionState[0]; + var setActiveSection = activeSectionState[1]; + var submittingState = useState(false); + var submitting = submittingState[0]; + var setSubmitting = submittingState[1]; + var previewState = useState({ open: false, url: '', title: '' }); + var ocrModalState = useState({ open: false, photoUrl: '', depth: '6.50' }); + var inspectionOpenState = useState(false); + var trainingInputRef = useRef(null); + + useEffect(function () { + if (open && record) { + setForm(buildInitialForm(record)); + setActiveSection('basic'); + setSubmitting(false); + } + }, [open, record && record.id]); + + function updateForm(patch) { + setForm(function (p) { return Object.assign({}, p, patch); }); + } + + function handlePlateChange(v) { + var veh = vehicleByPlate[v]; + updateForm({ + plateNo: v, + vehicleType: (veh && veh.vehicleType) || '', + brand: (veh && veh.brand) || '', + model: (veh && veh.model) || '', + vin: (veh && veh.vin) || '', + hasAd: !!(veh && veh.hasAd), + adPhoto: (veh && (veh.adPhoto || [])) || [], + bigWordPhoto: (veh && (veh.bigWordPhoto || [])) || [], + hasTailboard: !!(veh && veh.hasTailboard), + spareTirePhoto: [], + spareTireDepth: '', + trainingRecognized: false, + driverLicenses: [] + }); + } + + function makeThumb(url, onPreview, onRemove) { + return React.createElement('div', { style: { width: 72, height: 72, borderRadius: 8, border: '1px solid #e2e8f0', overflow: 'hidden', position: 'relative', background: '#f8fafc' } }, + React.createElement('img', { src: url, style: { width: '100%', height: '100%', objectFit: 'cover', cursor: 'pointer' }, onClick: onPreview }), + onRemove ? React.createElement('button', { + type: 'button', + 'aria-label': '删除图片', + style: { position: 'absolute', right: 4, top: 4, width: 22, height: 22, borderRadius: 999, border: 'none', background: 'rgba(15,23,42,.65)', color: '#fff', cursor: 'pointer', fontSize: 12, lineHeight: '22px', padding: 0 }, + onClick: function (e) { e.stopPropagation(); onRemove(); } + }, '×') : null + ); + } + + function UploadBox(uploadProps) { + var label = uploadProps.label; + var value = uploadProps.value || []; + var max = uploadProps.max || 1; + var onChange = uploadProps.onChange; + function handlePick(e) { + var f = e && e.target && e.target.files && e.target.files[0]; + if (!f) return; + fileToDataUrl(f, function (err, url) { + if (err) { message.error('上传失败'); return; } + var next = value.slice(); + next.push({ uid: String(Date.now()), name: f.name || 'image', url: url }); + if (next.length > max) next = next.slice(next.length - max); + onChange && onChange(next); + }); + e.target.value = ''; + } + return React.createElement('div', null, + label ? React.createElement('div', { style: { fontSize: 13, color: '#475569', marginBottom: 8, fontWeight: 500 } }, label) : null, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' } }, + value.map(function (f) { + return React.createElement('div', { key: f.uid }, + makeThumb(f.url, function () { previewState[1]({ open: true, url: f.url, title: f.name }); }, function () { onChange && onChange(value.filter(function (x) { return x.uid !== f.uid; })); }) + ); + }), + value.length >= max ? null : React.createElement('label', { style: { width: 72, height: 72, borderRadius: 8, border: '1px dashed #cbd5e1', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#64748b', cursor: 'pointer', fontSize: 13, background: '#fff', transition: 'border-color .2s' } }, + React.createElement('input', { type: 'file', accept: 'image/*', style: { display: 'none' }, onChange: handlePick }), + '上传' + ) + ) + ); + } + + function FormItem(itemProps) { + return React.createElement('div', { style: { marginBottom: 16, minWidth: 0 } }, + React.createElement('div', { style: { fontSize: 13, color: '#475569', marginBottom: 6, fontWeight: 500 } }, + itemProps.required ? RequiredLabel(itemProps.label) : itemProps.label + ), + itemProps.children + ); + } + + var inspectionListState = useState([{ key: 'ins-1', category: '车灯', item: '大灯', checked: true, treadDepth: '', remark: '' }]); + var inspectionList = inspectionListState[0]; + + var sectionNav = [ + { key: 'basic', label: '车辆信息' }, + { key: 'equip', label: '设备证照' }, + { key: 'metrics', label: '交车数据' }, + { key: 'photos', label: '交车照片' } + ]; + + function scrollToSection(key) { + setActiveSection(key); + var el = document.getElementById('dv-edit-section-' + key); + if (el && el.scrollIntoView) el.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + + function validateSubmit() { + if (isEmpty(form.plateNo)) return '请选择车牌号'; + if (form.hasAd) { + if (!form.adPhoto.length) return '请上传广告照片'; + if (!form.bigWordPhoto.length) return '请上传放大字照片'; + } + if (!form.spareTirePhoto.length) return '请上传备胎照片'; + if (isEmpty(form.spareTireDepth)) return '请填写备胎胎纹深度'; + if (!form.trainingRecognized) return '请上传验车码并完成识别'; + if (isEmpty(form.mileageKm)) return '请填写交车里程'; + if (isEmpty(form.batteryPct)) return '请填写交车电量'; + if (isEmpty(form.hydrogenAmount)) return '请填写交车氢量'; + return ''; + } + + function buildPatchFromForm() { + return { + plateNo: form.plateNo || '', + vehicleType: form.vehicleType, + brand: form.brand, + model: form.model, + vin: form.vin, + deliveryMileage: form.mileageKm === '' ? null : Number(form.mileageKm), + deliveryElec: form.batteryPct === '' ? null : Number(form.batteryPct), + deliveryH2: form.hydrogenAmount === '' ? null : Number(form.hydrogenAmount), + deliveryH2Unit: form.hydrogenUnit, + deliveryStatus: '已保存' + }; + } + + function handleSaveClick() { + onSave && onSave(buildPatchFromForm()); + message.success('已保存'); + } + + function handleSubmitClick() { + var err = validateSubmit(); + if (err) { message.error(err); return; } + Modal.confirm({ + title: '确认交车', + content: '请确认信息填写无误,点击确认完成该车辆交车。', + okText: '确认', + cancelText: '取消', + onOk: function () { + setSubmitting(true); + setTimeout(function () { + setSubmitting(false); + onSubmit && onSubmit(buildPatchFromForm()); + message.success('交车成功'); + onClose && onClose(); + }, 400); + } + }); + } + + function handleTrainingPick(e) { + var f = e && e.target && e.target.files && e.target.files[0]; + if (!f) return; + setTimeout(function () { + updateForm({ + trainingRecognized: true, + driverLicenses: [ + { uid: 'id1', name: '身份证正面', url: 'https://dummyimage.com/600x400/e2e8f0/475569&text=ID-F' }, + { uid: 'id2', name: '身份证反面', url: 'https://dummyimage.com/600x400/e2e8f0/475569&text=ID-B' }, + { uid: 'dl', name: '驾驶证', url: 'https://dummyimage.com/600x400/e2e8f0/475569&text=DL' }, + { uid: 'qc', name: '从业资格证', url: 'https://dummyimage.com/600x400/e2e8f0/475569&text=QC' } + ] + }); + message.success('验车码识别成功'); + }, 500); + e.target.value = ''; + } + + if (!open) return null; + + var drawerTitle = React.createElement('div', { style: { paddingRight: 24 } }, + React.createElement('div', { style: { fontSize: 16, fontWeight: 700, color: '#0f172a', lineHeight: 1.4 } }, '编辑交车单'), + record ? React.createElement('div', { style: { marginTop: 6, fontSize: 13, color: '#64748b', display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center' } }, + React.createElement(Tag, { style: { margin: 0, border: 'none', background: '#eff6ff', color: '#2563eb', fontWeight: 600 } }, record.customerName || '-'), + React.createElement('span', null, record.contractCode || '-'), + React.createElement(Tag, { style: { margin: 0, border: 'none', background: '#f1f5f9', color: '#475569' } }, record.deliveryStatus || '未开始') + ) : null + ); + + var summaryCard = record ? React.createElement('div', { style: { marginBottom: 16, padding: '12px 14px', borderRadius: 10, background: 'linear-gradient(135deg,#f8fafc 0%,#fff 100%)', border: '1px solid #e2e8f0', display: 'grid', gridTemplateColumns: 'repeat(2,minmax(0,1fr))', gap: '8px 16px', fontSize: 13 } }, + React.createElement('div', null, React.createElement('span', { style: { color: '#94a3b8' } }, '项目:'), record.projectName || '-'), + React.createElement('div', null, React.createElement('span', { style: { color: '#94a3b8' } }, '交车地点:'), record.deliveryAddress || '-'), + React.createElement('div', null, React.createElement('span', { style: { color: '#94a3b8' } }, '品牌型号:'), (record.brand || '-') + ' / ' + (record.model || '-')), + React.createElement('div', null, React.createElement('span', { style: { color: '#94a3b8' } }, '任务来源:'), record.taskSource || '-') + ) : null; + + var sectionNavEl = React.createElement('div', { style: { position: 'sticky', top: 0, zIndex: 3, background: '#fff', padding: '8px 0 12px', marginBottom: 8, borderBottom: '1px solid #f1f5f9', display: 'flex', gap: 8, flexWrap: 'wrap' } }, + sectionNav.map(function (s) { + var active = activeSection === s.key; + return React.createElement('button', { + key: s.key, + type: 'button', + onClick: function () { scrollToSection(s.key); }, + style: { + border: 'none', + cursor: 'pointer', + padding: '6px 14px', + borderRadius: 999, + fontSize: 13, + fontWeight: active ? 600 : 500, + background: active ? '#2563eb' : '#f1f5f9', + color: active ? '#fff' : '#475569', + transition: 'background .2s,color .2s' + } + }, s.label); + }) + ); + + var sectionBasic = React.createElement('div', { id: 'dv-edit-section-basic', style: { scrollMarginTop: 56 } }, + React.createElement('div', { style: { fontSize: 14, fontWeight: 700, color: '#0f172a', marginBottom: 12, paddingLeft: 10, borderLeft: '3px solid #2563eb' } }, '车辆信息'), + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(2,minmax(0,1fr))', gap: '0 16px' } }, + React.createElement(FormItem, { label: '车牌号', required: true }, + React.createElement(Select, { value: form.plateNo, options: plateOptions, showSearch: true, filterOption: filterOption, placeholder: '请输入或选择车牌号', allowClear: true, style: { width: '100%' }, onChange: handlePlateChange }) + ), + React.createElement(FormItem, { label: '车辆类型' }, React.createElement(Input, { value: form.vehicleType, disabled: true })), + React.createElement(FormItem, { label: '品牌' }, React.createElement(Input, { value: form.brand, disabled: true })), + React.createElement(FormItem, { label: '型号' }, React.createElement(Input, { value: form.model, disabled: true })), + React.createElement(FormItem, { label: '车辆识别代码' }, React.createElement(Input, { value: form.vin, disabled: true })) + ) + ); + + var sectionEquip = React.createElement('div', { id: 'dv-edit-section-equip', style: { scrollMarginTop: 56, marginTop: 24 } }, + React.createElement('div', { style: { fontSize: 14, fontWeight: 700, color: '#0f172a', marginBottom: 12, paddingLeft: 10, borderLeft: '3px solid #2563eb' } }, '设备与证照'), + React.createElement(FormItem, { label: '车身广告及放大字', required: true }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 10 } }, + React.createElement(Switch, { checked: !!form.hasAd, onChange: function (v) { updateForm({ hasAd: !!v }); } }), + React.createElement('span', { style: { fontSize: 13, color: '#64748b' } }, form.hasAd ? '有车身广告' : '无车身广告') + ) + ), + form.hasAd ? React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginBottom: 8 } }, + React.createElement(UploadBox, { label: RequiredLabel('广告照片'), value: form.adPhoto, max: 1, onChange: function (l) { updateForm({ adPhoto: l }); } }), + React.createElement(UploadBox, { label: RequiredLabel('放大字照片'), value: form.bigWordPhoto, max: 1, onChange: function (l) { updateForm({ bigWordPhoto: l }); } }) + ) : null, + React.createElement(FormItem, { label: '尾板', required: true }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 10 } }, + React.createElement(Switch, { checked: !!form.hasTailboard, onChange: function (v) { updateForm({ hasTailboard: !!v }); } }), + React.createElement('span', { style: { fontSize: 13, color: '#64748b' } }, form.hasTailboard ? '有尾板' : '无尾板') + ) + ), + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 } }, + React.createElement(UploadBox, { label: RequiredLabel('备胎照片'), value: form.spareTirePhoto, max: 1, onChange: function (l) { updateForm({ spareTirePhoto: l }); if (l && l.length) ocrModalState[1]({ open: true, photoUrl: l[0].url, depth: '6.50' }); } }), + React.createElement(FormItem, { label: '备胎胎纹深度', required: true }, + React.createElement(Input, { value: form.spareTireDepth, placeholder: '请输入', addonAfter: 'mm', onChange: function (e) { updateForm({ spareTireDepth: e.target.value }); } }) + ) + ), + React.createElement(FormItem, { label: '驾驶培训', required: true }, + form.trainingRecognized + ? React.createElement('span', { style: { color: '#16a34a', fontWeight: 600, fontSize: 13 } }, '已完成视频培训') + : React.createElement(React.Fragment, null, + React.createElement('input', { type: 'file', ref: trainingInputRef, accept: 'image/*', style: { display: 'none' }, onChange: handleTrainingPick }), + React.createElement(Button, { onClick: function () { trainingInputRef.current && trainingInputRef.current.click(); } }, '上传验车码') + ) + ), + form.driverLicenses && form.driverLicenses.length ? React.createElement(FormItem, { label: '司机证照' }, + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(4,minmax(0,1fr))', gap: 12 } }, + form.driverLicenses.map(function (f) { + return React.createElement('div', { key: f.uid }, + makeThumb(f.url, function () { previewState[1]({ open: true, url: f.url, title: f.name }); }), + React.createElement('div', { style: { marginTop: 4, fontSize: 12, color: '#64748b', textAlign: 'center' } }, f.name) + ); + }) + ) + ) : null, + React.createElement(FormItem, { label: '车辆检查' }, + React.createElement(Button, { onClick: function () { inspectionOpenState[1](true); } }, '交车检查单') + ) + ); + + var sectionMetrics = React.createElement('div', { id: 'dv-edit-section-metrics', style: { scrollMarginTop: 56, marginTop: 24 } }, + React.createElement('div', { style: { fontSize: 14, fontWeight: 700, color: '#0f172a', marginBottom: 12, paddingLeft: 10, borderLeft: '3px solid #2563eb' } }, '交车数据'), + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(2,minmax(0,1fr))', gap: '0 16px' } }, + React.createElement(FormItem, { label: '交车里程', required: true }, + React.createElement(Input, { value: form.mileageKm, placeholder: '请输入', addonAfter: 'km', onChange: function (e) { updateForm({ mileageKm: e.target.value }); } }) + ), + React.createElement(FormItem, { label: '交车电量', required: true }, + React.createElement(Input, { value: form.batteryPct, placeholder: '请输入', addonAfter: '%', onChange: function (e) { updateForm({ batteryPct: e.target.value }); } }) + ), + React.createElement(FormItem, { label: '交车氢量', required: true }, + React.createElement(Input, { value: form.hydrogenAmount, placeholder: '请输入', addonAfter: form.hydrogenUnit, onChange: function (e) { updateForm({ hydrogenAmount: e.target.value }); } }) + ), + React.createElement(FormItem, { label: '送车服务费' }, + React.createElement(Input, { value: form.serviceFee, placeholder: '选填', addonAfter: '元', onChange: function (e) { updateForm({ serviceFee: e.target.value }); } }) + ) + ) + ); + + var photoKeys = ['仪表盘', '车辆正面', '车辆左前方']; + var photoState = useState({ vehicle: { '仪表盘': [], '车辆正面': [], '车辆左前方': [] } }); + var photos = photoState[0]; + var setPhotos = photoState[1]; + + var sectionPhotos = React.createElement('div', { id: 'dv-edit-section-photos', style: { scrollMarginTop: 56, marginTop: 24, marginBottom: 24 } }, + React.createElement('div', { style: { fontSize: 14, fontWeight: 700, color: '#0f172a', marginBottom: 12, paddingLeft: 10, borderLeft: '3px solid #2563eb' } }, '交车照片'), + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(3,minmax(0,1fr))', gap: 16 } }, + photoKeys.map(function (k) { + return React.createElement(UploadBox, { + key: k, + label: RequiredLabel(k), + value: photos.vehicle[k] || [], + max: 1, + onChange: function (l) { + setPhotos(function (p) { + var n = Object.assign({}, p); + n.vehicle = Object.assign({}, p.vehicle); + n.vehicle[k] = l; + return n; + }); + } + }); + }) + ), + React.createElement('div', { style: { marginTop: 8, fontSize: 12, color: '#94a3b8' } }, '原型:完整照片清单见交车单编辑页需求说明') + ); + + return React.createElement(React.Fragment, null, + React.createElement(Drawer, { + open: open, + onClose: onClose, + width: Math.min(960, typeof window !== 'undefined' ? window.innerWidth - 24 : 960), + title: drawerTitle, + destroyOnClose: true, + styles: { + body: { padding: '16px 20px 88px', background: '#f8fafc' }, + footer: { borderTop: '1px solid #e2e8f0', padding: '12px 20px' } + }, + footer: React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 10 } }, + React.createElement(Button, { onClick: onClose, disabled: submitting }, '取消'), + React.createElement(Button, { onClick: handleSaveClick, disabled: submitting }, '保存'), + React.createElement(Button, { type: 'primary', loading: submitting, onClick: handleSubmitClick }, '提交') + ) + }, + summaryCard, + sectionNavEl, + React.createElement('div', { style: { background: '#fff', borderRadius: 12, border: '1px solid #e2e8f0', padding: '16px 16px 8px' } }, + sectionBasic, + sectionEquip, + sectionMetrics, + sectionPhotos + ) + ), + React.createElement(Modal, { + open: !!previewState[0].open, + title: previewState[0].title || '预览', + footer: null, + onCancel: function () { previewState[1]({ open: false, url: '', title: '' }); }, + width: 860 + }, previewState[0].url ? React.createElement('img', { src: previewState[0].url, alt: previewState[0].title, style: { width: '100%', maxHeight: '70vh', objectFit: 'contain' } }) : null), + React.createElement(Modal, { + open: !!ocrModalState[0].open, + title: '备胎识别', + onCancel: function () { ocrModalState[1](Object.assign({}, ocrModalState[0], { open: false })); }, + onOk: function () { updateForm({ spareTireDepth: ocrModalState[0].depth }); ocrModalState[1](Object.assign({}, ocrModalState[0], { open: false })); message.success('已反写胎纹深度'); }, + okText: '确认', + cancelText: '取消' + }, + React.createElement(Input, { value: ocrModalState[0].depth, addonAfter: 'mm', onChange: function (e) { ocrModalState[1](Object.assign({}, ocrModalState[0], { depth: e.target.value })); } }) + ), + React.createElement(Drawer, { + open: inspectionOpenState[0], + title: '交车检查单', + width: 720, + onClose: function () { inspectionOpenState[1](false); }, + footer: React.createElement(Button, { type: 'primary', onClick: function () { message.success('检查单已保存'); inspectionOpenState[1](false); } }, '确定') + }, + React.createElement(Table, { + rowKey: 'key', + size: 'small', + pagination: false, + bordered: true, + dataSource: inspectionList, + columns: [ + { title: '类别', dataIndex: 'category', width: 100 }, + { title: '检查项目', dataIndex: 'item', width: 140 }, + { title: '检查情况', dataIndex: 'checked', width: 100, render: function (v) { return React.createElement(Switch, { checked: !!v, disabled: true }); } }, + { title: '备注', dataIndex: 'remark' } + ] + }) + ) + ); +} + +if (typeof window !== 'undefined') { + window.DeliveryEditDrawer = DeliveryEditDrawer; +} diff --git a/web端/运维管理/车辆业务/交车管理.jsx b/web端/运维管理/车辆业务/交车管理.jsx index 8ad8878..218128f 100644 --- a/web端/运维管理/车辆业务/交车管理.jsx +++ b/web端/运维管理/车辆业务/交车管理.jsx @@ -1,10 +1,1614 @@ // 【重要】必须使用 const Component 作为组件变量名 // 车辆业务 - 交车管理(ONEOS运管平台,布局参照新增租赁合同) +var DV_KPI_STYLE = '' + + '.dv-kpi-stats-row{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;margin-bottom:16px;}' + + '@media (max-width:768px){.dv-kpi-stats-row{grid-template-columns:repeat(1,minmax(0,1fr));}}' + + '.lc-alert-card{display:flex;align-items:flex-start;gap:12px;padding:14px 30px 14px 16px;border-radius:12px;border:1px solid #e2e8f0;background:#fff;position:relative;overflow:hidden;min-width:0;}' + + '.lc-alert-card-main{flex:1;min-width:0;}' + + '.lc-alert-card-icon{flex-shrink:0;width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;}' + + '.lc-alert-card-val{font-size:26px;font-weight:800;line-height:1.1;color:#0f172a;font-variant-numeric:tabular-nums;}' + + '.lc-alert-card-title{font-size:13px;font-weight:600;color:#334155;margin-top:2px;}' + + '.lc-alert-card-tip-anchor{position:absolute;top:8px;right:8px;z-index:2;line-height:0;}' + + '.lc-alert-card-tip{width:18px;height:18px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;color:#94a3b8;background:rgba(255,255,255,.92);border:1px solid #e2e8f0;cursor:help;line-height:0;}' + + '.lc-alert-card-tip:hover{color:#64748b;border-color:#cbd5e1;background:#fff;}' + + '.lc-alert-card--total{background:linear-gradient(135deg,#f8fafc 0%,#fff 100%);}' + + '.lc-alert-card--total .lc-alert-card-icon{background:#e2e8f0;color:#475569;}' + + '.lc-alert-card--progress{background:linear-gradient(135deg,#fff7ed 0%,#fff 55%);border-color:#fed7aa;}' + + '.lc-alert-card--progress .lc-alert-card-icon{background:#ffedd5;color:#ea580c;}' + + '.lc-alert-card--progress .lc-alert-card-val{color:#c2410c;}' + + '.lc-alert-card--completed{background:linear-gradient(135deg,#ecfdf5 0%,#fff 55%);border-color:#bbf7d0;}' + + '.lc-alert-card--completed .lc-alert-card-icon{background:#d1fae5;color:#059669;}' + + '.lc-alert-card--completed .lc-alert-card-val{color:#047857;}' + + '.lc-alert-card-clickable{cursor:pointer;transition:box-shadow .2s ease,border-color .2s ease,transform .2s ease;}' + + '.lc-alert-card-clickable:hover{box-shadow:0 4px 14px rgba(15,23,42,.08);}' + + '.lc-alert-card-active{box-shadow:0 0 0 2px rgba(22,93,255,.2)!important;border-color:#165dff!important;}'; + +var DV_KPI_ICONS = { + total: React.createElement('svg', { width: 18, height: 18, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' }, + React.createElement('rect', { x: 3, y: 3, width: 7, height: 7 }), React.createElement('rect', { x: 14, y: 3, width: 7, height: 7 }), + React.createElement('rect', { x: 14, y: 14, width: 7, height: 7 }), React.createElement('rect', { x: 3, y: 14, width: 7, height: 7 })), + progress: React.createElement('svg', { width: 18, height: 18, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' }, + React.createElement('circle', { cx: 12, cy: 12, r: 10 }), React.createElement('polyline', { points: '12 6 12 12 16 14' })), + completed: React.createElement('svg', { width: 18, height: 18, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' }, + React.createElement('path', { d: 'M22 11.08V12a10 10 0 1 1-5.93-9.14' }), React.createElement('polyline', { points: '22 4 12 14.01 9 11.01' })) +}; + +var DV_KPI_TIP_SVG = React.createElement('svg', { width: 12, height: 12, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2.2, strokeLinecap: 'round', strokeLinejoin: 'round' }, + React.createElement('circle', { cx: 12, cy: 12, r: 10 }), React.createElement('line', { x1: 12, y1: 16, x2: 12, y2: 12 }), React.createElement('line', { x1: 12, y1: 8, x2: 12.01, y2: 8 })); + +function formatDeliveryRegion(region) { + if (!region || region === '-') return '-'; + var r = String(region).trim(); + if (r.indexOf('-') === -1) { + var m = r.match(/^(.+?(?:省|市|自治区|特别行政区))(.+)$/); + if (m && m[2]) r = m[1] + '-' + m[2]; + } + return r; +} + +function parseDeliveryRegionParts(region) { + if (!region || region === '-') return { province: '', city: '', raw: '' }; + var r = String(region).trim(); + var idx = r.indexOf('-'); + if (idx >= 0) { + return { province: r.slice(0, idx).trim(), city: r.slice(idx + 1).trim(), raw: r }; + } + if (r.indexOf('省') >= 0) return { province: r, city: '', raw: r }; + if (r.indexOf('市') >= 0) return { province: '', city: r, raw: r }; + return { province: r, city: '', raw: r }; +} + +function regionCityMatch(cityA, cityB) { + if (!cityA || !cityB) return false; + return cityA === cityB || cityA.indexOf(cityB) >= 0 || cityB.indexOf(cityA) >= 0; +} + +/** 运维人员区域权限是否覆盖目标区域(省-市) */ +function matchRegionPermission(permissions, targetRegion) { + var target = parseDeliveryRegionParts(formatDeliveryRegion(targetRegion)); + if (!target.province && !target.city) return true; + var perms = permissions || []; + for (var i = 0; i < perms.length; i++) { + var perm = String(perms[i] || '').trim(); + if (!perm) continue; + if (perm.indexOf('-') >= 0) { + var scoped = parseDeliveryRegionParts(perm); + if (scoped.province && target.province && scoped.province !== target.province) continue; + if (scoped.city) { + if (regionCityMatch(target.city, scoped.city)) return true; + continue; + } + if (scoped.province && scoped.province === target.province) return true; + continue; + } + if (perm.indexOf('省') >= 0) { + if (target.province === perm) return true; + continue; + } + if (regionCityMatch(target.city, perm)) return true; + } + return false; +} + +var DV_RESERVE_VEHICLE_STATUS_READY = '已备车'; + +function getOperatorRegionPermissions() { + if (typeof window !== 'undefined' && window.DV_MOCK_OPERATOR_REGION_PERMISSIONS && window.DV_MOCK_OPERATOR_REGION_PERMISSIONS.length) { + return window.DV_MOCK_OPERATOR_REGION_PERMISSIONS.slice(); + } + return ['浙江省-嘉兴市']; +} + +function filterRowsByOperatorRegion(rows) { + var perms = getOperatorRegionPermissions(); + return (rows || []).filter(function (row) { + return matchRegionPermission(perms, row.deliveryRegion); + }); +} + +function filterSelectableReserveVehicles(vehicles, operatorPermissions) { + var perms = operatorPermissions || getOperatorRegionPermissions(); + return (vehicles || []).filter(function (vehicle) { + if ((vehicle.vehicleStatus || '') !== DV_RESERVE_VEHICLE_STATUS_READY) return false; + return matchRegionPermission(perms, vehicle.parkingRegion); + }); +} + +function isDeliverySignedStatus(status) { + return status === '客户已签章' || status === '已签章'; +} + +function buildDeliverySignFileName(record) { + var plate = (record.plateNo && String(record.plateNo).trim()) ? String(record.plateNo).trim() : '车牌待选'; + var orderId = record.orderId != null ? String(record.orderId) : 'unknown'; + var vehicleKey = record.vehicleKey != null ? String(record.vehicleKey) : ''; + return '交车签章文件_' + orderId + (vehicleKey ? '_' + vehicleKey : '') + '_' + plate + '.pdf'; +} + +function buildDeliverySignFileContent(record) { + var plate = (record.plateNo && String(record.plateNo).trim()) ? String(record.plateNo).trim() : '车牌待选'; + return [ + '交车签章文件(原型 Mock,联调后对接 E 签宝签章 PDF)', + '', + '合同编号:' + (record.contractCode || '-'), + '项目名称:' + (record.projectName || '-'), + '客户名称:' + (record.customerName || '-'), + '车牌号:' + plate, + '品牌型号:' + (record.brand || '-') + ' / ' + (record.model || '-'), + '交车人:' + (record.deliveryPerson || '-'), + '完成交车时间:' + (record.deliveryTime || '-'), + '签章状态:客户已签章', + '', + '生成时间:' + new Date().toLocaleString('zh-CN', { hour12: false }) + ].join('\n'); +} + +function escapeSignHtml(text) { + return String(text == null ? '-' : text) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); +} + +function buildDeliverySignPreviewHtml(record) { + var filename = buildDeliverySignFileName(record); + var plate = (record.plateNo && String(record.plateNo).trim()) ? String(record.plateNo).trim() : '车牌待选'; + var rows = [ + ['文件名称', filename], + ['合同编号', record.contractCode || '-'], + ['项目名称', record.projectName || '-'], + ['客户名称', record.customerName || '-'], + ['车牌号', plate], + ['品牌型号', (record.brand || '-') + ' / ' + (record.model || '-')], + ['交车人', record.deliveryPerson || '-'], + ['完成交车时间', record.deliveryTime || '-'], + ['签章状态', '客户已签章'], + ['签章方', record.customerName || '-'], + ['预览时间', new Date().toLocaleString('zh-CN', { hour12: false })] + ]; + var bodyRows = rows.map(function (row) { + return ''; + }).join(''); + return '' + + '' + escapeSignHtml(filename) + '' + + '
E签宝签章文件' + + '

' + escapeSignHtml(filename) + '

数字化资产 ONEOS · 交车管理 · 签章文件预览(原型 Mock)
' + + '
' + escapeSignHtml(row[0]) + '' + escapeSignHtml(row[1]) + '
' + bodyRows + '
' + + '
本页为原型预览占位,联调后将展示 E 签宝返回的真实签章 PDF 或在线预览地址。
' + + '
'; +} + +function previewDeliverySignFile(record) { + if (!record || !isDeliverySignedStatus(record.deliveryStatus || record.status)) return; + if (typeof window === 'undefined') return; + var html = buildDeliverySignPreviewHtml(record); + var blob = new Blob([html], { type: 'text/html;charset=utf-8' }); + var url = URL.createObjectURL(blob); + var opened = window.open(url, '_blank', 'noopener,noreferrer'); + if (!opened) { + URL.revokeObjectURL(url); + if (typeof message !== 'undefined' && message.warning) message.warning('请允许浏览器弹出窗口以预览签章文件'); + return; + } + if (typeof message !== 'undefined' && message.success) message.success('已在新页面打开签章文件预览'); + setTimeout(function () { URL.revokeObjectURL(url); }, 120000); +} + +function downloadDeliverySignFile(record) { + if (!record || !isDeliverySignedStatus(record.deliveryStatus || record.status)) return; + var filename = buildDeliverySignFileName(record); + var content = buildDeliverySignFileContent(record); + var blob = new Blob([content], { type: 'application/octet-stream' }); + var url = URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); + if (typeof message !== 'undefined' && message.success) message.success('已开始下载签章文件'); +} + +// 查看交车单:禁用输入框/选择器可读性增强(背景更淡、文字纯黑) +var DV_VIEW_READONLY_CTRL_CSS = '' + + '.dv-edit-drawer-view .ant-input[disabled],' + + '.dv-edit-drawer-view textarea.ant-input[disabled]{' + + 'color:#000!important;-webkit-text-fill-color:#000!important;' + + 'background-color:#fafafa!important;border-color:#e5e7eb!important;' + + 'cursor:default!important;opacity:1!important;' + + '}' + + '.dv-edit-drawer-view .ant-input-affix-wrapper-disabled,' + + '.dv-edit-drawer-view .ant-input-affix-wrapper[disabled]{' + + 'color:#000!important;background-color:#fafafa!important;' + + 'border-color:#e5e7eb!important;cursor:default!important;opacity:1!important;' + + '}' + + '.dv-edit-drawer-view .ant-input-affix-wrapper-disabled input[disabled],' + + '.dv-edit-drawer-view .ant-input-affix-wrapper[disabled] input[disabled]{' + + 'color:#000!important;-webkit-text-fill-color:#000!important;' + + 'background-color:transparent!important;cursor:default!important;' + + '}' + + '.dv-edit-drawer-view .ant-input-affix-wrapper-disabled .ant-input-suffix,' + + '.dv-edit-drawer-view .ant-input-affix-wrapper[disabled] .ant-input-suffix{' + + 'color:rgba(0,0,0,.65)!important;' + + '}' + + '.dv-edit-drawer-view .ant-select-disabled.ant-select .ant-select-selector{' + + 'color:#000!important;background-color:#fafafa!important;' + + 'border-color:#e5e7eb!important;cursor:default!important;opacity:1!important;' + + '}' + + '.dv-edit-drawer-view .ant-select-disabled .ant-select-selection-item,' + + '.dv-edit-drawer-view .ant-select-disabled .ant-select-selection-placeholder{' + + 'color:#000!important;' + + '}'; + +// 使用方式:window.DeliveryEditDrawer(props) + +function DeliveryEditDrawer(props) { + var useState = React.useState; + var useMemo = React.useMemo; + var useCallback = React.useCallback; + var useRef = React.useRef; + var useEffect = React.useEffect; + + var open = props.open; + var record = props.record; + var onClose = props.onClose; + var onSave = props.onSave; + var onSubmit = props.onSubmit; + var readOnly = props.readOnly === true || props.mode === 'view'; + + var antd = window.antd; + var Drawer = antd.Drawer; + var Button = antd.Button; + var Input = antd.Input; + var Select = antd.Select; + var Switch = antd.Switch; + var Modal = antd.Modal; + var Table = antd.Table; + var Tag = antd.Tag; + var message = antd.message; + + function RequiredLabel(text) { + return React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 4 } }, + React.createElement('span', { style: { color: '#ef4444', fontWeight: 600 } }, '*'), + React.createElement('span', null, text) + ); + } + + function isEmpty(v) { + return v === null || v === undefined || String(v).trim() === ''; + } + + function filterOption(input, option) { + var label = (option && (option.label || option.children)) || ''; + return String(label).toLowerCase().indexOf(String(input || '').toLowerCase()) >= 0; + } + + function fileToDataUrl(file, cb) { + try { + var reader = new FileReader(); + reader.onload = function (e) { cb(null, (e && e.target && e.target.result) || ''); }; + reader.onerror = function () { cb(new Error('read error')); }; + reader.readAsDataURL(file); + } catch (e) { cb(e); } + } + + var reserveVehicles = useMemo(function () { + return [ + { plateNo: '浙F80088', vehicleStatus: DV_RESERVE_VEHICLE_STATUS_READY, parkingRegion: '浙江省-嘉兴市', parkingLot: '嘉兴港区氢能停车场', vehicleType: '厢式车', brand: '福田', model: 'BJ1180', vin: 'LJNAU1A2XK7654321', hasAd: false, adPhoto: [], bigWordPhoto: [], hasTailboard: false }, + { plateNo: '浙F88601', vehicleStatus: DV_RESERVE_VEHICLE_STATUS_READY, parkingRegion: '浙江省-嘉兴市', parkingLot: '平湖指定停车场', vehicleType: '4.5吨冷链车', brand: '现代', model: '帕力安牌4.5吨冷链车', vin: 'LNBSCPKB8RR123888', hasAd: false, adPhoto: [], bigWordPhoto: [], hasTailboard: true }, + { plateNo: '浙A10088', vehicleStatus: DV_RESERVE_VEHICLE_STATUS_READY, parkingRegion: '浙江省-杭州市', parkingLot: '未来科技城地下停车场', vehicleType: '城配货车', brand: '福田', model: 'BJ1190', vin: 'LJNAU1A2XK8888001', hasAd: true, adPhoto: [{ uid: 'ad-hz', name: '广告照片.jpg', url: 'https://dummyimage.com/600x400/e2e8f0/475569&text=Ad-HZ' }], bigWordPhoto: [], hasTailboard: false }, + { plateNo: '粤AGP4598', vehicleStatus: DV_RESERVE_VEHICLE_STATUS_READY, parkingRegion: '广东省-广州市', parkingLot: '广州南沙物流园停车场', vehicleType: '4.5吨冷链车', brand: '现代', model: '帕力安牌4.5吨冷链车', vin: 'LNBSCPKB9RR223402', hasAd: false, adPhoto: [], bigWordPhoto: [], hasTailboard: false }, + { plateNo: '川A99999', vehicleStatus: DV_RESERVE_VEHICLE_STATUS_READY, parkingRegion: '四川省-成都市', parkingLot: '成都龙泉驿停车场', vehicleType: '4.5吨冷链车', brand: '现代', model: '帕力安牌4.5吨冷链车', vin: 'LNBSCPKB9RR223999', hasAd: false, adPhoto: [], bigWordPhoto: [], hasTailboard: false }, + { plateNo: '京A12345', vehicleStatus: '备车中', parkingRegion: '浙江省-嘉兴市', parkingLot: '嘉兴测试停车场', vehicleType: '牵引车', brand: '东风', model: 'DFH1180', vin: 'LJNAU1A2XK1234567', hasAd: true, adPhoto: [{ uid: 'ad1', name: '广告照片.jpg', url: 'https://dummyimage.com/600x400/e2e8f0/475569&text=Ad' }], bigWordPhoto: [{ uid: 'bw1', name: '放大字.jpg', url: 'https://dummyimage.com/600x400/e2e8f0/475569&text=BigWord' }], hasTailboard: true }, + { plateNo: '沪A30003', vehicleStatus: DV_RESERVE_VEHICLE_STATUS_READY, parkingRegion: '上海市-上海市', parkingLot: '浦东停车场', vehicleType: '厢式车', brand: '重汽', model: 'HOWO-T5G', vin: 'LJNAU1A2XK9999000', hasAd: true, adPhoto: [{ uid: 'ad2', name: '广告2.jpg', url: 'https://dummyimage.com/600x400/e2e8f0/475569&text=Ad2' }], bigWordPhoto: [{ uid: 'bw2', name: '放大字2.jpg', url: 'https://dummyimage.com/600x400/e2e8f0/475569&text=BW2' }], hasTailboard: true } + ]; + }, []); + + var plateOptions = useMemo(function () { + return filterSelectableReserveVehicles(reserveVehicles, getOperatorRegionPermissions()).map(function (v) { + return { + value: v.plateNo, + label: v.plateNo + ' · ' + (v.parkingLot || formatDeliveryRegion(v.parkingRegion)) + }; + }); + }, [reserveVehicles]); + + var vehicleByPlate = useMemo(function () { + var map = {}; + reserveVehicles.forEach(function (v) { map[v.plateNo] = v; }); + return map; + }, [reserveVehicles]); + + function isPlatePendingInForm(plateNo) { + if (plateNo == null || plateNo === undefined) return true; + var s = String(plateNo).trim(); + return s === '' || s === '-'; + } + + function displayDisabledField(v) { + var s = v == null || v === undefined ? '' : String(v).trim(); + return s || '-'; + } + + function displayFormVin(plateNo, vin) { + if (isPlatePendingInForm(plateNo)) return '-'; + return displayDisabledField(vin); + } + + function syncVehicleInfoFromReserveRecord(reserveRecord, deliveryRec, plate) { + // 备车记录 → 交车单「车辆信息」同步(驾驶培训字段不参与同步,见 buildInitialForm / handlePlateChange) + var hasPlate = plate && !isPlatePendingInForm(plate); + return { + vehicleType: (reserveRecord && reserveRecord.vehicleType) || (deliveryRec && deliveryRec.vehicleType) || '', + brand: (reserveRecord && reserveRecord.brand) || (deliveryRec && deliveryRec.brand) || '', + model: (reserveRecord && reserveRecord.model) || (deliveryRec && deliveryRec.model) || '', + vin: hasPlate ? ((reserveRecord && reserveRecord.vin) || (deliveryRec && deliveryRec.vin) || '') : '', + hasAd: !!(reserveRecord && reserveRecord.hasAd), + adPhoto: (reserveRecord && reserveRecord.adPhoto) ? reserveRecord.adPhoto.slice() : [], + bigWordPhoto: (reserveRecord && reserveRecord.bigWordPhoto) ? reserveRecord.bigWordPhoto.slice() : [], + hasTailboard: reserveRecord ? !!reserveRecord.hasTailboard : false, + spareTirePhoto: (reserveRecord && reserveRecord.spareTirePhoto) ? reserveRecord.spareTirePhoto.slice() : [], + spareTireDepth: (reserveRecord && reserveRecord.spareTireDepth) || '' + }; + } + + function buildInitialForm(rec) { + var plate = (rec && rec.plateNo && String(rec.plateNo).trim()) || undefined; + var reserveRecord = plate ? vehicleByPlate[plate] : null; + if (!reserveRecord && rec && rec.brand && plate) { + reserveRecord = { plateNo: plate, vehicleType: rec.vehicleType || '', brand: rec.brand || '', model: rec.model || '', vin: rec.vin || '', hasAd: false, adPhoto: [], bigWordPhoto: [], hasTailboard: false }; + } + var synced = syncVehicleInfoFromReserveRecord(reserveRecord, rec, plate); + return Object.assign({ + plateNo: plate, + trainingRecognized: false, + driverLicenses: [], + driverFrontPhoto: [], + mileageKm: rec && rec.deliveryMileage != null ? String(rec.deliveryMileage) : '', + batteryPct: rec && rec.deliveryElec != null ? String(rec.deliveryElec) : '', + hydrogenAmount: rec && rec.deliveryH2 != null ? String(rec.deliveryH2) : '', + hydrogenUnit: (rec && rec.deliveryH2Unit === 'MPa') ? 'MPa' : '%', + serviceFee: '' + }, synced); + } + + var formState = useState(buildInitialForm(null)); + var form = formState[0]; + var setForm = formState[1]; + var activeSectionState = useState('basic'); + var activeSection = activeSectionState[0]; + var setActiveSection = activeSectionState[1]; + var submittingState = useState(false); + var submitting = submittingState[0]; + var setSubmitting = submittingState[1]; + var previewState = useState({ open: false, url: '', title: '', gallery: [], index: 0 }); + var ocrModalState = useState({ open: false, photoUrl: '', depth: '6.50' }); + var trainingInputRef = useRef(null); + + useEffect(function () { + if (open && record) { + var initial = buildInitialForm(record); + var nextPhotos = createEmptyPhotos(); + if (readOnly && record.deliveryStatus && record.deliveryStatus !== '未开始' && record.deliveryStatus !== '已保存') { + initial = Object.assign({}, initial, buildDriverInfoFromPickupCode(), buildViewFormPhotoExtras()); + nextPhotos = buildViewDeliveryPhotos(); + } + setForm(initial); + setActiveSection('basic'); + setSubmitting(false); + setPhotos(nextPhotos); + setInspectionList(buildInspectionList()); + } + }, [open, record && record.id, readOnly]); + + function updateForm(patch) { + setForm(function (p) { return Object.assign({}, p, patch); }); + } + + function handlePlateChange(v) { + if (!v) { + updateForm({ + plateNo: undefined, + vehicleType: '', + brand: '', + model: '', + vin: '', + hasAd: false, + adPhoto: [], + bigWordPhoto: [], + hasTailboard: false, + spareTirePhoto: [], + spareTireDepth: '', + trainingRecognized: false, + driverLicenses: [], + driverFrontPhoto: [] + }); + return; + } + var reserveRecord = vehicleByPlate[v]; + updateForm(Object.assign({ + plateNo: v, + trainingRecognized: false, + driverLicenses: [], + driverFrontPhoto: [] + }, syncVehicleInfoFromReserveRecord(reserveRecord, null, v))); + } + + function makeThumb(url, onPreview, onRemove) { + return React.createElement('div', { style: { width: 72, height: 72, borderRadius: 8, border: '1px solid #e2e8f0', overflow: 'hidden', position: 'relative', background: '#f8fafc' } }, + React.createElement('img', { src: url, style: { width: '100%', height: '100%', objectFit: 'cover', cursor: 'pointer' }, onClick: onPreview }), + onRemove ? React.createElement('button', { + type: 'button', + 'aria-label': '删除图片', + style: { position: 'absolute', right: 4, top: 4, width: 22, height: 22, borderRadius: 999, border: 'none', background: 'rgba(15,23,42,.65)', color: '#fff', cursor: 'pointer', fontSize: 12, lineHeight: '22px', padding: 0 }, + onClick: function (e) { e.stopPropagation(); onRemove(); } + }, '×') : null + ); + } + + function mockViewPhoto(uid, name) { + return [{ uid: uid, name: name + '.jpg', url: 'https://dummyimage.com/640x360/e2e8f0/475569&text=' + encodeURIComponent(name) }]; + } + + function mockDeliveryPhotoItem(uid, seed, slotLabel) { + return { + uid: uid, + name: slotLabel + '.jpg', + url: 'https://picsum.photos/seed/dv-' + seed + '/960/540' + }; + } + + function mockDeliveryPhotoSlot(uid, seed, slotLabel) { + return [mockDeliveryPhotoItem(uid, seed, slotLabel)]; + } + + function mockDeliveryPhotoList(items) { + return items.map(function (it) { + return mockDeliveryPhotoItem(it.uid, it.seed, it.label); + }); + } + + function buildDeliveryPhotoGallery(photoData) { + var list = []; + var slotGroups = [ + { key: 'vehicle', label: '车辆' }, + { key: 'chassis', label: '底盘' }, + { key: 'tire', label: '轮胎' } + ]; + slotGroups.forEach(function (group) { + var groupMap = (photoData && photoData[group.key]) || {}; + Object.keys(groupMap).forEach(function (slotLabel) { + (groupMap[slotLabel] || []).forEach(function (file) { + list.push({ + uid: file.uid, + url: file.url, + name: file.name, + title: group.label + ' · ' + slotLabel + }); + }); + }); + }); + ['defect', 'other'].forEach(function (groupKey) { + var groupLabel = groupKey === 'defect' ? '瑕疵' : '其他'; + ((photoData && photoData[groupKey]) || []).forEach(function (file, idx) { + var files = (photoData && photoData[groupKey]) || []; + list.push({ + uid: file.uid, + url: file.url, + name: file.name, + title: files.length > 1 ? groupLabel + ' · ' + (file.name || (groupLabel + (idx + 1))) : groupLabel + ' · ' + (file.name || groupLabel) + }); + }); + }); + return list; + } + + function closePhotoPreview() { + previewState[1]({ open: false, url: '', title: '', gallery: [], index: 0 }); + } + + function openPhotoPreview(options) { + var gallery = (options && options.gallery) || []; + var index = 0; + if (gallery.length && options && options.uid) { + for (var i = 0; i < gallery.length; i++) { + if (gallery[i].uid === options.uid) { + index = i; + break; + } + } + } + var current = gallery.length ? gallery[index] : { url: options.url, title: options.title }; + previewState[1]({ + open: true, + url: current.url, + title: current.title || (options && options.title) || '预览', + gallery: gallery, + index: index + }); + } + + function shiftPhotoPreview(step) { + previewState[1](function (prev) { + var gallery = prev.gallery || []; + if (gallery.length <= 1) return prev; + var nextIndex = prev.index + step; + if (nextIndex < 0) nextIndex = gallery.length - 1; + if (nextIndex >= gallery.length) nextIndex = 0; + var item = gallery[nextIndex]; + return Object.assign({}, prev, { + index: nextIndex, + url: item.url, + title: item.title + }); + }); + } + + function buildViewFormPhotoExtras() { + return { + spareTirePhoto: mockViewPhoto('sp1', '备胎照片'), + spareTireDepth: '6.50' + }; + } + + function buildViewDeliveryPhotos() { + return { + vehicle: { + '仪表盘': mockDeliveryPhotoSlot('v1', 'vehicle-dash', '仪表盘'), + '车辆正前': mockDeliveryPhotoSlot('v2', 'vehicle-front', '车辆正前'), + '车辆左前方': mockDeliveryPhotoSlot('v3', 'vehicle-left-front', '车辆左前方'), + '车辆左后方': mockDeliveryPhotoSlot('v4', 'vehicle-left-rear', '车辆左后方'), + '车辆右前方': mockDeliveryPhotoSlot('v5', 'vehicle-right-front', '车辆右前方'), + '车辆右后方': mockDeliveryPhotoSlot('v6', 'vehicle-right-rear', '车辆右后方') + }, + chassis: { + '正前方位底部': mockDeliveryPhotoSlot('c1', 'chassis-front', '正前方位底部'), + '左侧前方底部': mockDeliveryPhotoSlot('c2', 'chassis-left-front', '左侧前方底部'), + '左侧后方底部': mockDeliveryPhotoSlot('c3', 'chassis-left-rear', '左侧后方底部'), + '正后方位底部': mockDeliveryPhotoSlot('c4', 'chassis-rear', '正后方位底部'), + '右侧前方底部': mockDeliveryPhotoSlot('c5', 'chassis-right-front', '右侧前方底部'), + '右侧后方底部': mockDeliveryPhotoSlot('c6', 'chassis-right-rear', '右侧后方底部') + }, + tire: { + '左前': mockDeliveryPhotoSlot('t1', 'tire-left-front', '左前'), + '右前': mockDeliveryPhotoSlot('t2', 'tire-right-front', '右前'), + '左后内': mockDeliveryPhotoSlot('t3', 'tire-left-rear-in', '左后内'), + '左后外': mockDeliveryPhotoSlot('t4', 'tire-left-rear-out', '左后外'), + '右后内': mockDeliveryPhotoSlot('t5', 'tire-right-rear-in', '右后内'), + '右后外': mockDeliveryPhotoSlot('t6', 'tire-right-rear-out', '右后外') + }, + defect: mockDeliveryPhotoList([ + { uid: 'd1', seed: 'defect-scratch', label: '瑕疵-刮擦' }, + { uid: 'd2', seed: 'defect-dent', label: '瑕疵-凹陷' }, + { uid: 'd3', seed: 'defect-paint', label: '瑕疵-漆面' } + ]), + other: mockDeliveryPhotoList([ + { uid: 'o1', seed: 'other-tools', label: '其他-随车工具' }, + { uid: 'o2', seed: 'other-cargo', label: '其他-车厢物品' } + ]) + }; + } + + function ReadonlyPhotoBox(boxProps) { + var label = boxProps.label; + var value = boxProps.value || []; + var gallery = boxProps.gallery; + return React.createElement('div', null, + label ? React.createElement('div', { style: { fontSize: 13, color: '#475569', marginBottom: 8, fontWeight: 500 } }, label) : null, + value.length > 0 + ? React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' } }, + value.map(function (f) { + return React.createElement('div', { key: f.uid }, + makeThumb(f.url, function () { + openPhotoPreview({ + gallery: gallery && gallery.length ? gallery : null, + uid: f.uid, + url: f.url, + title: (gallery && gallery.length) + ? ((gallery.filter(function (g) { return g.uid === f.uid; })[0] || {}).title || f.name || label || '预览') + : (f.name || label || '预览') + }); + }) + ); + }) + ) + : React.createElement('span', { style: { fontSize: 13, color: '#94a3b8' } }, '-') + ); + } + + function UploadBox(uploadProps) { + var label = uploadProps.label; + var value = uploadProps.value || []; + var unlimited = !!uploadProps.unlimited; + var max = unlimited ? Infinity : (uploadProps.max || 1); + var onChange = uploadProps.onChange; + var disabled = !!uploadProps.disabled; + function handlePick(e) { + if (disabled) return; + var f = e && e.target && e.target.files && e.target.files[0]; + if (!f) return; + fileToDataUrl(f, function (err, url) { + if (err) { message.error('上传失败'); return; } + var next = value.slice(); + next.push({ uid: String(Date.now()), name: f.name || 'image', url: url }); + if (!unlimited && next.length > max) next = next.slice(next.length - max); + onChange && onChange(next); + }); + e.target.value = ''; + } + return React.createElement('div', null, + label ? React.createElement('div', { style: { fontSize: 13, color: '#475569', marginBottom: 8, fontWeight: 500 } }, label) : null, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' } }, + value.map(function (f) { + return React.createElement('div', { key: f.uid }, + makeThumb(f.url, function () { previewState[1]({ open: true, url: f.url, title: f.name }); }, disabled ? null : function () { onChange && onChange(value.filter(function (x) { return x.uid !== f.uid; })); }) + ); + }), + (!disabled && (unlimited || value.length < max)) ? React.createElement('label', { style: { width: 72, height: 72, borderRadius: 8, border: '1px dashed #cbd5e1', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', color: '#64748b', cursor: 'pointer', fontSize: 20, lineHeight: 1, background: '#fff', transition: 'border-color .2s' } }, + React.createElement('input', { type: 'file', accept: 'image/*', style: { display: 'none' }, onChange: handlePick }), + React.createElement('span', { style: { fontSize: 22, lineHeight: 1, marginBottom: 2 } }, '+'), + React.createElement('span', { style: { fontSize: 12 } }, '上传') + ) : null + ), + uploadProps.tip ? React.createElement('div', { style: { marginTop: 6, fontSize: 12, color: '#94a3b8', lineHeight: 1.5 } }, uploadProps.tip) : null + ); + } + + var EDIT_PHOTO_UPLOAD_HINT = '照片上传说明:jpg、jpeg、png、gif、webp 格式,单张不超过 5MB,支持预览与删除(适用于本页所有照片上传项)'; + + function createEmptyPhotos() { + return { + vehicle: { + '仪表盘': [], + '车辆正前': [], + '车辆左前方': [], + '车辆左后方': [], + '车辆右前方': [], + '车辆右后方': [] + }, + chassis: { + '正前方位底部': [], + '左侧前方底部': [], + '左侧后方底部': [], + '正后方位底部': [], + '右侧前方底部': [], + '右侧后方底部': [] + }, + tire: { + '左前': [], + '右前': [], + '左后内': [], + '左后外': [], + '右后内': [], + '右后外': [] + }, + defect: [], + other: [] + }; + } + + function updatePhotoSlot(groupKey, slotKey, list) { + setPhotos(function (p) { + var n = Object.assign({}, p); + if (slotKey) { + n[groupKey] = Object.assign({}, p[groupKey] || {}); + n[groupKey][slotKey] = list; + } else { + n[groupKey] = list; + } + return n; + }); + } + + function PhotoModuleTitle(title, required) { + return React.createElement('div', { + style: { + display: 'flex', + alignItems: 'center', + gap: 8, + marginBottom: 10, + fontSize: 14, + fontWeight: 700, + color: '#0f172a' + } + }, + React.createElement('span', { style: { width: 3, height: 14, borderRadius: 2, background: '#2563eb', flexShrink: 0 } }), + (required && !readOnly) ? RequiredLabel(title) : title + ); + } + + function PhotoGridColumn(title, items, required, previewGallery) { + return React.createElement('div', { style: { marginBottom: 20, minWidth: 0 } }, + PhotoModuleTitle(title, required), + React.createElement('div', { style: { border: '1px solid #e2e8f0', borderRadius: 10, padding: 12, background: '#fafbfc' } }, + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 12 } }, + items.map(function (it) { + return React.createElement('div', { key: it.key }, + readOnly + ? ReadonlyPhotoBox({ label: it.label, value: it.value, gallery: previewGallery }) + : UploadBox({ + label: required ? RequiredLabel(it.label) : it.label, + value: it.value, + max: 1, + disabled: readOnly, + onChange: it.onChange + }) + ); + }) + ) + ) + ); + } + + function PhotoGridSimple(title, value, onChange, previewGallery) { + return React.createElement('div', { style: { marginBottom: 20, minWidth: 0 } }, + PhotoModuleTitle(title, false), + React.createElement('div', { style: { border: '1px solid #e2e8f0', borderRadius: 10, padding: 12, background: '#fafbfc' } }, + readOnly + ? ReadonlyPhotoBox({ value: value, gallery: previewGallery }) + : UploadBox({ value: value, unlimited: true, disabled: readOnly, onChange: onChange }) + ) + ); + } + + function buildPhotoGridItems(groupKey) { + var group = photos[groupKey] || {}; + return Object.keys(group).map(function (k) { + return { + key: groupKey + '-' + k, + label: k, + value: group[k] || [], + onChange: function (l) { updatePhotoSlot(groupKey, k, l); } + }; + }); + } + + function FormItem(itemProps) { + return React.createElement('div', { style: { marginBottom: 16, minWidth: 0 } }, + React.createElement('div', { style: { fontSize: 13, color: '#475569', marginBottom: 6, fontWeight: 500 } }, + itemProps.required ? RequiredLabel(itemProps.label) : itemProps.label + ), + itemProps.children + ); + } + + var inspectionCategoryItems = { + '车灯': ['大灯', '转向灯', '小灯', '示廓灯', '刹车灯', '倒车灯', '牌照灯', '防雾灯', '室内灯'], + '仪表盘': ['氢系统指示', '电控系统指示', '数值清晰准确', '故障报警灯'], + '驾驶室': ['点烟器', '车窗升降', '按键开关', '雨刮器', '内后视镜是否正常', '内/外门把手', '安全带', '空调冷暖风', '仪表盘', '门锁功能', '手刹', '车钥匙功能是否正常', '喇叭', '音响功能', '遮阳板', '主副驾座椅', '方向盘', '内饰干净整洁'], + '轮胎': ['前左胎', '前右胎', '后左胎', '后右胎', '备胎'], + '液位检查': ['冷却液', '制动液', '玻璃水'], + '外观检查': ['车身外观', '漆面', '玻璃'], + '车辆外观': ['整车外观'], + '其他': ['其他检查项'], + '随车工具': ['三角牌', '灭火器', '反光背心'], + '随车证件': ['行驶证', '营运证', '保险单'], + '整车': ['整车状态'], + '燃料电池系统': ['氢系统', '储氢瓶'], + '冷机': ['冷机运行'], + '制动系统': ['制动踏板', '驻车制动'] + }; + + function buildInspectionList() { + var list = []; + var categories = Object.keys(inspectionCategoryItems); + for (var ci = 0; ci < categories.length; ci++) { + var cat = categories[ci]; + var items = inspectionCategoryItems[cat] || []; + for (var ji = 0; ji < items.length; ji++) { + var it = items[ji]; + var isTire = cat === '轮胎'; + list.push({ + key: 'ins-' + ci + '-' + ji, + category: cat, + item: it, + checked: true, + treadDepth: isTire ? '6.5' : '', + remark: '' + }); + } + } + return list; + } + + var inspectionListState = useState(buildInspectionList); + var inspectionList = inspectionListState[0]; + var setInspectionList = inspectionListState[1]; + var inspectionListRef = useRef(null); + inspectionListRef.current = inspectionList; + + function updateInspectionRow(key, patch) { + setInspectionList(function (prev) { + return (prev || []).map(function (r) { + if (r.key !== key) return r; + return Object.assign({}, r, patch); + }); + }); + } + + var inspectionColumns = useMemo(function () { + return [ + { + title: '类别', + dataIndex: 'category', + key: 'category', + width: 120, + render: function (text, record, index) { + var rows = inspectionListRef.current || []; + var cat = record && record.category; + if (!cat) return { children: text, props: { rowSpan: 1 } }; + var isFirst = true; + for (var i = index - 1; i >= 0; i--) { + if (!rows[i] || rows[i].category !== cat) break; + isFirst = false; + break; + } + if (!isFirst) return { children: null, props: { rowSpan: 0 } }; + var span = 1; + for (var j = index + 1; j < rows.length; j++) { + if (!rows[j] || rows[j].category !== cat) break; + span++; + } + return { children: text, props: { rowSpan: span } }; + } + }, + { title: '检查项目', dataIndex: 'item', key: 'item', width: 180 }, + { + title: '检查情况', + dataIndex: 'checked', + key: 'checked', + width: 160, + render: function (_, insRecord) { + var isTire = insRecord && insRecord.category === '轮胎'; + return isTire + ? React.createElement(Input, { + value: insRecord.treadDepth, + placeholder: '请输入胎纹深度', + addonAfter: 'mm', + disabled: readOnly, + onChange: function (e) { updateInspectionRow(insRecord.key, { treadDepth: e.target.value }); } + }) + : React.createElement(Switch, { + checked: !!insRecord.checked, + disabled: readOnly, + onChange: function (v) { updateInspectionRow(insRecord.key, { checked: !!v }); } + }); + } + }, + { + title: '备注', + dataIndex: 'remark', + key: 'remark', + render: function (_, insRecord) { + return React.createElement(Input, { + value: insRecord.remark, + placeholder: '请输入', + disabled: readOnly, + onChange: function (e) { updateInspectionRow(insRecord.key, { remark: e.target.value }); } + }); + } + } + ]; + }, [readOnly]); + + var photoState = useState(createEmptyPhotos); + var photos = photoState[0]; + var setPhotos = photoState[1]; + + var showSignFileSection = readOnly && record && isDeliverySignedStatus(record.deliveryStatus || record.status); + var signFileName = showSignFileSection ? buildDeliverySignFileName(record) : ''; + + var sectionNav = [ + { key: 'basic', label: '交车车辆' }, + { key: 'equip', label: '车辆信息' }, + { key: 'metrics', label: '交车数据' }, + { key: 'inspection', label: '交车检查单' }, + { key: 'photos', label: '交车照片' } + ]; + if (showSignFileSection) { + sectionNav.push({ key: 'esign', label: 'E签宝签章' }); + } + + var SECTION_SCROLL_MARGIN = 12; + + function scrollToSection(key) { + setActiveSection(key); + var el = document.getElementById('dv-edit-section-' + key); + if (el && el.scrollIntoView) el.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + + function SectionCard(cardProps) { + return React.createElement('div', { + id: cardProps.id, + style: { + scrollMarginTop: SECTION_SCROLL_MARGIN, + marginBottom: 16, + background: '#fff', + borderRadius: 12, + border: '1px solid #e2e8f0', + boxShadow: '0 1px 2px rgba(15,23,42,.04)', + overflow: 'hidden' + } + }, + React.createElement('div', { + style: { + padding: '14px 16px', + borderBottom: '1px solid #f1f5f9', + fontSize: 14, + fontWeight: 700, + color: '#0f172a', + display: 'flex', + alignItems: 'center', + gap: 8 + } + }, + React.createElement('span', { style: { width: 3, height: 14, borderRadius: 2, background: '#2563eb', flexShrink: 0 } }), + cardProps.title + ), + React.createElement('div', { style: { padding: '16px 16px 8px' } }, cardProps.children) + ); + } + + function validateSubmit() { + if (isEmpty(form.plateNo)) return '请选择车牌号'; + if (form.hasAd) { + if (!form.adPhoto.length) return '请上传广告照片'; + if (!form.bigWordPhoto.length) return '请上传放大字照片'; + } + if (!form.trainingRecognized) return '请上传司机提车码并完成识别'; + if (isEmpty(form.mileageKm)) return '请填写交车里程'; + if (isEmpty(form.batteryPct)) return '请填写交车电量'; + if (isEmpty(form.hydrogenAmount)) return '请填写交车氢量'; + var requiredPhotoGroups = [ + { key: 'vehicle', label: '车辆' }, + { key: 'chassis', label: '底盘' }, + { key: 'tire', label: '轮胎' } + ]; + for (var pi = 0; pi < requiredPhotoGroups.length; pi++) { + var pg = requiredPhotoGroups[pi]; + var groupMap = photos && photos[pg.key]; + var keys = groupMap ? Object.keys(groupMap) : []; + for (var ki = 0; ki < keys.length; ki++) { + var pk = keys[ki]; + if (!(groupMap[pk] && groupMap[pk].length)) return '请上传' + pg.label + '照片:' + pk; + } + } + return ''; + } + + function buildPatchFromForm() { + return { + plateNo: form.plateNo || '', + vehicleType: form.vehicleType, + brand: form.brand, + model: form.model, + vin: form.vin, + deliveryMileage: form.mileageKm === '' ? null : Number(form.mileageKm), + deliveryElec: form.batteryPct === '' ? null : Number(form.batteryPct), + deliveryH2: form.hydrogenAmount === '' ? null : Number(form.hydrogenAmount), + deliveryH2Unit: form.hydrogenUnit, + deliveryStatus: '已保存' + }; + } + + function handleSaveClick() { + onSave && onSave(buildPatchFromForm()); + message.success('已保存'); + } + + function handleSubmitClick() { + var err = validateSubmit(); + if (err) { message.error(err); return; } + Modal.confirm({ + title: '确认交车', + content: '请确认信息填写无误,点击确认完成该车辆交车。', + okText: '确认', + cancelText: '取消', + onOk: function () { + setSubmitting(true); + setTimeout(function () { + setSubmitting(false); + onSubmit && onSubmit(buildPatchFromForm()); + message.success('交车成功'); + onClose && onClose(); + }, 400); + } + }); + } + + function isInvalidPickupCodeFile(file) { + var name = ((file && file.name) || '').toLowerCase(); + return name.indexOf('invalid') >= 0 || name.indexOf('无效') >= 0; + } + + function buildDriverInfoFromPickupCode() { + return { + trainingRecognized: true, + driverFrontPhoto: [{ uid: 'front', name: '司机正面照', url: 'https://dummyimage.com/600x400/e2e8f0/475569&text=Driver-Front' }], + driverLicenses: [ + { uid: 'id1', name: '身份证(正面)', url: 'https://dummyimage.com/600x400/e2e8f0/475569&text=ID-F' }, + { uid: 'id2', name: '身份证(反面)', url: 'https://dummyimage.com/600x400/e2e8f0/475569&text=ID-B' }, + { uid: 'dl', name: '驾驶证', url: 'https://dummyimage.com/600x400/e2e8f0/475569&text=DL' }, + { uid: 'qc', name: '从业资格证', url: 'https://dummyimage.com/600x400/e2e8f0/475569&text=QC' } + ] + }; + } + + function handleTrainingPick(e) { + var f = e && e.target && e.target.files && e.target.files[0]; + if (!f) return; + var pickedFile = f; + setTimeout(function () { + if (isInvalidPickupCodeFile(pickedFile)) { + updateForm({ trainingRecognized: false, driverFrontPhoto: [], driverLicenses: [] }); + message.error('提车码无效,请重新选择'); + return; + } + updateForm(buildDriverInfoFromPickupCode()); + message.success('提车码识别成功,已加载司机证照'); + }, 500); + e.target.value = ''; + } + + function openPickupCodePicker() { + trainingInputRef.current && trainingInputRef.current.click(); + } + + if (!open) return null; + + var drawerTitle = readOnly ? '查看交车单' : '编辑交车单'; + + var summaryLabelWidth = 80; + function renderSummaryField(labelText, valueText) { + return React.createElement('div', { style: { display: 'flex', alignItems: 'flex-start', gap: 6, minWidth: 0 } }, + React.createElement('span', { style: { color: '#94a3b8', flexShrink: 0, width: summaryLabelWidth, textAlign: 'left' } }, labelText), + React.createElement('span', { style: { color: '#334155', flex: 1, minWidth: 0, textAlign: 'left', wordBreak: 'break-all', lineHeight: 1.5 } }, valueText) + ); + } + + var summaryCard = record ? React.createElement('div', { style: { marginBottom: 16, padding: '12px 14px', borderRadius: 10, background: 'linear-gradient(135deg,#f8fafc 0%,#fff 100%)', border: '1px solid #e2e8f0', fontSize: 13 } }, + React.createElement('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center', marginBottom: 10, paddingBottom: 10, borderBottom: '1px solid #f1f5f9' } }, + React.createElement(Tag, { style: { margin: 0, border: 'none', background: '#eff6ff', color: '#2563eb', fontWeight: 600 } }, record.customerName || '-'), + React.createElement('span', { style: { color: '#475569' } }, record.contractCode || '-'), + (function () { + var status = record.deliveryStatus || '未开始'; + var signed = isDeliverySignedStatus(status); + return React.createElement(Tag, { + style: { + margin: 0, + border: 'none', + background: signed ? '#16a34a' : '#f1f5f9', + color: signed ? '#fff' : '#475569', + fontWeight: signed ? 600 : 400, + cursor: signed ? 'pointer' : 'default', + textDecoration: signed ? 'underline' : 'none', + textUnderlineOffset: signed ? '2px' : undefined + }, + title: signed ? '点击下载签章文件' : undefined, + role: signed ? 'button' : undefined, + tabIndex: signed ? 0 : undefined, + onClick: signed ? function (e) { e.stopPropagation(); downloadDeliverySignFile(record); } : undefined, + onKeyDown: signed ? function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); downloadDeliverySignFile(record); } } : undefined + }, status); + })() + ), + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(2,minmax(0,1fr))', gap: '8px 16px' } }, + renderSummaryField('项目:', record.projectName || '-'), + renderSummaryField('交车地点:', formatDeliveryRegion(record.deliveryRegion)), + renderSummaryField('品牌型号:', (record.brand || '-') + ' / ' + (record.model || '-')), + renderSummaryField('任务来源:', record.taskSource || '-') + ) + ) : null; + + var sectionNavEl = React.createElement('nav', { + 'aria-label': '交车单分节导航', + style: { + width: 108, + flexShrink: 0, + position: 'sticky', + top: 0, + alignSelf: 'flex-start', + paddingTop: 4, + zIndex: 2 + } + }, + React.createElement('div', { style: { fontSize: 12, color: '#94a3b8', fontWeight: 600, marginBottom: 8, paddingLeft: 12, letterSpacing: '.02em' } }, '目录'), + sectionNav.map(function (s) { + var active = activeSection === s.key; + return React.createElement('button', { + key: s.key, + type: 'button', + onClick: function () { scrollToSection(s.key); }, + style: { + display: 'block', + width: '100%', + textAlign: 'left', + border: 'none', + cursor: 'pointer', + padding: '9px 12px', + borderRadius: 8, + fontSize: 13, + lineHeight: 1.4, + fontWeight: active ? 600 : 500, + background: active ? '#eff6ff' : 'transparent', + color: active ? '#2563eb' : '#64748b', + borderLeft: active ? '3px solid #2563eb' : '3px solid transparent', + marginBottom: 2, + transition: 'background .2s,color .2s' + } + }, s.label); + }) + ); + + var sectionBasic = React.createElement(SectionCard, { id: 'dv-edit-section-basic', title: '交车车辆' }, + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(2,minmax(0,1fr))', gap: '0 16px' } }, + React.createElement(FormItem, { label: '车牌号', required: !readOnly }, + React.createElement(Select, { value: form.plateNo, options: plateOptions, showSearch: true, filterOption: filterOption, placeholder: '请选择已备车车辆(按区域权限过滤)', allowClear: !readOnly, disabled: readOnly, style: { width: '100%' }, onChange: handlePlateChange }) + ), + React.createElement(FormItem, { label: '车辆类型' }, React.createElement(Input, { value: displayDisabledField(form.vehicleType), disabled: true })), + React.createElement(FormItem, { label: '品牌' }, React.createElement(Input, { value: displayDisabledField(form.brand), disabled: true })), + React.createElement(FormItem, { label: '型号' }, React.createElement(Input, { value: displayDisabledField(form.model), disabled: true })), + React.createElement(FormItem, { label: '车辆识别代码' }, React.createElement(Input, { value: displayFormVin(form.plateNo, form.vin), disabled: true })) + ) + ); + + var sectionEquip = React.createElement(SectionCard, { id: 'dv-edit-section-equip', title: '车辆信息' }, + !readOnly ? React.createElement('div', { style: { marginBottom: 12, fontSize: 12, color: '#94a3b8', lineHeight: 1.6 } }, EDIT_PHOTO_UPLOAD_HINT) : null, + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0 16px', marginBottom: form.hasAd ? 8 : 0 } }, + React.createElement(FormItem, { label: '车身广告及放大字', required: !readOnly }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 10 } }, + React.createElement(Switch, { checked: !!form.hasAd, disabled: readOnly, onChange: function (v) { updateForm({ hasAd: !!v }); } }), + React.createElement('span', { style: { fontSize: 13, color: '#64748b' } }, form.hasAd ? '有车身广告' : '无车身广告') + ) + ), + React.createElement(FormItem, { label: '尾板', required: !readOnly }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 10 } }, + React.createElement(Switch, { checked: !!form.hasTailboard, disabled: readOnly, onChange: function (v) { updateForm({ hasTailboard: !!v }); } }), + React.createElement('span', { style: { fontSize: 13, color: '#64748b' } }, form.hasTailboard ? '有尾板' : '无尾板') + ) + ) + ), + form.hasAd ? React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginBottom: 8 } }, + readOnly + ? React.createElement(React.Fragment, null, + React.createElement(ReadonlyPhotoBox, { label: '广告照片', value: form.adPhoto }), + React.createElement(ReadonlyPhotoBox, { label: '放大字照片', value: form.bigWordPhoto }) + ) + : React.createElement(React.Fragment, null, + React.createElement(UploadBox, { label: RequiredLabel('广告照片'), value: form.adPhoto, max: 1, disabled: readOnly, onChange: function (l) { updateForm({ adPhoto: l }); } }), + React.createElement(UploadBox, { label: RequiredLabel('放大字照片'), value: form.bigWordPhoto, max: 1, disabled: readOnly, onChange: function (l) { updateForm({ bigWordPhoto: l }); } }) + ) + ) : null, + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 } }, + readOnly + ? React.createElement(ReadonlyPhotoBox, { label: '备胎照片', value: form.spareTirePhoto }) + : React.createElement(UploadBox, { label: '备胎照片', value: form.spareTirePhoto, max: 1, disabled: readOnly, onChange: function (l) { updateForm({ spareTirePhoto: l }); if (!readOnly && l && l.length) ocrModalState[1]({ open: true, photoUrl: l[0].url, depth: '6.50' }); } }), + React.createElement(FormItem, { label: '备胎胎纹深度' }, + React.createElement(Input, { value: form.spareTireDepth, placeholder: readOnly ? undefined : '请输入', addonAfter: 'mm', disabled: readOnly, onChange: function (e) { updateForm({ spareTireDepth: e.target.value }); } }) + ) + ), + React.createElement(FormItem, { label: '驾驶培训', required: !readOnly }, + form.trainingRecognized + ? React.createElement('div', null, + readOnly + ? React.createElement('span', { style: { color: '#16a34a', fontWeight: 600, fontSize: 13 } }, '已完成视频培训') + : React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' } }, + React.createElement('span', { style: { color: '#16a34a', fontWeight: 600, fontSize: 13 } }, '已完成视频培训'), + React.createElement(Button, { type: 'link', size: 'small', style: { padding: 0, height: 'auto' }, onClick: openPickupCodePicker }, '重新选择提车码') + ), + !readOnly ? React.createElement('div', { style: { marginTop: 8, fontSize: 12, color: '#64748b', lineHeight: 1.5 } }, '已根据提车码内绑定的司机信息,自动加载司机证照等照片') : null, + form.driverFrontPhoto && form.driverFrontPhoto.length ? React.createElement('div', { style: { marginTop: 12 } }, + React.createElement('div', { style: { fontSize: 13, color: '#475569', marginBottom: 8, fontWeight: 500 } }, '司机正面照'), + makeThumb(form.driverFrontPhoto[0].url, function () { previewState[1]({ open: true, url: form.driverFrontPhoto[0].url, title: '司机正面照' }); }) + ) : null + ) + : (readOnly + ? React.createElement('span', { style: { fontSize: 13, color: '#94a3b8' } }, '-') + : React.createElement(React.Fragment, null, + React.createElement('input', { type: 'file', ref: trainingInputRef, accept: 'image/*', style: { display: 'none' }, onChange: handleTrainingPick }), + React.createElement(Button, { onClick: openPickupCodePicker }, '上传司机提车码'), + React.createElement('div', { style: { marginTop: 8, fontSize: 12, color: '#94a3b8', lineHeight: 1.5 } }, '上传后将自动识别提车码内绑定的司机信息,并展示司机证照等照片') + )) + ), + form.driverLicenses && form.driverLicenses.length ? React.createElement(FormItem, { label: '司机证照' }, + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(4,minmax(0,1fr))', gap: 12 } }, + form.driverLicenses.map(function (f) { + return React.createElement('div', { key: f.uid }, + makeThumb(f.url, function () { previewState[1]({ open: true, url: f.url, title: f.name }); }), + React.createElement('div', { style: { marginTop: 4, fontSize: 12, color: '#64748b', textAlign: 'center' } }, f.name) + ); + }) + ) + ) : null + ); + + var sectionInspection = React.createElement(SectionCard, { id: 'dv-edit-section-inspection', title: '交车检查单' }, + React.createElement(Table, { + rowKey: 'key', + size: 'small', + pagination: false, + bordered: true, + dataSource: inspectionList, + columns: inspectionColumns, + scroll: { x: 640 } + }) + ); + + var sectionMetrics = React.createElement(SectionCard, { id: 'dv-edit-section-metrics', title: '交车数据' }, + React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(2,minmax(0,1fr))', gap: '0 16px' } }, + React.createElement(FormItem, { label: '交车里程', required: !readOnly }, + React.createElement(Input, { value: form.mileageKm, placeholder: '请输入', addonAfter: 'km', disabled: readOnly, onChange: function (e) { updateForm({ mileageKm: e.target.value }); } }) + ), + React.createElement(FormItem, { label: '交车电量', required: !readOnly }, + React.createElement(Input, { value: form.batteryPct, placeholder: '请输入', addonAfter: '%', disabled: readOnly, onChange: function (e) { updateForm({ batteryPct: e.target.value }); } }) + ), + React.createElement(FormItem, { label: '交车氢量', required: !readOnly }, + React.createElement(Input, { value: form.hydrogenAmount, placeholder: '请输入', addonAfter: form.hydrogenUnit, disabled: readOnly, onChange: function (e) { updateForm({ hydrogenAmount: e.target.value }); } }) + ), + React.createElement(FormItem, { label: '送车服务费' }, + React.createElement(Input, { value: form.serviceFee, placeholder: '选填', addonAfter: '元', disabled: readOnly, onChange: function (e) { updateForm({ serviceFee: e.target.value }); } }) + ) + ) + ); + + var deliveryPhotoGallery = readOnly ? buildDeliveryPhotoGallery(photos) : []; + + var sectionPhotos = React.createElement(SectionCard, { id: 'dv-edit-section-photos', title: '交车照片' }, + !readOnly ? React.createElement('div', { style: { marginBottom: 12, fontSize: 12, color: '#94a3b8', lineHeight: 1.6 } }, EDIT_PHOTO_UPLOAD_HINT) : null, + PhotoGridColumn('车辆', buildPhotoGridItems('vehicle'), !readOnly, deliveryPhotoGallery), + PhotoGridColumn('底盘', buildPhotoGridItems('chassis'), !readOnly, deliveryPhotoGallery), + PhotoGridColumn('轮胎', buildPhotoGridItems('tire'), !readOnly, deliveryPhotoGallery), + PhotoGridSimple('瑕疵', photos.defect || [], function (l) { updatePhotoSlot('defect', null, l); }, deliveryPhotoGallery), + PhotoGridSimple('其他', photos.other || [], function (l) { updatePhotoSlot('other', null, l); }, deliveryPhotoGallery) + ); + + var sectionEsign = showSignFileSection ? React.createElement(SectionCard, { id: 'dv-edit-section-esign', title: 'E签宝签章文件' }, + React.createElement('div', { + style: { + display: 'flex', + alignItems: 'flex-start', + gap: 14, + padding: '12px 14px', + borderRadius: 10, + border: '1px solid #e2e8f0', + background: 'linear-gradient(135deg,#f8fafc 0%,#fff 100%)' + } + }, + React.createElement('div', { + style: { + width: 44, + height: 44, + borderRadius: 10, + background: '#fee2e2', + color: '#dc2626', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + fontSize: 12, + fontWeight: 700, + flexShrink: 0 + } + }, 'PDF'), + React.createElement('div', { style: { flex: 1, minWidth: 0 } }, + React.createElement('div', { style: { fontSize: 14, fontWeight: 600, color: '#0f172a', wordBreak: 'break-all' } }, signFileName), + React.createElement('div', { style: { marginTop: 6, fontSize: 12, color: '#64748b', lineHeight: 1.6 } }, + React.createElement('div', null, '签章时间:', record.deliveryTime || '-'), + React.createElement('div', null, '签章方:', record.customerName || '-') + ) + ), + React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 8, flexShrink: 0 } }, + React.createElement(Button, { size: 'small', onClick: function () { previewDeliverySignFile(record); } }, '预览'), + React.createElement(Button, { type: 'primary', size: 'small', onClick: function () { downloadDeliverySignFile(record); } }, '下载') + ) + ) + ) : null; + + return React.createElement(React.Fragment, null, + readOnly ? React.createElement('style', null, DV_VIEW_READONLY_CTRL_CSS) : null, + React.createElement(Drawer, { + open: open, + onClose: onClose, + width: Math.min(960, typeof window !== 'undefined' ? window.innerWidth - 24 : 960), + title: drawerTitle, + destroyOnClose: true, + styles: { + body: { padding: '16px 20px 88px', background: '#f8fafc' }, + footer: { borderTop: '1px solid #e2e8f0', padding: '12px 20px' } + }, + footer: readOnly + ? React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end' } }, + React.createElement(Button, { onClick: onClose }, '关闭') + ) + : React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 10 } }, + React.createElement(Button, { onClick: onClose, disabled: submitting }, '取消'), + React.createElement(Button, { onClick: handleSaveClick, disabled: submitting }, '保存'), + React.createElement(Button, { type: 'primary', loading: submitting, onClick: handleSubmitClick }, '提交') + ) + }, + React.createElement('div', { className: readOnly ? 'dv-edit-drawer-view' : undefined }, + summaryCard, + React.createElement('div', { style: { display: 'flex', gap: 16, alignItems: 'flex-start' } }, + sectionNavEl, + React.createElement('div', { style: { flex: 1, minWidth: 0 } }, + sectionBasic, + sectionEquip, + sectionMetrics, + sectionInspection, + sectionPhotos, + sectionEsign + ) + ) + ) + ), + React.createElement(Modal, { + open: !!previewState[0].open, + title: previewState[0].title || '预览', + footer: null, + onCancel: closePhotoPreview, + width: 920, + styles: { body: { padding: '12px 20px 20px' } } + }, + previewState[0].url ? React.createElement('div', null, + React.createElement('div', { style: { textAlign: 'center', background: '#0f172a', borderRadius: 10, padding: 12 } }, + React.createElement('img', { + src: previewState[0].url, + alt: previewState[0].title, + style: { width: '100%', maxHeight: '72vh', objectFit: 'contain' } + }) + ), + previewState[0].gallery && previewState[0].gallery.length > 1 ? React.createElement('div', { + style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, marginTop: 16 } + }, + React.createElement(Button, { onClick: function () { shiftPhotoPreview(-1); } }, '上一张'), + React.createElement('span', { style: { fontSize: 13, color: '#64748b', fontVariantNumeric: 'tabular-nums' } }, + String(previewState[0].index + 1) + ' / ' + previewState[0].gallery.length + ), + React.createElement(Button, { type: 'primary', onClick: function () { shiftPhotoPreview(1); } }, '下一张') + ) : null + ) : null + ), + React.createElement(Modal, { + open: !!ocrModalState[0].open, + title: '备胎识别', + onCancel: function () { ocrModalState[1](Object.assign({}, ocrModalState[0], { open: false })); }, + onOk: function () { updateForm({ spareTireDepth: ocrModalState[0].depth }); ocrModalState[1](Object.assign({}, ocrModalState[0], { open: false })); message.success('已反写胎纹深度'); }, + okText: '确认', + cancelText: '取消' + }, + React.createElement(Input, { value: ocrModalState[0].depth, addonAfter: 'mm', onChange: function (e) { ocrModalState[1](Object.assign({}, ocrModalState[0], { depth: e.target.value })); } }) + ) + ); +} + +if (typeof window !== 'undefined') { + window.DeliveryEditDrawer = DeliveryEditDrawer; +} + + +function getDeliveryManageRequirementDoc() { + return [ + '交车管理 — 产品需求说明', + '模块:数字化资产 ONEOS 运管平台 · 运维管理 · 车辆业务 · 交车管理', + '版本:与当前原型一致(列表一车一行 + 抽屉编辑交车单)', + '', + '══════════════════════════════════════', + '一、模块目标', + '══════════════════════════════════════', + '为运维人员提供交车任务的全流程管理能力:按车辆维度查看交车进度、筛选统计、导出报表,并在列表内通过抽屉完成单车交车单查看、编辑、保存与提交。', + '', + '══════════════════════════════════════', + '二、页面结构', + '══════════════════════════════════════', + '2.1 面包屑:运维管理 / 车辆业务 / 交车管理', + '2.2 右上角「查看需求说明」:打开本说明弹窗', + '2.3 页面自上而下:筛选区 → KPI 统计卡片 → 列表区(含车牌快捷筛选、导出)', + '', + '══════════════════════════════════════', + '三、数据统计(KPI 卡片)', + '══════════════════════════════════════', + '替代原 Tab,三张可点击卡片联动列表,统计范围均为「当前筛选条件命中后的全部车辆行」:', + '3.1 全部交车任务:进行中 + 已完成', + '3.2 进行中的交车任务:交车状态为「未开始」「已保存」「待客户签章」', + '3.3 已完成的交车任务:交车状态为「客户已签章」(运维与客户 E 签宝均完成)', + '3.4 卡片右上角问号:悬停展示指标说明;点击卡片切换列表数据,选中态高亮', + '3.5 默认选中:进行中的交车任务', + '', + '══════════════════════════════════════', + '四、筛选区', + '══════════════════════════════════════', + '4.1 默认展示 4 列,点击「展开」显示全部 16 项;「收起」恢复默认', + '4.2 筛选项(多条件且关系,点击「搜索」生效):', + ' · 合同编号、项目名称、客户名称:可搜索下拉', + ' · 交车区域:省-市二级 Cascader', + ' · 完成交车时间:日期段 RangePicker,精确至天', + ' · 交车人、车牌号、车辆识别代码、车辆类型、品牌、型号', + ' · 业务部门、业务负责人、任务来源、业务类型、是否延期', + '4.3 「重置」:清空筛选至默认', + '4.4 列表左上角「车牌号」:独立于筛选区,变更即生效(快捷筛选)', + '', + '══════════════════════════════════════', + '五、列表(一车一行)', + '══════════════════════════════════════', + '5.1 数据粒度:一个交车任务含多车时,按车辆拆分为独立行展示', + '5.1.1 区域权限过滤(列表数据范围):按当前登录运维人员区域权限过滤,无权限的交车任务不在列表展示(见十二、区域权限与车牌选择)', + '5.2 列顺序与含义:', + ' (1) 车辆信息:三行 — 车牌号(待选时橙色「车牌待选」,可点击)、品牌-型号、车辆识别代码(待选时不展示 VIN,显示 -);点击车牌号打开「查看交车单」抽屉', + ' (2) 合同信息:客户名称+业务类型 Tag、合同编号(链接)、项目名称(链接);点击合同编号或项目名称跳转「合同管理-该合同详情页」', + ' (3) 业务负责人:业务部门、业务负责人(两行)', + ' (4) 任务来源:实心 Tag;「替换车」悬停 Popover 展示 旧车 → 新车', + ' (5) 交车地点:第一行交车区域(省-市);第二行停车场(车牌待选时显示 -)', + ' (6) 交车状态:实心 Tag(未开始/已保存/待客户签章/客户已签章等);状态为「客户已签章」时可点击下载签章文件(指针+下划线,悬停提示「点击下载签章文件」)', + ' (7) 完成交车时间:单行展示', + ' (8) 交车人', + ' (9) 是否归还(仅车辆已交车后展示归还状态,未交车显示 -):', + ' · 未归还:车辆已交车(待客户签章/客户已签章),尚未还车', + ' · 已归还:车辆已交车且已还车(仅客户已签章后可标记;悬停展示还车时间、还车人)', + ' · 未开始/已保存等未交车状态:显示 -', + ' (10) 交车记录:三行合并展示 — 交车里程(km)、交车氢量(%或MPa)、交车电量(%)', + ' (11) 交车任务创建时间:单行', + ' (12) 交车任务创建人', + ' (13) 操作:「查看」始终展示;「编辑」仅交车状态为「未开始」「已保存」时展示', + '5.3 合同跳转:写入 sessionStorage 合同编号/交车任务 ID,跳转合同管理-该合同详情页(原型以 message 提示)', + '5.4 合同信息列宽可拖拽调整', + '5.5 分页:默认 10 条/页,可选 10/20/50', + '', + '══════════════════════════════════════', + '六、导出', + '══════════════════════════════════════', + '6.1 导出范围:当前 KPI 标签 + 全部筛选条件(含列表左上角车牌号)下的全部命中行,不受分页限制', + '6.2 导出列:与业务导出样表一致,交车里程/氢量/电量为分列(非列表「交车记录」合并列)', + '6.3 列表区展示当前 KPI 标签名称及导出说明', + '', + '══════════════════════════════════════', + '七、编辑交车单(抽屉)', + '══════════════════════════════════════', + '7.1 入口:列表操作列「编辑」(仅未开始/已保存)', + '7.2 抽屉标题:编辑交车单', + '7.3 顶部摘要卡:客户名称 Tag、合同编号、交车状态;下方展示项目、交车地点(省-市)、品牌型号、任务来源(标签固定宽度、内容左对齐,项目与品牌型号等内容列对齐);交车状态为「客户已签章」时可点击下载签章文件', + '7.4 左侧固定目录锚点(点击平滑滚动,不遮挡内容):', + ' · 交车车辆 · 车辆信息 · 交车数据 · 交车检查单 · 交车照片', + '7.5 底部操作:取消、保存、提交', + '', + '--- 7.6 交车车辆 ---', + ' · 车牌号(必填):可搜索选择停车场内「已备车」车辆;支持清空(详见十二、区域权限与车牌选择)', + ' · 车牌待选时:车辆识别代码显示 -,不展示 VIN', + ' · 车辆类型、品牌、型号、车辆识别代码:只读,随车牌联动', + '', + '--- 7.7 车辆信息 ---', + '【备车同步规则 — 后台逻辑,页面不单独提示】', + ' · 打开编辑抽屉或选择车牌后,系统自动从该车辆对应备车记录同步以下字段至交车单:', + ' 车身广告开关及广告/放大字照片、尾板、备胎照片、备胎胎纹深度,以及车辆类型/品牌/型号/VIN 等基础信息', + ' · 不同步字段:驾驶培训相关(提车码识别状态、司机正面照、司机证照等),须在交车环节单独上传识别', + ' · 手动修改车身广告开关时,与备车「后装设备-车身广告」双向同步(安装时间以备车记录提交成功为准)', + '', + ' · 车身广告及放大字(必填开关):有广告时展示广告照片、放大字照片上传(各 1 张,必填)', + ' · 尾板(必填开关):与车身广告同一行左右排列', + ' · 备胎照片、备胎胎纹深度:非必填;上传备胎照片可 OCR 识别胎纹深度并反写', + ' · 照片上传格式说明:编辑页同一条文案,在「车辆信息」「交车照片」卡片顶部展示,不在各照片位下方重复', + ' · 驾驶培训(必填):', + ' - 上传司机提车码;识别成功后显示「已完成视频培训」,并自动加载提车码绑定的司机正面照、身份证(正/反)、驾驶证、从业资格证', + ' - 识别失败提示:「提车码无效,请重新选择」', + ' - 支持「重新选择提车码」', + '', + '--- 7.8 交车数据 ---', + ' · 交车里程(必填,km)、交车电量(必填,%)、交车氢量(必填,单位随车辆为 % 或 MPa)', + ' · 送车服务费(选填,元)', + '', + '--- 7.9 交车检查单 ---', + ' · 独立卡片内嵌表格,非抽屉', + ' · 列:类别(同类合并)、检查项目、检查情况(开关或轮胎胎纹深度 mm)、备注', + ' · 清单覆盖:车灯、仪表盘、驾驶室、轮胎、液位、外观、随车工具/证件、燃料电池、冷机、制动等', + '', + '--- 7.10 交车照片 ---', + ' · 模块标题「车辆」「底盘」「轮胎」「瑕疵」「其他」独立展示(蓝色竖线 + 标题行),下方为对应照片上传/预览区', + ' · 车辆(必填 6 点):仪表盘、车辆正前、车辆左前/左后、车辆右前/右后方', + ' · 底盘(必填 6 点):正前/正后方位底部,左/右侧前/后方底部', + ' · 轮胎(必填 6 点):左前、右前、左后内/外、右后内/外', + ' · 瑕疵、其他:各独立上传区,不限制张数', + ' · 编辑页照片上传说明全页仅展示一条文案,在「车辆信息」「交车照片」卡片顶部各显示一次,各上传位下方不再重复格式提示', + ' · 单点照片:jpg/jpeg/png/gif/webp,不超过 5MB;支持预览、删除', + '', + '--- 7.11 保存与提交 ---', + ' · 保存:交车状态 →「已保存」,回写车牌及交车数据等,抽屉不关闭', + ' · 提交:校验必填项 → 二次确认 → 交车状态 →「待客户签章」,写入完成交车时间/交车人,关闭抽屉', + ' · 提交校验含:车牌、广告(若开启)、提车码、交车里程/电量/氢量、车辆/底盘/轮胎全部必传照片点', + '', + '--- 7.12 区域权限与车牌选择(特别说明,后台逻辑) ---', + ' · 详见「十二、区域权限与车牌选择(特别说明)」', + '', + '══════════════════════════════════════', + '八、查看交车单(抽屉)', + '══════════════════════════════════════', + '8.1 入口:列表车辆信息列点击车牌号;或操作列「查看」(所有状态均可用)', + '8.2 抽屉标题:查看交车单', + '8.3 布局与编辑页一致:摘要卡 + 左侧目录 + 交车车辆/车辆信息/交车数据/交车检查单/交车照片各独立卡片;状态为「客户已签章」时额外展示「E签宝签章文件」卡片', + '8.4 只读规则:全部表单、开关、检查单编辑禁用;照片区不展示上传按钮及格式/操作说明,直接展示照片缩略图(可点击预览),无照片显示 -', + '8.4.1 查看页可读性:禁用 Input/Select 背景色 #fafafa(更淡),文字纯黑 #000,增强对比度;含带后缀的交车里程/电量等输入框', + '8.5 底部仅「关闭」按钮,无保存/提交', + '8.6 已提交交车单(非未开始/已保存)查看时,Mock 加载完整交车照片样例(车辆/底盘/轮胎 18 点位 + 瑕疵/其他);点击缩略图放大预览,支持「上一张/下一张」按交车点位顺序切换;驾驶培训区仅展示状态与证照图片', + '8.7 摘要卡交车状态为「客户已签章」时,点击状态 Tag 可下载签章文件', + '8.8 E签宝签章文件(仅查看页且状态为「客户已签章」时展示):', + ' · 左侧目录增加「E签宝签章」锚点,卡片标题「E签宝签章文件」', + ' · 展示签章 PDF 文件名、签章时间、签章方(客户名称)', + ' · 预览:新开浏览器页签打开签章文件预览(原型 Mock HTML 预览页,联调后对接 E 签宝 PDF/预览地址)', + ' · 下载:下载签章文件(文件名含交车单 ID、车辆序号、车牌号)', + '', + '══════════════════════════════════════', + '九、交车状态流转', + '══════════════════════════════════════', + '未开始 → 已保存(保存)→ 待客户签章(提交)→ 客户已签章(客户 E 签宝完成,列表归入「已完成」)', + '仅「未开始」「已保存」可编辑;任意状态均可查看', + '', + '══════════════════════════════════════', + '十、指标单位约定', + '══════════════════════════════════════', + ' · 交车里程:km', + ' · 交车电量:%', + ' · 交车氢量:% 或 MPa(按车辆/合同配置)', + ' · 列表「交车记录」合并展示;导出仍为三列', + '', + '══════════════════════════════════════', + '十一、非功能说明(原型)', + '══════════════════════════════════════', + ' · 备车车辆、提车码识别、OCR、E 签宝、合同详情跳转、签章文件下载等为前端 Mock,联调后对接真实接口', + ' · 合同详情:sessionStorage 写入 oneos_contract_code / oneos_delivery_order_id', + ' · 签章文件:列表/摘要卡 Tag 点击下载;查看页「E签宝签章文件」卡片支持预览(新开页)与下载', + ' · 区域权限:列表与车牌下拉按 window.DV_MOCK_OPERATOR_REGION_PERMISSIONS 过滤(默认浙江省-嘉兴市)', + ' · 提车码 Mock:文件名含 invalid/无效 模拟识别失败', + '', + '══════════════════════════════════════', + '十二、区域权限与车牌选择(特别说明)', + '══════════════════════════════════════', + '适用于编辑交车单及列表数据可见范围,为后台权限逻辑,页面不单独弹窗提示。', + '', + '12.1 交车任务可见范围(列表/执行权限)', + ' · 以交车单「交车区域」(省-市)为任务所属区域', + ' · 当前运维人员须具备该区域或其上级区域的运维权限,方可查看并执行该交车任务', + ' · 示例:交车区域为「浙江省-嘉兴市」时,区域权限为「浙江省」(省级)或「嘉兴市」(市级)的运维人员均可执行;', + ' 仅具备「四川省」「浙江省-杭州市」等不重合权限的人员,列表中看不到该条数据', + ' · 无权限:整条交车任务(含其下所有车辆行)均不可见,不可编辑', + '', + '12.2 停车场车牌号可选范围', + ' · 编辑交车单选择车牌时,下拉仅展示车辆状态为「已备车」的车辆', + ' · 非「已备车」状态(如备车中、待备车等)不可选', + '', + '12.3 当前运维操作人与停车场区域权限', + ' · 在「已备车」前提下,还需满足:车辆所在停车场区域 ⊆ 当前运维人员区域权限覆盖范围', + ' · 省级权限示例:区域权限为「浙江省」→ 可选浙江省内各停车场(杭州、嘉兴、宁波等)的全部已备车车辆', + ' · 市级权限示例:区域权限为「嘉兴市」→ 仅可选嘉兴市各停车场内的已备车车辆,不可选杭州市等省内其他城市停车场车辆', + ' · 下拉展示建议:车牌号 + 停车场名称,便于运维确认来源', + '', + '12.4 权限匹配规则摘要', + ' · 省级权限:匹配该省全部省-市交车区域/停车场区域', + ' · 市级权限:仅匹配该市交车区域/停车场区域', + ' · 省-市组合权限(如「浙江省-嘉兴市」):等同市级嘉兴市范围', + '', + '12.5 原型 Mock', + ' · 默认当前运维人员区域权限:浙江省-嘉兴市', + ' · 可在控制台设置 window.DV_MOCK_OPERATOR_REGION_PERMISSIONS = [\'浙江省\'] 验证全省可见/可选', + '' + ].join('\n'); +} + const Component = function () { var useState = React.useState; var useCallback = React.useCallback; var useMemo = React.useMemo; + var useEffect = React.useEffect; var antd = window.antd; var Breadcrumb = antd.Breadcrumb; @@ -12,47 +1616,69 @@ const Component = function () { var DatePicker = antd.DatePicker; var Select = antd.Select; var Button = antd.Button; - var Tabs = antd.Tabs; var Table = antd.Table; - var Popover = antd.Popover; var Cascader = antd.Cascader; + var Tag = antd.Tag; + var Tooltip = antd.Tooltip; + var Popover = antd.Popover; var Modal = antd.Modal; var message = antd.message; var RangePicker = DatePicker.RangePicker; - var filterState = useState({ - contractCode: undefined, - projectName: undefined, - customerName: undefined, - deliveryRegion: undefined, - dateStart: '', - dateEnd: '', - deliveryPerson: undefined - }); + function createEmptyFilters() { + return { + contractCode: undefined, + projectName: undefined, + customerName: undefined, + deliveryRegion: undefined, + dateStart: '', + dateEnd: '', + deliveryPerson: undefined, + plateNo: undefined, + vin: undefined, + vehicleType: undefined, + brand: undefined, + model: undefined, + businessDept: undefined, + businessOwner: undefined, + taskSource: undefined, + bizType: undefined, + isDelayed: undefined + }; + } + + function patchFilters(prev, patch) { + var next = {}; + var k; + for (k in prev) next[k] = prev[k]; + for (k in patch) next[k] = patch[k]; + return next; + } + + var filterState = useState(createEmptyFilters); var filters = filterState[0]; var setFilters = filterState[1]; - var appliedFilterState = useState({ - contractCode: undefined, - projectName: undefined, - customerName: undefined, - deliveryRegion: undefined, - dateStart: '', - dateEnd: '', - deliveryPerson: undefined - }); + var appliedFilterState = useState(createEmptyFilters); var appliedFilters = appliedFilterState[0]; var setAppliedFilters = appliedFilterState[1]; + var filterExpandedState = useState(false); + var filterExpanded = filterExpandedState[0]; + var setFilterExpanded = filterExpandedState[1]; - var activeTabState = useState('pending'); - var activeTab = activeTabState[0]; - var setActiveTab = activeTabState[1]; + /** total | inProgress | completed */ + var kpiFilterState = useState('inProgress'); + var kpiFilter = kpiFilterState[0]; + var setKpiFilter = kpiFilterState[1]; var pageState = useState(1); var pageSizeState = useState(10); + var colWidthsState = useState({ contractInfo: 240 }); + var colWidths = colWidthsState[0]; + var setColWidths = colWidthsState[1]; var requirementModalOpen = useState(false); var setRequirementModalOpen = requirementModalOpen[1]; - var requirementDocContent = '交车管理\n一个「数字化资产ONEOS运管平台」中的「交车管理」模块\n\n1.面包屑:\n1.1.运维管理-车辆业务-交车管理\n\n2.筛选:\n2.1.合同编码:选择器,默认为所有合同;提示信息为:请输入或选择合同编码,支持从输入框输入内容进行模糊搜索,下拉显示结果;\n2.2.项目名称:选择器,默认为所有项目;提示信息为:请输入或选择项目名称,支持从输入框输入内容进行模糊搜索,下拉显示结果;\n2.3.客户名称:选择器,默认为所有客户;提示信息为:请输入或选择客户名称,支持从输入框输入内容进行模糊搜索,下拉显示结果;\n2.4.交车区域:地区选择器,支持省-市2级筛选;\n2.5.交车时间:日期选择器,日期选择器,默认提示信息为:请选择交车开始时间 请选择交车结束时间,单输入框,双日历,支持时间段选择,精确至天,格式为:YYYY-MM-DD - YYYY-MM-DD;\n2.6.交车人:选择器,默认为所有交车人;提示信息为:请输入或选择交车人姓名,支持从输入框输入内容进行模糊搜索,下拉显示结果;\n2.7.查询:点击查询,根据单个或多个筛选条件(且)联动表格进行查询;\n2.8.重置:点击清空查询条件至默认;\n\n3.列表:\n分为两个tab:待处理、历史记录,默认显示为待处理tab;\n3.1.待处理tab:列表显示以下字段:\n3.1.1.预计交车时间:支持单日及开始-结束日期两种方式,格式为:YYYY-MM-DD及YYYY-MM-DD至YYYY-MM-DD,取自对应交车任务中预计交车时间;\n3.1.2.合同编码:显示租赁/自营合同编码;\n3.1.3.项目名称:显示租赁/自营合同项目名称;\n3.1.4.客户名称:显示租赁/自营合同客户名称;\n3.1.5.交车区域:显示交车区域,交车区域来自车辆租赁合同-交车区域,格式为省-市;\n3.1.6.交车地点:显示交车地点,交车地点来自车辆租赁合同-交车地点,显示详细地址;\n3.1.7.交车数量:显示交车数量,重点色显示,点击弹出气泡卡片,列表显示:车辆类型、品牌、型号、车牌号、交车时间、交车人员;\n 3.1.7.1.车辆类型:显示车辆类型;\n 3.1.7.2.品牌:显示车辆品牌;\n 3.1.7.3.型号:显示车辆型号;\n 3.1.7.4.车牌号:显示交车车辆车牌号,如果该车还未交车则显示为-;\n 3.1.7.5.交车时间:显示实际车辆完成交车时间,格式为:YYYY-MM-DD;\n 3.1.7.6.交车人员:显示实际车辆完成交车操作人姓名;\n3.1.8.创建时间:显示交车任务单创建时间,格式为:YYYY-MM-DD HH:MM;\n3.1.9.创建人:显示交车任务单创建人;\n3.1.10.最后修改时间:显示交车任务单最后修改时间,格式为:YYYY-MM-DD HH:MM;\n3.1.11.最后修改人:显示交车任务单最后修改人姓名;\n3.1.12.操作:交车;\n 3.1.12.1.交车单:点击跳转交车管理-交车单;\n\n3.2.历史记录:显示以下字段:\n3.2.1.预计交车时间:支持单日及开始-结束日期两种方式,格式为:YYYY-MM-DD及YYYY-MM-DD至YYYY-MM-DD,取自对应交车任务中预计交车时间;\n3.1.2.合同编码:显示租赁/自营合同编码;\n3.1.3.项目名称:显示租赁/自营合同项目名称;\n3.1.4.客户名称:显示租赁/自营合同客户名称;\n3.1.5.交车区域:显示交车区域,交车区域来自车辆租赁合同-交车区域,格式为省-市;\n3.1.6.交车地点:显示交车地点,交车地点来自车辆租赁合同-交车地点,显示详细地址;\n3.1.7.交车数量:显示交车数量,重点色显示,点击弹出气泡卡片,列表显示:车辆类型、品牌、型号、车牌号、交车时间、交车人员;\n 3.1.7.1.车辆类型:显示车辆类型;\n 3.1.7.2.品牌:显示车辆品牌;\n 3.1.7.3.型号:显示车辆型号;\n 3.1.7.4.车牌号:显示交车车辆车牌号,如果该车还未交车则显示为-;\n 3.1.7.5.交车时间:显示实际车辆完成交车时间,格式为:YYYY-MM-DD;\n 3.1.7.6.交车人员:显示实际车辆完成交车操作人姓名;\n3.1.8.创建时间:显示交车任务单创建时间,格式为:YYYY-MM-DD HH:MM;\n3.1.9.创建人:显示交车任务单创建人;\n3.1.10.最后修改时间:显示交车任务单最后修改时间,格式为:YYYY-MM-DD HH:MM;\n3.1.11.最后修改人:显示交车任务单最后修改人姓名;\n3.1.12.操作:查看、交车;\n 3.1.12.1.查看:跳转交车管理-查看页;'; + var requirementDocContent = getDeliveryManageRequirementDoc(); // 交车区域:省-市 二级 var regionOptions = [ @@ -62,127 +1688,459 @@ const Component = function () { ]; var contractCodeOptions = [ - { value: 'HT-ZL-2025-001', label: 'HT-ZL-2025-001' }, - { value: 'HT-ZL-2025-002', label: 'HT-ZL-2025-002' }, - { value: 'HT-ZL-2025-003', label: 'HT-ZL-2025-003' } + { value: 'LNZLHT', label: 'LNZLHT' }, + { value: 'HT-ZL-2024', label: 'HT-ZL-2024' }, + { value: 'HT-ZL-2025', label: 'HT-ZL-2025' } ]; var projectNameOptions = [ - { value: 'p1', label: '嘉兴氢能示范项目' }, - { value: 'p2', label: '上海物流租赁项目' }, - { value: 'p3', label: '杭州城配租赁项目' } + { value: 'p1', label: '桐乡韵达租赁4.5T*10' }, + { value: 'p2', label: '洛安供应链-租赁帕力安4.5T*30' }, + { value: 'p3', label: '嘉兴氢能示范项目' }, + { value: 'p4', label: '杭州城配租赁项目' } ]; var customerNameOptions = [ - { value: 'c1', label: '嘉兴某某物流有限公司' }, - { value: 'c2', label: '上海某某运输公司' }, - { value: 'c3', label: '杭州某某租赁有限公司' } + { value: 'c1', label: '桐乡市丰韵快递有限责任公司' }, + { value: 'c2', label: '武汉洛安供应链有限公司' }, + { value: 'c3', label: '嘉兴某某物流有限公司' }, + { value: 'c4', label: '杭州某某租赁有限公司' } ]; var deliveryPersonOptions = [ { value: '张三', label: '张三' }, { value: '李四', label: '李四' }, - { value: '王五', label: '王五' } + { value: '王五', label: '王五' }, + { value: '魏山', label: '魏山' }, + { value: '何苗苗', label: '何苗苗' } + ]; + var taskSourceOptions = [ + { value: '替换车', label: '替换车' }, + { value: '交车任务', label: '交车任务' } + ]; + var bizTypeOptions = [ + { value: '租赁', label: '租赁' }, + { value: '自营', label: '自营' } + ]; + var isDelayedOptions = [ + { value: 'yes', label: '是' }, + { value: 'no', label: '否' } ]; - // 待处理 mock:含创建时间、创建人、最后修改时间、最后修改人;vehicleList 含交车时间、交车人员(未交车为-) - var pendingListState = useState([ - { id: 'd1', expectedDate: '2025-02-28', contractCode: 'HT-ZL-2025-001', projectName: '嘉兴氢能示范项目', customerName: '嘉兴某某物流有限公司', deliveryRegion: '浙江省-嘉兴市', deliveryAddress: '嘉兴市南湖区科技大道1号', deliveryCount: 2, vehicleList: [{ vehicleType: '厢式货车', brand: '东风', model: 'DFH1180', plateNo: '京A12345', deliveryTime: '2025-02-28', deliveryPerson: '张三' }, { vehicleType: '平板货车', brand: '福田', model: 'BJ1180', plateNo: '-', deliveryTime: '-', deliveryPerson: '-' }], createTime: '2025-02-20 09:00', createBy: '系统', lastModifyTime: '2025-02-22 14:30', lastModifyBy: '李四', assignedDeliveryPerson: '张三' }, - { id: 'd2', expectedDate: '2025-03-01 至 2025-03-05', contractCode: 'HT-ZL-2025-002', projectName: '上海物流租赁项目', customerName: '上海某某运输公司', deliveryRegion: '上海市-上海市', deliveryAddress: '浦东新区张江高科技园区', deliveryCount: 1, vehicleList: [{ vehicleType: '厢式货车', brand: '江淮', model: 'HFC1180', plateNo: '-', deliveryTime: '-', deliveryPerson: '-' }], createTime: '2025-02-21 10:30', createBy: '王五', lastModifyTime: '2025-02-21 10:30', lastModifyBy: '王五', assignedDeliveryPerson: '李四' }, - { id: 'd3', expectedDate: '2025-02-25', contractCode: 'HT-ZL-2025-003', projectName: '杭州城配租赁项目', customerName: '杭州某某租赁有限公司', deliveryRegion: '浙江省-杭州市', deliveryAddress: '余杭区未来科技城', deliveryCount: 3, vehicleList: [{ vehicleType: '栏板货车', brand: '重汽', model: 'ZZ1180', plateNo: '浙A10001', deliveryTime: '2025-02-25', deliveryPerson: '张三' }, { vehicleType: '厢式货车', brand: '东风', model: 'DFH1190', plateNo: '-', deliveryTime: '-', deliveryPerson: '-' }, { vehicleType: '平板货车', brand: '福田', model: 'BJ1190', plateNo: '-', deliveryTime: '-', deliveryPerson: '-' }], createTime: '2025-02-18 14:00', createBy: '李四', lastModifyTime: '2025-02-24 11:00', lastModifyBy: '王五', assignedDeliveryPerson: '张三' }, - { id: 'd4', expectedDate: '2025-03-08', contractCode: 'HT-ZL-2025-004', projectName: '嘉兴氢能示范项目', customerName: '嘉兴某某物流有限公司', deliveryRegion: '浙江省-嘉兴市', deliveryAddress: '嘉兴市秀洲区洪兴西路288号', deliveryCount: 1, vehicleList: [{ vehicleType: '厢式货车', brand: '重汽', model: 'ZZ1160', plateNo: '-', deliveryTime: '-', deliveryPerson: '-' }], createTime: '2025-02-25 08:15', createBy: '张三', lastModifyTime: '2025-02-25 08:15', lastModifyBy: '张三', assignedDeliveryPerson: '李四' }, - { id: 'd5', expectedDate: '2025-03-10 至 2025-03-12', contractCode: 'HT-ZL-2025-005', projectName: '上海物流租赁项目', customerName: '上海某某运输公司', deliveryRegion: '上海市-上海市', deliveryAddress: '闵行区莘庄工业区申富路669号', deliveryCount: 2, vehicleList: [{ vehicleType: '平板货车', brand: '福田', model: 'BJ1180', plateNo: '-', deliveryTime: '-', deliveryPerson: '-' }, { vehicleType: '栏板货车', brand: '东风', model: 'DFH1160', plateNo: '-', deliveryTime: '-', deliveryPerson: '-' }], createTime: '2025-02-26 11:20', createBy: '王五', lastModifyTime: '2025-02-26 11:20', lastModifyBy: '王五', assignedDeliveryPerson: '王五' }, - { id: 'd6', expectedDate: '2025-03-15', contractCode: 'HT-ZL-2025-006', projectName: '杭州城配租赁项目', customerName: '杭州某某租赁有限公司', deliveryRegion: '浙江省-杭州市', deliveryAddress: '萧山区市心北路108号', deliveryCount: 4, vehicleList: [{ vehicleType: '厢式货车', brand: '江淮', model: 'HFC1180', plateNo: '-', deliveryTime: '-', deliveryPerson: '-' }, { vehicleType: '厢式货车', brand: '东风', model: 'DFH1180', plateNo: '-', deliveryTime: '-', deliveryPerson: '-' }, { vehicleType: '平板货车', brand: '福田', model: 'BJ1190', plateNo: '-', deliveryTime: '-', deliveryPerson: '-' }, { vehicleType: '栏板货车', brand: '重汽', model: 'ZZ1180', plateNo: '-', deliveryTime: '-', deliveryPerson: '-' }], createTime: '2025-03-01 09:30', createBy: '李四', lastModifyTime: '2025-03-01 09:30', lastModifyBy: '李四', assignedDeliveryPerson: '张三' }, - { id: 'd7', expectedDate: '2025-03-18', contractCode: 'HT-ZL-2025-007', projectName: '嘉兴氢能示范项目', customerName: '嘉兴某某物流有限公司', deliveryRegion: '浙江省-嘉兴市', deliveryAddress: '嘉兴市经开区昌盛路1号', deliveryCount: 1, vehicleList: [{ vehicleType: '平板货车', brand: '福田', model: 'BJ1180', plateNo: '浙F80088', deliveryTime: '-', deliveryPerson: '-' }], createTime: '2025-03-03 14:00', createBy: '张三', lastModifyTime: '2025-03-05 10:00', lastModifyBy: '李四', assignedDeliveryPerson: '李四' }, - { id: 'd8', expectedDate: '2025-03-20 至 2025-03-22', contractCode: 'HT-ZL-2025-008', projectName: '上海物流租赁项目', customerName: '上海某某运输公司', deliveryRegion: '上海市-上海市', deliveryAddress: '宝山区沪太路5008号', deliveryCount: 3, vehicleList: [{ vehicleType: '厢式货车', brand: '江淮', model: 'HFC1190', plateNo: '-', deliveryTime: '-', deliveryPerson: '-' }, { vehicleType: '栏板货车', brand: '重汽', model: 'ZZ1190', plateNo: '-', deliveryTime: '-', deliveryPerson: '-' }, { vehicleType: '厢式货车', brand: '东风', model: 'DFH1190', plateNo: '-', deliveryTime: '-', deliveryPerson: '-' }], createTime: '2025-03-05 10:00', createBy: '王五', lastModifyTime: '2025-03-05 10:00', lastModifyBy: '王五', assignedDeliveryPerson: '王五' }, - { id: 'd9', expectedDate: '2025-03-25', contractCode: 'HT-ZL-2025-009', projectName: '杭州城配租赁项目', customerName: '杭州某某租赁有限公司', deliveryRegion: '浙江省-杭州市', deliveryAddress: '滨江区网商路699号', deliveryCount: 2, vehicleList: [{ vehicleType: '栏板货车', brand: '东风', model: 'DFH1160', plateNo: '-', deliveryTime: '-', deliveryPerson: '-' }, { vehicleType: '平板货车', brand: '福田', model: 'BJ1180', plateNo: '浙A20002', deliveryTime: '-', deliveryPerson: '-' }], createTime: '2025-03-08 08:45', createBy: '李四', lastModifyTime: '2025-03-08 08:45', lastModifyBy: '李四', assignedDeliveryPerson: '张三' }, - { id: 'd10', expectedDate: '2025-03-28', contractCode: 'HT-ZL-2025-010', projectName: '嘉兴氢能示范项目', customerName: '嘉兴某某物流有限公司', deliveryRegion: '浙江省-嘉兴市', deliveryAddress: '嘉兴市南湖区广益路与由拳路交叉口', deliveryCount: 1, vehicleList: [{ vehicleType: '厢式货车', brand: '重汽', model: 'ZZ1180', plateNo: '-', deliveryTime: '-', deliveryPerson: '-' }], createTime: '2025-03-10 16:20', createBy: '张三', lastModifyTime: '2025-03-10 16:20', lastModifyBy: '张三', assignedDeliveryPerson: '李四' } + var DV_ORDER_KEY = 'oneos_delivery_order_id'; + var DV_VEHICLE_KEY = 'oneos_delivery_vehicle_key'; + var DV_NAV_KEY = 'oneos_delivery_navigate_target'; + var DV_CONTRACT_CODE_KEY = 'oneos_contract_code'; + var DV_LIST_LINK_STYLE = { color: '#165dff', cursor: 'pointer' }; + + /** 交车状态:进行中=未开始/已保存/待客户签章;历史=客户已签章(运维+客户 E 签宝均完成) */ + var DELIVERY_STATUS_HISTORY = '客户已签章'; + var DELIVERY_STATUS_IN_PROGRESS = ['未开始', '已保存', '待客户签章']; + + function isDeliveryHistoryStatus(status) { + return status === DELIVERY_STATUS_HISTORY || status === '已签章'; + } + + function isDeliveryInProgressStatus(status) { + return DELIVERY_STATUS_IN_PROGRESS.indexOf(status || '未开始') >= 0; + } + + /** 运维侧已完成交车提交(待签章或已签章)视为车辆已交车 */ + function isVehicleDelivered(status) { + return status === '待客户签章' || isDeliveryHistoryStatus(status); + } + + function canEditDeliveryRow(record) { + var s = record.deliveryStatus; + return s === '未开始' || s === '已保存'; + } + + // 交车任务(一单多车);列表按车辆拆分为独立行,表头对齐导出样表 + var deliveryOrdersState = useState([ + { + id: 'o1', expectedDate: '2025-02-28 至 2025-03-05', contractCode: 'LNZLHT 20260104001', projectName: '桐乡韵达租赁4.5T*10', customerName: '桐乡市丰韵快递有限责任公司', businessDept: '业务二部', businessOwner: '刘念念', taskSource: '替换车', replaceOldPlate: '浙A88601F', bizType: '租赁', deliveryRegion: '浙江省-嘉兴市', deliveryAddress: '平湖指定停车场', createTime: '2026-06-04 11:28', createBy: '赵小峰', + vehicleList: [ + { vehicleKey: 1, seq: 1, vehicleType: '4.5吨冷链车', brand: '现代', model: '帕力安牌4.5吨冷链车', vin: 'LNBSCPKB8RR123401', replaceOldPlate: '浙A88601F', plateNo: '', deliveryTime: '', deliveryPerson: '', deliveryStatus: '未开始', deliveryMileage: null, deliveryH2: null, deliveryElec: null }, + { vehicleKey: 2, seq: 2, vehicleType: '4.5吨冷链车', brand: '现代', model: '帕力安牌4.5吨冷链车', vin: 'LNBSCPKB8RR123402', replaceOldPlate: '浙A88602F', plateNo: '', deliveryTime: '', deliveryPerson: '', deliveryStatus: '未开始', deliveryMileage: null, deliveryH2: null, deliveryElec: null } + ] + }, + { + id: 'o2', expectedDate: '2025-03-01', contractCode: 'LNZLHT2026040301-042', projectName: '洛安供应链-租赁帕力安4.5T*30', customerName: '武汉洛安供应链有限公司', businessDept: '业务三部', businessOwner: '金可鹏', taskSource: '交车任务', bizType: '租赁', deliveryRegion: '四川省-成都市', deliveryAddress: '成都龙泉驿停车场', createTime: '2026-05-31 14:07', createBy: '何苗苗', + vehicleList: [ + { vehicleKey: 1, seq: 1, vehicleType: '4.5吨冷链车', brand: '现代', model: '帕力安牌4.5吨冷链车', vin: 'LNBSCPKB9RR223401', plateNo: '粤AGP9827', deliveryTime: '2026-06-03 18:20', deliveryPerson: '魏山', deliveryStatus: '待客户签章', deliveryMileage: 48202, deliveryH2: 19, deliveryH2Unit: '%', deliveryElec: 82 }, + { vehicleKey: 2, seq: 2, vehicleType: '4.5吨冷链车', brand: '现代', model: '帕力安牌4.5吨冷链车', vin: 'LNBSCPKB9RR223402', plateNo: '粤AGP4598', deliveryTime: '2026-06-02 11:00', deliveryPerson: '魏山', deliveryStatus: '已保存', deliveryMileage: null, deliveryH2: null, deliveryElec: null } + ] + }, + { + id: 'o3', expectedDate: '2025-03-08', contractCode: 'LNZLHT2025042201', projectName: '炽瑞-租赁现代4.5T', customerName: '东莞沙田炽瑞物流有限公司', businessDept: '业务三部', businessOwner: '金可鹏', taskSource: '替换车', replaceOldPlate: '粤B58888F', bizType: '租赁', deliveryRegion: '广东省-广州市', deliveryAddress: '广州南沙物流园停车场', createTime: '2026-05-28 20:30', createBy: '童军林', + vehicleList: [ + { vehicleKey: 1, seq: 1, vehicleType: '4.5吨货车', brand: '现代', model: '4.5吨货车', vin: 'LNBSCPKB7RR323401', replaceOldPlate: '粤B58888F', plateNo: '', deliveryTime: '', deliveryPerson: '', deliveryStatus: '未开始', deliveryMileage: null, deliveryH2: null, deliveryElec: null } + ] + }, + { + id: 'o4', expectedDate: '2024-11-15', contractCode: 'LNZLHT2024111401', projectName: '聚德11月新增苏龙18T*2', customerName: '沈阳聚德物流有限公司', businessDept: '业务三部', businessOwner: '金可鹏', taskSource: '交车任务', bizType: '租赁', deliveryRegion: '浙江省-嘉兴市', deliveryAddress: '嘉兴港区氢能停车场', createTime: '2024-11-15 15:05', createBy: '何苗苗', + vehicleList: [ + { vehicleKey: 1, seq: 1, vehicleType: '18吨双飞翼货车', brand: '苏龙', model: '海格牌18吨双飞翼货车', vin: 'LKLG7C4E4NA774701', plateNo: '浙F80088', deliveryTime: '2026-06-02 16:00', deliveryPerson: '魏山', deliveryStatus: '待客户签章', deliveryMileage: 46200, deliveryH2: 21, deliveryH2Unit: '%', deliveryElec: 80 }, + { vehicleKey: 2, seq: 2, vehicleType: '18吨双飞翼货车', brand: '苏龙', model: '海格牌18吨双飞翼货车', vin: 'LKLG7C4E4NA774702', plateNo: '沪A03802F', deliveryTime: '2025-11-20 09:30', deliveryPerson: '何苗苗', deliveryStatus: '已保存', deliveryMileage: null, deliveryH2: null, deliveryElec: null } + ] + }, + { + id: 'o5', expectedDate: '2025-02-15', contractCode: 'HT-ZL-2024-001', projectName: '嘉兴氢能示范项目', customerName: '嘉兴某某物流有限公司', businessDept: '业务一部', businessOwner: '张经理', taskSource: '交车任务', bizType: '租赁', deliveryRegion: '浙江省-嘉兴市', deliveryAddress: '南湖科技大道停车场', createTime: '2025-02-10 09:00', createBy: '系统', + vehicleList: [ + { vehicleKey: 1, seq: 1, vehicleType: '厢式货车', brand: '东风', model: 'DFH1180', vin: 'LKLG7C4E4NA774759', plateNo: '京A12345', deliveryTime: '2025-02-15 10:30', deliveryPerson: '张三', deliveryStatus: '客户已签章', deliveryMileage: 12580, deliveryH2: 35, deliveryH2Unit: 'MPa', deliveryElec: 45, vehicleReturned: true, returnTime: '2025-08-20 11:30', returnPerson: '王五' }, + { vehicleKey: 2, seq: 2, vehicleType: '厢式货车', brand: '福田', model: 'BJ1180', vin: 'LKLG7C4E4NA774760', plateNo: '京C11111', deliveryTime: '2025-02-15 14:00', deliveryPerson: '李四', deliveryStatus: '客户已签章', deliveryMileage: 13200, deliveryH2: 68, deliveryH2Unit: '%', deliveryElec: 38, vehicleReturned: false } + ] + }, + { + id: 'o6', expectedDate: '2025-02-18', contractCode: 'HT-ZL-2024-003', projectName: '杭州城配租赁项目', customerName: '杭州某某租赁有限公司', businessDept: '业务二部', businessOwner: '李经理', taskSource: '交车任务', bizType: '租赁', deliveryRegion: '浙江省-杭州市', deliveryAddress: '未来科技城地下停车场', createTime: '2025-02-12 08:30', createBy: '李四', + vehicleList: [ + { vehicleKey: 1, seq: 1, vehicleType: '城配货车', brand: '重汽', model: 'ZZ1180', vin: 'LKLG7C4E4NA774801', plateNo: '浙A10001', deliveryTime: '2025-02-18 09:15', deliveryPerson: '张三', deliveryStatus: '客户已签章', deliveryMileage: 9800, deliveryH2: 10.8, deliveryH2Unit: '%', deliveryElec: 52, vehicleReturned: true, returnTime: '2026-05-12 09:20', returnPerson: '张三' }, + { vehicleKey: 2, seq: 2, vehicleType: '城配货车', brand: '东风', model: 'DFH1190', vin: 'LKLG7C4E4NA774802', plateNo: '浙A10002', deliveryTime: '2025-02-18 11:40', deliveryPerson: '李四', deliveryStatus: '客户已签章', deliveryMileage: 10120, deliveryH2: 9.6, deliveryH2Unit: '%', deliveryElec: 48, vehicleReturned: false }, + { vehicleKey: 3, seq: 3, vehicleType: '城配货车', brand: '福田', model: 'BJ1190', vin: 'LKLG7C4E4NA774803', plateNo: '浙A10003', deliveryTime: '2025-02-18 16:20', deliveryPerson: '王五', deliveryStatus: '客户已签章', deliveryMileage: 11500, deliveryH2: 32, deliveryH2Unit: 'MPa', deliveryElec: 55, vehicleReturned: false } + ] + } ]); - var pendingList = pendingListState[0]; + var deliveryOrders = deliveryOrdersState[0]; + var setDeliveryOrders = deliveryOrdersState[1]; - // 历史记录 mock:与待处理同结构,含创建时间、创建人、最后修改时间、最后修改人;vehicleList 含交车时间(actualDate)、交车人员(deliveryPerson) - var historyListState = useState([ - { id: 'h1', expectedDate: '2025-02-15', contractCode: 'HT-ZL-2024-001', projectName: '嘉兴氢能示范项目', customerName: '嘉兴某某物流有限公司', deliveryRegion: '浙江省-嘉兴市', deliveryAddress: '嘉兴市南湖区科技大道1号', deliveryCount: 2, vehicleList: [{ vehicleType: '厢式货车', brand: '东风', model: 'DFH1180', plateNo: '京A12345', actualDate: '2025-02-15', deliveryPerson: '张三' }, { vehicleType: '平板货车', brand: '福田', model: 'BJ1180', plateNo: '京C11111', actualDate: '2025-02-15', deliveryPerson: '李四' }], createTime: '2025-02-10 09:00', createBy: '系统', lastModifyTime: '2025-02-15 14:30', lastModifyBy: '张三' }, - { id: 'h2', expectedDate: '2025-02-10 至 2025-02-12', contractCode: 'HT-ZL-2024-002', projectName: '上海物流租赁项目', customerName: '上海某某运输公司', deliveryRegion: '上海市-上海市', deliveryAddress: '浦东新区张江路100号', deliveryCount: 1, vehicleList: [{ vehicleType: '厢式货车', brand: '江淮', model: 'HFC1180', plateNo: '沪A20002', actualDate: '2025-02-11', deliveryPerson: '王五' }], createTime: '2025-02-05 10:00', createBy: '王五', lastModifyTime: '2025-02-12 11:00', lastModifyBy: '王五' }, - { id: 'h3', expectedDate: '2025-02-18', contractCode: 'HT-ZL-2024-003', projectName: '杭州城配租赁项目', customerName: '杭州某某租赁有限公司', deliveryRegion: '浙江省-杭州市', deliveryAddress: '余杭区未来科技城', deliveryCount: 3, vehicleList: [{ vehicleType: '栏板货车', brand: '重汽', model: 'ZZ1180', plateNo: '浙A10001', actualDate: '2025-02-18', deliveryPerson: '张三' }, { vehicleType: '厢式货车', brand: '东风', model: 'DFH1190', plateNo: '浙A10002', actualDate: '2025-02-18', deliveryPerson: '李四' }, { vehicleType: '平板货车', brand: '福田', model: 'BJ1190', plateNo: '浙A10003', actualDate: '2025-02-18', deliveryPerson: '王五' }], createTime: '2025-02-12 08:30', createBy: '李四', lastModifyTime: '2025-02-18 15:00', lastModifyBy: '张三' }, - { id: 'h4', expectedDate: '2025-02-20', contractCode: 'HT-ZL-2024-004', projectName: '嘉兴氢能示范项目', customerName: '嘉兴某某物流有限公司', deliveryRegion: '浙江省-嘉兴市', deliveryAddress: '嘉兴市秀洲区洪兴西路288号', deliveryCount: 1, vehicleList: [{ vehicleType: '厢式货车', brand: '江淮', model: 'HFC1180', plateNo: '浙F60001', actualDate: '2025-02-20', deliveryPerson: '李四' }], createTime: '2025-02-14 11:00', createBy: '张三', lastModifyTime: '2025-02-20 10:15', lastModifyBy: '李四' }, - { id: 'h5', expectedDate: '2025-02-22 至 2025-02-24', contractCode: 'HT-ZL-2024-005', projectName: '上海物流租赁项目', customerName: '上海某某运输公司', deliveryRegion: '上海市-上海市', deliveryAddress: '闵行区莘庄工业区申富路669号', deliveryCount: 2, vehicleList: [{ vehicleType: '平板货车', brand: '福田', model: 'BJ1180', plateNo: '沪B30001', actualDate: '2025-02-23', deliveryPerson: '王五' }, { vehicleType: '栏板货车', brand: '东风', model: 'DFH1160', plateNo: '沪B30002', actualDate: '2025-02-24', deliveryPerson: '张三' }], createTime: '2025-02-16 09:45', createBy: '李四', lastModifyTime: '2025-02-24 16:30', lastModifyBy: '王五' }, - { id: 'h6', expectedDate: '2025-02-25', contractCode: 'HT-ZL-2024-006', projectName: '杭州城配租赁项目', customerName: '杭州某某租赁有限公司', deliveryRegion: '浙江省-杭州市', deliveryAddress: '萧山区市心北路108号', deliveryCount: 1, vehicleList: [{ vehicleType: '厢式货车', brand: '重汽', model: 'ZZ1160', plateNo: '浙A40001', actualDate: '2025-02-25', deliveryPerson: '张三' }], createTime: '2025-02-18 14:20', createBy: '王五', lastModifyTime: '2025-02-25 11:00', lastModifyBy: '张三' }, - { id: 'h7', expectedDate: '2025-02-28', contractCode: 'HT-ZL-2024-007', projectName: '嘉兴氢能示范项目', customerName: '嘉兴某某物流有限公司', deliveryRegion: '浙江省-嘉兴市', deliveryAddress: '嘉兴市经开区昌盛路1号', deliveryCount: 2, vehicleList: [{ vehicleType: '厢式货车', brand: '东风', model: 'DFH1180', plateNo: '浙F70001', actualDate: '2025-02-28', deliveryPerson: '李四' }, { vehicleType: '平板货车', brand: '福田', model: 'BJ1180', plateNo: '浙F70002', actualDate: '2025-02-28', deliveryPerson: '王五' }], createTime: '2025-02-20 10:10', createBy: '张三', lastModifyTime: '2025-02-28 14:45', lastModifyBy: '李四' }, - { id: 'h8', expectedDate: '2025-03-02', contractCode: 'HT-ZL-2024-008', projectName: '上海物流租赁项目', customerName: '上海某某运输公司', deliveryRegion: '上海市-上海市', deliveryAddress: '宝山区沪太路5008号', deliveryCount: 1, vehicleList: [{ vehicleType: '栏板货车', brand: '江淮', model: 'HFC1160', plateNo: '沪A50001', actualDate: '2025-03-02', deliveryPerson: '王五' }], createTime: '2025-02-22 08:00', createBy: '李四', lastModifyTime: '2025-03-02 09:30', lastModifyBy: '王五' }, - { id: 'h9', expectedDate: '2025-03-05 至 2025-03-07', contractCode: 'HT-ZL-2024-009', projectName: '杭州城配租赁项目', customerName: '杭州某某租赁有限公司', deliveryRegion: '浙江省-杭州市', deliveryAddress: '滨江区网商路699号', deliveryCount: 2, vehicleList: [{ vehicleType: '厢式货车', brand: '江淮', model: 'HFC1190', plateNo: '浙A60001', actualDate: '2025-03-06', deliveryPerson: '张三' }, { vehicleType: '栏板货车', brand: '重汽', model: 'ZZ1190', plateNo: '浙A60002', actualDate: '2025-03-07', deliveryPerson: '李四' }], createTime: '2025-02-25 15:30', createBy: '王五', lastModifyTime: '2025-03-07 13:20', lastModifyBy: '张三' }, - { id: 'h10', expectedDate: '2025-03-10', contractCode: 'HT-ZL-2024-010', projectName: '嘉兴氢能示范项目', customerName: '嘉兴某某物流有限公司', deliveryRegion: '浙江省-嘉兴市', deliveryAddress: '嘉兴市南湖区广益路与由拳路交叉口', deliveryCount: 1, vehicleList: [{ vehicleType: '平板货车', brand: '福田', model: 'BJ1190', plateNo: '浙F90001', actualDate: '2025-03-10', deliveryPerson: '李四' }], createTime: '2025-03-01 11:00', createBy: '张三', lastModifyTime: '2025-03-10 10:00', lastModifyBy: '李四' } - ]); - var historyList = historyListState[0]; + var editDrawerState = useState({ open: false, record: null, mode: 'edit' }); + var editDrawer = editDrawerState[0]; + var setEditDrawer = editDrawerState[1]; - var filteredPending = useMemo(function () { - var list = pendingList.slice(); - if (appliedFilters.contractCode) list = list.filter(function (r) { return (r.contractCode || '').indexOf(appliedFilters.contractCode) !== -1; }); - if (appliedFilters.projectName) list = list.filter(function (r) { return r.projectName === appliedFilters.projectName; }); - if (appliedFilters.customerName) list = list.filter(function (r) { return r.customerName === appliedFilters.customerName; }); - if (appliedFilters.deliveryRegion) list = list.filter(function (r) { return (r.deliveryRegion || '').indexOf(appliedFilters.deliveryRegion) !== -1; }); - if (appliedFilters.dateStart) list = list.filter(function (r) { var d = (r.expectedDate || '').slice(0, 10); return d >= appliedFilters.dateStart; }); - if (appliedFilters.dateEnd) list = list.filter(function (r) { var d = (r.expectedDate || '').slice(0, 10); return d <= appliedFilters.dateEnd; }); - if (appliedFilters.deliveryPerson) list = list.filter(function (r) { return (r.assignedDeliveryPerson || '').indexOf(appliedFilters.deliveryPerson) !== -1; }); + function resolveVehicleReturnedFields(deliveryStatus, v) { + if (!isVehicleDelivered(deliveryStatus || '未开始')) { + return { vehicleReturned: false, returnTime: '', returnPerson: '' }; + } + if (!isDeliveryHistoryStatus(deliveryStatus || '未开始')) { + return { vehicleReturned: false, returnTime: '', returnPerson: '' }; + } + return { + vehicleReturned: v.vehicleReturned === true || !!(v.returnTime && String(v.returnTime).trim()), + returnTime: v.returnTime || '', + returnPerson: (v.returnPerson && v.returnPerson !== '-') ? String(v.returnPerson).trim() : '' + }; + } + + function expandOrdersToVehicleRows(orders) { + var rows = []; + (orders || []).forEach(function (order) { + (order.vehicleList || []).forEach(function (v, idx) { + var vk = v.vehicleKey != null ? v.vehicleKey : idx + 1; + var deliveryStatus = v.deliveryStatus || '未开始'; + var returnFields = resolveVehicleReturnedFields(deliveryStatus, v); + rows.push({ + id: order.id + '_v' + vk, + orderId: order.id, + vehicleKey: vk, + seq: v.seq != null ? v.seq : idx + 1, + deliveryTime: v.deliveryTime || v.actualDate || '', + deliveryPerson: (v.deliveryPerson && v.deliveryPerson !== '-') ? v.deliveryPerson : '', + plateNo: (v.plateNo && v.plateNo !== '-') ? String(v.plateNo).trim() : '', + vin: (v.plateNo && v.plateNo !== '-' && String(v.plateNo).trim()) ? (v.vin || '') : '', + vehicleType: v.vehicleType || '', + brand: v.brand || '', + model: v.model || '', + contractCode: order.contractCode, + customerName: order.customerName, + projectName: order.projectName, + businessDept: order.businessDept || '-', + businessOwner: order.businessOwner || '-', + taskSource: order.taskSource || '-', + replaceOldPlate: (v.replaceOldPlate || order.replaceOldPlate || '').trim(), + bizType: order.bizType || '-', + deliveryRegion: order.deliveryRegion || '-', + deliveryAddress: order.deliveryAddress || '-', + deliveryStatus: deliveryStatus, + deliveryMileage: v.deliveryMileage, + deliveryH2: v.deliveryH2, + deliveryH2Unit: v.deliveryH2Unit === 'MPa' ? 'MPa' : '%', + deliveryElec: v.deliveryElec, + createTime: order.createTime, + createBy: order.createBy, + expectedDate: order.expectedDate, + vehicleReturned: returnFields.vehicleReturned, + returnTime: returnFields.returnTime, + returnPerson: returnFields.returnPerson + }); + }); + }); + return rows; + } + + function formatDeliveryMileage(v) { + if (v === null || v === undefined || v === '') return '-'; + return String(v) + ' km'; + } + + function formatDeliveryH2(v, unit) { + if (v === null || v === undefined || v === '') return '-'; + var u = unit === 'MPa' ? 'MPa' : '%'; + return String(v) + ' ' + u; + } + + function formatDeliveryElec(v) { + if (v === null || v === undefined || v === '') return '-'; + return String(v) + ' %'; + } + + function renderDeliveryRecordCell(record) { + return React.createElement('div', null, + React.createElement('div', { style: cellLineSubStyle }, '里程 ', formatDeliveryMileage(record.deliveryMileage)), + React.createElement('div', { style: cellLineSubStyle }, '氢量 ', formatDeliveryH2(record.deliveryH2, record.deliveryH2Unit)), + React.createElement('div', { style: cellLineSubStyle }, '电量 ', formatDeliveryElec(record.deliveryElec)) + ); + } + + function rowDateKey(row) { + var t = row.deliveryTime || ''; + if (t && t.length >= 10) return t.slice(0, 10); + return (row.createTime || '').slice(0, 10); + } + + function parseExpectedEndDate(expectedDate) { + if (!expectedDate) return ''; + var s = String(expectedDate).trim(); + if (s.indexOf('至') >= 0) { + var seg = s.split('至'); + return (seg[seg.length - 1] || '').trim().slice(0, 10); + } + return s.slice(0, 10); + } + + function computeRowDelayed(row) { + if (isDeliveryHistoryStatus(row.deliveryStatus)) return false; + var end = parseExpectedEndDate(row.expectedDate); + if (!end) return false; + return end < '2026-06-01'; + } + + function filterSelectOption(input, option) { + var label = (option && (option.label || option.children)) || ''; + return String(label).toLowerCase().indexOf(String(input || '').toLowerCase()) >= 0; + } + + function filterVehicleRows(rows) { + var list = rows.slice(); + var f = appliedFilters; + if (f.contractCode) { + var cc = (contractCodeOptions.find(function (o) { return o.value === f.contractCode; }) || {}).label || f.contractCode; + list = list.filter(function (r) { return (r.contractCode || '').indexOf(cc) !== -1; }); + } + if (f.projectName) { + var projLabel = (projectNameOptions.find(function (o) { return o.value === f.projectName; }) || {}).label; + if (projLabel) list = list.filter(function (r) { return r.projectName === projLabel; }); + } + if (f.customerName) { + var custLabel = (customerNameOptions.find(function (o) { return o.value === f.customerName; }) || {}).label; + if (custLabel) list = list.filter(function (r) { return r.customerName === custLabel; }); + } + if (f.deliveryRegion) list = list.filter(function (r) { return (r.deliveryRegion || '').indexOf(f.deliveryRegion) !== -1; }); + if (f.dateStart) list = list.filter(function (r) { return rowDateKey(r) >= f.dateStart; }); + if (f.dateEnd) list = list.filter(function (r) { return rowDateKey(r) <= f.dateEnd; }); + if (f.deliveryPerson) list = list.filter(function (r) { return (r.deliveryPerson || '').indexOf(f.deliveryPerson) !== -1; }); + if (f.plateNo) list = list.filter(function (r) { return !isPlatePending(r.plateNo) && String(r.plateNo).indexOf(f.plateNo) !== -1; }); + if (f.vin) list = list.filter(function (r) { return (r.vin || '').indexOf(f.vin) !== -1; }); + if (f.vehicleType) list = list.filter(function (r) { return r.vehicleType === f.vehicleType; }); + if (f.brand) list = list.filter(function (r) { return r.brand === f.brand; }); + if (f.model) list = list.filter(function (r) { return r.model === f.model; }); + if (f.businessDept) list = list.filter(function (r) { return (r.businessDept || '').indexOf(f.businessDept) !== -1; }); + if (f.businessOwner) list = list.filter(function (r) { return (r.businessOwner || '').indexOf(f.businessOwner) !== -1; }); + if (f.taskSource) list = list.filter(function (r) { return r.taskSource === f.taskSource; }); + if (f.bizType) list = list.filter(function (r) { return r.bizType === f.bizType; }); + if (f.isDelayed === 'yes') list = list.filter(function (r) { return computeRowDelayed(r); }); + if (f.isDelayed === 'no') list = list.filter(function (r) { return !computeRowDelayed(r); }); return list; - }, [pendingList, appliedFilters]); + } - var filteredHistory = useMemo(function () { - var list = historyList.slice(); - if (appliedFilters.contractCode) list = list.filter(function (r) { return (r.contractCode || '').indexOf(appliedFilters.contractCode) !== -1; }); - if (appliedFilters.projectName) list = list.filter(function (r) { return r.projectName === appliedFilters.projectName; }); - if (appliedFilters.customerName) list = list.filter(function (r) { return r.customerName === appliedFilters.customerName; }); - if (appliedFilters.deliveryRegion) list = list.filter(function (r) { return (r.deliveryRegion || '').indexOf(appliedFilters.deliveryRegion) !== -1; }); - if (appliedFilters.dateStart) list = list.filter(function (r) { var d = (r.expectedDate || '').slice(0, 10); return d >= appliedFilters.dateStart; }); - if (appliedFilters.dateEnd) list = list.filter(function (r) { var d = (r.expectedDate || '').slice(0, 10); return d <= appliedFilters.dateEnd; }); - if (appliedFilters.deliveryPerson) list = list.filter(function (r) { return (r.lastModifyBy || '').indexOf(appliedFilters.deliveryPerson) !== -1; }); - return list; - }, [historyList, appliedFilters]); + var allVehicleRows = useMemo(function () { + return expandOrdersToVehicleRows(deliveryOrders); + }, [deliveryOrders]); + + var operatorVisibleRows = useMemo(function () { + return filterRowsByOperatorRegion(allVehicleRows); + }, [allVehicleRows]); + + function buildSelectOptions(values) { + var seen = {}; + var opts = []; + (values || []).forEach(function (v) { + var s = v == null ? '' : String(v).trim(); + if (!s || seen[s]) return; + seen[s] = true; + opts.push({ value: s, label: s }); + }); + opts.sort(function (a, b) { return a.label.localeCompare(b.label, 'zh-CN'); }); + return opts; + } + + var dynamicFilterOptions = useMemo(function () { + var plates = []; + var vins = []; + var vehicleTypes = []; + var brands = []; + var models = []; + var depts = []; + var owners = []; + operatorVisibleRows.forEach(function (r) { + if (!isPlatePending(r.plateNo)) plates.push(r.plateNo); + if (r.vin) vins.push(r.vin); + if (r.vehicleType) vehicleTypes.push(r.vehicleType); + if (r.brand) brands.push(r.brand); + if (r.model) models.push(r.model); + if (r.businessDept && r.businessDept !== '-') depts.push(r.businessDept); + if (r.businessOwner && r.businessOwner !== '-') owners.push(r.businessOwner); + }); + return { + plateNoOptions: buildSelectOptions(plates), + vinOptions: buildSelectOptions(vins), + vehicleTypeOptions: buildSelectOptions(vehicleTypes), + brandOptions: buildSelectOptions(brands), + modelOptions: buildSelectOptions(models), + businessDeptOptions: buildSelectOptions(depts), + businessOwnerOptions: buildSelectOptions(owners) + }; + }, [operatorVisibleRows]); + + var filteredBySearch = useMemo(function () { + return filterVehicleRows(operatorVisibleRows); + }, [operatorVisibleRows, appliedFilters]); + + var kpiStats = useMemo(function () { + var inProgress = 0; + var completed = 0; + filteredBySearch.forEach(function (r) { + if (isDeliveryHistoryStatus(r.deliveryStatus)) completed++; + else if (isDeliveryInProgressStatus(r.deliveryStatus)) inProgress++; + }); + return { total: filteredBySearch.length, inProgress: inProgress, completed: completed }; + }, [filteredBySearch]); + + function matchKpiFilter(row, filterKey) { + if (filterKey === 'total') return true; + if (filterKey === 'inProgress') return isDeliveryInProgressStatus(row.deliveryStatus); + if (filterKey === 'completed') return isDeliveryHistoryStatus(row.deliveryStatus); + return true; + } + + var filteredList = useMemo(function () { + return filteredBySearch.filter(function (r) { return matchKpiFilter(r, kpiFilter); }); + }, [filteredBySearch, kpiFilter]); + + function patchVehicleInOrder(orderId, vehicleKey, patch) { + setDeliveryOrders(function (orders) { + return orders.map(function (order) { + if (order.id !== orderId) return order; + return Object.assign({}, order, { + vehicleList: (order.vehicleList || []).map(function (v) { + var vk = v.vehicleKey != null ? v.vehicleKey : v.seq; + if (vk !== vehicleKey) return v; + return Object.assign({}, v, patch); + }) + }); + }); + }); + } + + var openEditDrawer = useCallback(function (record) { + setEditDrawer({ open: true, record: record, mode: 'edit' }); + }, []); + + var openViewDrawer = useCallback(function (record) { + setEditDrawer({ open: true, record: record, mode: 'view' }); + }, []); + + var closeEditDrawer = useCallback(function () { + setEditDrawer({ open: false, record: null, mode: 'edit' }); + }, []); + + var handleEditSave = useCallback(function (patch) { + var rec = editDrawer.record; + if (!rec) return; + patchVehicleInOrder(rec.orderId, rec.vehicleKey, Object.assign({}, patch, { deliveryStatus: '已保存' })); + }, [editDrawer.record]); + + var handleEditSubmit = useCallback(function (patch) { + var rec = editDrawer.record; + if (!rec) return; + patchVehicleInOrder(rec.orderId, rec.vehicleKey, Object.assign({}, patch, { deliveryStatus: '待客户签章', deliveryTime: patch.deliveryTime || '2026-06-04 10:00', deliveryPerson: patch.deliveryPerson || '魏山' })); + closeEditDrawer(); + }, [editDrawer.record, closeEditDrawer]); + + var navigateToContractDetail = useCallback(function (record) { + try { + sessionStorage.setItem(DV_CONTRACT_CODE_KEY, record.contractCode || ''); + sessionStorage.setItem(DV_ORDER_KEY, record.orderId || ''); + } catch (e) {} + message.info('跳转 合同管理-该合同详情页(合同编号 ' + (record.contractCode || '-') + ')'); + }, []); + + var navigateDelivery = useCallback(function (record, target) { + try { + sessionStorage.setItem(DV_ORDER_KEY, record.orderId || ''); + sessionStorage.setItem(DV_VEHICLE_KEY, String(record.vehicleKey != null ? record.vehicleKey : '')); + sessionStorage.setItem(DV_NAV_KEY, target || 'order'); + } catch (e) {} + var pageMap = { order: '交车管理-交车单', edit: '交车管理-交车单-编辑', view: '交车管理-交车单-查看' }; + message.info('跳转 ' + (pageMap[target] || target) + '(交车单 ' + record.orderId + ' · 车辆 ' + record.plateNo + ')'); + }, []); var page = pageState[0]; var setPage = pageState[1]; var pageSize = pageSizeState[0]; var setPageSize = pageSizeState[1]; - var totalCount = activeTab === 'pending' ? filteredPending.length : filteredHistory.length; - var displayPending = useMemo(function () { - var start = (page - 1) * pageSize; - return filteredPending.slice(start, start + pageSize); - }, [filteredPending, page, pageSize]); - var displayHistory = useMemo(function () { - var start = (page - 1) * pageSize; - return filteredHistory.slice(start, start + pageSize); - }, [filteredHistory, page, pageSize]); - var handleQuery = useCallback(function () { - var regionVal = filters.deliveryRegion; + var handleKpiCardClick = useCallback(function (key) { + setKpiFilter(key); + setPage(1); + }, []); + + var totalCount = filteredList.length; + var displayList = useMemo(function () { + var start = (page - 1) * pageSize; + return filteredList.slice(start, start + pageSize); + }, [filteredList, page, pageSize]); + + var kpiExportLabelMap = { total: '全部交车任务', inProgress: '进行中的交车任务', completed: '已完成的交车任务' }; + + function normalizeRegionFilter(regionVal) { if (Array.isArray(regionVal) && regionVal.length >= 2) { var prov = regionOptions.find(function (r) { return r.value === regionVal[0]; }); var city = prov && prov.children && prov.children.find(function (c) { return c.value === regionVal[1]; }); - regionVal = prov && city ? prov.label + '-' + city.label : undefined; - } else if (Array.isArray(regionVal) && regionVal.length === 1) { - var p = regionOptions.find(function (r) { return r.value === regionVal[0]; }); - regionVal = p ? p.label : undefined; + return prov && city ? prov.label + '-' + city.label : undefined; } - setAppliedFilters({ - contractCode: filters.contractCode, - projectName: filters.projectName, - customerName: filters.customerName, - deliveryRegion: regionVal, - dateStart: filters.dateStart, - dateEnd: filters.dateEnd, - deliveryPerson: filters.deliveryPerson - }); + if (Array.isArray(regionVal) && regionVal.length === 1) { + var pOnly = regionOptions.find(function (r) { return r.value === regionVal[0]; }); + return pOnly ? pOnly.label : undefined; + } + return regionVal; + } + + var handleQuery = useCallback(function () { + var next = patchFilters(filters, { deliveryRegion: normalizeRegionFilter(filters.deliveryRegion) }); + setFilters(next); + setAppliedFilters(patchFilters(next, {})); setPage(1); }, [filters]); var handleReset = useCallback(function () { - var empty = { contractCode: undefined, projectName: undefined, customerName: undefined, deliveryRegion: undefined, dateStart: '', dateEnd: '', deliveryPerson: undefined }; + var empty = createEmptyFilters(); setFilters(empty); setAppliedFilters(empty); setPage(1); }, []); + var handleListPlateNoChange = useCallback(function (v) { + setFilters(function (f) { return patchFilters(f, { plateNo: v }); }); + setAppliedFilters(function (f) { return patchFilters(f, { plateNo: v }); }); + setPage(1); + }, []); + + var getExportRowsByKpi = useCallback(function (filterKey) { + return filteredBySearch.filter(function (r) { return matchKpiFilter(r, filterKey); }); + }, [filteredBySearch]); + var handleExport = useCallback(function () { - var rows = activeTab === 'pending' ? filteredPending : filteredHistory; + var tabLabel = kpiExportLabelMap[kpiFilter] || '交车任务'; + var rows = getExportRowsByKpi(kpiFilter); if (!rows || rows.length === 0) { - message.warning('当前无数据可导出'); + message.warning('当前「' + tabLabel + '」无数据可导出'); return; } var escapeCsv = function (v) { @@ -190,9 +2148,9 @@ const Component = function () { if (s.indexOf(',') !== -1 || s.indexOf('"') !== -1 || s.indexOf('\n') !== -1) return '"' + s.replace(/"/g, '""') + '"'; return s; }; - var headers = ['预计交车时间', '合同编码', '项目名称', '客户名称', '交车区域', '交车地点', '交车数量', '创建时间', '创建人', '最后修改时间', '最后修改人']; + var headers = ['完成交车时间', '交车人', '车牌号', '品牌', '型号', '合同编号', '客户名称', '项目名称', '业务部门', '业务负责人', '任务来源', '业务类型', '交车区域', '交车地点', '交车状态', '交车里程', '交车氢量', '交车电量', '创建时间', '创建人']; var rowToCells = function (r) { - return [r.expectedDate, r.contractCode, r.projectName, r.customerName, r.deliveryRegion, r.deliveryAddress, r.deliveryCount, r.createTime, r.createBy, r.lastModifyTime, r.lastModifyBy]; + return [formatCompletedDeliveryTimeExport(r.deliveryTime), r.deliveryPerson || '-', displayPlateNo(r.plateNo), r.brand || '-', r.model || '-', r.contractCode, r.customerName, r.projectName, r.businessDept, r.businessOwner, r.taskSource, r.bizType, r.deliveryRegion, r.deliveryAddress, r.deliveryStatus, formatDeliveryMileage(r.deliveryMileage), formatDeliveryH2(r.deliveryH2, r.deliveryH2Unit), formatDeliveryElec(r.deliveryElec), r.createTime, r.createBy]; }; var csv = headers.map(escapeCsv).join(',') + '\n'; rows.forEach(function (r) { @@ -202,11 +2160,11 @@ const Component = function () { var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; - a.download = '交车管理_' + (activeTab === 'pending' ? '待处理' : '历史记录') + '_' + new Date().getTime() + '.csv'; + a.download = '交车管理_' + tabLabel + '_' + new Date().getTime() + '.csv'; a.click(); URL.revokeObjectURL(url); - message.success('导出成功'); - }, [activeTab, filteredPending, filteredHistory]); + message.success('已导出「' + tabLabel + '」共 ' + rows.length + ' 条(含当前筛选条件)'); + }, [kpiFilter, getExportRowsByKpi]); var dateRangeValue = useMemo(function () { if (!filters.dateStart && !filters.dateEnd) return null; @@ -243,116 +2201,447 @@ const Component = function () { return undefined; }, [filters.deliveryRegion]); - // 列表列:待处理 - var popoverTableStyle = { width: '100%', borderCollapse: 'collapse', fontSize: 12 }; - var popoverThStyle = { padding: '6px 8px', textAlign: 'left', borderBottom: '1px solid #f0f0f0', backgroundColor: '#fafafa', fontWeight: 600 }; - var popoverTdStyle = { padding: '6px 8px', borderBottom: '1px solid #f0f0f0' }; + var solidTagBaseStyle = { margin: 0, border: 'none', fontWeight: 600, color: '#fff', lineHeight: '20px', flexShrink: 0, flexGrow: 0 }; - // 气泡卡片:车辆类型、品牌、型号、车牌号、交车时间、交车人员(待处理用 deliveryTime/deliveryPerson,未交车为-) - function renderPendingQuantity(record) { - var list = record.vehicleList || []; - var content = React.createElement('div', { style: { padding: 8, minWidth: 420 } }, - React.createElement('div', { style: { marginBottom: 8, fontWeight: 600 } }, '车辆明细'), - React.createElement('table', { style: popoverTableStyle }, - React.createElement('thead', null, - React.createElement('tr', null, - React.createElement('th', { style: popoverThStyle }, '车辆类型'), - React.createElement('th', { style: popoverThStyle }, '品牌'), - React.createElement('th', { style: popoverThStyle }, '型号'), - React.createElement('th', { style: popoverThStyle }, '车牌号'), - React.createElement('th', { style: popoverThStyle }, '交车时间'), - React.createElement('th', { style: popoverThStyle }, '交车人员') - ) - ), - React.createElement('tbody', null, - list.map(function (v, i) { - return React.createElement('tr', { key: i }, - React.createElement('td', { style: popoverTdStyle }, v.vehicleType || '-'), - React.createElement('td', { style: popoverTdStyle }, v.brand || '-'), - React.createElement('td', { style: popoverTdStyle }, v.model || '-'), - React.createElement('td', { style: popoverTdStyle }, v.plateNo || '-'), - React.createElement('td', { style: popoverTdStyle }, (v.deliveryTime && v.deliveryTime !== '-') ? v.deliveryTime : '-'), - React.createElement('td', { style: popoverTdStyle }, v.deliveryPerson || '-') - ); - }) - ) - ) - ); - return React.createElement(Popover, { content: content, title: null }, - React.createElement('span', { style: { color: '#1890ff', cursor: 'pointer', fontWeight: 600 } }, record.deliveryCount + ' 辆') + function renderSolidTag(text, bgColor) { + if (!text || text === '-') return '-'; + return React.createElement(Tag, { + style: Object.assign({}, solidTagBaseStyle, { backgroundColor: bgColor || '#64748b' }) + }, text); + } + + function renderDeliveryStatus(status, record) { + var bg = '#8c8c8c'; + if (status === '客户已签章' || status === '已签章') bg = '#16a34a'; + else if (status === '待客户签章') bg = '#2563eb'; + else if (status === '已保存') bg = '#ea580c'; + else if (status === '未开始') bg = '#64748b'; + var signed = isDeliverySignedStatus(status); + var style = Object.assign({}, solidTagBaseStyle, { backgroundColor: bg || '#64748b' }); + if (signed) { + style.cursor = 'pointer'; + style.textDecoration = 'underline'; + style.textUnderlineOffset = '2px'; + } + return React.createElement(Tag, { + style: style, + title: signed ? '点击下载签章文件' : undefined, + role: signed ? 'button' : undefined, + tabIndex: signed ? 0 : undefined, + onClick: signed ? function (e) { e.stopPropagation(); downloadDeliverySignFile(record); } : undefined, + onKeyDown: signed ? function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); downloadDeliverySignFile(record); } } : undefined + }, status); + } + + function renderBizTypeTag(bizType) { + if (!bizType || bizType === '-') return null; + var bg = '#64748b'; + if (bizType === '租赁') bg = '#2563eb'; + else if (bizType === '自营') bg = '#7c3aed'; + return React.createElement(Tag, { + style: Object.assign({}, solidTagBaseStyle, { + backgroundColor: bg, + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + textAlign: 'center', + alignSelf: 'center', + minWidth: 44, + height: 22, + padding: '0 10px', + boxSizing: 'border-box' + }) + }, bizType); + } + + function renderTaskSourceTag(text, bgColor) { + return React.createElement(Tag, { + style: Object.assign({}, solidTagBaseStyle, { backgroundColor: bgColor, cursor: 'default' }) + }, text); + } + + var handleColumnResizeStart = useCallback(function (colKey, e) { + e.preventDefault(); + e.stopPropagation(); + var startX = e.clientX; + var startW = colWidths[colKey] || 240; + function onMove(ev) { + var nextW = Math.max(140, Math.min(560, startW + ev.clientX - startX)); + setColWidths(function (prev) { + var next = {}; + for (var k in prev) next[k] = prev[k]; + next[colKey] = nextW; + return next; + }); + } + function onUp() { + document.removeEventListener('mousemove', onMove); + document.removeEventListener('mouseup', onUp); + } + document.addEventListener('mousemove', onMove); + document.addEventListener('mouseup', onUp); + }, [colWidths]); + + function renderResizableTitle(title, colKey) { + return React.createElement('div', { style: { position: 'relative', paddingRight: 10, userSelect: 'none' } }, + title, + React.createElement('span', { + role: 'separator', + 'aria-orientation': 'vertical', + title: '拖动调整列宽', + style: { + position: 'absolute', + right: 0, + top: 0, + bottom: 0, + width: 8, + cursor: 'col-resize', + zIndex: 1 + }, + onMouseDown: function (ev) { handleColumnResizeStart(colKey, ev); } + }) ); } - // 气泡卡片:车辆类型、品牌、型号、车牌号、交车时间、交车人员(历史用 actualDate、deliveryPerson) - function renderHistoryQuantity(record) { - var list = record.vehicleList || []; - var content = React.createElement('div', { style: { padding: 8, minWidth: 420 } }, - React.createElement('div', { style: { marginBottom: 8, fontWeight: 600 } }, '车辆明细'), - React.createElement('table', { style: popoverTableStyle }, - React.createElement('thead', null, - React.createElement('tr', null, - React.createElement('th', { style: popoverThStyle }, '车辆类型'), - React.createElement('th', { style: popoverThStyle }, '品牌'), - React.createElement('th', { style: popoverThStyle }, '型号'), - React.createElement('th', { style: popoverThStyle }, '车牌号'), - React.createElement('th', { style: popoverThStyle }, '交车时间'), - React.createElement('th', { style: popoverThStyle }, '交车人员') - ) + function renderContractInfoCell(record) { + var customerName = record.customerName || '-'; + var showTip = customerName !== '-'; + var contractCode = record.contractCode || '-'; + var projectName = record.projectName || '-'; + return React.createElement('div', { style: { minWidth: 0, width: '100%' } }, + React.createElement('div', { + style: { + display: 'flex', + alignItems: 'center', + flexWrap: 'nowrap', + gap: 6, + minWidth: 0, + width: '100%', + lineHeight: 1.45 + } + }, + React.createElement(Tooltip, { title: showTip ? customerName : null, placement: 'topLeft' }, + React.createElement('span', { + style: { + flex: '1 1 auto', + minWidth: 0, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + color: '#333', + display: 'block' + } + }, customerName) ), - React.createElement('tbody', null, - list.map(function (v, i) { - return React.createElement('tr', { key: i }, - React.createElement('td', { style: popoverTdStyle }, v.vehicleType || '-'), - React.createElement('td', { style: popoverTdStyle }, v.brand || '-'), - React.createElement('td', { style: popoverTdStyle }, v.model || '-'), - React.createElement('td', { style: popoverTdStyle }, v.plateNo || '-'), - React.createElement('td', { style: popoverTdStyle }, v.actualDate || '-'), - React.createElement('td', { style: popoverTdStyle }, v.deliveryPerson || '-') - ); - }) - ) - ) - ); - return React.createElement(Popover, { content: content, title: null }, - React.createElement('span', { style: { color: '#1890ff', cursor: 'pointer', fontWeight: 600 } }, record.deliveryCount + ' 辆') + React.createElement('span', { + style: { + flexShrink: 0, + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center' + } + }, renderBizTypeTag(record.bizType)) + ), + contractCode !== '-' + ? React.createElement('div', { + style: Object.assign({}, cellLineSubStyle, DV_LIST_LINK_STYLE, { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }), + role: 'button', + tabIndex: 0, + onClick: function () { navigateToContractDetail(record); }, + onKeyDown: function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); navigateToContractDetail(record); } } + }, contractCode) + : React.createElement('div', { style: cellLineSubStyle }, '-'), + projectName !== '-' + ? React.createElement('div', { + style: Object.assign({}, cellLineSubStyle, DV_LIST_LINK_STYLE, { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }), + role: 'button', + tabIndex: 0, + onClick: function () { navigateToContractDetail(record); }, + onKeyDown: function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); navigateToContractDetail(record); } } + }, projectName) + : React.createElement('div', { style: Object.assign({}, cellLineSubStyle, { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }) }, '-') ); } - // 待处理:预计交车时间、合同编码、项目名称、客户名称、交车区域、交车地点、交车数量、创建时间、创建人、最后修改时间、最后修改人、操作(交车单) - var pendingColumns = [ - { title: '预计交车时间', dataIndex: 'expectedDate', key: 'expectedDate', width: 220, ellipsis: true }, - { title: '合同编码', dataIndex: 'contractCode', key: 'contractCode', width: 150, ellipsis: true }, - { title: '项目名称', dataIndex: 'projectName', key: 'projectName', width: 150, ellipsis: true }, - { title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 160, ellipsis: true }, - { title: '交车区域', dataIndex: 'deliveryRegion', key: 'deliveryRegion', width: 120, ellipsis: true }, - { title: '交车地点', dataIndex: 'deliveryAddress', key: 'deliveryAddress', width: 200, ellipsis: true }, - { title: '交车数量', key: 'deliveryCount', width: 90, render: function (_, r) { return renderPendingQuantity(r); } }, - { title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 160, ellipsis: true }, - { title: '创建人', dataIndex: 'createBy', key: 'createBy', width: 90, ellipsis: true }, - { title: '最后修改时间', dataIndex: 'lastModifyTime', key: 'lastModifyTime', width: 160, ellipsis: true }, - { title: '最后修改人', dataIndex: 'lastModifyBy', key: 'lastModifyBy', width: 90, ellipsis: true }, - { title: '操作', key: 'action', width: 80, fixed: 'right', render: function (_, r) { - return React.createElement(Button, { type: 'link', size: 'small', onClick: function () { message.info('跳转交车管理-交车单'); } }, '交车单'); - } } - ]; + function renderDeliveryPlaceCell(record) { + var regionText = '交车区域:' + formatDeliveryRegion(record.deliveryRegion); + var parkingText = isPlatePending(record.plateNo) ? '-' : (record.deliveryAddress || '-'); + return renderCellLines(regionText, [parkingText]); + } - // 历史记录:同列,操作仅 查看 - var historyColumns = [ - { title: '预计交车时间', dataIndex: 'expectedDate', key: 'expectedDate', width: 220, ellipsis: true }, - { title: '合同编码', dataIndex: 'contractCode', key: 'contractCode', width: 150, ellipsis: true }, - { title: '项目名称', dataIndex: 'projectName', key: 'projectName', width: 150, ellipsis: true }, - { title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 160, ellipsis: true }, - { title: '交车区域', dataIndex: 'deliveryRegion', key: 'deliveryRegion', width: 120, ellipsis: true }, - { title: '交车地点', dataIndex: 'deliveryAddress', key: 'deliveryAddress', width: 200, ellipsis: true }, - { title: '交车数量', key: 'deliveryCount', width: 90, render: function (_, r) { return renderHistoryQuantity(r); } }, - { title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 160, ellipsis: true }, - { title: '创建人', dataIndex: 'createBy', key: 'createBy', width: 90, ellipsis: true }, - { title: '最后修改时间', dataIndex: 'lastModifyTime', key: 'lastModifyTime', width: 160, ellipsis: true }, - { title: '最后修改人', dataIndex: 'lastModifyBy', key: 'lastModifyBy', width: 90, ellipsis: true }, - { title: '操作', key: 'action', width: 80, fixed: 'right', render: function () { - return React.createElement(Button, { type: 'link', size: 'small', onClick: function () { message.info('跳转交车管理-查看页'); } }, '查看'); - } } + function parseCompletedDeliveryTime(raw) { + if (!raw || String(raw).trim() === '' || raw === '-') return { date: '-', time: '-' }; + var s = String(raw).trim(); + if (s.indexOf(' ') >= 0) { + var parts = s.split(/\s+/); + var datePart = parts[0] || '-'; + var timePart = parts[1] || '-'; + if (timePart.length >= 5) timePart = timePart.slice(0, 5); + return { date: datePart, time: timePart }; + } + return { date: s, time: '-' }; + } + + function renderCompletedDeliveryTimeCell(record) { + var parsed = parseCompletedDeliveryTime(record.deliveryTime); + return renderCellLines(parsed.date, [parsed.time]); + } + + function renderTaskCreateTimeCell(record) { + var parsed = parseCompletedDeliveryTime(record.createTime); + if (parsed.date === '-' && parsed.time === '-') return '-'; + if (parsed.time === '-') return parsed.date; + return parsed.date + ' ' + parsed.time; + } + + function renderTaskCreateByCell(createBy) { + if (createBy == null || createBy === undefined) return '-'; + var name = String(createBy).trim(); + return name === '' || name === '-' ? '-' : name; + } + + function isVehicleReturned(record) { + if (!record || !isVehicleDelivered(record.deliveryStatus) || !isDeliveryHistoryStatus(record.deliveryStatus)) return false; + return record.vehicleReturned === true; + } + + function renderVehicleReturnedCell(record) { + if (!record || !isVehicleDelivered(record.deliveryStatus)) { + return '-'; + } + if (!isVehicleReturned(record)) { + return React.createElement('span', { style: { color: '#64748b' } }, '未归还'); + } + var label = React.createElement('span', { + style: { color: '#16a34a', fontWeight: 600, cursor: 'default', borderBottom: '1px dashed #86efac' } + }, '已归还'); + return React.createElement(Popover, { + content: renderReturnVehiclePopoverContent(record), + trigger: 'hover', + placement: 'topLeft', + mouseEnterDelay: 0.15, + mouseLeaveDelay: 0.1, + destroyTooltipOnHide: true + }, label); + } + + function formatReturnTimeDisplay(raw) { + var parsed = parseCompletedDeliveryTime(raw); + if (parsed.date === '-' && parsed.time === '-') return '-'; + if (parsed.time === '-') return parsed.date; + return parsed.date + ' ' + parsed.time; + } + + function renderReturnVehiclePopoverContent(record) { + return React.createElement('div', { style: { minWidth: 168, fontSize: 13, lineHeight: 1.65 } }, + React.createElement('div', null, + React.createElement('span', { style: { color: '#64748b' } }, '还车时间:'), + React.createElement('span', { style: { color: '#334155', fontWeight: 600 } }, formatReturnTimeDisplay(record.returnTime)) + ), + React.createElement('div', { style: { marginTop: 4 } }, + React.createElement('span', { style: { color: '#64748b' } }, '还车人:'), + React.createElement('span', { style: { color: '#334155', fontWeight: 600 } }, record.returnPerson || '-') + ) + ); + } + + function formatCompletedDeliveryTimeExport(raw) { + var parsed = parseCompletedDeliveryTime(raw); + if (parsed.date === '-' && parsed.time === '-') return '-'; + if (parsed.time === '-') return parsed.date; + return parsed.date + ' ' + parsed.time; + } + + var cellLineMainStyle = { lineHeight: 1.45, color: '#333', wordBreak: 'break-all' }; + var cellLineSubStyle = { lineHeight: 1.4, fontSize: 12, color: '#8c8c8c', marginTop: 2, wordBreak: 'break-all' }; + + function renderCellLines(mainText, subLines) { + var subs = subLines || []; + return React.createElement('div', null, + React.createElement('div', { style: cellLineMainStyle }, mainText || '-'), + subs.map(function (line, i) { + return React.createElement('div', { key: i, style: cellLineSubStyle }, line || '-'); + }) + ); + } + + function isPlatePending(plateNo) { + if (plateNo == null || plateNo === undefined) return true; + var s = String(plateNo).trim(); + return s === '' || s === '-'; + } + + function displayPlateNo(plateNo) { + return isPlatePending(plateNo) ? '车牌待选' : String(plateNo).trim(); + } + + function displayBrandModel(brand, model) { + var b = brand && brand !== '-' ? String(brand).trim() : ''; + var m = model && model !== '-' ? String(model).trim() : ''; + if (b && m) return b + '-' + m; + if (b) return b; + if (m) return m; + return '-'; + } + + function displayVin(vin, plateNo) { + if (plateNo != null && isPlatePending(plateNo)) return '-'; + var s = vin == null || vin === undefined ? '' : String(vin).trim(); + return s || '-'; + } + + function displayReplaceOldPlate(plate) { + var s = plate == null ? '' : String(plate).trim(); + return s || '-'; + } + + function renderReplaceVehiclePopoverContent(record) { + var oldPlate = displayReplaceOldPlate(record.replaceOldPlate); + var newPlate = displayPlateNo(record.plateNo); + return React.createElement('div', { style: { minWidth: 168, fontSize: 13, lineHeight: 1.5 } }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' } }, + React.createElement('span', { style: { color: '#64748b' } }, '旧车'), + React.createElement('span', { style: { fontWeight: 600, color: '#334155' } }, oldPlate), + React.createElement('span', { style: { color: '#94a3b8', fontWeight: 600 } }, '→'), + React.createElement('span', { style: { color: '#64748b' } }, '新车'), + React.createElement('span', { + style: { fontWeight: 600, color: isPlatePending(record.plateNo) ? '#d48806' : '#334155' } + }, newPlate) + ) + ); + } + + function renderTaskSourceCell(value, record) { + if (value === '替换车') { + return React.createElement(Popover, { + content: renderReplaceVehiclePopoverContent(record), + trigger: 'hover', + placement: 'topLeft', + mouseEnterDelay: 0.15, + mouseLeaveDelay: 0.1, + destroyTooltipOnHide: true + }, renderTaskSourceTag('替换车', '#ea580c')); + } + if (value === '交车任务') return renderTaskSourceTag('交车任务', '#2563eb'); + return value || '-'; + } + + function renderVehicleInfoCell(record) { + var pending = isPlatePending(record.plateNo); + var plateStyle = Object.assign({}, cellLineMainStyle, { cursor: 'pointer' }, pending ? { color: '#d48806', fontWeight: 500 } : { color: '#165dff' }); + return React.createElement('div', null, + React.createElement('div', { + style: plateStyle, + role: 'button', + tabIndex: 0, + onClick: function () { openViewDrawer(record); }, + onKeyDown: function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openViewDrawer(record); } } + }, displayPlateNo(record.plateNo)), + React.createElement('div', { style: cellLineSubStyle }, displayBrandModel(record.brand, record.model)), + React.createElement('div', { style: cellLineSubStyle }, displayVin(record.vin, record.plateNo)) + ); + } + + // 列表列(按车辆一行;待处理/历史记录列顺序一致) + var colDeliveryTime = { title: '完成交车时间', key: 'completedDeliveryTime', width: 118, render: function (_, r) { return renderCompletedDeliveryTimeCell(r); } }; + var colDeliveryPerson = { title: '交车人', dataIndex: 'deliveryPerson', key: 'deliveryPerson', width: 80, render: function (v) { return v || '-'; } }; + var colDeliveryStatus = { title: '交车状态', dataIndex: 'deliveryStatus', key: 'deliveryStatus', width: 108, render: function (v, r) { return renderDeliveryStatus(v, r); } }; + var sharedListColumns = useMemo(function () { + return [ + { + title: '车辆信息', + key: 'vehicleInfo', + width: 188, + render: function (_, r) { return renderVehicleInfoCell(r); } + }, + { + title: renderResizableTitle('合同信息', 'contractInfo'), + key: 'contractInfo', + width: colWidths.contractInfo, + render: function (_, r) { return renderContractInfoCell(r); } + }, + { + title: '业务负责人', + key: 'businessInfo', + width: 120, + render: function (_, r) { + return renderCellLines(r.businessDept, [r.businessOwner]); + } + }, + { title: '任务来源', dataIndex: 'taskSource', key: 'taskSource', width: 92, ellipsis: true, render: function (v, r) { return renderTaskSourceCell(v, r); } }, + { title: '交车地点', key: 'deliveryPlace', width: 168, render: function (_, r) { return renderDeliveryPlaceCell(r); } }, + colDeliveryStatus, + colDeliveryTime, + colDeliveryPerson, + { title: '是否归还', key: 'vehicleReturned', width: 88, render: function (_, r) { return renderVehicleReturnedCell(r); } }, + { + title: '交车记录', + key: 'deliveryRecord', + width: 118, + render: function (_, r) { return renderDeliveryRecordCell(r); } + }, + { title: '交车任务创建时间', key: 'taskCreateTime', width: 150, ellipsis: true, render: function (_, r) { return renderTaskCreateTimeCell(r); } }, + { title: '交车任务创建人', dataIndex: 'createBy', key: 'taskCreateBy', width: 108, ellipsis: true, render: function (v) { return renderTaskCreateByCell(v); } } ]; + }, [colWidths.contractInfo, handleColumnResizeStart, navigateToContractDetail]); + + var listColumns = useMemo(function () { + return sharedListColumns.concat([ + { + title: '操作', key: 'action', width: 96, fixed: 'right', render: function (_, r) { + var nodes = [ + React.createElement(Button, { key: 'view', type: 'link', size: 'small', onClick: function () { openViewDrawer(r); } }, '查看') + ]; + if (canEditDeliveryRow(r)) { + nodes.push(React.createElement(Button, { key: 'edit', type: 'link', size: 'small', onClick: function () { openEditDrawer(r); } }, '编辑')); + } + return React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 2, flexWrap: 'nowrap' } }, nodes); + } + } + ]); + }, [sharedListColumns, openEditDrawer, openViewDrawer]); + + var kpiCards = useMemo(function () { + return [ + { key: 'total', type: 'total', title: '全部交车任务', desc: '当前筛选条件下的全部交车任务(含进行中与已完成)', val: kpiStats.total, icon: DV_KPI_ICONS.total }, + { key: 'inProgress', type: 'progress', title: '进行中的交车任务', desc: '交车状态为「未开始」「已保存」「待客户签章」的任务(客户未完成最终签章)', val: kpiStats.inProgress, icon: DV_KPI_ICONS.progress }, + { key: 'completed', type: 'completed', title: '已完成的交车任务', desc: '客户已完成最终签章步骤的所有交车任务(状态为「客户已签章」)', val: kpiStats.completed, icon: DV_KPI_ICONS.completed } + ]; + }, [kpiStats]); + + function renderKpiCard(card) { + var active = kpiFilter === card.key; + return React.createElement('div', { + key: card.key, + role: 'button', + tabIndex: 0, + className: 'lc-alert-card lc-alert-card--' + card.type + ' lc-alert-card-clickable' + (active ? ' lc-alert-card-active' : ''), + onClick: function () { handleKpiCardClick(card.key); }, + onKeyDown: function (e) { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleKpiCardClick(card.key); + } + } + }, + React.createElement('div', { className: 'lc-alert-card-tip-anchor' }, + React.createElement(Tooltip, { title: card.desc, placement: 'topRight', overlayStyle: { maxWidth: 360 } }, + React.createElement('span', { + className: 'lc-alert-card-tip', + role: 'img', + 'aria-label': card.title + '说明', + onClick: function (e) { e.stopPropagation(); }, + onMouseDown: function (e) { e.stopPropagation(); } + }, DV_KPI_TIP_SVG) + ) + ), + React.createElement('div', { className: 'lc-alert-card-icon' }, card.icon), + React.createElement('div', { className: 'lc-alert-card-main' }, + React.createElement('div', { className: 'lc-alert-card-val' }, card.val), + React.createElement('div', { className: 'lc-alert-card-title' }, card.title) + ) + ); + } var tablePagination = useMemo(function () { return { @@ -366,17 +2655,222 @@ const Component = function () { }; }, [page, pageSize, totalCount]); + var filterLabelStyle = { display: 'block', marginBottom: 4, color: '#333', fontSize: 14 }; + var filterControlStyle = { width: '100%' }; + var filterItemStyle = { minWidth: 0 }; + var styles = { page: { padding: '16px 24px 48px', backgroundColor: '#f5f5f5', minHeight: '100vh', fontSize: 14 }, breadcrumb: { marginBottom: 16, color: '#666' }, breadcrumbSep: { margin: '0 8px', color: '#999' }, card: { backgroundColor: '#fff', borderRadius: 8, marginBottom: 16, boxShadow: '0 1px 2px rgba(0,0,0,0.05)', overflow: 'hidden' }, cardBody: { padding: '20px 24px' }, - formRow: { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '16px 24px', alignItems: 'start' }, - formItem: { marginBottom: 4 }, - formLabel: { display: 'block', marginBottom: 4, color: '#333' } + filterGrid: { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr 1fr', gap: '16px 24px', alignItems: 'start' }, + filterActions: { display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 8, marginTop: 16, paddingTop: 16, borderTop: '1px solid #f1f5f9' } }; + var filterItems = [ + React.createElement('div', { key: 'contractCode', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '合同编号'), + React.createElement(Select, { + placeholder: '请输入或选择合同编号', + allowClear: true, + showSearch: true, + optionFilterProp: 'label', + style: filterControlStyle, + value: filters.contractCode, + onChange: function (v) { setFilters(function (f) { return patchFilters(f, { contractCode: v }); }); }, + options: contractCodeOptions + }) + ), + React.createElement('div', { key: 'projectName', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '项目名称'), + React.createElement(Select, { + placeholder: '请输入或选择项目名称', + allowClear: true, + showSearch: true, + optionFilterProp: 'label', + style: filterControlStyle, + value: filters.projectName, + onChange: function (v) { setFilters(function (f) { return patchFilters(f, { projectName: v }); }); }, + options: projectNameOptions + }) + ), + React.createElement('div', { key: 'customerName', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '客户名称'), + React.createElement(Select, { + placeholder: '请输入或选择客户名称', + allowClear: true, + showSearch: true, + optionFilterProp: 'label', + style: filterControlStyle, + value: filters.customerName, + onChange: function (v) { setFilters(function (f) { return patchFilters(f, { customerName: v }); }); }, + options: customerNameOptions + }) + ), + React.createElement('div', { key: 'deliveryRegion', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '交车区域'), + React.createElement(Cascader, { + options: regionOptions, + placeholder: '请选择省-市', + allowClear: true, + style: filterControlStyle, + value: deliveryRegionValue, + onChange: function (value) { + var s; + if (value && value.length >= 2) { + var prov = regionOptions.find(function (r) { return r.value === value[0]; }); + var city = prov && prov.children && prov.children.find(function (c) { return c.value === value[1]; }); + s = prov && city ? prov.label + '-' + city.label : undefined; + } else { s = undefined; } + setFilters(function (f) { return patchFilters(f, { deliveryRegion: s }); }); + }, + displayRender: function (labels) { return labels && labels.length ? labels.join(' / ') : ''; } + }) + ), + React.createElement('div', { key: 'completedTime', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '完成交车时间'), + React.createElement(RangePicker, { + style: filterControlStyle, + placeholder: ['请选择开始时间', '请选择结束时间'], + value: dateRangeValue, + onChange: onDateRangeChange + }) + ), + React.createElement('div', { key: 'deliveryPerson', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '交车人'), + React.createElement(Select, { + placeholder: '请输入或选择交车人', + allowClear: true, + showSearch: true, + optionFilterProp: 'label', + style: filterControlStyle, + value: filters.deliveryPerson, + onChange: function (v) { setFilters(function (f) { return patchFilters(f, { deliveryPerson: v }); }); }, + options: deliveryPersonOptions + }) + ), + React.createElement('div', { key: 'vin', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '车辆识别代码'), + React.createElement(Select, { + placeholder: '请输入或选择车辆识别代码', + allowClear: true, + showSearch: true, + style: filterControlStyle, + value: filters.vin, + onChange: function (v) { setFilters(function (f) { return patchFilters(f, { vin: v }); }); }, + options: dynamicFilterOptions.vinOptions, + filterOption: filterSelectOption + }) + ), + React.createElement('div', { key: 'vehicleType', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '车辆类型'), + React.createElement(Select, { + placeholder: '请选择车辆类型', + allowClear: true, + showSearch: true, + style: filterControlStyle, + value: filters.vehicleType, + onChange: function (v) { setFilters(function (f) { return patchFilters(f, { vehicleType: v }); }); }, + options: dynamicFilterOptions.vehicleTypeOptions, + filterOption: filterSelectOption + }) + ), + React.createElement('div', { key: 'brand', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '品牌'), + React.createElement(Select, { + placeholder: '请选择品牌', + allowClear: true, + showSearch: true, + style: filterControlStyle, + value: filters.brand, + onChange: function (v) { setFilters(function (f) { return patchFilters(f, { brand: v }); }); }, + options: dynamicFilterOptions.brandOptions, + filterOption: filterSelectOption + }) + ), + React.createElement('div', { key: 'model', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '型号'), + React.createElement(Select, { + placeholder: '请选择或输入型号', + allowClear: true, + showSearch: true, + style: filterControlStyle, + value: filters.model, + onChange: function (v) { setFilters(function (f) { return patchFilters(f, { model: v }); }); }, + options: dynamicFilterOptions.modelOptions, + filterOption: filterSelectOption + }) + ), + React.createElement('div', { key: 'businessDept', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '业务部门'), + React.createElement(Select, { + placeholder: '请输入或选择业务部门', + allowClear: true, + showSearch: true, + style: filterControlStyle, + value: filters.businessDept, + onChange: function (v) { setFilters(function (f) { return patchFilters(f, { businessDept: v }); }); }, + options: dynamicFilterOptions.businessDeptOptions, + filterOption: filterSelectOption + }) + ), + React.createElement('div', { key: 'businessOwner', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '业务负责人'), + React.createElement(Select, { + placeholder: '请输入或选择业务负责人', + allowClear: true, + showSearch: true, + style: filterControlStyle, + value: filters.businessOwner, + onChange: function (v) { setFilters(function (f) { return patchFilters(f, { businessOwner: v }); }); }, + options: dynamicFilterOptions.businessOwnerOptions, + filterOption: filterSelectOption + }) + ), + React.createElement('div', { key: 'taskSource', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '任务来源'), + React.createElement(Select, { + placeholder: '请选择任务来源', + allowClear: true, + style: filterControlStyle, + value: filters.taskSource, + onChange: function (v) { setFilters(function (f) { return patchFilters(f, { taskSource: v }); }); }, + options: taskSourceOptions + }) + ), + React.createElement('div', { key: 'bizType', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '业务类型'), + React.createElement(Select, { + placeholder: '请选择业务类型', + allowClear: true, + style: filterControlStyle, + value: filters.bizType, + onChange: function (v) { setFilters(function (f) { return patchFilters(f, { bizType: v }); }); }, + options: bizTypeOptions + }) + ), + React.createElement('div', { key: 'isDelayed', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '是否延期'), + React.createElement(Select, { + placeholder: '请选择是否延期', + allowClear: true, + style: filterControlStyle, + value: filters.isDelayed, + onChange: function (v) { setFilters(function (f) { return patchFilters(f, { isDelayed: v }); }); }, + options: isDelayedOptions + }) + ) + ]; + + var filterVisibleCount = filterExpanded ? filterItems.length : 4; + var filterNodes = []; + var fi; + for (fi = 0; fi < filterVisibleCount && fi < filterItems.length; fi++) { + filterNodes.push(filterItems[fi]); + } + var breadcrumbItems = [ { title: '运维管理' }, { title: '车辆业务' }, @@ -384,142 +2878,92 @@ const Component = function () { ]; return React.createElement('div', { style: styles.page }, + React.createElement('style', null, DV_KPI_STYLE), React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 } }, React.createElement(Breadcrumb, { items: breadcrumbItems }), React.createElement(Button, { type: 'link', onClick: function () { setRequirementModalOpen(true); } }, '查看需求说明') ), React.createElement(Card, { style: { marginBottom: 16 } }, React.createElement('div', { style: styles.cardBody }, - React.createElement('div', { style: styles.formRow }, - React.createElement('div', null, - React.createElement('div', { style: styles.formLabel }, '合同编码'), - React.createElement(Select, { - placeholder: '请输入或选择合同编码', - allowClear: true, - showSearch: true, - optionFilterProp: 'label', - value: filters.contractCode, - onChange: function (v) { setFilters(function (f) { var g = {}; for (var k in f) g[k] = f[k]; g.contractCode = v; return g; }); }, - style: { width: '100%' }, - options: contractCodeOptions - }) - ), - React.createElement('div', null, - React.createElement('div', { style: styles.formLabel }, '项目名称'), - React.createElement(Select, { - placeholder: '请输入或选择项目名称', - allowClear: true, - showSearch: true, - optionFilterProp: 'label', - value: filters.projectName, - onChange: function (v) { setFilters(function (f) { var g = {}; for (var k in f) g[k] = f[k]; g.projectName = v; return g; }); }, - style: { width: '100%' }, - options: projectNameOptions - }) - ), - React.createElement('div', null, - React.createElement('div', { style: styles.formLabel }, '客户名称'), - React.createElement(Select, { - placeholder: '请输入或选择客户名称', - allowClear: true, - showSearch: true, - optionFilterProp: 'label', - value: filters.customerName, - onChange: function (v) { setFilters(function (f) { var g = {}; for (var k in f) g[k] = f[k]; g.customerName = v; return g; }); }, - style: { width: '100%' }, - options: customerNameOptions - }) - ), - React.createElement('div', null, - React.createElement('div', { style: styles.formLabel }, '交车区域'), - React.createElement(Cascader, { - options: regionOptions, - placeholder: '请选择省-市', - allowClear: true, - style: { width: '100%' }, - value: deliveryRegionValue, - onChange: function (value) { - var s; - if (value && value.length >= 2) { - var prov = regionOptions.find(function (r) { return r.value === value[0]; }); - var city = prov && prov.children && prov.children.find(function (c) { return c.value === value[1]; }); - s = prov && city ? prov.label + '-' + city.label : undefined; - } else { s = undefined; } - setFilters(function (f) { var g = {}; for (var k in f) g[k] = f[k]; g.deliveryRegion = s; return g; }); - }, - displayRender: function (labels) { return labels && labels.length ? labels.join(' / ') : ''; } - }) - ), - React.createElement('div', null, - React.createElement('div', { style: styles.formLabel }, '交车时间'), - React.createElement(RangePicker, { - style: { width: '100%' }, - placeholder: ['请选择交车开始时间', '请选择交车结束时间'], - value: dateRangeValue, - onChange: onDateRangeChange - }) - ), - React.createElement('div', null, - React.createElement('div', { style: styles.formLabel }, '交车人'), - React.createElement(Select, { - placeholder: '请输入或选择交车人姓名', - allowClear: true, - showSearch: true, - optionFilterProp: 'label', - value: filters.deliveryPerson, - onChange: function (v) { setFilters(function (f) { var g = {}; for (var k in f) g[k] = f[k]; g.deliveryPerson = v; return g; }); }, - style: { width: '100%' }, - options: deliveryPersonOptions - }) - ) - ), - React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 16 } }, + React.createElement('div', { style: styles.filterGrid }, filterNodes), + React.createElement('div', { style: styles.filterActions }, React.createElement(Button, { onClick: handleReset }, '重置'), - React.createElement(Button, { type: 'primary', onClick: handleQuery }, '查询') + React.createElement(Button, { type: 'primary', onClick: handleQuery }, '搜索'), + React.createElement(Button, { + type: 'link', + size: 'small', + onClick: function () { setFilterExpanded(!filterExpanded); }, + style: { display: 'inline-flex', alignItems: 'center', gap: 4, padding: '0 4px' } + }, + filterExpanded ? '收起' : '展开', + React.createElement('svg', { + width: 12, + height: 12, + viewBox: '0 0 24 24', + fill: 'none', + stroke: 'currentColor', + strokeWidth: 2, + strokeLinecap: 'round', + strokeLinejoin: 'round', + style: { transform: filterExpanded ? 'rotate(180deg)' : 'none', transition: 'transform 0.2s ease' } + }, React.createElement('polyline', { points: '6 9 12 15 18 9' })) + ) ) ) ), React.createElement(Card, null, - React.createElement(Tabs, { - activeKey: activeTab, - onChange: function (k) { setActiveTab(k); setPage(1); }, - tabBarExtraContent: React.createElement(Button, { onClick: handleExport }, '导出'), - items: [ - { - key: 'pending', - label: '待处理', - children: React.createElement(Table, { - columns: pendingColumns, - dataSource: displayPending, - rowKey: 'id', - pagination: tablePagination, - scroll: { x: 1680 }, - size: 'middle' + React.createElement('div', { style: styles.cardBody }, + React.createElement('div', { className: 'dv-kpi-stats-row' }, kpiCards.map(renderKpiCard)), + React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12, flexWrap: 'wrap', gap: 12 } }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, minWidth: 0 } }, + React.createElement('span', { style: { color: '#333', fontSize: 14, whiteSpace: 'nowrap' } }, '车牌号'), + React.createElement(Select, { + placeholder: '请输入或选择车牌号', + allowClear: true, + showSearch: true, + style: { width: 220 }, + value: appliedFilters.plateNo, + onChange: handleListPlateNoChange, + options: dynamicFilterOptions.plateNoOptions, + filterOption: filterSelectOption }) - }, - { - key: 'history', - label: '历史记录', - children: React.createElement(Table, { - columns: historyColumns, - dataSource: displayHistory, - rowKey: 'id', - pagination: tablePagination, - scroll: { x: 1680 }, - size: 'middle' - }) - } - ] - }) + ), + React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', marginLeft: 'auto' } }, + React.createElement('span', { style: { fontSize: 13, color: '#64748b' } }, + '当前标签:', + React.createElement('span', { style: { color: '#334155', fontWeight: 600 } }, kpiExportLabelMap[kpiFilter] || '-'), + ' · 导出与列表一致(含筛选,全部命中行)' + ), + React.createElement(Button, { onClick: handleExport }, '导出') + ) + ), + React.createElement(Table, { + columns: listColumns, + dataSource: displayList, + rowKey: 'id', + pagination: tablePagination, + tableLayout: 'fixed', + scroll: { x: 1760 }, + size: 'middle' + }) + ) ), React.createElement(Modal, { title: '需求说明', open: requirementModalOpen[0], onCancel: function () { setRequirementModalOpen(false); }, footer: React.createElement(Button, { onClick: function () { setRequirementModalOpen(false); } }, '关闭'), - width: 640, + width: 800, destroyOnClose: true - }, React.createElement('div', { style: { maxHeight: 560, overflowY: 'auto', whiteSpace: 'pre-wrap', lineHeight: 1.6, fontSize: 13 } }, requirementDocContent)) + }, React.createElement('div', { style: { maxHeight: '72vh', overflowY: 'auto', whiteSpace: 'pre-wrap', lineHeight: 1.65, fontSize: 13, color: '#334155' } }, requirementDocContent)), + React.createElement(DeliveryEditDrawer, { + open: editDrawer.open, + record: editDrawer.record, + mode: editDrawer.mode, + onClose: closeEditDrawer, + onSave: handleEditSave, + onSubmit: handleEditSubmit + }) ); }; diff --git a/web端/运维管理/车辆业务/故障管理-编辑.jsx b/web端/运维管理/车辆业务/故障管理-编辑.jsx new file mode 100644 index 0000000..362dd62 --- /dev/null +++ b/web端/运维管理/车辆业务/故障管理-编辑.jsx @@ -0,0 +1,267 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// ONEOS-web - 运维管理 - 车辆业务 - 故障管理-编辑(已回填数据,可修改) + +const Component = function () { + var antd = window.antd; + var Button = antd.Button; + var Input = antd.Input; + var Select = antd.Select; + var DatePicker = antd.DatePicker; + var Form = antd.Form; + var Row = antd.Row; + var Col = antd.Col; + var Breadcrumb = antd.Breadcrumb; + var Layout = antd.Layout; + var message = antd.message; + var Card = antd.Card; + var Upload = antd.Upload; + + var _form = Form.useForm(); + var faultForm = _form[0]; + + // 故障等级枚举 + var faultLevelOptions = [ + { label: '特急', value: '特急' }, + { label: '紧急', value: '紧急' }, + { label: '一般', value: '一般' }, + { label: '提示', value: '提示' } + ]; + + // 故障类型枚举 + var faultTypeOptions = [ + { label: '底盘故障', value: '底盘故障' }, + { label: '三电故障', value: '三电故障' }, + { label: '整车系统', value: '整车系统' }, + { label: '燃料电池系统故障', value: '燃料电池系统故障' }, + { label: '供氢系统故障', value: '供氢系统故障' }, + { label: '空调系统故障', value: '空调系统故障' }, + { label: '冷机故障', value: '冷机故障' }, + { label: '其他故障', value: '其他故障' } + ]; + + // 故障来源枚举 + var faultSourceOptions = [ + { label: '客户报告', value: '客户报告' }, + { label: '定期保养', value: '定期保养' }, + { label: '司机操作问题', value: '司机操作问题' } + ]; + + // 解决情况枚举 + var resolveStatusOptions = [ + { label: '未解决', value: '未解决' }, + { label: '临时排故', value: '临时排故' }, + { label: '已解决', value: '已解决' } + ]; + + var plateOptions = [ + { label: '沪A12345', value: '沪A12345', brand: '一汽解放', model: 'J6P', company: '上海羚牛', vin: 'LNW1234567890ABCD' }, + { label: '浙B88888', value: '浙B88888', brand: '东风商用车', model: '天龙', company: '浙江羚牛', vin: 'LNW0987654321EFGH' }, + { label: '苏C66666', value: '苏C66666', brand: '福田欧曼', model: 'EST', company: '苏州冷链速运有限公司', vin: 'LNW1357924680IJKL' }, + { label: '沪D99999', value: '沪D99999', brand: '陕汽重卡', model: '德龙', company: '上海城配物流有限公司', vin: 'LNW2468013579MNOP' } + ]; + + var PlusIcon = function() { return React.createElement('svg', { viewBox: '0 0 1024 1024', width: 14, height: 14, fill: 'currentColor' }, React.createElement('path', { d: 'M482 152h60q8 0 8 8v704q0 8-8 8h-60q-8 0-8-8V160q0-8 8-8z' }), React.createElement('path', { d: 'M192 474h672q8 0 8 8v60q0 8-8 8H192q-8 0-8-8v-60q0-8 8-8z' })); }; + + var handleBack = function () { + message.info('返回列表'); + }; + + // 编辑态:模拟从列表带入的已存在数据(实际接入路由/接口后替换为 record) + React.useEffect(function () { + var initial = { + code: 'F-2024-001', + plate: '沪A12345', + brand: '一汽解放', + model: 'J6P', + company: '上海羚牛', + vin: 'LNW1234567890ABCD', + type: '底盘故障', + source: '客户报告', + level: '紧急', + reportTime: '2024-05-10 08:30:00', + status: '未解决', + desc: '车辆重载下制动踏板偏软,制动距离明显变长' + }; + if (initial.reportTime) { + if (typeof window.dayjs === 'function') { + initial.reportTime = window.dayjs(initial.reportTime); + } else if (typeof window.moment === 'function') { + initial.reportTime = window.moment(initial.reportTime); + } + } + faultForm.setFieldsValue(initial); + }, []); + + return React.createElement(Layout, { className: 'arco-theme-overrides', style: { minHeight: '100vh', background: '#f2f3f5', fontFamily: 'Inter, Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif' } }, + React.createElement('style', null, ` + .arco-theme-overrides .ant-btn { border-radius: 4px; } + .arco-theme-overrides .ant-btn-primary { background-color: #165dff; border-color: #165dff; } + .arco-theme-overrides .ant-btn-primary:hover { background-color: #4080ff; border-color: #4080ff; } + + .arco-grouped-form-page { display: flex; flex-direction: column; min-height: 100vh; } + .arco-grouped-form-page-content { flex: 1; padding: 16px 20px 24px; } + .arco-grouped-form-page .ant-card { margin-bottom: 16px; border-radius: 4px; border: none; box-shadow: 0 1px 2px rgba(0,0,0,0.06); } + .arco-grouped-form-page .ant-card-head { border-bottom: none; padding: 20px 24px 0; min-height: auto; } + .arco-grouped-form-page .ant-card-head-title { font-size: 16px; font-weight: 500; color: #1d2129; padding: 0; } + .arco-grouped-form-page .ant-card-body { padding: 24px; } + .arco-grouped-form-page .ant-form-vertical .ant-form-item-label { padding-bottom: 8px; height: auto; line-height: 1.5715; } + .arco-grouped-form-page .ant-form-item { margin-bottom: 24px; } + + .arco-grouped-form-page .ant-input, + .arco-grouped-form-page .ant-select-selector, + .arco-grouped-form-page .ant-picker, + .arco-grouped-form-page .ant-input-affix-wrapper { background-color: #f2f3f5; border: 1px solid #e5e6eb; border-radius: 2px; transition: all 0.1s cubic-bezier(0, 0, 1, 1); } + .arco-grouped-form-page .ant-input:hover, + .arco-grouped-form-page .ant-select:not(.ant-select-disabled):hover .ant-select-selector, + .arco-grouped-form-page .ant-picker:hover, + .arco-grouped-form-page .ant-input-affix-wrapper:hover { background-color: #f2f3f5; border-color: #165dff; } + .arco-grouped-form-page .ant-input:focus, + .arco-grouped-form-page .ant-input-focused, + .arco-grouped-form-page .ant-select-focused .ant-select-selector, + .arco-grouped-form-page .ant-picker-focused, + .arco-grouped-form-page .ant-input-affix-wrapper-focused { background-color: #fff; border: 1px solid #165dff !important; box-shadow: 0 0 0 2px rgba(22, 93, 255, 0.2) !important; outline: 0; } + .arco-grouped-form-page .ant-input[disabled], + .arco-grouped-form-page .ant-select-disabled .ant-select-selector, + .arco-grouped-form-page .ant-picker-disabled { color: #86909c; background-color: #f2f3f5; border-color: #e5e6eb; cursor: not-allowed; } + .arco-grouped-form-page .ant-input-affix-wrapper[disabled] { background-color: #f2f3f5; border-color: #e5e6eb; cursor: not-allowed; } + .arco-grouped-form-page .ant-input-affix-wrapper > input.ant-input { background-color: transparent; } + .arco-grouped-form-page .ant-input-affix-wrapper > input.ant-input:focus { background-color: transparent; box-shadow: none !important; border: none !important; } + + .arco-grouped-form-footer { background: #fff; padding: 16px 24px; border-top: 1px solid #e5e6eb; display: flex; justify-content: flex-end; align-items: center; gap: 12px; position: sticky; bottom: 0; z-index: 100; box-shadow: 0 -2px 10px rgba(0,0,0,0.05); } + .arco-grouped-form-footer .ant-btn { border-radius: 5px; height: 32px; padding: 4px 16px; font-size: 14px; } + + .arco-theme-overrides .ant-breadcrumb { color: #86909c; font-size: 14px; white-space: nowrap; flex-shrink: 0; margin-bottom: 20px; } + .arco-theme-overrides .ant-breadcrumb a { color: #4e5969; } + .arco-theme-overrides .ant-breadcrumb a:hover { color: #165dff; background-color: transparent; } + .arco-theme-overrides .ant-form-item-label > label { color: #4e5969; white-space: nowrap; } + .arco-theme-overrides .ant-form-item-label > label::after { display: none !important; content: "" !important; margin: 0 !important; } + `), + React.createElement('div', { className: 'arco-grouped-form-page' }, + React.createElement('div', { className: 'arco-grouped-form-page-content' }, + React.createElement(Breadcrumb, { + separator: React.createElement('span', { style: { color: '#c9cdd4' } }, '/'), + items: [ + { title: '首页' }, + { title: '运维管理' }, + { title: '车辆业务' }, + { title: React.createElement('a', { onClick: function(e) { e.preventDefault(); handleBack(); } }, '故障管理') }, + { title: React.createElement('span', { style: { color: '#1d2129' } }, '编辑故障单') } + ] + }), + + React.createElement(Form, { form: faultForm, layout: 'vertical' }, + React.createElement(Card, { title: '车辆信息', bordered: false }, + React.createElement(Row, { gutter: 24 }, + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '故障编码', name: 'code' }, + React.createElement(Input, { disabled: true, style: { backgroundColor: '#f2f3f5', color: '#86909c' } }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '车牌号', name: 'plate', rules: [{ required: true, message: '请选择车牌号' }] }, + React.createElement(Select, { + placeholder: '请选择车牌号', + options: plateOptions, + showSearch: true, + onChange: function(val, option) { + if (option) { + faultForm.setFieldsValue({ + brand: option.brand, + model: option.model, + company: option.company, + vin: option.vin + }); + } else { + faultForm.setFieldsValue({ brand: undefined, model: undefined, company: undefined, vin: undefined }); + } + } + }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '车辆品牌', name: 'brand' }, + React.createElement(Input, { placeholder: '自动带入', disabled: true, style: { backgroundColor: '#f2f3f5', color: '#86909c' } }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '车辆型号', name: 'model' }, + React.createElement(Input, { placeholder: '自动带入', disabled: true, style: { backgroundColor: '#f2f3f5', color: '#86909c' } }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '运营公司', name: 'company' }, + React.createElement(Input, { placeholder: '自动带入', disabled: true, style: { backgroundColor: '#f2f3f5', color: '#86909c' } }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '车辆识别代码', name: 'vin' }, + React.createElement(Input, { placeholder: '自动带入', disabled: true, style: { backgroundColor: '#f2f3f5', color: '#86909c' } }) + ) + ) + ) + ), + + React.createElement(Card, { title: '故障信息', bordered: false }, + React.createElement(Row, { gutter: 24 }, + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '故障类型', name: 'type', rules: [{ required: true, message: '请选择故障类型' }] }, + React.createElement(Select, { placeholder: '请选择故障类型', options: faultTypeOptions }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '故障来源', name: 'source', rules: [{ required: true, message: '请选择故障来源' }] }, + React.createElement(Select, { placeholder: '请选择故障来源', options: faultSourceOptions }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '故障等级', name: 'level', rules: [{ required: true, message: '请选择故障等级' }] }, + React.createElement(Select, { placeholder: '请选择故障等级', options: faultLevelOptions }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '故障上报时间', name: 'reportTime', rules: [{ required: true, message: '请选择故障上报时间' }] }, + React.createElement(DatePicker, { style: { width: '100%' }, placeholder: '请选择上报时间', showTime: true }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '解决情况', name: 'status', rules: [{ required: true, message: '请选择故障解决情况' }] }, + React.createElement(Select, { placeholder: '请选择故障解决情况', options: resolveStatusOptions }) + ) + ) + ) + ), + + React.createElement(Card, { title: '故障证据与描述', bordered: false }, + React.createElement(Row, { gutter: 24 }, + React.createElement(Col, { span: 24 }, + React.createElement(Form.Item, { label: '故障描述', name: 'desc', rules: [{ required: true, message: '请填写故障描述' }] }, + React.createElement(Input.TextArea, { + placeholder: '在何种状态下,产生何种现象,导致何种事故', + style: { height: 80, minHeight: 80, resize: 'none' } + }) + ) + ), + React.createElement(Col, { span: 24 }, + React.createElement(Form.Item, { label: '故障证据', name: 'evidence' }, + React.createElement(Upload, { listType: 'picture-card' }, + React.createElement('div', null, + React.createElement(PlusIcon, null), + React.createElement('div', { style: { marginTop: 8 } }, '上传文件') + ) + ), + React.createElement('div', { style: { fontSize: 12, color: '#86909c', marginTop: 8 } }, '支持上传照片、视频、录音') + ) + ) + ) + ) + ) + ), + React.createElement('div', { className: 'arco-grouped-form-footer' }, + React.createElement(Button, { onClick: handleBack, style: { borderRadius: 5 } }, '取消'), + React.createElement(Button, { type: 'primary', onClick: function () { faultForm.submit(); message.success('保存成功'); }, style: { borderRadius: 5 } }, '保存') + ) + ) + ); +}; + +if (typeof module !== 'undefined' && module.exports) module.exports = Component; diff --git a/web端/运维管理/车辆业务/故障管理.jsx b/web端/运维管理/车辆业务/故障管理.jsx new file mode 100644 index 0000000..3330eec --- /dev/null +++ b/web端/运维管理/车辆业务/故障管理.jsx @@ -0,0 +1,503 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// ONEOS-web - 运维管理 - 车辆业务 - 故障管理(列表与表单) + +const Component = function () { + var useState = React.useState; + var useEffect = React.useEffect; + + var antd = window.antd; + var Table = antd.Table; + var Button = antd.Button; + var Space = antd.Space; + var Input = antd.Input; + var Select = antd.Select; + var DatePicker = antd.DatePicker; + var Form = antd.Form; + var Row = antd.Row; + var Col = antd.Col; + var Divider = antd.Divider; + var Breadcrumb = antd.Breadcrumb; + var Layout = antd.Layout; + var message = antd.message; + var Tag = antd.Tag; + var Card = antd.Card; + var Modal = antd.Modal; + var Tabs = antd.Tabs; + var Tooltip = antd.Tooltip; + var RangePicker = DatePicker.RangePicker; + + var _filterFormInst = Form.useForm(); + var filterForm = _filterFormInst[0]; + + var _specOpen = useState(false); + var specModalOpen = _specOpen[0]; + var setSpecModalOpen = _specOpen[1]; + + var _listTab = useState('pending'); + var listTab = _listTab[0]; + var setListTab = _listTab[1]; + + var _tableRows = useState(null); + var tableRowsOverride = _tableRows[0]; + var setTableRowsOverride = _tableRows[1]; + + var faultLevelOptions = [ + { label: 'L1-特急', value: '特急' }, + { label: 'L2-紧急', value: '紧急' }, + { label: 'L3-一般', value: '一般' }, + { label: 'L4-提示', value: '提示' } + ]; + + var lastOperatorOptions = [ + { label: '王婷婷', value: '王婷婷' }, + { label: '刘若楠', value: '刘若楠' }, + { label: '陈嘉豪', value: '陈嘉豪' }, + { label: '赵海峰', value: '赵海峰' } + ]; + + // 故障类型枚举 + var faultTypeOptions = [ + { label: '底盘故障', value: '底盘故障' }, + { label: '三电故障', value: '三电故障' }, + { label: '整车系统', value: '整车系统' }, + { label: '燃料电池系统故障', value: '燃料电池系统故障' }, + { label: '供氢系统故障', value: '供氢系统故障' }, + { label: '空调系统故障', value: '空调系统故障' }, + { label: '冷机故障', value: '冷机故障' }, + { label: '其他故障', value: '其他故障' } + ]; + + // 解决情况枚举 + var resolveStatusOptions = [ + { label: '未解决', value: '未解决' }, + { label: '临时排故', value: '临时排故' }, + { label: '已解决', value: '已解决' } + ]; + + var SearchIcon = function() { return React.createElement('svg', { viewBox: '0 0 1024 1024', width: 14, height: 14, fill: 'currentColor' }, React.createElement('path', { d: 'M909.6 854.5L649.9 594.8C690.2 542.7 712 479 712 412c0-80.2-31.3-155.4-87.9-212.1-56.6-56.7-132-87.9-212.1-87.9s-155.5 31.3-212.1 87.9C143.2 256.5 112 331.8 112 412c0 80.1 31.3 155.5 87.9 212.1C256.5 680.8 331.8 712 412 712c67 0 130.6-21.8 182.7-62l259.7 259.6a8.2 8.2 0 0011.6 0l43.6-43.5a8.2 8.2 0 000-11.6zM570.4 570.4C528 612.7 471.8 636 412 636s-116-23.3-158.4-65.6C211.3 528 188 471.8 188 412s23.3-116.1 65.6-158.4C296 211.3 352.2 188 412 188s116.1 23.2 158.4 65.6S636 352.2 636 412s-23.3 116.1-65.6 158.4z' })); }; + var FileTextIcon = function() { return React.createElement('svg', { viewBox: '0 0 1024 1024', width: 16, height: 16, fill: 'currentColor' }, React.createElement('path', { d: 'M854.6 288.7L639.4 73.4c-6-6-14.2-9.4-22.7-9.4H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V311.3c0-8.5-3.4-16.7-9.4-22.7zM790.2 326H602V137.8L790.2 326zm1.8 562H232V136h302v216c0 22.1 17.9 40 40 40h216v494zM504 618H320c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h184c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8zM702 458H320c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h382c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z' })); }; + var ResetIcon = function() { return React.createElement('svg', { viewBox: '0 0 1024 1024', width: 14, height: 14, fill: 'currentColor' }, React.createElement('path', { d: 'M793 242H366v-74c0-6.7-7.7-10.4-12.9-6.3l-142 112a8 8 0 000 12.6l142 112c5.2 4.1 12.9.4 12.9-6.3v-74h415v470H175c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h618c35.3 0 64-28.7 64-64V306c0-35.3-28.7-64-64-64z' })); }; + var PlusIcon = function() { return React.createElement('svg', { viewBox: '0 0 1024 1024', width: 14, height: 14, fill: 'currentColor' }, React.createElement('path', { d: 'M482 152h60q8 0 8 8v704q0 8-8 8h-60q-8 0-8-8V160q0-8 8-8z' }), React.createElement('path', { d: 'M192 474h672q8 0 8 8v60q0 8-8 8H192q-8 0-8-8v-60q0-8 8-8z' })); }; + var DownloadIcon = function() { return React.createElement('svg', { viewBox: '0 0 1024 1024', width: 16, height: 16, fill: 'currentColor' }, React.createElement('path', { d: 'M505.7 661a8 8 0 0012.6 0l112-141.7c4.1-5.2.4-12.9-6.3-12.9h-74.1V168c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v338.3H400c-6.7 0-10.4 7.7-6.3 12.9l112 141.8zM878 626h-60c-4.4 0-8 3.6-8 8v154H214V634c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v198c0 17.7 14.3 32 32 32h684c17.7 0 32-14.3 32-32V634c0-4.4-3.6-8-8-8z' })); }; + var ListIcon = function() { return React.createElement('svg', { viewBox: '0 0 48 48', width: 14, height: 14, fill: 'none', stroke: 'currentColor', strokeWidth: 4, strokeLinecap: 'butt', strokeLinejoin: 'miter' }, React.createElement('path', { d: 'M17 12H42' }), React.createElement('path', { d: 'M17 24H42' }), React.createElement('path', { d: 'M17 36H42' }), React.createElement('path', { d: 'M8 12H9' }), React.createElement('path', { d: 'M8 24H9' }), React.createElement('path', { d: 'M8 36H9' })); }; + var UploadIcon = function() { return React.createElement('svg', { viewBox: '0 0 1024 1024', width: 14, height: 14, fill: 'currentColor' }, React.createElement('path', { d: 'M512 256l-256 256h160v256h192V512h160L512 256zM256 832v64h512v-64H256z' })); }; + + var levelLabelMap = { 特急: 'L1-特急', 紧急: 'L2-紧急', 一般: 'L3-一般', 提示: 'L4-提示' }; + var renderFaultLevel = function (text) { + var color = 'default'; + if (text === '特急') color = 'red'; + else if (text === '紧急') color = 'orange'; + else if (text === '一般') color = 'blue'; + else if (text === '提示') color = 'green'; + var label = levelLabelMap[text] || text; + return React.createElement(Tag, { color: color }, label); + }; + + var renderResolveStatus = function (text) { + var dot = '#86909c'; + if (text === '已解决') dot = '#00b42a'; + else if (text === '临时排故') dot = '#ff7d00'; + else if (text === '未解决') dot = '#f53f3f'; + return React.createElement(Space, { size: 6 }, + React.createElement('span', { style: { display: 'inline-block', width: 6, height: 6, borderRadius: '50%', backgroundColor: dot } }), + React.createElement('span', null, text) + ); + }; + + var columns = [ + { title: '故障编码', dataIndex: 'code', key: 'code', width: 132, fixed: 'left' }, + { title: '解决情况', dataIndex: 'status', key: 'status', width: 120, render: renderResolveStatus }, + { title: '车牌号码', dataIndex: 'plate', key: 'plate', width: 112 }, + { title: '车辆品牌', dataIndex: 'brand', key: 'brand', width: 100, ellipsis: true }, + { title: '车辆型号', dataIndex: 'model', key: 'model', width: 100, ellipsis: true }, + { title: '运营公司', dataIndex: 'company', key: 'company', width: 140, ellipsis: true }, + { title: '故障等级', dataIndex: 'level', key: 'level', width: 108, render: renderFaultLevel }, + { title: '故障类型', dataIndex: 'type', key: 'type', width: 128, ellipsis: true }, + { + title: '故障描述', + dataIndex: 'desc', + key: 'desc', + width: 200, + ellipsis: { showTitle: false }, + render: function (text) { + return React.createElement(Tooltip, { placement: 'topLeft', title: text }, + React.createElement('span', { style: { cursor: 'default' } }, text) + ); + } + }, + { title: '故障上报时间', dataIndex: 'reportTime', key: 'reportTime', width: 176, ellipsis: true, className: 'fault-col-report-time' }, + { title: '最后操作时间', dataIndex: 'lastOperationTime', key: 'lastOperationTime', width: 176, ellipsis: true }, + { title: '最后操作人', dataIndex: 'lastOperator', key: 'lastOperator', width: 104, ellipsis: true }, + { + title: '操作', + key: 'action', + fixed: 'right', + width: listTab === 'history' ? 72 : 104, + render: function (_, record) { + if (listTab === 'history') { + return React.createElement(Button, { type: 'link', size: 'small', style: { padding: '0 4px' }, onClick: function () { handleView(record); } }, '查看'); + } + return React.createElement(Space, { size: 0, split: React.createElement('span', { style: { color: '#e5e6eb' } }, '|') }, + React.createElement(Button, { type: 'link', size: 'small', style: { padding: '0 4px' }, onClick: function () { handleView(record); } }, '查看'), + React.createElement(Button, { type: 'link', size: 'small', style: { padding: '0 4px' }, onClick: function () { handleEdit(record); } }, '编辑') + ); + } + } + ]; + + var ALL_DATA = [ + { key: '1', code: 'F-2024-001', plate: '沪A12345', brand: '一汽解放', model: 'J6P', company: '上海羚牛', type: '底盘故障', level: '紧急', source: '客户报告', status: '未解决', reportTime: '2024-05-10 08:30:00', lastOperator: '王婷婷', lastOperationTime: '2024-05-10 09:00:00', desc: '车辆重载下制动踏板偏软,制动距离明显变长' }, + { key: '2', code: 'F-2024-002', plate: '浙B88888', brand: '东风商用车', model: '天龙', company: '浙江羚牛', type: '三电故障', level: '特急', source: '司机操作问题', status: '临时排故', reportTime: '2024-05-11 14:15:00', lastOperator: '刘若楠', lastOperationTime: '2024-05-11 16:20:00', desc: '高压系统绝缘报警频繁触发,车辆进入限扭模式' }, + { key: '3', code: 'F-2024-003', plate: '苏C66666', brand: '福田欧曼', model: 'EST', company: '苏州冷链速运有限公司', type: '整车系统', level: '一般', source: '定期保养', status: '已解决', reportTime: '2024-05-12 10:00:00', lastOperator: '陈嘉豪', lastOperationTime: '2024-05-12 11:30:00', desc: '后桥轮胎偏磨,建议更换并做四轮定位' }, + { key: '4', code: 'F-2024-004', plate: '沪D99999', brand: '陕汽重卡', model: '德龙', company: '上海城配物流有限公司', type: '空调系统故障', level: '提示', source: '客户报告', status: '已解决', reportTime: '2024-05-13 09:45:00', lastOperator: '赵海峰', lastOperationTime: '2024-05-13 10:10:00', desc: '空调制冷效果差,出风口温度偏高' } + ]; + + var parseReportTime = function (str) { + if (!str) return null; + var p = str.replace(/-/g, '/'); + var t = new Date(p); + return isNaN(t.getTime()) ? null : t; + }; + + var applyListFilter = function () { + var v = filterForm.getFieldsValue(); + var rows = ALL_DATA.filter(function (r) { + if (listTab === 'pending') return r.status !== '已解决'; + return r.status === '已解决'; + }); + if (v.filterPlate && String(v.filterPlate).trim()) { + var q = String(v.filterPlate).trim(); + rows = rows.filter(function (r) { return (r.plate || '').indexOf(q) !== -1; }); + } + if (v.filterLevels && v.filterLevels.length) { + rows = rows.filter(function (r) { return v.filterLevels.indexOf(r.level) !== -1; }); + } + if (v.filterTypes && v.filterTypes.length) { + rows = rows.filter(function (r) { return v.filterTypes.indexOf(r.type) !== -1; }); + } + if (v.filterLastOperator) { + rows = rows.filter(function (r) { return r.lastOperator === v.filterLastOperator; }); + } + if (v.filterDateRange && v.filterDateRange.length === 2 && v.filterDateRange[0] && v.filterDateRange[1]) { + var start = v.filterDateRange[0]; + var end = v.filterDateRange[1]; + var startMs = start.valueOf ? start.valueOf() : new Date(start).getTime(); + var endMs = end.valueOf ? end.valueOf() : new Date(end).getTime(); + var startDay = new Date(startMs); + startDay.setHours(0, 0, 0, 0); + startMs = startDay.getTime(); + var endDay = new Date(endMs); + endDay.setHours(23, 59, 59, 999); + endMs = endDay.getTime(); + rows = rows.filter(function (r) { + var rt = parseReportTime(r.reportTime); + if (!rt) return false; + var m = rt.getTime(); + return m >= startMs && m <= endMs; + }); + } + setTableRowsOverride(rows); + }; + + useEffect(function () { + applyListFilter(); + }, [listTab]); + + var displayData = tableRowsOverride !== null && tableRowsOverride !== undefined + ? tableRowsOverride + : ALL_DATA.filter(function (r) { + if (listTab === 'pending') return r.status !== '已解决'; + return r.status === '已解决'; + }); + + var handleCreate = function () { + message.info('跳转到新增故障页面'); + }; + + var handleEdit = function (record) { + message.info('跳转到编辑故障页面'); + }; + + var handleView = function (record) { + message.info('跳转到查看故障页面'); + }; + + + var formItemLayout = { + labelAlign: 'left', + colon: false, + labelCol: { flex: '0 0 100px' }, + wrapperCol: { flex: '1 1 0' } + }; + + var specSection = function (title, children) { + return React.createElement('div', { style: { marginBottom: 16 } }, + React.createElement('div', { style: { fontWeight: 600, color: '#1d2129', marginBottom: 8, fontSize: 14 } }, title), + children + ); + }; + var specLine = function (text) { + return React.createElement('div', { style: { fontSize: 13, color: '#4e5969', lineHeight: 1.75, paddingLeft: 0 } }, text); + }; + + var renderSpecModal = function () { + return React.createElement(Modal, { + title: '故障列表页 — 需求说明', + open: specModalOpen, + onCancel: function () { setSpecModalOpen(false); }, + footer: React.createElement(Button, { type: 'primary', onClick: function () { setSpecModalOpen(false); } }, '知道了'), + width: 720, + centered: true, + destroyOnClose: true + }, + React.createElement('div', { style: { maxHeight: '70vh', overflowY: 'auto', paddingRight: 4 } }, + specSection('2. 故障列表页', React.createElement(React.Fragment, null, + specLine('用于展示待处理与历史记录,支持多条件检索、查看、编辑、删除、导入导出。') + )), + specSection('2.1 顶部筛选区(支持单条件或多条件组合)', React.createElement(React.Fragment, null, + specLine('2.1.1 车牌号:输入框,支持模糊搜索;'), + specLine('2.1.2 故障等级:多选下拉,选项:L1-特急、L2-紧急、L3-一般、L4-提示;'), + specLine('2.1.3 上报时间:日期范围选择(开始~结束);'), + specLine('2.1.4 故障类型:多选下拉,选项为故障类型枚举;'), + specLine('2.1.5 最后操作人:下拉选择器;'), + specLine('2.1.6 查询按钮:执行筛选;'), + specLine('2.1.7 重置按钮:清空筛选条件。') + )), + specSection('2.2 列表工具栏', React.createElement(React.Fragment, null, + specLine('2.2.1 新建:进入故障表单页;'), + specLine('2.2.2 导出:导出当前筛选结果;'), + specLine('2.2.3 导入:批量导入故障数据;'), + specLine('2.2.4 展示设置:字段展示配置(当前版本可预留)。') + )), + specSection('2.3 Tab分区', React.createElement(React.Fragment, null, + specLine('2.3.1 待处理;'), + specLine('2.3.2 历史记录。') + )), + specSection('2.4 列表字段展示(当前版本)', React.createElement(React.Fragment, null, + specLine('故障编码、解决情况、车牌号码、车辆品牌、车辆型号、运营公司、故障等级、故障类型、故障描述(省略展示,悬停显示完整)、故障上报时间、最后操作时间、最后操作人、操作(待处理:查看/编辑;历史记录:仅查看)。'), + specLine('说明:车辆识别代码不在列表展示,仅在表单车辆信息中展示。') + )), + specSection('2.5 状态与操作规则', React.createElement(React.Fragment, null, + specLine('2.5.1 解决情况状态值:临时排故、已解决、未解决;'), + specLine('2.5.2 操作列:待处理 Tab 为查看、编辑(无删除);历史记录 Tab 仅查看;'), + specLine('2.5.3 分页:每页10条(固定分页)。') + )) + ) + ); + }; + + return React.createElement(Layout, { className: 'arco-theme-overrides', style: { minHeight: '100vh', background: '#f2f3f5', fontFamily: 'Inter, Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif' } }, + React.createElement('style', null, ` + .arco-theme-overrides .ant-btn { border-radius: 4px; } + .arco-theme-overrides .ant-btn-primary { background-color: #165dff; border-color: #165dff; } + .arco-theme-overrides .ant-btn-primary:hover { background-color: #4080ff; border-color: #4080ff; } + .arco-theme-overrides .ant-btn-link { color: #165dff; } + .arco-theme-overrides .ant-btn-link:hover { background-color: transparent; color: #4080ff; } + + .arco-theme-overrides .ant-table-thead > tr > th { + background-color: #f2f3f5; + color: #909399; + font-weight: 400; + font-size: 14px; + height: 40px; + padding: 9px 16px; + line-height: 22px; + box-sizing: border-box; + border-bottom: 1px solid #e5e6eb; + border-top: none; + vertical-align: middle; + } + .arco-theme-overrides .ant-table-thead > tr > th .ant-table-column-title { + font-size: 14px; + color: #909399; + line-height: 22px; + min-height: 22px; + white-space: nowrap; + } + .arco-theme-overrides .ant-table-thead > tr > th::before { display: none !important; } + .arco-theme-overrides .ant-table-tbody > tr > td { border-bottom: 1px solid #e5e6eb; padding: 13px 16px; color: #4e5969; vertical-align: middle; line-height: 22px; } + .arco-theme-overrides .ant-table-tbody > tr { height: 52px; } + .arco-theme-overrides .customer-table-scroll-wrap { width: 100%; min-width: 0; max-width: 100%; } + .arco-theme-overrides .ant-table-wrapper { border: none; } + .arco-theme-overrides .ant-table { border: none; } + .arco-theme-overrides .ant-table-container { border: none; } + .arco-theme-overrides .ant-table-pagination.ant-pagination { margin: 16px 0 0 0; } + + .arco-theme-overrides .ant-card { border: none; border-radius: 4px; } + .arco-theme-overrides .customer-page-card.ant-card { border: 1px solid #e5e6eb !important; border-radius: 4px; box-shadow: 0 1px 2px rgba(0,0,0,0.06); background: #fff; } + .arco-theme-overrides .customer-page-card .ant-card-body { padding: 20px 24px 24px; } + + .arco-theme-overrides .ant-input, .arco-theme-overrides .ant-select-selector, .arco-theme-overrides .ant-picker, .arco-theme-overrides .ant-input-affix-wrapper { border-radius: 2px; border: 1px solid #e5e6eb; background-color: #fff; transition: all 0.1s cubic-bezier(0, 0, 1, 1); } + .arco-theme-overrides .ant-input:hover, .arco-theme-overrides .ant-select:not(.ant-select-disabled):hover .ant-select-selector, .arco-theme-overrides .ant-picker:hover, .arco-theme-overrides .ant-input-affix-wrapper:hover { background-color: #fff; border-color: #165dff; } + .arco-theme-overrides .ant-input:focus, .arco-theme-overrides .ant-input-focused, .arco-theme-overrides .ant-select-focused .ant-select-selector, .arco-theme-overrides .ant-picker-focused, .arco-theme-overrides .ant-input-affix-wrapper-focused { background-color: #fff; border: 1px solid #165dff !important; box-shadow: 0 0 0 2px rgba(22, 93, 255, 0.2) !important; outline: 0; } + + .arco-theme-overrides .ant-breadcrumb { color: #86909c; font-size: 14px; white-space: nowrap; flex-shrink: 0; } + .arco-theme-overrides .customer-page-header-row { display: flex; align-items: center; flex-wrap: nowrap; gap: 0; margin-bottom: 20px; } + .arco-theme-overrides .ant-breadcrumb a { color: #4e5969; } + .arco-theme-overrides .ant-breadcrumb a:hover { color: #165dff; background-color: transparent; } + + .arco-theme-overrides .ant-form-item-label { padding: 0 16px 0 0 !important; line-height: 32px; height: 32px; display: flex; align-items: center; } + .arco-theme-overrides .ant-form-item-control { display: flex; align-items: center; line-height: 32px; min-height: 32px; } + .arco-theme-overrides .ant-form-item-control-input { min-height: 32px; width: 100%; display: flex; align-items: center; } + .arco-theme-overrides .ant-form-item-control-input-content { display: flex; align-items: center; width: 100%; height: 100%; } + .arco-theme-overrides .ant-select { width: 100%; height: 32px; } + .arco-theme-overrides .ant-select-selector { height: 32px !important; min-height: 32px !important; display: flex; align-items: center; width: 100%; padding: 0 12px; } + .arco-theme-overrides .ant-input { height: 32px; padding: 4px 12px; } + .arco-theme-overrides .ant-input-affix-wrapper { height: 32px; padding: 0 12px; display: flex; align-items: center; } + .arco-theme-overrides .ant-input-affix-wrapper > input.ant-input, + .arco-theme-overrides .ant-input-affix-wrapper > input.ant-input:focus, + .arco-theme-overrides .ant-input-affix-wrapper > input.ant-input:hover { height: 100%; padding: 0; border: none !important; box-shadow: none !important; outline: none !important; background-color: transparent !important; } + .arco-theme-overrides .ant-picker { height: 32px !important; min-height: 32px !important; display: flex; align-items: center; padding: 4px 12px; width: 100%; } + .arco-theme-overrides .ant-form-item-label > label { color: #4e5969; white-space: nowrap; } + .arco-theme-overrides .ant-form-item-label > label::after { display: none !important; content: "" !important; margin: 0 !important; } + .arco-theme-overrides .ant-select-multiple .ant-select-selector { height: auto !important; min-height: 32px !important; align-items: center; padding-top: 2px; padding-bottom: 2px; } + .arco-theme-overrides .fault-col-report-time { white-space: nowrap; } + `), + React.createElement('div', { style: { padding: '16px 20px 24px', display: 'flex', flexDirection: 'column', flex: 1 } }, + React.createElement(Card, { className: 'customer-page-card', bordered: false }, + React.createElement('div', { className: 'customer-page-header-row', style: { marginBottom: 16, justifyContent: 'space-between', width: '100%' } }, + React.createElement(Breadcrumb, { + separator: React.createElement('span', { style: { color: '#c9cdd4' } }, '/'), + items: [ + { title: React.createElement(ListIcon, { style: { display: 'inline-flex', alignItems: 'center', fontSize: 14, transform: 'translate(-2px, 1px)' } }) }, + { title: '运维管理' }, + { title: '车辆业务' }, + { title: React.createElement('span', { style: { color: '#1d2129', fontWeight: 700, fontSize: 16, lineHeight: '22px' } }, '故障管理') } + ] + }), + React.createElement(Button, { + type: 'link', + icon: React.createElement(FileTextIcon, null), + style: { display: 'flex', alignItems: 'center', gap: 4, padding: '0 4px', color: '#165dff', fontWeight: 500 }, + onClick: function () { setSpecModalOpen(true); } + }, '查看需求说明') + ), + React.createElement('div', { style: { fontSize: 18, fontWeight: 600, color: '#1d2129', lineHeight: '26px', marginBottom: 20 } }, '故障管理'), + + // 搜索表单区域 + React.createElement('div', { style: { marginBottom: 0 } }, + React.createElement(Row, { style: { flexWrap: 'nowrap', alignItems: 'stretch' } }, + React.createElement(Col, { flex: 1, style: { minWidth: 0, paddingRight: 40 } }, + React.createElement(Form, Object.assign({ layout: 'horizontal', form: filterForm }, formItemLayout), + React.createElement(Row, { gutter: 24, style: { rowGap: 0 } }, + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { name: 'filterPlate', label: '车牌号', style: { marginBottom: 16 } }, + React.createElement(Input, { placeholder: '支持模糊搜索', allowClear: true }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { name: 'filterLevels', label: '故障等级', style: { marginBottom: 16 } }, + React.createElement(Select, { + mode: 'multiple', + allowClear: true, + placeholder: '多选:L1~L4', + options: faultLevelOptions, + maxTagCount: 'responsive' + }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { name: 'filterDateRange', label: '上报时间', style: { marginBottom: 16 } }, + React.createElement(RangePicker, { style: { width: '100%' }, showTime: false, placeholder: ['开始', '结束'] }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { name: 'filterTypes', label: '故障类型', style: { marginBottom: 16 } }, + React.createElement(Select, { + mode: 'multiple', + allowClear: true, + placeholder: '多选故障类型', + options: faultTypeOptions, + maxTagCount: 'responsive' + }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { name: 'filterLastOperator', label: '最后操作人', style: { marginBottom: 16 } }, + React.createElement(Select, { + allowClear: true, + placeholder: '请选择', + options: lastOperatorOptions + }) + ) + ) + ) + ) + ), + React.createElement(Divider, { + type: 'vertical', + style: { + alignSelf: 'stretch', + height: 'auto', + minHeight: 100, + borderLeftColor: 'rgb(229, 230, 235)', + borderLeftStyle: 'dashed', + marginLeft: 20, + marginRight: 20 + } + }), + React.createElement(Col, { flex: 'none', style: { textAlign: 'right', display: 'flex', flexDirection: 'column', alignItems: 'flex-end', minWidth: 100, maxWidth: 168 } }, + React.createElement(Space, { direction: 'vertical', size: 12, style: { width: '100%', alignItems: 'flex-end' } }, + React.createElement(Button, { type: 'primary', icon: React.createElement(SearchIcon, null), style: { display: 'flex', alignItems: 'center', gap: 6, justifyContent: 'center', width: 86 }, onClick: function () { applyListFilter(); message.success('已按条件筛选'); } }, '查询'), + React.createElement(Button, { + icon: React.createElement(ResetIcon, null), + style: { display: 'flex', alignItems: 'center', gap: 6, justifyContent: 'center', width: 86 }, + onClick: function () { + filterForm.resetFields(); + applyListFilter(); + message.info('已清空筛选条件'); + } + }, '重置') + ) + ) + ) + ), + React.createElement(Divider, { style: { margin: '16px 0 20px', borderColor: '#e5e6eb' } }), + + // 工具栏 + 表格 + React.createElement('div', { style: { display: 'flex', flexDirection: 'column', flex: 1, minWidth: 0 } }, + React.createElement(Row, { justify: 'space-between', align: 'middle', style: { marginBottom: 16 } }, + React.createElement(Col, null, + React.createElement(Space, { size: 12 }, + React.createElement(Button, { type: 'primary', icon: React.createElement(PlusIcon, null), style: { display: 'flex', alignItems: 'center', gap: 6 }, onClick: handleCreate }, '新建'), + React.createElement(Button, { + style: { padding: '4px 10px', height: 32, display: 'flex', alignItems: 'center', gap: 6, color: '#4e5969' }, + onClick: function () { message.info('已导出当前筛选结果'); } + }, React.createElement(DownloadIcon, null), React.createElement('span', null, '导出')), + React.createElement(Button, { icon: React.createElement(UploadIcon, null), style: { display: 'flex', alignItems: 'center', gap: 6 }, onClick: function () { message.info('批量导入故障数据'); } }, '导入'), + React.createElement(Button, { style: { color: '#4e5969' }, onClick: function () { message.info('字段展示配置(当前版本预留)'); } }, '展示设置') + ) + ), + React.createElement(Col, null) + ), + React.createElement(Tabs, { + activeKey: listTab, + onChange: function (k) { + setListTab(k); + }, + style: { marginBottom: 12 }, + items: [ + { key: 'pending', label: '待处理' }, + { key: 'history', label: '历史记录' } + ] + }), + React.createElement('div', { className: 'customer-table-scroll-wrap', style: { flex: 1, minHeight: 0, minWidth: 0 } }, + React.createElement(Table, { + columns: columns, + dataSource: displayData, + pagination: { + pageSize: 10, + showSizeChanger: false, + showTotal: function (total) { return '共 ' + total + ' 条'; } + }, + scroll: { x: 2100 } + }) + ) + ) + ) + ), + renderSpecModal() + ); +}; + +if (typeof module !== 'undefined' && module.exports) module.exports = Component; diff --git a/web端/运维管理/车辆业务/新增故障.jsx b/web端/运维管理/车辆业务/新增故障.jsx new file mode 100644 index 0000000..ff8696e --- /dev/null +++ b/web端/运维管理/车辆业务/新增故障.jsx @@ -0,0 +1,417 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// ONEOS-web - 运维管理 - 车辆业务 - 新增故障 + +const Component = function () { + var useState = React.useState; + var useEffect = React.useEffect; + + var antd = window.antd; + var Button = antd.Button; + var Input = antd.Input; + var Select = antd.Select; + var DatePicker = antd.DatePicker; + var Form = antd.Form; + var Row = antd.Row; + var Col = antd.Col; + var Breadcrumb = antd.Breadcrumb; + var Layout = antd.Layout; + var message = antd.message; + var Card = antd.Card; + var Upload = antd.Upload; + var Modal = antd.Modal; + + var _form = Form.useForm(); + var faultForm = _form[0]; + + var _specOpen = useState(false); + var specModalOpen = _specOpen[0]; + var setSpecModalOpen = _specOpen[1]; + + var _formDirty = useState(false); + var formDirty = _formDirty[0]; + var setFormDirty = _formDirty[1]; + + var faultLevelOptions = [ + { label: 'L1-特急', value: '特急' }, + { label: 'L2-紧急', value: '紧急' }, + { label: 'L3-一般', value: '一般' }, + { label: 'L4-提示', value: '提示' } + ]; + + var faultTypeOptions = [ + { label: '底盘故障', value: '底盘故障' }, + { label: '三电故障', value: '三电故障' }, + { label: '整车系统', value: '整车系统' }, + { label: '燃料电池系统故障', value: '燃料电池系统故障' }, + { label: '供氢系统故障', value: '供氢系统故障' }, + { label: '空调系统故障', value: '空调系统故障' }, + { label: '冷机故障', value: '冷机故障' }, + { label: '其他故障', value: '其他故障' } + ]; + + var faultSourceOptions = [ + { label: '客户报告', value: '客户报告' }, + { label: '定期保养', value: '定期保养' }, + { label: '周期性维护', value: '周期性维护' }, + { label: '预防性维护', value: '预防性维护' }, + { label: '整备', value: '整备' } + ]; + + var resolveStatusOptions = [ + { label: '未解决', value: '未解决' }, + { label: '临时排故', value: '临时排故' }, + { label: '已解决', value: '已解决' } + ]; + + /** 运营公司枚举:浙江羚牛、上海羚牛、广东羚牛 */ + var plateOptions = [ + { label: '沪A12345', value: '沪A12345', brand: '一汽解放', model: 'J6P', company: '上海羚牛', vin: 'LNW1234567890ABCD' }, + { label: '浙B88888', value: '浙B88888', brand: '东风商用车', model: '天龙', company: '浙江羚牛', vin: 'LNW0987654321EFGH' }, + { label: '粤A00001', value: '粤A00001', brand: '福田欧曼', model: 'EST', company: '广东羚牛', vin: 'LNW1357924680IJKL' }, + { label: '沪D99999', value: '沪D99999', brand: '陕汽重卡', model: '德龙', company: '上海羚牛', vin: 'LNW2468013579MNOP' } + ]; + + var PlusIcon = function() { return React.createElement('svg', { viewBox: '0 0 1024 1024', width: 14, height: 14, fill: 'currentColor' }, React.createElement('path', { d: 'M482 152h60q8 0 8 8v704q0 8-8 8h-60q-8 0-8-8V160q0-8 8-8z' }), React.createElement('path', { d: 'M192 474h672q8 0 8 8v60q0 8-8 8H192q-8 0-8-8v-60q0-8 8-8z' })); }; + var FileTextIcon = function() { return React.createElement('svg', { viewBox: '0 0 1024 1024', width: 16, height: 16, fill: 'currentColor' }, React.createElement('path', { d: 'M854.6 288.7L639.4 73.4c-6-6-14.2-9.4-22.7-9.4H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V311.3c0-8.5-3.4-16.7-9.4-22.7zM790.2 326H602V137.8L790.2 326zm1.8 562H232V136h302v216c0 22.1 17.9 40 40 40h216v494zM504 618H320c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h184c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8zM702 458H320c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h382c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z' })); }; + + var DESC_PLACEHOLDER = '在何种状态下\n产生何种现象\n导致何种事故'; + + var goBackList = function () { + message.info('返回列表'); + }; + + var requestExit = function () { + if (formDirty) { + Modal.confirm({ + title: '确定要退出吗?', + content: '未保存的数据将丢失', + okText: '确定', + cancelText: '取消', + onOk: function () { + setFormDirty(false); + goBackList(); + } + }); + } else { + goBackList(); + } + }; + + var openResultModal = function (title, okText, cancelText, onPrimary) { + Modal.success({ + title: title, + content: title === '提交成功' ? '故障已提交并加入历史记录。' : '单据已保存在待处理,可继续编辑或稍后提交。', + okText: okText, + cancelText: cancelText, + okCancel: true, + centered: true, + onOk: function () { + if (onPrimary) onPrimary(); + }, + onCancel: function () {} + }); + }; + + var handleSave = function () { + // 弱校验:不触发表单校验;落库时解决情况空值按「未解决」与列表一致 + faultForm.getFieldsValue(); + setFormDirty(false); + openResultModal('保存成功', '返回列表', '确定', goBackList); + }; + + var runSubmit = function () { + faultForm.validateFields(['plate', 'type', 'source', 'level', 'reportTime', 'desc']).then(function () { + var list = faultForm.getFieldValue('evidence') || []; + if (!list.length) { + message.error('请上传故障证据'); + return; + } + setFormDirty(false); + openResultModal('提交成功', '返回列表', '确定', goBackList); + }).catch(function () {}); + }; + + var handleSubmitClick = function () { + Modal.confirm({ + title: '确认提交', + content: '故障提交后无法修改,如未完成所有步骤填写请先进行保存。点击确认加入历史记录', + okText: '确认', + cancelText: '取消', + centered: true, + onOk: function () { + runSubmit(); + } + }); + }; + + useEffect(function () { + var fn = function (e) { + if (formDirty) { + e.preventDefault(); + e.returnValue = ''; + } + }; + window.addEventListener('beforeunload', fn); + return function () { window.removeEventListener('beforeunload', fn); }; + }, [formDirty]); + + var specSection = function (title, lines) { + return React.createElement('div', { style: { marginBottom: 16 } }, + React.createElement('div', { style: { fontWeight: 600, color: '#1d2129', marginBottom: 8, fontSize: 14 } }, title), + lines.map(function (text, i) { + return React.createElement('div', { key: i, style: { fontSize: 13, color: '#4e5969', lineHeight: 1.75 } }, text); + }) + ); + }; + + var renderSpecModal = function () { + return React.createElement(Modal, { + title: '故障信息表单页 — 需求说明', + open: specModalOpen, + onCancel: function () { setSpecModalOpen(false); }, + footer: React.createElement(Button, { type: 'primary', onClick: function () { setSpecModalOpen(false); } }, '知道了'), + width: 720, + centered: true, + destroyOnClose: true + }, + React.createElement('div', { style: { maxHeight: '70vh', overflowY: 'auto', paddingRight: 4 } }, + specSection('3. 故障信息表单页', ['用于新增/编辑故障记录,保存到待处理或提交到历史记录。']), + specSection('3.1 车辆信息卡片(自动带出)', [ + '3.1.1 车牌号:必填,下拉可搜索;', + '3.1.2 车辆品牌:禁用,随车牌自动反填;', + '3.1.3 车辆型号:禁用,随车牌自动反填;', + '3.1.4 车辆识别代码:禁用,随车牌自动反填;', + '3.1.5 运营公司:禁用,随车牌自动反填。', + '3.1.6 运营公司枚举统一为:浙江羚牛、上海羚牛、广东羚牛。' + ]), + specSection('3.2 故障信息卡片', [ + '3.2.1 故障类型:必填,下拉选择;', + '3.2.2 故障来源:必填,下拉选择,选项:客户报告、定期保养、周期性维护、预防性维护、整备;', + '3.2.3 故障等级:必填,下拉选择;', + '3.2.4 故障上报时间:必填,日期时间选择器;', + '3.2.5 解决情况:选填,下拉选择,选项:临时排故、已解决、未解决。' + ]), + specSection('3.3 故障描述与证据卡片(顺序已调整)', [ + '3.3.1 故障描述:必填,多行文本,默认引导语:在何种状态下 / 产生何种现象 / 导致何种事故;', + '3.3.2 故障证据:提交时必填,上传控件,支持照片/视频/录音;', + '提示文案:支持上传照片、视频、录音。' + ]), + specSection('4. 底部操作按钮组', [ + '控制保存与提交流程。', + '4.1 保存按钮:保存当前已填写内容,不做严格必填校验,单据留在待处理;', + '4.2 提交按钮:执行严格校验(含带*字段及故障证据);', + '4.3 点击提交先弹确认提示:故障提交后无法修改… 按钮:取消 / 确认;', + '4.4 提交成功后提示提交成功,并提供确定/返回列表;', + '4.5 保存成功后提示保存成功,并提供确定/返回列表。' + ]), + specSection('5. 联动与校验规则', [ + '5.1 车牌联动:自动反填车辆品牌、车辆型号、车辆识别代码、运营公司;', + '5.2 保存校验:弱校验(允许未填必填项);', + '5.3 提交校验:强校验(必填项+故障证据);', + '5.4 解决情况默认值兜底:未填写时按未解决处理(列表展示与保存一致)。' + ]), + specSection('6. 退出与返回拦截规则', [ + '6.1 表单发生修改且未保存时,触发返回/关闭/刷新拦截;', + '6.2 弹窗文案:确定要退出吗?未保存的数据将丢失;', + '6.3 按钮:确定 / 取消;', + '6.4 已保存后再退出,不触发该拦截。' + ]) + ) + ); + }; + + return React.createElement(Layout, { className: 'arco-theme-overrides', style: { minHeight: '100vh', background: '#f2f3f5', fontFamily: 'Inter, Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif' } }, + React.createElement('style', null, ` + .arco-theme-overrides .ant-btn { border-radius: 4px; } + .arco-theme-overrides .ant-btn-primary { background-color: #165dff; border-color: #165dff; } + .arco-theme-overrides .ant-btn-primary:hover { background-color: #4080ff; border-color: #4080ff; } + + .arco-grouped-form-page { display: flex; flex-direction: column; min-height: 100vh; } + .arco-grouped-form-page-content { flex: 1; padding: 16px 20px 24px; } + .arco-grouped-form-page .ant-card { margin-bottom: 16px; border-radius: 4px; border: none; box-shadow: 0 1px 2px rgba(0,0,0,0.06); } + .arco-grouped-form-page .ant-card-head { border-bottom: none; padding: 20px 24px 0; min-height: auto; } + .arco-grouped-form-page .ant-card-head-title { font-size: 16px; font-weight: 500; color: #1d2129; padding: 0; } + .arco-grouped-form-page .ant-card-body { padding: 24px; } + .arco-grouped-form-page .ant-form-vertical .ant-form-item-label { padding-bottom: 8px; height: auto; line-height: 1.5715; } + .arco-grouped-form-page .ant-form-item { margin-bottom: 24px; } + + .arco-grouped-form-page .ant-input, + .arco-grouped-form-page .ant-select-selector, + .arco-grouped-form-page .ant-picker, + .arco-grouped-form-page .ant-input-affix-wrapper { background-color: #f2f3f5; border: 1px solid #e5e6eb; border-radius: 2px; transition: all 0.1s cubic-bezier(0, 0, 1, 1); } + .arco-grouped-form-page .ant-input:hover, + .arco-grouped-form-page .ant-select:not(.ant-select-disabled):hover .ant-select-selector, + .arco-grouped-form-page .ant-picker:hover, + .arco-grouped-form-page .ant-input-affix-wrapper:hover { background-color: #f2f3f5; border-color: #165dff; } + .arco-grouped-form-page .ant-input:focus, + .arco-grouped-form-page .ant-input-focused, + .arco-grouped-form-page .ant-select-focused .ant-select-selector, + .arco-grouped-form-page .ant-picker-focused, + .arco-grouped-form-page .ant-input-affix-wrapper-focused { background-color: #fff; border: 1px solid #165dff !important; box-shadow: 0 0 0 2px rgba(22, 93, 255, 0.2) !important; outline: 0; } + .arco-grouped-form-page .ant-input[disabled], + .arco-grouped-form-page .ant-select-disabled .ant-select-selector, + .arco-grouped-form-page .ant-picker-disabled { color: #86909c; background-color: #f2f3f5; border-color: #e5e6eb; cursor: not-allowed; } + .arco-grouped-form-page .ant-input-affix-wrapper[disabled] { background-color: #f2f3f5; border-color: #e5e6eb; cursor: not-allowed; } + .arco-grouped-form-page .ant-input-affix-wrapper > input.ant-input { background-color: transparent; } + .arco-grouped-form-page .ant-input-affix-wrapper > input.ant-input:focus { background-color: transparent; box-shadow: none !important; border: none !important; } + + .arco-grouped-form-footer { background: #fff; padding: 16px 24px; border-top: 1px solid #e5e6eb; display: flex; justify-content: flex-end; align-items: center; gap: 12px; position: sticky; bottom: 0; z-index: 100; box-shadow: 0 -2px 10px rgba(0,0,0,0.05); } + .arco-grouped-form-footer .ant-btn { border-radius: 5px; height: 32px; padding: 4px 16px; font-size: 14px; } + + .arco-theme-overrides .ant-breadcrumb { color: #86909c; font-size: 14px; white-space: nowrap; flex-shrink: 0; margin-bottom: 0; } + .arco-theme-overrides .ant-breadcrumb a { color: #4e5969; } + .arco-theme-overrides .ant-breadcrumb a:hover { color: #165dff; background-color: transparent; } + .arco-theme-overrides .ant-form-item-label > label { color: #4e5969; white-space: nowrap; } + .arco-theme-overrides .ant-form-item-label > label::after { display: none !important; content: "" !important; margin: 0 !important; } + `), + React.createElement('div', { className: 'arco-grouped-form-page' }, + React.createElement('div', { className: 'arco-grouped-form-page-content' }, + React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20, flexWrap: 'nowrap', gap: 12 } }, + React.createElement(Breadcrumb, { + separator: React.createElement('span', { style: { color: '#c9cdd4' } }, '/'), + items: [ + { title: '首页' }, + { title: '运维管理' }, + { title: '车辆业务' }, + { title: React.createElement('a', { href: '#', onClick: function (e) { e.preventDefault(); requestExit(); } }, '故障管理') }, + { title: React.createElement('span', { style: { color: '#1d2129' } }, '新建故障单') } + ] + }), + React.createElement(Button, { + type: 'link', + icon: React.createElement(FileTextIcon, null), + style: { display: 'flex', alignItems: 'center', gap: 4, padding: '0 4px', color: '#165dff', fontWeight: 500, flexShrink: 0 }, + onClick: function () { setSpecModalOpen(true); } + }, '查看需求说明') + ), + + React.createElement(Form, { + form: faultForm, + layout: 'vertical', + onValuesChange: function () { + setFormDirty(true); + } + }, + React.createElement(Card, { title: '车辆信息', bordered: false }, + React.createElement(Row, { gutter: 24 }, + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '车牌号', name: 'plate', rules: [{ required: true, message: '请选择车牌号' }] }, + React.createElement(Select, { + placeholder: '请选择车牌号(可搜索)', + options: plateOptions, + showSearch: true, + optionFilterProp: 'label', + onChange: function (val, option) { + if (option) { + faultForm.setFieldsValue({ + brand: option.brand, + model: option.model, + company: option.company, + vin: option.vin + }); + } else { + faultForm.setFieldsValue({ brand: undefined, model: undefined, company: undefined, vin: undefined }); + } + } + }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '车辆品牌', name: 'brand' }, + React.createElement(Input, { placeholder: '自动带入', disabled: true, style: { backgroundColor: '#f2f3f5', color: '#86909c' } }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '车辆型号', name: 'model' }, + React.createElement(Input, { placeholder: '自动带入', disabled: true, style: { backgroundColor: '#f2f3f5', color: '#86909c' } }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '车辆识别代码', name: 'vin' }, + React.createElement(Input, { placeholder: '自动带入', disabled: true, style: { backgroundColor: '#f2f3f5', color: '#86909c' } }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '运营公司', name: 'company' }, + React.createElement(Input, { placeholder: '自动带入', disabled: true, style: { backgroundColor: '#f2f3f5', color: '#86909c' } }) + ) + ) + ) + ), + + React.createElement(Card, { title: '故障信息', bordered: false }, + React.createElement(Row, { gutter: 24 }, + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '故障类型', name: 'type', rules: [{ required: true, message: '请选择故障类型' }] }, + React.createElement(Select, { placeholder: '请选择故障类型', options: faultTypeOptions }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '故障来源', name: 'source', rules: [{ required: true, message: '请选择故障来源' }] }, + React.createElement(Select, { placeholder: '请选择故障来源', options: faultSourceOptions }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '故障等级', name: 'level', rules: [{ required: true, message: '请选择故障等级' }] }, + React.createElement(Select, { placeholder: '请选择故障等级', options: faultLevelOptions }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '故障上报时间', name: 'reportTime', rules: [{ required: true, message: '请选择故障上报时间' }] }, + React.createElement(DatePicker, { style: { width: '100%' }, placeholder: '请选择上报时间', showTime: true }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '解决情况', name: 'status', extra: '选填;保存时未填按「未解决」处理' }, + React.createElement(Select, { allowClear: true, placeholder: '请选择(可选)', options: resolveStatusOptions }) + ) + ) + ) + ), + + React.createElement(Card, { title: '故障描述与证据', bordered: false }, + React.createElement(Row, { gutter: 24 }, + React.createElement(Col, { span: 24 }, + React.createElement(Form.Item, { label: '故障描述', name: 'desc', rules: [{ required: true, message: '请填写故障描述' }] }, + React.createElement(Input.TextArea, { + placeholder: DESC_PLACEHOLDER, + style: { minHeight: 104, height: 104, resize: 'none' } + }) + ) + ), + React.createElement(Col, { span: 24 }, + React.createElement(Form.Item, { + label: '故障证据', + name: 'evidence', + valuePropName: 'fileList', + getValueFromEvent: function (e) { return (e && e.fileList) ? e.fileList : []; } + }, + React.createElement(Upload, { + listType: 'picture-card', + beforeUpload: function () { return false; }, + multiple: true, + accept: 'image/*,video/*,audio/*' + }, + React.createElement('div', null, + React.createElement(PlusIcon, null), + React.createElement('div', { style: { marginTop: 8 } }, '上传文件') + ) + ), + React.createElement('div', { style: { fontSize: 12, color: '#86909c', marginTop: 8 } }, '支持上传照片、视频、录音') + ) + ) + ) + ) + ) + ), + React.createElement('div', { className: 'arco-grouped-form-footer' }, + React.createElement(Button, { onClick: requestExit, style: { borderRadius: 5 } }, '取消'), + React.createElement(Button, { onClick: handleSave, style: { borderRadius: 5 } }, '保存'), + React.createElement(Button, { type: 'primary', onClick: handleSubmitClick, style: { borderRadius: 5 } }, '提交') + ) + ), + renderSpecModal() + ); +}; + +if (typeof module !== 'undefined' && module.exports) module.exports = Component; diff --git a/web端/运维管理/车辆业务/替换车管理-新增.jsx b/web端/运维管理/车辆业务/替换车管理-新增.jsx index 869557a..62bb6ea 100644 --- a/web端/运维管理/车辆业务/替换车管理-新增.jsx +++ b/web端/运维管理/车辆业务/替换车管理-新增.jsx @@ -1,10 +1,11 @@ // 【重要】必须使用 const Component 作为组件变量名 -// 运维管理 - 车辆业务 - 替换车管理 - 新增(2026年3月3日版本) +// 运维管理 - 车辆业务 - 替换车管理 - 新增 const Component = function () { var useState = React.useState; var useCallback = React.useCallback; var useMemo = React.useMemo; + var useRef = React.useRef; var antd = window.antd; var Breadcrumb = antd.Breadcrumb; @@ -14,66 +15,107 @@ const Component = function () { var Button = antd.Button; var Modal = antd.Modal; var message = antd.message; - var DatePicker = antd.DatePicker; + var Tag = antd.Tag; + var Empty = antd.Empty; - // 模拟:进行中的车辆租赁合同列表(选项目后反写合同信息) - var contractList = [ - { projectId: 'p1', projectName: '嘉兴氢能示范项目', contractCode: 'HT-ZL-2025-001', customerName: '嘉兴某某物流有限公司', contactPerson: '张三', signDate: '2025-01-15', contactPhone: '13800138001', businessDept: '业务1部', businessPerson: '张经理' }, - { projectId: 'p2', projectName: '上海物流租赁项目', contractCode: 'HT-ZL-2025-002', customerName: '上海某某运输公司', contactPerson: '李四', signDate: '2025-02-01', contactPhone: '13800138002', businessDept: '业务2部', businessPerson: '李专员' }, - { projectId: 'p3', projectName: '杭州城配租赁项目', contractCode: 'HT-ZL-2025-003', customerName: '杭州某某租赁有限公司', contactPerson: '王五', signDate: '2025-02-10', contactPhone: '13800138003', businessDept: '业务3部', businessPerson: '王专员' } + var pairIdRef = useRef(1); + + function createEmptyPair() { + pairIdRef.current += 1; + return { + id: 'pair_' + pairIdRef.current, + replaceType: undefined, + replaceReason: undefined, + replaceReasonDesc: '', + originalPlate: undefined, + originalVin: '', + originalBrand: '', + originalModel: '', + contractId: '', + replacePlate: undefined, + replaceVin: '', + replaceBrand: '', + replaceModel: '' + }; + } + + var EMPTY_PROJECT = { + contractId: '', + projectId: '', + projectName: '', + projectType: '', + customerName: '', + contractCode: '', + deliveryRegion: '' + }; + + // 模拟:合同状态为「合同进行中」的租赁合同 + var activeContracts = [ + { + contractId: 'c1', + contractStatus: '合同进行中', + projectId: 'p1', + projectName: '嘉兴氢能示范项目', + projectType: '租赁', + contractCode: 'HT-ZL-2025-001', + customerName: '嘉兴某某物流有限公司', + deliveryRegion: '浙江省-嘉兴市' + }, + { + contractId: 'c2', + contractStatus: '合同进行中', + projectId: 'p2', + projectName: '上海物流租赁项目', + projectType: '租赁', + contractCode: 'HT-ZL-2025-002', + customerName: '上海某某运输公司', + deliveryRegion: '上海市-上海市' + }, + { + contractId: 'c3', + contractStatus: '合同进行中', + projectId: 'p3', + projectName: '杭州城配自营项目', + projectType: '自营', + contractCode: 'HT-ZY-2025-003', + customerName: '杭州某某租赁有限公司', + deliveryRegion: '浙江省-杭州市' + } ]; - // 模拟:按合同对应的已交车未还车车辆(项目 p1 下) - var originalPlateByProject = { - p1: [ - { plateNo: '浙A12345', vin: 'LGHXCAE28M1234567', brand: '东风', model: 'DFH1180' }, - { plateNo: '浙A55555', vin: 'LGHXCAE28M5555555', brand: '重汽', model: 'ZZ1160' } + var deliveredVehicles = [ + { plateNo: '浙A12345', vin: 'LGHXCAE28M1234567', brand: '东风', model: 'DFH1180', contractId: 'c1', vehicleStatus: '已交车' }, + { plateNo: '浙A55555', vin: 'LGHXCAE28M5555555', brand: '重汽', model: 'ZZ1160', contractId: 'c1', vehicleStatus: '已交车' }, + { plateNo: '沪B11111', vin: 'LGHXCAE28M7654321', brand: '江淮', model: 'HFC1180', contractId: 'c2', vehicleStatus: '已交车' }, + { plateNo: '浙C33333', vin: 'LGHXCAE28M8888888', brand: '东风', model: 'DFH1190', contractId: 'c3', vehicleStatus: '已交车' } + ]; + + var preparedVehiclesByRegion = { + '浙江省-嘉兴市': [ + { plateNo: '浙A67890', vin: 'LGHXCAE28M6789012', brand: '福田', model: 'BJ1180', vehicleStatus: '已备车' }, + { plateNo: '浙A66666', vin: 'LGHXCAE28M6666666', brand: '江淮', model: 'HFC1190', vehicleStatus: '已备车' }, + { plateNo: '浙F88888', vin: 'LGHXCAE28M8888888', brand: '东风', model: 'DFH1180', vehicleStatus: '已备车' } ], - p2: [ - { plateNo: '浙B11111', vin: 'LGHXCAE28M7654321', brand: '江淮', model: 'HFC1180' } + '上海市-上海市': [ + { plateNo: '沪B22222', vin: 'LGHXCAE28M2222222', brand: '重汽', model: 'ZZ1180', vehicleStatus: '已备车' }, + { plateNo: '沪B33333', vin: 'LGHXCAE28M3333333', brand: '福田', model: 'BJ1190', vehicleStatus: '已备车' } ], - p3: [ - { plateNo: '浙C33333', vin: 'LGHXCAE28M8888888', brand: '东风', model: 'DFH1190' } + '浙江省-杭州市': [ + { plateNo: '浙C44444', vin: 'LGHXCAE28M4444444', brand: '东风', model: 'DFH1180', vehicleStatus: '已备车' } ] }; - // 模拟:当前人员权限下已备车车辆 - var replacePlateOptions = [ - { plateNo: '浙A67890', vin: 'LGHXCAE28M6789012', brand: '福田', model: 'BJ1180' }, - { plateNo: '浙B22222', vin: 'LGHXCAE28M2222222', brand: '重汽', model: 'ZZ1180' }, - { plateNo: '浙C44444', vin: 'LGHXCAE28M4444444', brand: '福田', model: 'BJ1190' }, - { plateNo: '浙A66666', vin: 'LGHXCAE28M6666666', brand: '江淮', model: 'HFC1190' }, - { plateNo: '浙F88888', vin: 'LGHXCAE28M8888888', brand: '东风', model: 'DFH1180' } - ]; + var contractById = useMemo(function () { + var map = {}; + activeContracts.forEach(function (c) { + if (c.contractStatus === '合同进行中') map[c.contractId] = c; + }); + return map; + }, []); - var projectOptions = contractList.map(function (c) { - return { value: c.projectId, label: c.projectName }; - }); - - var formState = useState({ - projectId: undefined, - contractCode: '', - customerName: '', - contactPerson: '', - signDate: '', - contactPhone: '', - businessDept: '', - businessPerson: '', - replaceDate: null, - replaceType: undefined, - replaceReason: undefined, - replaceReasonDesc: '', - originalPlate: undefined, - originalVin: '', - originalBrand: '', - originalModel: '', - replacePlate: undefined, - replaceVin: '', - replaceBrand: '', - replaceModel: '' - }); - var form = formState[0]; - var setForm = formState[1]; + var pairsState = useState(function () { return []; }); + var pairs = pairsState[0]; + var setPairs = pairsState[1]; var editedState = useState(false); var setEdited = editedState[1]; var cancelModalVisible = useState(false); @@ -81,85 +123,188 @@ const Component = function () { var requirementModalVisible = useState(false); var setRequirementModalVisible = requirementModalVisible[1]; - var selectedContract = useMemo(function () { - if (!form.projectId) return null; - return contractList.find(function (c) { return c.projectId === form.projectId; }) || null; - }, [form.projectId]); + var projectInfo = useMemo(function () { + var anchor = pairs.find(function (p) { return p.originalPlate && p.contractId; }); + if (!anchor || !anchor.contractId) return EMPTY_PROJECT; + var c = contractById[anchor.contractId]; + if (!c) return EMPTY_PROJECT; + return { + contractId: c.contractId, + projectId: c.projectId, + projectName: c.projectName, + projectType: c.projectType, + customerName: c.customerName, + contractCode: c.contractCode, + deliveryRegion: c.deliveryRegion + }; + }, [pairs, contractById]); - var originalPlateList = useMemo(function () { - if (!form.projectId) return []; - return (originalPlateByProject[form.projectId] || []).map(function (v) { return { value: v.plateNo, label: v.plateNo }; }); - }, [form.projectId]); - - var replacePlateSelectOptions = useMemo(function () { - return replacePlateOptions.map(function (v) { return { value: v.plateNo, label: v.plateNo }; }); + var plateFilterOption = useCallback(function (input, opt) { + return (opt.label || '').toString().toLowerCase().indexOf((input || '').toLowerCase()) !== -1; }, []); - var onProjectChange = useCallback(function (projectId) { + var getDeliveredVehicle = useCallback(function (plateNo) { + return deliveredVehicles.find(function (v) { return v.plateNo === plateNo; }) || null; + }, []); + + var getUsedPlates = useCallback(function (pairsList, field, exceptPairId) { + var set = {}; + pairsList.forEach(function (p) { + if (p.id === exceptPairId) return; + if (p[field]) set[p[field]] = true; + }); + return set; + }, []); + + var selectedOriginalPlates = useMemo(function () { + return pairs.map(function (p) { return p.originalPlate; }).filter(Boolean); + }, [pairs]); + + var multiOldPlateOptions = useMemo(function () { + var lockedContractId = projectInfo.contractId; + return deliveredVehicles + .filter(function (v) { + if (v.vehicleStatus !== '已交车' || !contractById[v.contractId]) return false; + if (lockedContractId && v.contractId !== lockedContractId) return false; + return true; + }) + .map(function (v) { return { value: v.plateNo, label: v.plateNo }; }); + }, [projectInfo.contractId, contractById]); + + var getNewOptionsForPair = useCallback(function (pair) { + if (!projectInfo.deliveryRegion) return []; + var used = getUsedPlates(pairs, 'replacePlate', pair.id); + var list = preparedVehiclesByRegion[projectInfo.deliveryRegion] || []; + return list + .filter(function (v) { + return v.vehicleStatus === '已备车' && !used[v.plateNo]; + }) + .map(function (v) { return { value: v.plateNo, label: v.plateNo }; }); + }, [pairs, projectInfo.deliveryRegion, getUsedPlates]); + + var updatePair = useCallback(function (pairId, patch) { setEdited(true); - var contract = contractList.find(function (c) { return c.projectId === projectId; }); - setForm(function (prev) { - var next = {}; - for (var k in prev) next[k] = prev[k]; - next.projectId = projectId; - next.contractCode = contract ? contract.contractCode : ''; - next.customerName = contract ? contract.customerName : ''; - next.contactPerson = contract ? contract.contactPerson : ''; - next.signDate = contract ? contract.signDate : ''; - next.contactPhone = contract ? contract.contactPhone : ''; - next.businessDept = contract ? contract.businessDept : ''; - next.businessPerson = contract ? contract.businessPerson : ''; - next.originalPlate = undefined; - next.originalVin = ''; - next.originalBrand = ''; - next.originalModel = ''; - return next; + setPairs(function (prev) { + return prev.map(function (p) { + if (p.id !== pairId) return p; + var next = {}; + for (var k in p) next[k] = p[k]; + for (var pk in patch) next[pk] = patch[pk]; + return next; + }); }); }, []); - var onOriginalPlateChange = useCallback(function (plateNo) { + var buildPairForPlate = useCallback(function (plateNo, existing) { + var vehicle = getDeliveredVehicle(plateNo); + if (!vehicle) return null; + var row = existing ? Object.assign({}, existing) : createEmptyPair(); + row.originalPlate = plateNo; + row.originalVin = vehicle.vin; + row.originalBrand = vehicle.brand; + row.originalModel = vehicle.model; + row.contractId = vehicle.contractId; + if (!existing) { + row.replacePlate = undefined; + row.replaceVin = ''; + row.replaceBrand = ''; + row.replaceModel = ''; + } + return row; + }, [getDeliveredVehicle]); + + var onMultiOriginalPlateChange = useCallback(function (plateNos) { + var list = Array.isArray(plateNos) ? plateNos : []; + if (list.length === 0) { + setEdited(true); + setPairs([]); + return; + } + var anchorContractId = null; + var validPlates = []; + var rejected = false; + list.forEach(function (plate) { + var vehicle = getDeliveredVehicle(plate); + if (!vehicle || !contractById[vehicle.contractId]) return; + if (!anchorContractId) anchorContractId = vehicle.contractId; + if (vehicle.contractId !== anchorContractId) { + rejected = true; + return; + } + validPlates.push(plate); + }); + if (rejected) { + message.warning('多台替换须为同一客户、同一项目,已忽略不同项目的车辆'); + } + if (validPlates.length === 0) { + setPairs([]); + return; + } setEdited(true); - var list = form.projectId ? (originalPlateByProject[form.projectId] || []) : []; + setPairs(function (prev) { + var prevByPlate = {}; + prev.forEach(function (p) { + if (p.originalPlate) prevByPlate[p.originalPlate] = p; + }); + return validPlates.map(function (plate) { + return buildPairForPlate(plate, prevByPlate[plate]); + }); + }); + }, [getDeliveredVehicle, contractById, buildPairForPlate]); + + var onReplacePlateChange = useCallback(function (pairId, plateNo) { + if (!plateNo) { + updatePair(pairId, { + replacePlate: undefined, + replaceVin: '', + replaceBrand: '', + replaceModel: '' + }); + return; + } + var pair = pairs.find(function (p) { return p.id === pairId; }); + if (!pair || !pair.originalPlate) { + message.info('请先选择被替换车辆'); + return; + } + var list = preparedVehiclesByRegion[projectInfo.deliveryRegion] || []; var vehicle = list.find(function (v) { return v.plateNo === plateNo; }); - setForm(function (prev) { - var next = {}; - for (var k in prev) next[k] = prev[k]; - next.originalPlate = plateNo; - next.originalVin = vehicle ? vehicle.vin : ''; - next.originalBrand = vehicle ? vehicle.brand : ''; - next.originalModel = vehicle ? vehicle.model : ''; - return next; + if (!vehicle) return; + var used = getUsedPlates(pairs, 'replacePlate', pairId); + if (used[plateNo]) { + message.warning('该新车已在其他替换项中选择'); + return; + } + updatePair(pairId, { + replacePlate: plateNo, + replaceVin: vehicle.vin, + replaceBrand: vehicle.brand, + replaceModel: vehicle.model }); - }, [form.projectId]); - - var onReplacePlateChange = useCallback(function (plateNo) { - setEdited(true); - var vehicle = replacePlateOptions.find(function (v) { return v.plateNo === plateNo; }); - setForm(function (prev) { - var next = {}; - for (var k in prev) next[k] = prev[k]; - next.replacePlate = plateNo; - next.replaceVin = vehicle ? vehicle.vin : ''; - next.replaceBrand = vehicle ? vehicle.brand : ''; - next.replaceModel = vehicle ? vehicle.model : ''; - return next; - }); - }, []); + }, [updatePair, pairs, projectInfo.deliveryRegion, getUsedPlates]); var handleSubmit = useCallback(function () { - message.success('替换车申请已提交审核'); - }, []); + if (!pairs.length || !projectInfo.contractId) { + message.warning('请选择被替换车辆并完善替换信息'); + return; + } + var incomplete = pairs.find(function (p) { + return !p.originalPlate || !p.replacePlate || !p.replaceType || !p.replaceReason; + }); + if (incomplete) { + message.warning('请完善每条替换的新车、替换类型与替换原因'); + return; + } + message.success('已提交 ' + pairs.length + ' 条替换车申请(原型)'); + }, [pairs, projectInfo.contractId]); var handleSave = useCallback(function () { message.success('已保存,该条数据仅您可查看并编辑(原型)'); }, []); var handleCancel = useCallback(function () { - if (editedState[0]) { - setCancelModalVisible(true); - } else { - message.info('返回替换车管理列表(原型)'); - } + if (editedState[0]) setCancelModalVisible(true); + else message.info('返回替换车管理列表(原型)'); }, [editedState[0]]); var confirmCancel = useCallback(function () { @@ -167,16 +312,411 @@ const Component = function () { message.info('已取消,返回替换车管理列表(原型)'); }, []); - var layoutStyle = { padding: '16px 24px', background: '#f5f5f5', minHeight: '100vh' }; - var cardStyle = { marginBottom: 16 }; - var labelStyle = { marginBottom: 6, fontSize: 14, color: 'rgba(0,0,0,0.65)' }; - var formRowStyle = { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '16px 24px', marginBottom: 16 }; - var formItemStyle = { marginBottom: 12 }; + var pageCss = + '.vr-add-page{font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif}' + + '.vr-add-page .vr-page-header{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:20px}' + + '.vr-add-page .vr-main-card{border-radius:16px;border:none;box-shadow:0 4px 24px -6px rgba(15,23,42,0.08),0 0 0 1px rgba(15,23,42,0.05)}' + + '.vr-add-page .vr-main-card>.ant-card-head{border-bottom:1px solid #f1f5f9;padding:16px 24px;min-height:auto}' + + '.vr-add-page .vr-main-card>.ant-card-head .ant-card-head-title{font-size:16px;font-weight:600;color:#0f172a;padding:0}' + + '.vr-add-page .vr-main-card>.ant-card-body{padding:20px 24px 24px}' + + '.vr-add-page .vr-pair-list{display:flex;flex-direction:column;gap:16px}' + + '.vr-add-page .vr-pair-card{border-radius:12px;border:1px solid #e2e8f0;background:linear-gradient(180deg,#fff 0%,#f8fafc 100%);overflow:hidden;transition:border-color .2s ease,box-shadow .2s ease}' + + '.vr-add-page .vr-pair-card:hover{border-color:#93c5fd;box-shadow:0 4px 16px -4px rgba(22,119,255,0.12)}' + + '.vr-add-page .vr-pair-card__head{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 16px;background:#f0f9ff;border-bottom:1px solid #e0f2fe}' + + '.vr-add-page .vr-pair-card__title{display:flex;align-items:center;gap:8px;font-size:14px;font-weight:600;color:#0f172a}' + + '.vr-add-page .vr-pair-card__index{display:inline-flex;align-items:center;justify-content:center;min-width:24px;height:24px;padding:0 8px;border-radius:6px;background:#1677ff;color:#fff;font-size:12px;font-weight:700}' + + '.vr-add-page .vr-pair-card__body{padding:16px}' + + '.vr-add-page .vr-block{margin-bottom:14px}' + + '.vr-add-page .vr-block:last-child{margin-bottom:0}' + + '.vr-add-page .vr-block-label{font-size:12px;font-weight:600;color:#475569;margin-bottom:10px;letter-spacing:.02em}' + + '.vr-add-page .vr-block-label--old{color:#b45309}' + + '.vr-add-page .vr-block-label--new{color:#047857}' + + '.vr-add-page .vr-form-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px 16px}' + + '.vr-add-page .vr-form-grid--reason .vr-field:last-child{grid-column:1/-1}' + + '@media(max-width:900px){.vr-add-page .vr-form-grid{grid-template-columns:1fr}}' + + '.vr-add-page .vr-field{display:flex;flex-direction:column;gap:6px;min-width:0}' + + '.vr-add-page .vr-field__label{font-size:13px;font-weight:500;color:#334155;line-height:1.4}' + + '.vr-add-page .vr-field__label .vr-req{color:#ef4444;margin-right:2px}' + + '.vr-add-page .vr-swap-divider{display:flex;align-items:center;gap:12px;margin:14px 0;color:#94a3b8;font-size:12px;font-weight:500}' + + '.vr-add-page .vr-swap-divider::before,.vr-add-page .vr-swap-divider::after{content:"";flex:1;height:1px;background:linear-gradient(90deg,transparent,#cbd5e1,transparent)}' + + '.vr-add-page .vr-swap-divider__icon{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:50%;background:#eff6ff;color:#1677ff;font-size:14px;flex-shrink:0}' + + '.vr-add-page .vr-multi-pick{margin-bottom:20px;padding:16px 18px;border-radius:12px;background:#fff;border:1px solid #e2e8f0;box-shadow:0 1px 2px rgba(15,23,42,0.04)}' + + '.vr-add-page .vr-multi-pick__title{font-size:14px;font-weight:600;color:#0f172a;margin-bottom:4px}' + + '.vr-add-page .vr-multi-pick__hint{font-size:12px;color:#64748b;margin-bottom:12px;line-height:1.5}' + + '.vr-add-page .vr-vehicle-summary{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px 16px;padding:12px 14px;margin-bottom:14px;border-radius:8px;background:#fffbeb;border:1px solid #fde68a}' + + '@media(max-width:900px){.vr-add-page .vr-vehicle-summary{grid-template-columns:1fr}}' + + '.vr-add-page .vr-pair-list-empty{padding:32px 16px;text-align:center;color:#94a3b8;font-size:13px}' + + '.vr-add-page .vr-project-panel{margin-top:20px;padding:16px 18px;border-radius:12px;background:#f8fafc;border:1px solid #e2e8f0}' + + '.vr-add-page .vr-project-panel__head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:14px}' + + '.vr-add-page .vr-project-panel__title{font-size:14px;font-weight:600;color:#0f172a}' + + '.vr-add-page .vr-project-panel__hint{font-size:12px;color:#64748b}' + + '.vr-add-page .vr-project-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px 20px}' + + '@media(max-width:900px){.vr-add-page .vr-project-grid{grid-template-columns:1fr}}' + + '.vr-add-page .vr-readonly{display:flex;flex-direction:column;gap:4px;min-width:0}' + + '.vr-add-page .vr-readonly__label{font-size:12px;color:#64748b;font-weight:500}' + + '.vr-add-page .vr-readonly__value{font-size:14px;color:#0f172a;font-weight:500;word-break:break-all}' + + '.vr-add-page .vr-readonly__value--muted{color:#94a3b8;font-weight:400}' + + '.vr-add-page .vr-footer{display:flex;flex-wrap:wrap;gap:10px;margin-top:24px;padding-top:20px;border-top:1px solid #f1f5f9}' + + '.vr-add-page .vr-remove-btn{color:#64748b!important}' + + '.vr-add-page .vr-remove-btn:hover{color:#ef4444!important}' + + '@media(prefers-reduced-motion:reduce){.vr-add-page .vr-pair-card{transition:none}}' + + '.vr-req-doc{padding:4px 2px 8px}' + + '.vr-req-doc__meta{font-size:12px;color:#64748b;line-height:1.6;margin-bottom:16px;padding-bottom:12px;border-bottom:1px solid #f1f5f9}' + + '.vr-req-doc__section{margin-bottom:20px}' + + '.vr-req-doc__section:last-child{margin-bottom:0}' + + '.vr-req-doc__title{font-size:15px;font-weight:600;color:#0f172a;margin:0 0 10px;line-height:1.4}' + + '.vr-req-doc__line{font-size:13px;color:#475569;line-height:1.75;margin:0 0 6px;padding-left:0}' + + '.vr-req-doc__line--sub{padding-left:14px;color:#64748b}' + + '.vr-req-doc__line:last-child{margin-bottom:0}' + + '.vr-req-doc__tag{display:inline-block;margin:0 4px 4px 0;padding:0 6px;font-size:12px;line-height:20px;border-radius:4px;background:#f1f5f9;color:#334155}'; - var requirementContent = '替换车管理-新增(2026年3月3日版本)\n一个「数字化资产ONEOS运管平台」中的「运维管理」「车辆业务」「替换车管理」「新增」模块\n1.面包屑:\n#运维管理-车辆业务-替换车管理-新增\n页面由选择项目、替换车详情、两个单独卡片组成;\n\n2.选择项目:\n#可通过选择进行中的车辆租赁合同,拉取租赁合同中对应车辆进行替换;\n2.1.项目名称:选择器,可选择所有进行中的合同,支持输入框中输入关键词进行模糊搜索,下拉显示对应项;\n2.2.合同编码:根据项目名称自动反查,不可编辑;\n2.3.客户名称:根据项目名称自动反查,不可编辑;\n2.4.对接人:根据项目名称自动反查,不可编辑;\n2.5.合同签订时间:根据项目名称自动反查,不可编辑;\n2.6.客户联系电话:根据项目名称自动反查,不可编辑;\n2.7.业务部门:根据项目名称自动反查,不可编辑;\n2.8.业务人员:根据项目名称自动反查,不可编辑;\n\n3.替换车详情:\n3.1.替换时间:日期选择器,格式为:YYYY-MM-DD,精确至天;\n3.2.替换类型:选择器,分为「永久替换」「临时替换」两个选项;\n 3.2.1.类型为永久替换时,该申请通过审核后替换车进行交车(交车时间为流程结束当天),由运维手动将被替换车进行还车;\n 3.2.2.类型为临时替换时,该申请通过审核后替换车进行交车(交车时间为流程结束当天),被替换车不用还车,在被替换车重新交付客户时,由运维手动将替换车进行还车;\n 重新生成交车任务时,交车地点会自动继承自合同,由对应区域运维人员才能操作;\n 交车任务完成后,所有涉及到被替换车辆显示(例如车辆租赁合同、租赁账单、提车应收款等功能)会替换为新替换车的对应信息,如果是临时替换,在新替换车完成还车后,对应车辆记录会恢复为原有车辆数据。如果是永久替换,则由运维自主进行被替换车辆还车;\n3.3.替换原因:选择器,分为「客户原因」「车辆原因」;\n3.4.替换原因说明:文本域,默认提示信息为:请说明替换原因;\n3.5.被替换车牌号:选择器,只能选择该租赁合同当前对应的已交车但未还车车牌号,已被替换但替换车申请还在审核中时,该车辆不可选;\n3.6.被替换车识别代码:输入框(禁用),选择被替换车车牌号后自动反写该车识别代码;\n3.7.被替换车品牌:输入框(禁用),选择被替换车车牌号后自动反写该车品牌;\n3.8.被替换车型号:输入框(禁用),选择被替换车车牌号后自动反写该车型号;\n3.9.替换车车牌号:选择器,只能选择该人员权限下所有已备车车辆,在选择项目名称前,不能选择替换车车牌号;\n3.10.替换车识别代码:输入框(禁用),选择替换车车牌号后自动反写该车识别代码;\n3.11.替换车品牌:输入框(禁用),选择替换车车牌号后自动反写该车品牌;\n3.12.替换车型号:输入框(禁用),选择替换车车牌号后自动反写该车型号;\n\n下方为提交审核、保存、取消按钮;\n4.1.点击提交并审核,toast提示:替换车申请已提交审核;\n4.2.点击保存,会存储租赁订单已填写内容,不做必填项校验,同时显示在租赁合同列表中(待审批),该条数据只能保存人自己查看并编辑,其他人无法操作;\n4.3.点击取消,如当前页面有已编辑内容时,点击取消会进行二次提示,内容为:取消将会丢失所有已填写内容,是否确认?点击确认返回替换车管理列表页;\n'; + function specSection(title, lines) { + return React.createElement( + 'section', + { className: 'vr-req-doc__section' }, + React.createElement('h3', { className: 'vr-req-doc__title' }, title), + (lines || []).map(function (text, i) { + var isSub = typeof text === 'string' && (text.indexOf(' ') === 0 || /^\d+\.\d+/.test(text)); + return React.createElement( + 'p', + { + key: i, + className: 'vr-req-doc__line' + (isSub ? ' vr-req-doc__line--sub' : '') + }, + text + ); + }) + ); + } - return React.createElement('div', { style: layoutStyle }, - React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 } }, + function renderRequirementDoc() { + return React.createElement( + 'div', + { className: 'vr-req-doc' }, + React.createElement( + 'div', + { className: 'vr-req-doc__meta' }, + '数字化资产 ONEOS 运管平台 · 运维管理 · 车辆业务 · 替换车管理 · 新增' + ), + specSection('1. 页面定位', [ + '用于发起车辆替换申请:将合同下已交付客户的车辆(被替换车)替换为交车区域内已备车的替换车。', + '支持一次申请内多辆车替换,但须属于同一客户、同一项目;项目信息在页面底部统一展示一份。' + ]), + specSection('2. 导航与入口', [ + '面包屑:运维管理 / 车辆业务 / 替换车管理 / 新增。', + '右上角「查看需求说明」:打开本文档。', + '主卡片标题:新增替换车;已选被替换车时展示「N 辆车」数量标签。' + ]), + specSection('3. 被替换车辆(多选)', [ + '3.1 车牌号:必填,多选下拉,支持输入关键词模糊搜索;占位「请输入或选择车牌号,可多选」。', + '3.2 可选范围:合同状态为「合同进行中」的合同下,车辆状态为「已交车」的全部车辆。', + '3.3 交互:每选中一辆自动生成一条「车辆替换」明细卡片;取消勾选则移除对应卡片。', + '3.4 约束:多选车辆须属于同一合同(同一客户、同一项目);若混入其他项目车辆,提示「多台替换须为同一客户、同一项目,已忽略不同项目的车辆」,且仅保留同项目车辆。', + '3.5 锁定:首辆车选定后,后续多选仅展示同一合同下的已交车车辆。', + '3.6 未选车时,下方展示空状态:「请在上方选择被替换车辆车牌号,将自动生成替换明细」。' + ]), + specSection('4. 车辆替换明细(每条被替换车一张卡片)', [ + '4.1 卡片标题:序号 +「车辆替换」+ 被替换车牌号;若已选替换车,展示「→ 替换车牌号」。', + '4.2 被替换车辆信息(只读):车牌号、品牌、型号;品牌/型号占位「选择车辆后自动显示」。', + '4.3 替换说明', + ' 4.3.1 替换类型:必填,单选——永久替换、临时替换。', + ' 4.3.2 替换原因:必填,单选——客户原因、车辆原因。', + ' 4.3.3 替换原因说明:选填,多行文本,最多 500 字,占位「请说明替换原因」,显示字数统计。', + '4.4 替换车辆', + ' 4.4.1 新车:必填,单选下拉,支持搜索;未选被替换车时禁用,占位「请先选择被替换车辆」。', + ' 4.4.2 已选被替换车后,占位展示交车区域,如「交车区域:浙江省-嘉兴市」。', + ' 4.4.3 可选范围:与被替换车所属合同「交车区域」停车场内,车辆状态为「已备车」的车辆。', + ' 4.4.4 同一申请内,各明细的替换车车牌号不可重复;重复时提示「该新车已在其他替换项中选择」。', + ' 4.4.5 品牌、型号:选择新车后自动反显,禁用编辑,占位「选择车辆后自动显示」。', + '4.5 保留策略:取消多选某车牌时移除卡片;再次选中同一车牌时,若此前已填写替换说明/新车,尽量保留原填写内容。' + ]), + specSection('5. 项目信息(全单共用一份)', [ + '5.1 展示时机:至少选择一辆被替换车后,根据所属合同自动反显。', + '5.2 未选车时展示空状态:「选择被替换车辆后自动显示」。', + '5.3 字段(均不可编辑):客户名称、项目名称、项目类型(租赁 / 自营,标签展示)。' + ]), + specSection('6. 替换类型业务规则', [ + '6.1 永久替换:审批通过后替换车交车(交车时间为流程结束当天),运维手动将被替换车还车。', + '6.2 临时替换:审批通过后替换车交车;被替换车无需还车;被替换车重新交付客户后,运维手动将替换车还车。', + '6.3 交车任务继承合同交车地点,由对应区域运维人员操作。', + '6.4 交车完成后,租赁账单、提车应收等涉及被替换车的展示信息切换为替换车;临时替换在替换车还车后恢复原被替换车数据,永久替换由运维自主还车。' + ]), + specSection('7. 底部操作', [ + '7.1 提交审核:校验至少一条明细,且每条须已选被替换车、替换车、替换类型、替换原因;通过后 Toast「已提交 N 条替换车申请」。', + '7.2 保存:不做必填校验,保存草稿,仅保存人可见可编辑(原型提示)。', + '7.3 取消:无编辑内容直接返回列表;有编辑内容时二次确认「取消将会丢失所有已填写内容,是否确认?」,确认后返回列表。' + ]), + specSection('8. 校验与提示汇总', [ + '请选择被替换车辆并完善替换信息', + '请完善每条替换的新车、替换类型与替换原因', + '多台替换须为同一客户、同一项目,已忽略不同项目的车辆', + '该新车已在其他替换项中选择', + '请先选择被替换车辆' + ]) + ); + } + + function renderField(label, required, node) { + return React.createElement( + 'div', + { className: 'vr-field' }, + React.createElement( + 'div', + { className: 'vr-field__label' }, + required ? React.createElement('span', { className: 'vr-req' }, '*') : null, + label + ), + node + ); + } + + function renderMultiPickSection() { + return React.createElement( + 'section', + { className: 'vr-multi-pick', 'aria-label': '选择被替换车辆' }, + React.createElement('div', { className: 'vr-multi-pick__title' }, '被替换车辆'), + React.createElement( + 'div', + { className: 'vr-multi-pick__hint' }, + '车牌号支持多选,每选中一辆将生成一条替换明细;须为同一客户、同一项目。' + ), + renderField( + '车牌号', + true, + React.createElement(Select, { + mode: 'multiple', + placeholder: '请输入或选择车牌号,可多选', + style: { width: '100%' }, + value: selectedOriginalPlates, + onChange: onMultiOriginalPlateChange, + allowClear: true, + showSearch: true, + options: multiOldPlateOptions, + filterOption: plateFilterOption, + optionFilterProp: 'label', + maxTagCount: 'responsive' + }) + ) + ); + } + + function renderPairCard(pair, index) { + var newOptions = getNewOptionsForPair(pair); + + return React.createElement( + 'article', + { key: pair.id, className: 'vr-pair-card', 'aria-label': '替换车辆第' + (index + 1) + '项' }, + React.createElement( + 'div', + { className: 'vr-pair-card__head' }, + React.createElement( + 'div', + { className: 'vr-pair-card__title' }, + React.createElement('span', { className: 'vr-pair-card__index' }, index + 1), + React.createElement('span', null, '车辆替换'), + pair.originalPlate + ? React.createElement(Tag, { style: { margin: 0 } }, pair.originalPlate) + : null, + pair.originalPlate && pair.replacePlate + ? React.createElement(Tag, { color: 'processing', style: { margin: 0 } }, '→ ' + pair.replacePlate) + : null + ) + ), + React.createElement( + 'div', + { className: 'vr-pair-card__body' }, + React.createElement( + 'div', + { className: 'vr-vehicle-summary' }, + renderField( + '车牌号', + false, + React.createElement(Input, { + value: pair.originalPlate || '', + disabled: true + }) + ), + renderField( + '品牌', + false, + React.createElement(Input, { + value: pair.originalBrand || '', + disabled: true, + placeholder: '选择车辆后自动显示' + }) + ), + renderField( + '型号', + false, + React.createElement(Input, { + value: pair.originalModel || '', + disabled: true, + placeholder: '选择车辆后自动显示' + }) + ) + ), + React.createElement( + 'div', + { className: 'vr-block' }, + React.createElement('div', { className: 'vr-block-label' }, '替换说明'), + React.createElement( + 'div', + { className: 'vr-form-grid vr-form-grid--reason' }, + renderField( + '替换类型', + true, + React.createElement(Select, { + placeholder: '请选择', + style: { width: '100%' }, + value: pair.replaceType, + onChange: function (v) { updatePair(pair.id, { replaceType: v }); }, + allowClear: true, + options: [ + { value: '永久替换', label: '永久替换' }, + { value: '临时替换', label: '临时替换' } + ] + }) + ), + renderField( + '替换原因', + true, + React.createElement(Select, { + placeholder: '请选择', + style: { width: '100%' }, + value: pair.replaceReason, + onChange: function (v) { updatePair(pair.id, { replaceReason: v }); }, + allowClear: true, + options: [ + { value: '客户原因', label: '客户原因' }, + { value: '车辆原因', label: '车辆原因' } + ] + }) + ), + renderField( + '替换原因说明', + false, + React.createElement(Input.TextArea, { + placeholder: '请说明替换原因', + value: pair.replaceReasonDesc || '', + onChange: function (e) { updatePair(pair.id, { replaceReasonDesc: e.target.value }); }, + rows: 2, + style: { width: '100%' }, + maxLength: 500, + showCount: true + }) + ) + ) + ), + React.createElement( + 'div', + { className: 'vr-swap-divider', role: 'presentation' }, + React.createElement('span', { className: 'vr-swap-divider__icon', 'aria-hidden': true }, '↓'), + React.createElement('span', null, '替换为') + ), + React.createElement( + 'div', + { className: 'vr-block' }, + React.createElement('div', { className: 'vr-block-label vr-block-label--new' }, '替换车辆'), + React.createElement( + 'div', + { className: 'vr-form-grid' }, + renderField( + '新车', + true, + React.createElement(Select, { + placeholder: projectInfo.deliveryRegion + ? '交车区域:' + projectInfo.deliveryRegion + : '请先选择被替换车辆', + style: { width: '100%' }, + value: pair.replacePlate, + onChange: function (v) { onReplacePlateChange(pair.id, v); }, + allowClear: true, + showSearch: true, + options: newOptions, + filterOption: plateFilterOption, + disabled: !pair.originalPlate, + optionFilterProp: 'label' + }) + ), + renderField( + '品牌', + false, + React.createElement(Input, { + value: pair.replaceBrand || '', + disabled: true, + placeholder: '选择车辆后自动显示' + }) + ), + renderField( + '型号', + false, + React.createElement(Input, { + value: pair.replaceModel || '', + disabled: true, + placeholder: '选择车辆后自动显示' + }) + ) + ) + ) + ) + ); + } + + function renderProjectPanel() { + var hasProject = !!projectInfo.contractId; + return React.createElement( + 'section', + { className: 'vr-project-panel', 'aria-label': '项目信息' }, + React.createElement( + 'div', + { className: 'vr-project-panel__head' }, + React.createElement('div', { className: 'vr-project-panel__title' }, '项目信息') + ), + hasProject + ? React.createElement( + 'div', + { className: 'vr-project-grid' }, + React.createElement( + 'div', + { className: 'vr-readonly' }, + React.createElement('span', { className: 'vr-readonly__label' }, '客户名称'), + React.createElement('span', { className: 'vr-readonly__value' }, projectInfo.customerName) + ), + React.createElement( + 'div', + { className: 'vr-readonly' }, + React.createElement('span', { className: 'vr-readonly__label' }, '项目名称'), + React.createElement('span', { className: 'vr-readonly__value' }, projectInfo.projectName) + ), + React.createElement( + 'div', + { className: 'vr-readonly' }, + React.createElement('span', { className: 'vr-readonly__label' }, '项目类型'), + React.createElement( + 'span', + { className: 'vr-readonly__value' }, + React.createElement( + Tag, + { color: projectInfo.projectType === '自营' ? 'purple' : 'blue', style: { margin: 0 } }, + projectInfo.projectType + ) + ) + ) + ) + : React.createElement(Empty, { + image: Empty.PRESENTED_IMAGE_SIMPLE, + description: '选择被替换车辆后自动显示' + }) + ); + } + + return React.createElement( + 'div', + { className: 'vr-add-page', style: { padding: '20px 24px 32px', minHeight: '100vh', background: 'linear-gradient(165deg,#eef4ff 0%,#f5f7fa 42%,#f0f2f5 100%)' } }, + React.createElement('style', null, pageCss), + React.createElement( + 'header', + { className: 'vr-page-header' }, React.createElement(Breadcrumb, { items: [ { title: '运维管理' }, @@ -185,174 +725,54 @@ const Component = function () { { title: '新增' } ] }), - React.createElement(Button, { type: 'link', style: { padding: 0 }, onClick: function () { setRequirementModalVisible(true); } }, '查看需求说明') - ), - React.createElement(Card, { title: '选择项目', style: cardStyle }, - React.createElement('div', { style: formRowStyle }, - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '项目名称'), - React.createElement(Select, { - placeholder: '请选择或输入项目名称', - style: { width: '100%' }, - value: form.projectId, - onChange: onProjectChange, - allowClear: true, - showSearch: true, - options: projectOptions, - filterOption: function (input, opt) { return (opt.label || '').toString().toLowerCase().indexOf((input || '').toLowerCase()) !== -1; } - }) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '合同编码'), - React.createElement(Input, { value: form.contractCode || '', disabled: true, placeholder: '根据项目名称自动反查' }) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '客户名称'), - React.createElement(Input, { value: form.customerName || '', disabled: true, placeholder: '根据项目名称自动反查' }) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '对接人'), - React.createElement(Input, { value: form.contactPerson || '', disabled: true, placeholder: '根据项目名称自动反查' }) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '合同签订时间'), - React.createElement(Input, { value: form.signDate || '', disabled: true, placeholder: '根据项目名称自动反查' }) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '客户联系电话'), - React.createElement(Input, { value: form.contactPhone || '', disabled: true, placeholder: '根据项目名称自动反查' }) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '业务部门'), - React.createElement(Input, { value: form.businessDept || '', disabled: true, placeholder: '根据项目名称自动反查' }) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '业务人员'), - React.createElement(Input, { value: form.businessPerson || '', disabled: true, placeholder: '根据项目名称自动反查' }) - ) + React.createElement( + Button, + { type: 'link', style: { padding: 0, flexShrink: 0 }, onClick: function () { setRequirementModalVisible(true); } }, + '查看需求说明' ) ), - React.createElement(Card, { title: '替换车详情', style: cardStyle }, - React.createElement('div', { style: formRowStyle }, - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '替换时间'), - React.createElement(DatePicker, { - placeholder: '请选择日期', - style: { width: '100%' }, - format: 'YYYY-MM-DD', - value: form.replaceDate, - onChange: function (d) { - setEdited(true); - setForm(function (p) { - var n = {}; - for (var k in p) n[k] = p[k]; - n.replaceDate = d; - return n; - }); - }, - allowClear: true - }) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '替换类型'), - React.createElement(Select, { - placeholder: '请选择', - style: { width: '100%' }, - value: form.replaceType, - onChange: function (v) { setEdited(true); setForm(function (p) { var n = {}; for (var k in p) n[k] = p[k]; n.replaceType = v; return n; }); }, - allowClear: true, - options: [{ value: '永久替换', label: '永久替换' }, { value: '临时替换', label: '临时替换' }] - }) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '替换原因'), - React.createElement(Select, { - placeholder: '请选择', - style: { width: '100%' }, - value: form.replaceReason, - onChange: function (v) { setEdited(true); setForm(function (p) { var n = {}; for (var k in p) n[k] = p[k]; n.replaceReason = v; return n; }); }, - allowClear: true, - options: [{ value: '客户原因', label: '客户原因' }, { value: '车辆原因', label: '车辆原因' }] - }) - ), - React.createElement('div', { style: Object.assign({}, formItemStyle, { gridColumn: '1 / -1' }) }, - React.createElement('div', { style: labelStyle }, '替换原因说明'), - React.createElement(Input.TextArea, { - placeholder: '请说明替换原因', - value: form.replaceReasonDesc || '', - onChange: function (e) { setEdited(true); setForm(function (p) { var n = {}; for (var k in p) n[k] = p[k]; n.replaceReasonDesc = e.target.value; return n; }); }, - rows: 3, - style: { width: '100%' } - }) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '被替换车牌号'), - React.createElement(Select, { - placeholder: '请选择该合同已交车未还车车牌号', - style: { width: '100%' }, - value: form.originalPlate, - onChange: onOriginalPlateChange, - allowClear: true, - options: originalPlateList, - disabled: !form.projectId - }) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '被替换车识别代码'), - React.createElement(Input, { value: form.originalVin || '', disabled: true }) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '被替换车品牌'), - React.createElement(Input, { value: form.originalBrand || '', disabled: true }) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '被替换车型号'), - React.createElement(Input, { value: form.originalModel || '', disabled: true }) - ), - React.createElement('div', { style: { gridColumn: '1 / -1', width: '100%' } }), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '替换车车牌号'), - React.createElement(Select, { - placeholder: '请选择已备车车辆', - style: { width: '100%' }, - value: form.replacePlate, - onChange: onReplacePlateChange, - allowClear: true, - showSearch: true, - options: replacePlateSelectOptions, - filterOption: function (input, opt) { return (opt.label || '').toString().toLowerCase().indexOf((input || '').toLowerCase()) !== -1; }, - disabled: !form.projectId - }) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '替换车识别代码'), - React.createElement(Input, { value: form.replaceVin || '', disabled: true }) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '替换车品牌'), - React.createElement(Input, { value: form.replaceBrand || '', disabled: true }) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '替换车型号'), - React.createElement(Input, { value: form.replaceModel || '', disabled: true }) + React.createElement( + Card, + { + className: 'vr-main-card', + title: React.createElement( + 'span', + null, + '新增替换车 ', + pairs.length > 0 + ? React.createElement(Tag, { style: { marginLeft: 8, fontWeight: 400 } }, pairs.length + ' 辆车') + : null ) - ), - React.createElement('div', { style: { display: 'flex', gap: 8, marginTop: 24 } }, - React.createElement(Button, { type: 'primary', onClick: handleSubmit }, '提交审核'), - React.createElement(Button, { onClick: handleSave }, '保存'), - React.createElement(Button, { onClick: handleCancel }, '取消') + }, + renderMultiPickSection(), + pairs.length > 0 + ? React.createElement( + 'div', + { className: 'vr-pair-list' }, + pairs.map(function (pair, index) { return renderPairCard(pair, index); }) + ) + : React.createElement( + 'div', + { className: 'vr-pair-list-empty' }, + '请在上方选择被替换车辆车牌号,将自动生成替换明细' + ), + renderProjectPanel(), + React.createElement( + 'div', + { className: 'vr-footer' }, + React.createElement(Button, { type: 'primary', size: 'large', onClick: handleSubmit }, '提交审核'), + React.createElement(Button, { size: 'large', onClick: handleSave }, '保存'), + React.createElement(Button, { size: 'large', onClick: handleCancel }, '取消') ) ), React.createElement(Modal, { - title: '需求说明', + title: '替换车管理 - 新增 · 需求说明', open: requirementModalVisible[0], onCancel: function () { setRequirementModalVisible(false); }, - width: 720, - footer: React.createElement(Button, { onClick: function () { setRequirementModalVisible(false); } }, '关闭'), - bodyStyle: { maxHeight: '70vh', overflow: 'auto' } - }, React.createElement('div', { style: { padding: '8px 0' } }, - React.createElement('div', { style: { whiteSpace: 'pre-wrap', fontSize: 13, lineHeight: 1.6 } }, requirementContent)) - ), + width: 760, + footer: React.createElement(Button, { type: 'primary', onClick: function () { setRequirementModalVisible(false); } }, '关闭'), + bodyStyle: { maxHeight: '72vh', overflow: 'auto', paddingTop: 8 } + }, renderRequirementDoc()), React.createElement(Modal, { title: '取消将会丢失所有已填写内容,是否确认?', open: cancelModalVisible[0], diff --git a/web端/运维管理/车辆业务/替换车管理-查看.jsx b/web端/运维管理/车辆业务/替换车管理-查看.jsx index e185b1f..038c085 100644 --- a/web端/运维管理/车辆业务/替换车管理-查看.jsx +++ b/web端/运维管理/车辆业务/替换车管理-查看.jsx @@ -1,5 +1,5 @@ // 【重要】必须使用 const Component 作为组件变量名 -// 运维管理 - 车辆业务 - 替换车管理 - 查看(2026年3月3日版本) +// 运维管理 - 车辆业务 - 替换车管理 - 查看 const Component = function () { var useState = React.useState; @@ -10,61 +10,290 @@ const Component = function () { var Input = antd.Input; var Button = antd.Button; var Modal = antd.Modal; + var Tag = antd.Tag; var Steps = antd.Steps; + var EMPTY_PROJECT = { + contractId: '', + projectId: '', + projectName: '', + projectType: '', + customerName: '', + contractCode: '', + deliveryRegion: '' + }; + + var activeContracts = [ + { + contractId: 'c1', + contractStatus: '合同进行中', + projectId: 'p1', + projectName: '嘉兴氢能示范项目', + projectType: '租赁', + contractCode: 'HT-ZL-2025-001', + customerName: '嘉兴某某物流有限公司', + deliveryRegion: '浙江省-嘉兴市' + } + ]; + + var MOCK_PAIRS = [ + { + id: 'pair_1', + replaceType: '永久替换', + replaceReason: '车辆原因', + replaceReasonDesc: '原车故障需维修,临时用替换车保障客户用车。', + originalPlate: '浙A12345', + originalBrand: '东风', + originalModel: 'DFH1180', + contractId: 'c1', + replacePlate: '浙A67890', + replaceBrand: '福田', + replaceModel: 'BJ1180' + }, + { + id: 'pair_2', + replaceType: '临时替换', + replaceReason: '客户原因', + replaceReasonDesc: '', + originalPlate: '浙A55555', + originalBrand: '重汽', + originalModel: 'ZZ1160', + contractId: 'c1', + replacePlate: '浙A66666', + replaceBrand: '江淮', + replaceModel: 'HFC1190' + } + ]; + + var contractById = (function () { + var map = {}; + activeContracts.forEach(function (c) { map[c.contractId] = c; }); + return map; + })(); + + var pairs = MOCK_PAIRS; var requirementModalVisible = useState(false); var setRequirementModalVisible = requirementModalVisible[1]; - // 模拟:根据已填信息反查的一条替换车记录(实际由路由参数或接口拉取) - var detail = useState({ - projectName: '嘉兴氢能示范项目', - contractCode: 'HT-ZL-2025-001', - customerName: '嘉兴某某物流有限公司', - contactPerson: '张三', - signDate: '2025-01-15', - contactPhone: '13800138001', - businessDept: '业务1部', - businessPerson: '张经理', - replaceDate: '2026-02-18', - replaceType: '永久替换', - replaceReason: '车辆原因', - replaceReasonDesc: '原车故障需维修,临时用替换车保障客户用车。', - originalPlate: '浙A12345', - originalVin: 'LGHXCAE28M1234567', - originalBrand: '东风', - originalModel: 'DFH1180', - replacePlate: '浙A67890', - replaceVin: 'LGHXCAE28M6789012', - replaceBrand: '福田', - replaceModel: 'BJ1180' - }); - var data = detail[0]; + var projectInfo = (function () { + var anchor = pairs.find(function (p) { return p.originalPlate && p.contractId; }); + if (!anchor || !anchor.contractId) return EMPTY_PROJECT; + var c = contractById[anchor.contractId]; + if (!c) return EMPTY_PROJECT; + return { + contractId: c.contractId, + projectId: c.projectId, + projectName: c.projectName, + projectType: c.projectType, + customerName: c.customerName, + contractCode: c.contractCode, + deliveryRegion: c.deliveryRegion + }; + })(); - var handleBack = function () { - // 返回替换车管理列表页(实际为路由或平台跳转) - if (window.__replaceCarBack) { - window.__replaceCarBack(); - } else { - antd.message.info('返回替换车管理列表(原型)'); - } - }; - - var layoutStyle = { padding: '16px 24px', background: '#f5f5f5', minHeight: '100vh' }; - var cardStyle = { marginBottom: 16 }; - var labelStyle = { marginBottom: 6, fontSize: 14, color: 'rgba(0,0,0,0.65)' }; - var formRowStyle = { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '16px 24px', marginBottom: 16 }; - var formItemStyle = { marginBottom: 12 }; - - // 审批情况(竖排步骤条,参照提车应收款-审核) var approvalSteps = [ { title: '业务部主管', person: '姚守涛', status: 'finish', approveTime: '2026-02-20 09:30' }, { title: '事业部主管', person: '尚建华', status: 'finish', approveTime: '2026-02-20 10:15' }, { title: '运维主管', person: '王运维', status: 'finish', approveTime: '2026-02-20 11:00' } ]; - var requirementContent = '替换车管理-查看(2026年3月3日版本)\n一个「数字化资产ONEOS运管平台」中的「运维管理」「车辆业务」「替换车管理」「查看」模块\n1.面包屑:\n#运维管理-车辆业务-替换车管理-查看\n页面由选择项目、替换车详情、两个单独卡片组成;\n\n2.选择项目:\n#可通过选择进行中的车辆租赁合同,拉取租赁合同中对应车辆进行替换;\n2.1.项目名称:根据已填信息反查,不可编辑;\n2.2.合同编码:根据项目名称自动反查,不可编辑;\n2.3.客户名称:根据项目名称自动反查,不可编辑;\n2.4.对接人:根据项目名称自动反查,不可编辑;\n2.5.合同签订时间:根据项目名称自动反查,不可编辑;\n2.6.客户联系电话:根据项目名称自动反查,不可编辑;\n2.7.业务部门:根据项目名称自动反查,不可编辑;\n2.8.业务人员:根据项目名称自动反查,不可编辑;\n\n3.替换车详情:\n3.1.替换时间:日期选择器(禁用),显示退换时间,格式为YYYY-MM-DD;\n3.2.替换类型:选择器,根据已填信息反查,不可编辑;\n 3.2.1.类型为永久替换时,该申请通过审核后替换车进行交车(交车时间为流程结束当天),由运维手动将被替换车进行还车;\n 3.2.2.类型为临时替换时,该申请通过审核后替换车进行交车(交车时间为流程结束当天),被替换车不用还车,在被替换车重新交付客户时,由运维手动将替换车进行还车;\n 重新生成交车任务时,交车地点会自动继承自合同,由对应区域运维人员才能操作;\n 交车任务完成后,所有涉及到被替换车辆显示(例如车辆租赁合同、租赁账单、提车应收款等功能)会替换为新替换车的对应信息,如果是临时替换,在新替换车完成还车后,对应车辆记录会恢复为原有车辆数据。如果是永久替换,则由运维自主进行被替换车辆还车;\n3.3.替换原因:选择器,根据已填信息反查,不可编辑;\n3.4.替换原因说明:文本域,根据已填信息反查,不可编辑;\n3.5.被替换车牌号:选择器,根据已填信息反查,不可编辑;\n3.6.被替换车识别代码:输入框(禁用),选择被替换车车牌号后自动反写该车识别代码;\n3.7.被替换车品牌:输入框(禁用),选择被替换车车牌号后自动反写该车品牌;\n3.8.被替换车型号:输入框(禁用),选择被替换车车牌号后自动反写该车型号;\n3.9.替换车车牌号:选择器,根据已填信息反查,不可编辑;\n3.10.替换车识别代码:输入框(禁用),选择替换车车牌号后自动反写该车识别代码;\n3.11.替换车品牌:输入框(禁用),选择替换车车牌号后自动反写该车品牌;\n3.12.替换车型号:输入框(禁用),选择替换车车牌号后自动反写该车型号;\n\n下方为返回按钮;\n4.1.点击返回,返回替换车管理列表页;'; - return React.createElement('div', { style: layoutStyle }, - React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 } }, + var handleBack = function () { + if (window.__replaceCarBack) window.__replaceCarBack(); + else antd.message.info('返回替换车管理列表(原型)'); + }; + + var pageCss = + '.vr-add-page{font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif}' + + '.vr-add-page .vr-page-header{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:20px}' + + '.vr-add-page .vr-main-card{border-radius:16px;border:none;box-shadow:0 4px 24px -6px rgba(15,23,42,0.08),0 0 0 1px rgba(15,23,42,0.05)}' + + '.vr-add-page .vr-main-card>.ant-card-head{border-bottom:1px solid #f1f5f9;padding:16px 24px;min-height:auto}' + + '.vr-add-page .vr-main-card>.ant-card-head .ant-card-head-title{font-size:16px;font-weight:600;color:#0f172a;padding:0}' + + '.vr-add-page .vr-main-card>.ant-card-body{padding:20px 24px 24px}' + + '.vr-add-page .vr-approval-card{border-radius:16px;border:none;box-shadow:0 4px 24px -6px rgba(15,23,42,0.08),0 0 0 1px rgba(15,23,42,0.05);margin-top:16px}' + + '.vr-add-page .vr-pair-list{display:flex;flex-direction:column;gap:16px}' + + '.vr-add-page .vr-pair-card{border-radius:12px;border:1px solid #e2e8f0;background:#f8fafc;overflow:hidden}' + + '.vr-add-page .vr-pair-card__head{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 16px;background:#f0f9ff;border-bottom:1px solid #e0f2fe}' + + '.vr-add-page .vr-pair-card__title{display:flex;align-items:center;gap:8px;font-size:14px;font-weight:600;color:#0f172a;flex-wrap:wrap}' + + '.vr-add-page .vr-pair-card__arrow{display:inline-flex;align-items:center;justify-content:center;color:#1677ff;font-size:16px;font-weight:600;line-height:1;padding:0 2px;flex-shrink:0}' + + '.vr-add-page .vr-pair-card__index{display:inline-flex;align-items:center;justify-content:center;min-width:24px;height:24px;padding:0 8px;border-radius:6px;background:#1677ff;color:#fff;font-size:12px;font-weight:700}' + + '.vr-add-page .vr-pair-card__body{padding:16px}' + + '.vr-add-page .vr-block{margin-bottom:14px}' + + '.vr-add-page .vr-block:last-child{margin-bottom:0}' + + '.vr-add-page .vr-block-label{font-size:12px;font-weight:600;color:#475569;margin-bottom:10px}' + + '.vr-add-page .vr-block-label--new{color:#047857}' + + '.vr-add-page .vr-form-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px 16px}' + + '.vr-add-page .vr-form-grid--reason .vr-field:last-child{grid-column:1/-1}' + + '@media(max-width:900px){.vr-add-page .vr-form-grid{grid-template-columns:1fr}}' + + '.vr-add-page .vr-field{display:flex;flex-direction:column;gap:6px;min-width:0}' + + '.vr-add-page .vr-field__label{font-size:13px;font-weight:500;color:#334155}' + + '.vr-add-page .vr-swap-divider{display:flex;align-items:center;gap:12px;margin:14px 0;color:#94a3b8;font-size:12px;font-weight:500}' + + '.vr-add-page .vr-swap-divider::before,.vr-add-page .vr-swap-divider::after{content:"";flex:1;height:1px;background:linear-gradient(90deg,transparent,#cbd5e1,transparent)}' + + '.vr-add-page .vr-swap-divider__icon{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:50%;background:#eff6ff;color:#1677ff;font-size:14px}' + + '.vr-add-page .vr-vehicle-summary{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px 16px;padding:12px 14px;margin-bottom:14px;border-radius:8px;background:#fffbeb;border:1px solid #fde68a}' + + '.vr-add-page .vr-project-panel{margin-top:20px;padding:16px 18px;border-radius:12px;background:#f8fafc;border:1px solid #e2e8f0}' + + '.vr-add-page .vr-project-panel__title{font-size:14px;font-weight:600;color:#0f172a;margin-bottom:14px}' + + '.vr-add-page .vr-project-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px 20px}' + + '.vr-add-page .vr-readonly{display:flex;flex-direction:column;gap:4px}' + + '.vr-add-page .vr-readonly__label{font-size:12px;color:#64748b;font-weight:500}' + + '.vr-add-page .vr-readonly__value{font-size:14px;color:#0f172a;font-weight:500}' + + '.vr-add-page .vr-footer{display:flex;gap:10px;margin-top:24px;padding-top:20px;border-top:1px solid #f1f5f9}' + + '.vr-req-doc{padding:4px 2px 8px}' + + '.vr-req-doc__meta{font-size:12px;color:#64748b;line-height:1.6;margin-bottom:16px;padding-bottom:12px;border-bottom:1px solid #f1f5f9}' + + '.vr-req-doc__section{margin-bottom:20px}' + + '.vr-req-doc__title{font-size:15px;font-weight:600;color:#0f172a;margin:0 0 10px}' + + '.vr-req-doc__line{font-size:13px;color:#475569;line-height:1.75;margin:0 0 6px}' + + '.vr-req-doc__line--sub{padding-left:14px;color:#64748b}'; + + function specSection(title, lines) { + return React.createElement( + 'section', + { className: 'vr-req-doc__section' }, + React.createElement('h3', { className: 'vr-req-doc__title' }, title), + (lines || []).map(function (text, i) { + var isSub = text.indexOf(' ') === 0; + return React.createElement('p', { key: i, className: 'vr-req-doc__line' + (isSub ? ' vr-req-doc__line--sub' : '') }, text); + }) + ); + } + + function renderRequirementDoc() { + return React.createElement( + 'div', + { className: 'vr-req-doc' }, + React.createElement('div', { className: 'vr-req-doc__meta' }, '数字化资产 ONEOS 运管平台 · 运维管理 · 车辆业务 · 替换车管理 · 查看'), + specSection('1. 页面定位', ['只读查看替换车申请详情,布局与新增/编辑一致,不可修改任何字段。']), + specSection('2. 展示内容', [ + '2.1 车辆替换明细:每条被替换车一张卡片,含被替换车信息、替换说明、替换车辆;卡片标题展示被替换与替换车牌。', + '2.2 项目信息:客户名称、项目名称、项目类型(全单一份)。', + '2.3 审批情况:竖向步骤条展示审批节点、审批人、审批时间。' + ]), + specSection('3. 操作', ['底部仅「返回」按钮,返回替换车管理列表。']) + ); + } + + function renderField(label, node) { + return React.createElement( + 'div', + { className: 'vr-field' }, + React.createElement('div', { className: 'vr-field__label' }, label), + node + ); + } + + function renderPairCard(pair, index) { + return React.createElement( + 'article', + { key: pair.id, className: 'vr-pair-card' }, + React.createElement( + 'div', + { className: 'vr-pair-card__head' }, + React.createElement( + 'div', + { className: 'vr-pair-card__title' }, + React.createElement('span', { className: 'vr-pair-card__index' }, index + 1), + React.createElement('span', null, '车辆替换'), + React.createElement(Tag, { style: { margin: 0 } }, pair.originalPlate), + pair.replacePlate + ? React.createElement('span', { className: 'vr-pair-card__arrow', 'aria-hidden': true }, '→') + : null, + pair.replacePlate + ? React.createElement(Tag, { color: 'processing', style: { margin: 0 } }, pair.replacePlate) + : null + ) + ), + React.createElement( + 'div', + { className: 'vr-pair-card__body' }, + React.createElement( + 'div', + { className: 'vr-vehicle-summary' }, + renderField('车牌号', React.createElement(Input, { value: pair.originalPlate, disabled: true })), + renderField('品牌', React.createElement(Input, { value: pair.originalBrand, disabled: true })), + renderField('型号', React.createElement(Input, { value: pair.originalModel, disabled: true })) + ), + React.createElement( + 'div', + { className: 'vr-block' }, + React.createElement('div', { className: 'vr-block-label' }, '替换说明'), + React.createElement( + 'div', + { className: 'vr-form-grid vr-form-grid--reason' }, + renderField('替换类型', React.createElement(Input, { value: pair.replaceType, disabled: true })), + renderField('替换原因', React.createElement(Input, { value: pair.replaceReason, disabled: true })), + renderField( + '替换原因说明', + React.createElement(Input.TextArea, { + value: pair.replaceReasonDesc || '—', + disabled: true, + rows: 2, + style: { width: '100%' } + }) + ) + ) + ), + React.createElement( + 'div', + { className: 'vr-swap-divider' }, + React.createElement('span', { className: 'vr-swap-divider__icon' }, '↓'), + React.createElement('span', null, '替换为') + ), + React.createElement( + 'div', + { className: 'vr-block' }, + React.createElement('div', { className: 'vr-block-label vr-block-label--new' }, '替换车辆'), + React.createElement( + 'div', + { className: 'vr-form-grid' }, + renderField('新车', React.createElement(Input, { value: pair.replacePlate, disabled: true })), + renderField('品牌', React.createElement(Input, { value: pair.replaceBrand, disabled: true })), + renderField('型号', React.createElement(Input, { value: pair.replaceModel, disabled: true })) + ) + ) + ) + ); + } + + function renderProjectPanel() { + return React.createElement( + 'section', + { className: 'vr-project-panel' }, + React.createElement('div', { className: 'vr-project-panel__title' }, '项目信息'), + React.createElement( + 'div', + { className: 'vr-project-grid' }, + React.createElement( + 'div', + { className: 'vr-readonly' }, + React.createElement('span', { className: 'vr-readonly__label' }, '客户名称'), + React.createElement('span', { className: 'vr-readonly__value' }, projectInfo.customerName) + ), + React.createElement( + 'div', + { className: 'vr-readonly' }, + React.createElement('span', { className: 'vr-readonly__label' }, '项目名称'), + React.createElement('span', { className: 'vr-readonly__value' }, projectInfo.projectName) + ), + React.createElement( + 'div', + { className: 'vr-readonly' }, + React.createElement('span', { className: 'vr-readonly__label' }, '项目类型'), + React.createElement( + 'span', + { className: 'vr-readonly__value' }, + React.createElement(Tag, { color: projectInfo.projectType === '自营' ? 'purple' : 'blue', style: { margin: 0 } }, projectInfo.projectType) + ) + ) + ) + ); + } + + return React.createElement( + 'div', + { className: 'vr-add-page', style: { padding: '20px 24px 32px', minHeight: '100vh', background: 'linear-gradient(165deg,#eef4ff 0%,#f5f7fa 42%,#f0f2f5 100%)' } }, + React.createElement('style', null, pageCss), + React.createElement( + 'header', + { className: 'vr-page-header' }, React.createElement(Breadcrumb, { items: [ { title: '运维管理' }, @@ -75,126 +304,49 @@ const Component = function () { }), React.createElement(Button, { type: 'link', style: { padding: 0 }, onClick: function () { setRequirementModalVisible(true); } }, '查看需求说明') ), - React.createElement(Card, { title: '选择项目', style: cardStyle }, - React.createElement('div', { style: formRowStyle }, - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '项目名称'), - React.createElement(Input, { value: data.projectName || '', disabled: true }) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '合同编码'), - React.createElement(Input, { value: data.contractCode || '', disabled: true }) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '客户名称'), - React.createElement(Input, { value: data.customerName || '', disabled: true }) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '对接人'), - React.createElement(Input, { value: data.contactPerson || '', disabled: true }) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '合同签订时间'), - React.createElement(Input, { value: data.signDate || '', disabled: true }) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '客户联系电话'), - React.createElement(Input, { value: data.contactPhone || '', disabled: true }) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '业务部门'), - React.createElement(Input, { value: data.businessDept || '', disabled: true }) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '业务人员'), - React.createElement(Input, { value: data.businessPerson || '', disabled: true }) + React.createElement( + Card, + { + className: 'vr-main-card', + title: React.createElement( + 'span', + null, + '查看替换车 ', + React.createElement(Tag, { style: { marginLeft: 8, fontWeight: 400 } }, pairs.length + ' 辆车') ) - ) + }, + React.createElement('div', { className: 'vr-pair-list' }, pairs.map(function (pair, index) { return renderPairCard(pair, index); })), + renderProjectPanel(), + React.createElement('div', { className: 'vr-footer' }, React.createElement(Button, { size: 'large', onClick: handleBack }, '返回')) ), - React.createElement(Card, { title: '替换车详情', style: cardStyle }, - React.createElement('div', { style: formRowStyle }, - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '替换时间'), - React.createElement(Input, { value: data.replaceDate || '', disabled: true, placeholder: 'YYYY-MM-DD' }) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '替换类型'), - React.createElement(Input, { value: data.replaceType || '', disabled: true }) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '替换原因'), - React.createElement(Input, { value: data.replaceReason || '', disabled: true }) - ), - React.createElement('div', { style: Object.assign({}, formItemStyle, { gridColumn: '1 / -1' }) }, - React.createElement('div', { style: labelStyle }, '替换原因说明'), - React.createElement(Input.TextArea, { value: data.replaceReasonDesc || '', disabled: true, rows: 3, style: { width: '100%' } }) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '被替换车牌号'), - React.createElement(Input, { value: data.originalPlate || '', disabled: true }) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '被替换车识别代码'), - React.createElement(Input, { value: data.originalVin || '', disabled: true }) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '被替换车品牌'), - React.createElement(Input, { value: data.originalBrand || '', disabled: true }) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '被替换车型号'), - React.createElement(Input, { value: data.originalModel || '', disabled: true }) - ), - React.createElement('div', { style: { gridColumn: '1 / -1', width: '100%' } }), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '替换车车牌号'), - React.createElement(Input, { value: data.replacePlate || '', disabled: true }) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '替换车识别代码'), - React.createElement(Input, { value: data.replaceVin || '', disabled: true }) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '替换车品牌'), - React.createElement(Input, { value: data.replaceBrand || '', disabled: true }) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '替换车型号'), - React.createElement(Input, { value: data.replaceModel || '', disabled: true }) - ) - ), - React.createElement('div', { style: { display: 'flex', gap: 8, marginTop: 24 } }, - React.createElement(Button, { onClick: handleBack }, '返回') - ) - ), - React.createElement(Card, { title: '审批情况', style: cardStyle }, + React.createElement( + Card, + { className: 'vr-approval-card', title: '审批情况' }, React.createElement(Steps, { direction: 'vertical', current: approvalSteps.length, items: approvalSteps.map(function (s) { - var statusText = s.status === 'finish' ? '审批通过' : '待审批'; - var desc = React.createElement('div', { style: { fontSize: 13, color: 'rgba(0,0,0,0.65)', marginTop: 4 } }, - React.createElement('div', null, '审批状态:', statusText), - React.createElement('div', null, '审批人:', s.person || '—'), - s.approveTime ? React.createElement('div', null, '审批时间:', s.approveTime) : null - ); return { title: s.title, - description: desc, - status: s.status === 'finish' ? 'finish' : 'wait' + status: s.status === 'finish' ? 'finish' : 'wait', + description: React.createElement( + 'div', + { style: { fontSize: 13, color: 'rgba(0,0,0,0.65)', marginTop: 4 } }, + React.createElement('div', null, '审批状态:', s.status === 'finish' ? '审批通过' : '待审批'), + React.createElement('div', null, '审批人:', s.person || '—'), + s.approveTime ? React.createElement('div', null, '审批时间:', s.approveTime) : null + ) }; }) }) ), React.createElement(Modal, { - title: '需求说明', + title: '替换车管理 - 查看 · 需求说明', open: requirementModalVisible[0], onCancel: function () { setRequirementModalVisible(false); }, - width: 720, - footer: React.createElement(Button, { onClick: function () { setRequirementModalVisible(false); } }, '关闭'), - bodyStyle: { maxHeight: '70vh', overflow: 'auto' } - }, React.createElement('div', { style: { padding: '8px 0' } }, - React.createElement('div', { style: { whiteSpace: 'pre-wrap', fontSize: 13, lineHeight: 1.6 } }, requirementContent)) - ) + width: 760, + footer: React.createElement(Button, { type: 'primary', onClick: function () { setRequirementModalVisible(false); } }, '关闭'), + bodyStyle: { maxHeight: '72vh', overflow: 'auto', paddingTop: 8 } + }, renderRequirementDoc()) ); }; diff --git a/web端/运维管理/车辆业务/替换车管理-编辑.jsx b/web端/运维管理/车辆业务/替换车管理-编辑.jsx index 8140899..bf11ace 100644 --- a/web端/运维管理/车辆业务/替换车管理-编辑.jsx +++ b/web端/运维管理/车辆业务/替换车管理-编辑.jsx @@ -1,173 +1,761 @@ // 【重要】必须使用 const Component 作为组件变量名 -// 运维管理 - 车辆业务 - 替换车管理 - 编辑(2026年3月3日版本) +// 运维管理 - 车辆业务 - 替换车管理 - 编辑 const Component = function () { var useState = React.useState; + var useCallback = React.useCallback; + var useMemo = React.useMemo; + var useRef = React.useRef; var antd = window.antd; var Breadcrumb = antd.Breadcrumb; var Card = antd.Card; + var Select = antd.Select; var Input = antd.Input; var Button = antd.Button; - var Select = antd.Select; - var DatePicker = antd.DatePicker; var Modal = antd.Modal; - var Option = Select.Option; + var message = antd.message; + var Tag = antd.Tag; + var Empty = antd.Empty; + var pairIdRef = useRef(2); + + function createMockPairs() { + return [ + { + id: 'pair_1', + replaceType: '永久替换', + replaceReason: '车辆原因', + replaceReasonDesc: '原车故障需维修,临时用替换车保障客户用车。', + originalPlate: '浙A12345', + originalVin: 'LGHXCAE28M1234567', + originalBrand: '东风', + originalModel: 'DFH1180', + contractId: 'c1', + replacePlate: '浙A67890', + replaceVin: 'LGHXCAE28M6789012', + replaceBrand: '福田', + replaceModel: 'BJ1180' + }, + { + id: 'pair_2', + replaceType: '临时替换', + replaceReason: '客户原因', + replaceReasonDesc: '', + originalPlate: '浙A55555', + originalVin: 'LGHXCAE28M5555555', + originalBrand: '重汽', + originalModel: 'ZZ1160', + contractId: 'c1', + replacePlate: '浙A66666', + replaceVin: 'LGHXCAE28M6666666', + replaceBrand: '江淮', + replaceModel: 'HFC1190' + } + ]; + } + + function createEmptyPair() { + pairIdRef.current += 1; + return { + id: 'pair_' + pairIdRef.current, + replaceType: undefined, + replaceReason: undefined, + replaceReasonDesc: '', + originalPlate: undefined, + originalVin: '', + originalBrand: '', + originalModel: '', + contractId: '', + replacePlate: undefined, + replaceVin: '', + replaceBrand: '', + replaceModel: '' + }; + } + + var EMPTY_PROJECT = { + contractId: '', + projectId: '', + projectName: '', + projectType: '', + customerName: '', + contractCode: '', + deliveryRegion: '' + }; + + // 模拟:合同状态为「合同进行中」的租赁合同 + var activeContracts = [ + { + contractId: 'c1', + contractStatus: '合同进行中', + projectId: 'p1', + projectName: '嘉兴氢能示范项目', + projectType: '租赁', + contractCode: 'HT-ZL-2025-001', + customerName: '嘉兴某某物流有限公司', + deliveryRegion: '浙江省-嘉兴市' + }, + { + contractId: 'c2', + contractStatus: '合同进行中', + projectId: 'p2', + projectName: '上海物流租赁项目', + projectType: '租赁', + contractCode: 'HT-ZL-2025-002', + customerName: '上海某某运输公司', + deliveryRegion: '上海市-上海市' + }, + { + contractId: 'c3', + contractStatus: '合同进行中', + projectId: 'p3', + projectName: '杭州城配自营项目', + projectType: '自营', + contractCode: 'HT-ZY-2025-003', + customerName: '杭州某某租赁有限公司', + deliveryRegion: '浙江省-杭州市' + } + ]; + + var deliveredVehicles = [ + { plateNo: '浙A12345', vin: 'LGHXCAE28M1234567', brand: '东风', model: 'DFH1180', contractId: 'c1', vehicleStatus: '已交车' }, + { plateNo: '浙A55555', vin: 'LGHXCAE28M5555555', brand: '重汽', model: 'ZZ1160', contractId: 'c1', vehicleStatus: '已交车' }, + { plateNo: '沪B11111', vin: 'LGHXCAE28M7654321', brand: '江淮', model: 'HFC1180', contractId: 'c2', vehicleStatus: '已交车' }, + { plateNo: '浙C33333', vin: 'LGHXCAE28M8888888', brand: '东风', model: 'DFH1190', contractId: 'c3', vehicleStatus: '已交车' } + ]; + + var preparedVehiclesByRegion = { + '浙江省-嘉兴市': [ + { plateNo: '浙A67890', vin: 'LGHXCAE28M6789012', brand: '福田', model: 'BJ1180', vehicleStatus: '已备车' }, + { plateNo: '浙A66666', vin: 'LGHXCAE28M6666666', brand: '江淮', model: 'HFC1190', vehicleStatus: '已备车' }, + { plateNo: '浙F88888', vin: 'LGHXCAE28M8888888', brand: '东风', model: 'DFH1180', vehicleStatus: '已备车' } + ], + '上海市-上海市': [ + { plateNo: '沪B22222', vin: 'LGHXCAE28M2222222', brand: '重汽', model: 'ZZ1180', vehicleStatus: '已备车' }, + { plateNo: '沪B33333', vin: 'LGHXCAE28M3333333', brand: '福田', model: 'BJ1190', vehicleStatus: '已备车' } + ], + '浙江省-杭州市': [ + { plateNo: '浙C44444', vin: 'LGHXCAE28M4444444', brand: '东风', model: 'DFH1180', vehicleStatus: '已备车' } + ] + }; + + var contractById = useMemo(function () { + var map = {}; + activeContracts.forEach(function (c) { + if (c.contractStatus === '合同进行中') map[c.contractId] = c; + }); + return map; + }, []); + + var pairsState = useState(function () { return createMockPairs(); }); + var pairs = pairsState[0]; + var setPairs = pairsState[1]; + var editedState = useState(false); + var setEdited = editedState[1]; + var cancelModalVisible = useState(false); + var setCancelModalVisible = cancelModalVisible[1]; var requirementModalVisible = useState(false); var setRequirementModalVisible = requirementModalVisible[1]; - var edited = useState(false); - var setEdited = edited[1]; - // 模拟:进行中的车辆租赁合同列表(实际由接口拉取) - var contractList = [ - { projectName: '嘉兴氢能示范项目', contractCode: 'HT-ZL-2025-001', customerName: '嘉兴某某物流有限公司', contactPerson: '张三', signDate: '2025-01-15', contactPhone: '13800138001', businessDept: '业务1部', businessPerson: '张经理' }, - { projectName: '上海氢能试点项目', contractCode: 'HT-ZL-2025-002', customerName: '上海某某运输公司', contactPerson: '李四', signDate: '2025-02-01', contactPhone: '13900139001', businessDept: '业务2部', businessPerson: '李经理' }, - { projectName: '杭州示范运营项目', contractCode: 'HT-ZL-2025-003', customerName: '杭州某某物流', contactPerson: '王五', signDate: '2025-01-20', contactPhone: '13700137001', businessDept: '业务1部', businessPerson: '张经理' } - ]; - // 该合同下已交车未还车车辆(被替换车可选) - var contractDeliveredVehicles = { - '嘉兴氢能示范项目': [ - { plateNo: '浙A12345', vin: 'LGHXCAE28M1234567', brand: '东风', model: 'DFH1180' }, - { plateNo: '浙A11111', vin: 'LGHXCAE28M1111111', brand: '东风', model: 'DFH1180' } - ], - '上海氢能试点项目': [ - { plateNo: '沪B22222', vin: 'LGHXCAE28M2222222', brand: '福田', model: 'BJ1180' } - ], - '杭州示范运营项目': [] - }; - // 人员权限下已备车车辆(替换车可选,需先选项目名称后才能选) - var preparedVehicleList = [ - { plateNo: '浙A67890', vin: 'LGHXCAE28M6789012', brand: '福田', model: 'BJ1180' }, - { plateNo: '浙B33333', vin: 'LGHXCAE28M3333333', brand: '东风', model: 'DFH1180' } - ]; + var projectInfo = useMemo(function () { + var anchor = pairs.find(function (p) { return p.originalPlate && p.contractId; }); + if (!anchor || !anchor.contractId) return EMPTY_PROJECT; + var c = contractById[anchor.contractId]; + if (!c) return EMPTY_PROJECT; + return { + contractId: c.contractId, + projectId: c.projectId, + projectName: c.projectName, + projectType: c.projectType, + customerName: c.customerName, + contractCode: c.contractCode, + deliveryRegion: c.deliveryRegion + }; + }, [pairs, contractById]); - // 编辑表单状态(实际由路由参数或接口拉取初始值) - var detail = useState({ - projectName: '嘉兴氢能示范项目', - contractCode: 'HT-ZL-2025-001', - customerName: '嘉兴某某物流有限公司', - contactPerson: '张三', - signDate: '2025-01-15', - contactPhone: '13800138001', - businessDept: '业务1部', - businessPerson: '张经理', - replaceDate: '2026-02-18', - replaceType: '永久替换', - replaceReason: '车辆原因', - replaceReasonDesc: '原车故障需维修,临时用替换车保障客户用车。', - originalPlate: '浙A12345', - originalVin: 'LGHXCAE28M1234567', - originalBrand: '东风', - originalModel: 'DFH1180', - replacePlate: '浙A67890', - replaceVin: 'LGHXCAE28M6789012', - replaceBrand: '福田', - replaceModel: 'BJ1180' - }); - var data = detail[0]; - var setData = detail[1]; + var plateFilterOption = useCallback(function (input, opt) { + return (opt.label || '').toString().toLowerCase().indexOf((input || '').toLowerCase()) !== -1; + }, []); - var updateDetail = function (field, value) { - setEdited(true); - setData(function (prev) { - var next = Object.assign({}, prev); - next[field] = value; - if (field === 'projectName') { - var c = contractList.find(function (x) { return x.projectName === value; }); - if (c) { - next.contractCode = c.contractCode; - next.customerName = c.customerName; - next.contactPerson = c.contactPerson; - next.signDate = c.signDate; - next.contactPhone = c.contactPhone; - next.businessDept = c.businessDept; - next.businessPerson = c.businessPerson; - } else { - next.contractCode = ''; - next.customerName = ''; - next.contactPerson = ''; - next.signDate = ''; - next.contactPhone = ''; - next.businessDept = ''; - next.businessPerson = ''; - } - next.originalPlate = ''; - next.originalVin = ''; - next.originalBrand = ''; - next.originalModel = ''; - next.replacePlate = ''; - next.replaceVin = ''; - next.replaceBrand = ''; - next.replaceModel = ''; - } - if (field === 'originalPlate') { - var list = contractDeliveredVehicles[prev.projectName] || []; - var v = list.find(function (x) { return x.plateNo === value; }); - next.originalVin = v ? v.vin : ''; - next.originalBrand = v ? v.brand : ''; - next.originalModel = v ? v.model : ''; - } - if (field === 'replacePlate') { - var v = preparedVehicleList.find(function (x) { return x.plateNo === value; }); - next.replaceVin = v ? v.vin : ''; - next.replaceBrand = v ? v.brand : ''; - next.replaceModel = v ? v.model : ''; - } - return next; + var getDeliveredVehicle = useCallback(function (plateNo) { + return deliveredVehicles.find(function (v) { return v.plateNo === plateNo; }) || null; + }, []); + + var getUsedPlates = useCallback(function (pairsList, field, exceptPairId) { + var set = {}; + pairsList.forEach(function (p) { + if (p.id === exceptPairId) return; + if (p[field]) set[p[field]] = true; }); - }; + return set; + }, []); - var handleSubmit = function () { - if (window.__replaceCarSubmit) { - window.__replaceCarSubmit(data); - } else { - antd.message.success('替换车申请已提交审核'); - } - }; + var selectedOriginalPlates = useMemo(function () { + return pairs.map(function (p) { return p.originalPlate; }).filter(Boolean); + }, [pairs]); - var handleSave = function () { - if (window.__replaceCarSave) { - window.__replaceCarSave(data); - } else { - antd.message.success('保存成功(原型)'); - } - }; + var multiOldPlateOptions = useMemo(function () { + var lockedContractId = projectInfo.contractId; + return deliveredVehicles + .filter(function (v) { + if (v.vehicleStatus !== '已交车' || !contractById[v.contractId]) return false; + if (lockedContractId && v.contractId !== lockedContractId) return false; + return true; + }) + .map(function (v) { return { value: v.plateNo, label: v.plateNo }; }); + }, [projectInfo.contractId, contractById]); - var handleCancel = function () { - if (edited[0]) { - antd.Modal.confirm({ - title: '确认取消', - content: '取消将会丢失所有已填写内容,是否确认?', - okText: '确认', - cancelText: '取消', - onOk: function () { - if (window.__replaceCarBack) { - window.__replaceCarBack(); - } else { - antd.message.info('返回替换车管理列表(原型)'); - } - } + var getNewOptionsForPair = useCallback(function (pair) { + if (!projectInfo.deliveryRegion) return []; + var used = getUsedPlates(pairs, 'replacePlate', pair.id); + var list = preparedVehiclesByRegion[projectInfo.deliveryRegion] || []; + return list + .filter(function (v) { + return v.vehicleStatus === '已备车' && !used[v.plateNo]; + }) + .map(function (v) { return { value: v.plateNo, label: v.plateNo }; }); + }, [pairs, projectInfo.deliveryRegion, getUsedPlates]); + + var updatePair = useCallback(function (pairId, patch) { + setEdited(true); + setPairs(function (prev) { + return prev.map(function (p) { + if (p.id !== pairId) return p; + var next = {}; + for (var k in p) next[k] = p[k]; + for (var pk in patch) next[pk] = patch[pk]; + return next; }); - } else { - if (window.__replaceCarBack) { - window.__replaceCarBack(); - } else { - antd.message.info('返回替换车管理列表(原型)'); - } + }); + }, []); + + var buildPairForPlate = useCallback(function (plateNo, existing) { + var vehicle = getDeliveredVehicle(plateNo); + if (!vehicle) return null; + var row = existing ? Object.assign({}, existing) : createEmptyPair(); + row.originalPlate = plateNo; + row.originalVin = vehicle.vin; + row.originalBrand = vehicle.brand; + row.originalModel = vehicle.model; + row.contractId = vehicle.contractId; + if (!existing) { + row.replacePlate = undefined; + row.replaceVin = ''; + row.replaceBrand = ''; + row.replaceModel = ''; } - }; + return row; + }, [getDeliveredVehicle]); - var layoutStyle = { padding: '16px 24px', background: '#f5f5f5', minHeight: '100vh' }; - var cardStyle = { marginBottom: 16 }; - var labelStyle = { marginBottom: 6, fontSize: 14, color: 'rgba(0,0,0,0.65)' }; - var formRowStyle = { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '16px 24px', marginBottom: 16 }; - var formItemStyle = { marginBottom: 12 }; + var onMultiOriginalPlateChange = useCallback(function (plateNos) { + var list = Array.isArray(plateNos) ? plateNos : []; + if (list.length === 0) { + setEdited(true); + setPairs([]); + return; + } + var anchorContractId = null; + var validPlates = []; + var rejected = false; + list.forEach(function (plate) { + var vehicle = getDeliveredVehicle(plate); + if (!vehicle || !contractById[vehicle.contractId]) return; + if (!anchorContractId) anchorContractId = vehicle.contractId; + if (vehicle.contractId !== anchorContractId) { + rejected = true; + return; + } + validPlates.push(plate); + }); + if (rejected) { + message.warning('多台替换须为同一客户、同一项目,已忽略不同项目的车辆'); + } + if (validPlates.length === 0) { + setPairs([]); + return; + } + setEdited(true); + setPairs(function (prev) { + var prevByPlate = {}; + prev.forEach(function (p) { + if (p.originalPlate) prevByPlate[p.originalPlate] = p; + }); + return validPlates.map(function (plate) { + return buildPairForPlate(plate, prevByPlate[plate]); + }); + }); + }, [getDeliveredVehicle, contractById, buildPairForPlate]); - var requirementContent = '替换车管理-编辑(2026年3月3日版本)\n一个「数字化资产ONEOS运管平台」中的「运维管理」「车辆业务」「替换车管理」「编辑」模块\n1.面包屑:\n#运维管理-车辆业务-替换车管理-编辑\n页面由选择项目、替换车详情、两个单独卡片组成;\n\n2.选择项目:\n#可通过选择进行中的车辆租赁合同,拉取租赁合同中对应车辆进行替换;\n2.1.项目名称:选择器,反写已新增完成的内容,可编辑,可选择所有进行中的合同,支持输入框中输入关键词进行模糊搜索,下拉显示对应项;\n2.2.合同编码:根据项目名称自动反查,不可编辑;\n2.3.客户名称:根据项目名称自动反查,不可编辑;\n2.4.对接人:根据项目名称自动反查,不可编辑;\n2.5.合同签订时间:根据项目名称自动反查,不可编辑;\n2.6.客户联系电话:根据项目名称自动反查,不可编辑;\n2.7.业务部门:根据项目名称自动反查,不可编辑;\n2.8.业务人员:根据项目名称自动反查,不可编辑;\n\n3.替换车详情:\n3.1.替换时间:日历选择器,精确至天,格式为:YYYY-MM-DD,反写新增时填写的日期;\n3.2.替换类型:选择器,反写已新增完成的内容,可编辑,分为「永久替换」「临时替换」两个选项;\n 3.2.1.类型为永久替换时,该申请通过审核后替换车进行交车(交车时间为流程结束当天),由运维手动将被替换车进行还车;\n 3.2.2.类型为临时替换时,该申请通过审核后替换车进行交车(交车时间为流程结束当天),被替换车不用还车,在被替换车重新交付客户时,由运维手动将替换车进行还车;\n 重新生成交车任务时,交车地点会自动继承自合同,由对应区域运维人员才能操作;\n 交车任务完成后,所有涉及到被替换车辆显示(例如车辆租赁合同、租赁账单、提车应收款等功能)会替换为新替换车的对应信息,如果是临时替换,在新替换车完成还车后,对应车辆记录会恢复为原有车辆数据。如果是永久替换,则由运维自主进行被替换车辆还车;\n3.3.替换原因:选择器,反写已新增完成的内容,可编辑,分为「客户原因」「车辆原因」;\n3.4.替换原因说明:文本域,反写已新增完成的内容,可编辑,默认提示信息为:请说明替换原因;\n3.5.被替换车牌号:选择器,反写已新增完成的内容,可编辑,只能选择该租赁合同当前对应的已交车但未还车车牌号;\n3.6.被替换车识别代码:输入框(禁用),选择被替换车车牌号后自动反写该车识别代码;\n3.7.被替换车品牌:输入框(禁用),选择被替换车车牌号后自动反写该车品牌;\n3.8.被替换车型号:输入框(禁用),选择被替换车车牌号后自动反写该车型号;\n3.9.替换车车牌号:选择器,反写已新增完成的内容,可编辑,只能选择该人员权限下所有已备车车辆,在选择项目名称前,不能选择替换车车牌号;\n3.10.替换车识别代码:输入框(禁用),选择替换车车牌号后自动反写该车识别代码;\n3.11.替换车品牌:输入框(禁用),选择替换车车牌号后自动反写该车品牌;\n3.12.替换车型号:输入框(禁用),选择替换车车牌号后自动反写该车型号;\n\n下方为提交审核、保存、取消按钮;\n4.1.点击提交并审核,toast提示:替换车申请已提交审核;\n4.2.点击保存,会存储租赁订单已填写内容,不做必填项校验,同时显示在租赁合同列表中(待审批),该条数据只能保存人自己查看并编辑,其他人无法操作;\n4.3.点击取消,如当前页面有已编辑内容时,点击取消会进行二次提示,内容为:取消将会丢失所有已填写内容,是否确认?点击确认返回替换车管理列表页;\n'; + var onReplacePlateChange = useCallback(function (pairId, plateNo) { + if (!plateNo) { + updatePair(pairId, { + replacePlate: undefined, + replaceVin: '', + replaceBrand: '', + replaceModel: '' + }); + return; + } + var pair = pairs.find(function (p) { return p.id === pairId; }); + if (!pair || !pair.originalPlate) { + message.info('请先选择被替换车辆'); + return; + } + var list = preparedVehiclesByRegion[projectInfo.deliveryRegion] || []; + var vehicle = list.find(function (v) { return v.plateNo === plateNo; }); + if (!vehicle) return; + var used = getUsedPlates(pairs, 'replacePlate', pairId); + if (used[plateNo]) { + message.warning('该新车已在其他替换项中选择'); + return; + } + updatePair(pairId, { + replacePlate: plateNo, + replaceVin: vehicle.vin, + replaceBrand: vehicle.brand, + replaceModel: vehicle.model + }); + }, [updatePair, pairs, projectInfo.deliveryRegion, getUsedPlates]); - return React.createElement('div', { style: layoutStyle }, - React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 } }, + var handleSubmit = useCallback(function () { + if (!pairs.length || !projectInfo.contractId) { + message.warning('请选择被替换车辆并完善替换信息'); + return; + } + var incomplete = pairs.find(function (p) { + return !p.originalPlate || !p.replacePlate || !p.replaceType || !p.replaceReason; + }); + if (incomplete) { + message.warning('请完善每条替换的新车、替换类型与替换原因'); + return; + } + message.success('已提交 ' + pairs.length + ' 条替换车申请(原型)'); + }, [pairs, projectInfo.contractId]); + + var handleSave = useCallback(function () { + message.success('已保存,该条数据仅您可查看并编辑(原型)'); + }, []); + + var handleCancel = useCallback(function () { + if (editedState[0]) setCancelModalVisible(true); + else message.info('返回替换车管理列表(原型)'); + }, [editedState[0]]); + + var confirmCancel = useCallback(function () { + setCancelModalVisible(false); + message.info('已取消,返回替换车管理列表(原型)'); + }, []); + + var pageCss = + '.vr-add-page{font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif}' + + '.vr-add-page .vr-page-header{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:20px}' + + '.vr-add-page .vr-main-card{border-radius:16px;border:none;box-shadow:0 4px 24px -6px rgba(15,23,42,0.08),0 0 0 1px rgba(15,23,42,0.05)}' + + '.vr-add-page .vr-main-card>.ant-card-head{border-bottom:1px solid #f1f5f9;padding:16px 24px;min-height:auto}' + + '.vr-add-page .vr-main-card>.ant-card-head .ant-card-head-title{font-size:16px;font-weight:600;color:#0f172a;padding:0}' + + '.vr-add-page .vr-main-card>.ant-card-body{padding:20px 24px 24px}' + + '.vr-add-page .vr-pair-list{display:flex;flex-direction:column;gap:16px}' + + '.vr-add-page .vr-pair-card{border-radius:12px;border:1px solid #e2e8f0;background:linear-gradient(180deg,#fff 0%,#f8fafc 100%);overflow:hidden;transition:border-color .2s ease,box-shadow .2s ease}' + + '.vr-add-page .vr-pair-card:hover{border-color:#93c5fd;box-shadow:0 4px 16px -4px rgba(22,119,255,0.12)}' + + '.vr-add-page .vr-pair-card__head{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 16px;background:#f0f9ff;border-bottom:1px solid #e0f2fe}' + + '.vr-add-page .vr-pair-card__title{display:flex;align-items:center;gap:8px;font-size:14px;font-weight:600;color:#0f172a}' + + '.vr-add-page .vr-pair-card__index{display:inline-flex;align-items:center;justify-content:center;min-width:24px;height:24px;padding:0 8px;border-radius:6px;background:#1677ff;color:#fff;font-size:12px;font-weight:700}' + + '.vr-add-page .vr-pair-card__body{padding:16px}' + + '.vr-add-page .vr-block{margin-bottom:14px}' + + '.vr-add-page .vr-block:last-child{margin-bottom:0}' + + '.vr-add-page .vr-block-label{font-size:12px;font-weight:600;color:#475569;margin-bottom:10px;letter-spacing:.02em}' + + '.vr-add-page .vr-block-label--old{color:#b45309}' + + '.vr-add-page .vr-block-label--new{color:#047857}' + + '.vr-add-page .vr-form-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px 16px}' + + '.vr-add-page .vr-form-grid--reason .vr-field:last-child{grid-column:1/-1}' + + '@media(max-width:900px){.vr-add-page .vr-form-grid{grid-template-columns:1fr}}' + + '.vr-add-page .vr-field{display:flex;flex-direction:column;gap:6px;min-width:0}' + + '.vr-add-page .vr-field__label{font-size:13px;font-weight:500;color:#334155;line-height:1.4}' + + '.vr-add-page .vr-field__label .vr-req{color:#ef4444;margin-right:2px}' + + '.vr-add-page .vr-swap-divider{display:flex;align-items:center;gap:12px;margin:14px 0;color:#94a3b8;font-size:12px;font-weight:500}' + + '.vr-add-page .vr-swap-divider::before,.vr-add-page .vr-swap-divider::after{content:"";flex:1;height:1px;background:linear-gradient(90deg,transparent,#cbd5e1,transparent)}' + + '.vr-add-page .vr-swap-divider__icon{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:50%;background:#eff6ff;color:#1677ff;font-size:14px;flex-shrink:0}' + + '.vr-add-page .vr-multi-pick{margin-bottom:20px;padding:16px 18px;border-radius:12px;background:#fff;border:1px solid #e2e8f0;box-shadow:0 1px 2px rgba(15,23,42,0.04)}' + + '.vr-add-page .vr-multi-pick__title{font-size:14px;font-weight:600;color:#0f172a;margin-bottom:4px}' + + '.vr-add-page .vr-multi-pick__hint{font-size:12px;color:#64748b;margin-bottom:12px;line-height:1.5}' + + '.vr-add-page .vr-vehicle-summary{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px 16px;padding:12px 14px;margin-bottom:14px;border-radius:8px;background:#fffbeb;border:1px solid #fde68a}' + + '@media(max-width:900px){.vr-add-page .vr-vehicle-summary{grid-template-columns:1fr}}' + + '.vr-add-page .vr-pair-list-empty{padding:32px 16px;text-align:center;color:#94a3b8;font-size:13px}' + + '.vr-add-page .vr-project-panel{margin-top:20px;padding:16px 18px;border-radius:12px;background:#f8fafc;border:1px solid #e2e8f0}' + + '.vr-add-page .vr-project-panel__head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:14px}' + + '.vr-add-page .vr-project-panel__title{font-size:14px;font-weight:600;color:#0f172a}' + + '.vr-add-page .vr-project-panel__hint{font-size:12px;color:#64748b}' + + '.vr-add-page .vr-project-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px 20px}' + + '@media(max-width:900px){.vr-add-page .vr-project-grid{grid-template-columns:1fr}}' + + '.vr-add-page .vr-readonly{display:flex;flex-direction:column;gap:4px;min-width:0}' + + '.vr-add-page .vr-readonly__label{font-size:12px;color:#64748b;font-weight:500}' + + '.vr-add-page .vr-readonly__value{font-size:14px;color:#0f172a;font-weight:500;word-break:break-all}' + + '.vr-add-page .vr-readonly__value--muted{color:#94a3b8;font-weight:400}' + + '.vr-add-page .vr-footer{display:flex;flex-wrap:wrap;gap:10px;margin-top:24px;padding-top:20px;border-top:1px solid #f1f5f9}' + + '.vr-add-page .vr-remove-btn{color:#64748b!important}' + + '.vr-add-page .vr-remove-btn:hover{color:#ef4444!important}' + + '@media(prefers-reduced-motion:reduce){.vr-add-page .vr-pair-card{transition:none}}' + + '.vr-req-doc{padding:4px 2px 8px}' + + '.vr-req-doc__meta{font-size:12px;color:#64748b;line-height:1.6;margin-bottom:16px;padding-bottom:12px;border-bottom:1px solid #f1f5f9}' + + '.vr-req-doc__section{margin-bottom:20px}' + + '.vr-req-doc__section:last-child{margin-bottom:0}' + + '.vr-req-doc__title{font-size:15px;font-weight:600;color:#0f172a;margin:0 0 10px;line-height:1.4}' + + '.vr-req-doc__line{font-size:13px;color:#475569;line-height:1.75;margin:0 0 6px;padding-left:0}' + + '.vr-req-doc__line--sub{padding-left:14px;color:#64748b}' + + '.vr-req-doc__line:last-child{margin-bottom:0}' + + '.vr-req-doc__tag{display:inline-block;margin:0 4px 4px 0;padding:0 6px;font-size:12px;line-height:20px;border-radius:4px;background:#f1f5f9;color:#334155}'; + + function specSection(title, lines) { + return React.createElement( + 'section', + { className: 'vr-req-doc__section' }, + React.createElement('h3', { className: 'vr-req-doc__title' }, title), + (lines || []).map(function (text, i) { + var isSub = typeof text === 'string' && (text.indexOf(' ') === 0 || /^\d+\.\d+/.test(text)); + return React.createElement( + 'p', + { + key: i, + className: 'vr-req-doc__line' + (isSub ? ' vr-req-doc__line--sub' : '') + }, + text + ); + }) + ); + } + + function renderRequirementDoc() { + return React.createElement( + 'div', + { className: 'vr-req-doc' }, + React.createElement( + 'div', + { className: 'vr-req-doc__meta' }, + '数字化资产 ONEOS 运管平台 · 运维管理 · 车辆业务 · 替换车管理 · 编辑' + ), + specSection('1. 页面定位', [ + '用于编辑未提交、审批驳回或撤回状态的替换车申请,交互与新增页一致。', + '支持多辆车替换明细编辑,须为同一客户、同一项目;项目信息在页面底部统一展示一份。' + ]), + specSection('2. 导航与入口', [ + '面包屑:运维管理 / 车辆业务 / 替换车管理 / 编辑。', + '右上角「查看需求说明」:打开本文档。', + '主卡片标题:编辑替换车;已选被替换车时展示「N 辆车」数量标签。' + ]), + specSection('3. 被替换车辆(多选)', [ + '3.1 车牌号:必填,多选下拉,支持输入关键词模糊搜索;占位「请输入或选择车牌号,可多选」。', + '3.2 可选范围:合同状态为「合同进行中」的合同下,车辆状态为「已交车」的全部车辆。', + '3.3 交互:每选中一辆自动生成一条「车辆替换」明细卡片;取消勾选则移除对应卡片。', + '3.4 约束:多选车辆须属于同一合同(同一客户、同一项目);若混入其他项目车辆,提示「多台替换须为同一客户、同一项目,已忽略不同项目的车辆」,且仅保留同项目车辆。', + '3.5 锁定:首辆车选定后,后续多选仅展示同一合同下的已交车车辆。', + '3.6 未选车时,下方展示空状态:「请在上方选择被替换车辆车牌号,将自动生成替换明细」。' + ]), + specSection('4. 车辆替换明细(每条被替换车一张卡片)', [ + '4.1 卡片标题:序号 +「车辆替换」+ 被替换车牌号;若已选替换车,展示「→ 替换车牌号」。', + '4.2 被替换车辆信息(只读):车牌号、品牌、型号;品牌/型号占位「选择车辆后自动显示」。', + '4.3 替换说明', + ' 4.3.1 替换类型:必填,单选——永久替换、临时替换。', + ' 4.3.2 替换原因:必填,单选——客户原因、车辆原因。', + ' 4.3.3 替换原因说明:选填,多行文本,最多 500 字,占位「请说明替换原因」,显示字数统计。', + '4.4 替换车辆', + ' 4.4.1 新车:必填,单选下拉,支持搜索;未选被替换车时禁用,占位「请先选择被替换车辆」。', + ' 4.4.2 已选被替换车后,占位展示交车区域,如「交车区域:浙江省-嘉兴市」。', + ' 4.4.3 可选范围:与被替换车所属合同「交车区域」停车场内,车辆状态为「已备车」的车辆。', + ' 4.4.4 同一申请内,各明细的替换车车牌号不可重复;重复时提示「该新车已在其他替换项中选择」。', + ' 4.4.5 品牌、型号:选择新车后自动反显,禁用编辑,占位「选择车辆后自动显示」。', + '4.5 保留策略:取消多选某车牌时移除卡片;再次选中同一车牌时,若此前已填写替换说明/新车,尽量保留原填写内容。' + ]), + specSection('5. 项目信息(全单共用一份)', [ + '5.1 展示时机:至少选择一辆被替换车后,根据所属合同自动反显。', + '5.2 未选车时展示空状态:「选择被替换车辆后自动显示」。', + '5.3 字段(均不可编辑):客户名称、项目名称、项目类型(租赁 / 自营,标签展示)。' + ]), + specSection('6. 替换类型业务规则', [ + '6.1 永久替换:审批通过后替换车交车(交车时间为流程结束当天),运维手动将被替换车还车。', + '6.2 临时替换:审批通过后替换车交车;被替换车无需还车;被替换车重新交付客户后,运维手动将替换车还车。', + '6.3 交车任务继承合同交车地点,由对应区域运维人员操作。', + '6.4 交车完成后,租赁账单、提车应收等涉及被替换车的展示信息切换为替换车;临时替换在替换车还车后恢复原被替换车数据,永久替换由运维自主还车。' + ]), + specSection('7. 底部操作', [ + '7.1 提交审核:校验规则同新增页;通过后 Toast「已提交 N 条替换车申请」。', + '7.2 保存:不做必填校验,保存草稿,仅保存人可见可编辑(原型提示)。', + '7.3 取消:有编辑内容时二次确认后返回列表;无编辑内容直接返回。' + ]), + specSection('8. 与新增页差异', [ + '进入页面时反写已保存的申请数据(含多条替换明细)。', + '被替换车辆多选、项目信息、卡片结构及校验规则与新增页保持一致。' + ]), + specSection('9. 校验与提示汇总', [ + '请选择被替换车辆并完善替换信息', + '请完善每条替换的新车、替换类型与替换原因', + '多台替换须为同一客户、同一项目,已忽略不同项目的车辆', + '该新车已在其他替换项中选择', + '请先选择被替换车辆' + ]) + ); + } + + function renderField(label, required, node) { + return React.createElement( + 'div', + { className: 'vr-field' }, + React.createElement( + 'div', + { className: 'vr-field__label' }, + required ? React.createElement('span', { className: 'vr-req' }, '*') : null, + label + ), + node + ); + } + + function renderMultiPickSection() { + return React.createElement( + 'section', + { className: 'vr-multi-pick', 'aria-label': '选择被替换车辆' }, + React.createElement('div', { className: 'vr-multi-pick__title' }, '被替换车辆'), + React.createElement( + 'div', + { className: 'vr-multi-pick__hint' }, + '车牌号支持多选,每选中一辆将生成一条替换明细;须为同一客户、同一项目。' + ), + renderField( + '车牌号', + true, + React.createElement(Select, { + mode: 'multiple', + placeholder: '请输入或选择车牌号,可多选', + style: { width: '100%' }, + value: selectedOriginalPlates, + onChange: onMultiOriginalPlateChange, + allowClear: true, + showSearch: true, + options: multiOldPlateOptions, + filterOption: plateFilterOption, + optionFilterProp: 'label', + maxTagCount: 'responsive' + }) + ) + ); + } + + function renderPairCard(pair, index) { + var newOptions = getNewOptionsForPair(pair); + + return React.createElement( + 'article', + { key: pair.id, className: 'vr-pair-card', 'aria-label': '替换车辆第' + (index + 1) + '项' }, + React.createElement( + 'div', + { className: 'vr-pair-card__head' }, + React.createElement( + 'div', + { className: 'vr-pair-card__title' }, + React.createElement('span', { className: 'vr-pair-card__index' }, index + 1), + React.createElement('span', null, '车辆替换'), + pair.originalPlate + ? React.createElement(Tag, { style: { margin: 0 } }, pair.originalPlate) + : null, + pair.originalPlate && pair.replacePlate + ? React.createElement(Tag, { color: 'processing', style: { margin: 0 } }, '→ ' + pair.replacePlate) + : null + ) + ), + React.createElement( + 'div', + { className: 'vr-pair-card__body' }, + React.createElement( + 'div', + { className: 'vr-vehicle-summary' }, + renderField( + '车牌号', + false, + React.createElement(Input, { + value: pair.originalPlate || '', + disabled: true + }) + ), + renderField( + '品牌', + false, + React.createElement(Input, { + value: pair.originalBrand || '', + disabled: true, + placeholder: '选择车辆后自动显示' + }) + ), + renderField( + '型号', + false, + React.createElement(Input, { + value: pair.originalModel || '', + disabled: true, + placeholder: '选择车辆后自动显示' + }) + ) + ), + React.createElement( + 'div', + { className: 'vr-block' }, + React.createElement('div', { className: 'vr-block-label' }, '替换说明'), + React.createElement( + 'div', + { className: 'vr-form-grid vr-form-grid--reason' }, + renderField( + '替换类型', + true, + React.createElement(Select, { + placeholder: '请选择', + style: { width: '100%' }, + value: pair.replaceType, + onChange: function (v) { updatePair(pair.id, { replaceType: v }); }, + allowClear: true, + options: [ + { value: '永久替换', label: '永久替换' }, + { value: '临时替换', label: '临时替换' } + ] + }) + ), + renderField( + '替换原因', + true, + React.createElement(Select, { + placeholder: '请选择', + style: { width: '100%' }, + value: pair.replaceReason, + onChange: function (v) { updatePair(pair.id, { replaceReason: v }); }, + allowClear: true, + options: [ + { value: '客户原因', label: '客户原因' }, + { value: '车辆原因', label: '车辆原因' } + ] + }) + ), + renderField( + '替换原因说明', + false, + React.createElement(Input.TextArea, { + placeholder: '请说明替换原因', + value: pair.replaceReasonDesc || '', + onChange: function (e) { updatePair(pair.id, { replaceReasonDesc: e.target.value }); }, + rows: 2, + style: { width: '100%' }, + maxLength: 500, + showCount: true + }) + ) + ) + ), + React.createElement( + 'div', + { className: 'vr-swap-divider', role: 'presentation' }, + React.createElement('span', { className: 'vr-swap-divider__icon', 'aria-hidden': true }, '↓'), + React.createElement('span', null, '替换为') + ), + React.createElement( + 'div', + { className: 'vr-block' }, + React.createElement('div', { className: 'vr-block-label vr-block-label--new' }, '替换车辆'), + React.createElement( + 'div', + { className: 'vr-form-grid' }, + renderField( + '新车', + true, + React.createElement(Select, { + placeholder: projectInfo.deliveryRegion + ? '交车区域:' + projectInfo.deliveryRegion + : '请先选择被替换车辆', + style: { width: '100%' }, + value: pair.replacePlate, + onChange: function (v) { onReplacePlateChange(pair.id, v); }, + allowClear: true, + showSearch: true, + options: newOptions, + filterOption: plateFilterOption, + disabled: !pair.originalPlate, + optionFilterProp: 'label' + }) + ), + renderField( + '品牌', + false, + React.createElement(Input, { + value: pair.replaceBrand || '', + disabled: true, + placeholder: '选择车辆后自动显示' + }) + ), + renderField( + '型号', + false, + React.createElement(Input, { + value: pair.replaceModel || '', + disabled: true, + placeholder: '选择车辆后自动显示' + }) + ) + ) + ) + ) + ); + } + + function renderProjectPanel() { + var hasProject = !!projectInfo.contractId; + return React.createElement( + 'section', + { className: 'vr-project-panel', 'aria-label': '项目信息' }, + React.createElement( + 'div', + { className: 'vr-project-panel__head' }, + React.createElement('div', { className: 'vr-project-panel__title' }, '项目信息') + ), + hasProject + ? React.createElement( + 'div', + { className: 'vr-project-grid' }, + React.createElement( + 'div', + { className: 'vr-readonly' }, + React.createElement('span', { className: 'vr-readonly__label' }, '客户名称'), + React.createElement('span', { className: 'vr-readonly__value' }, projectInfo.customerName) + ), + React.createElement( + 'div', + { className: 'vr-readonly' }, + React.createElement('span', { className: 'vr-readonly__label' }, '项目名称'), + React.createElement('span', { className: 'vr-readonly__value' }, projectInfo.projectName) + ), + React.createElement( + 'div', + { className: 'vr-readonly' }, + React.createElement('span', { className: 'vr-readonly__label' }, '项目类型'), + React.createElement( + 'span', + { className: 'vr-readonly__value' }, + React.createElement( + Tag, + { color: projectInfo.projectType === '自营' ? 'purple' : 'blue', style: { margin: 0 } }, + projectInfo.projectType + ) + ) + ) + ) + : React.createElement(Empty, { + image: Empty.PRESENTED_IMAGE_SIMPLE, + description: '选择被替换车辆后自动显示' + }) + ); + } + + return React.createElement( + 'div', + { className: 'vr-add-page', style: { padding: '20px 24px 32px', minHeight: '100vh', background: 'linear-gradient(165deg,#eef4ff 0%,#f5f7fa 42%,#f0f2f5 100%)' } }, + React.createElement('style', null, pageCss), + React.createElement( + 'header', + { className: 'vr-page-header' }, React.createElement(Breadcrumb, { items: [ { title: '运维管理' }, @@ -176,159 +764,61 @@ const Component = function () { { title: '编辑' } ] }), - React.createElement(Button, { type: 'link', style: { padding: 0 }, onClick: function () { setRequirementModalVisible(true); } }, '查看需求说明') - ), - React.createElement(Card, { title: '选择项目', style: cardStyle }, - React.createElement('div', { style: formRowStyle }, - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '项目名称'), - React.createElement(Select, { - style: { width: '100%' }, - placeholder: '请选择或输入关键词搜索', - value: data.projectName || undefined, - onChange: function (v) { updateDetail('projectName', v || ''); }, - showSearch: true, - allowClear: true, - filterOption: function (input, opt) { return opt && opt.children && String(opt.children).toLowerCase().indexOf((input || '').toLowerCase()) >= 0; } - }, contractList.map(function (c) { return React.createElement(Option, { key: c.projectName, value: c.projectName }, c.projectName); })) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '合同编码'), - React.createElement(Input, { value: data.contractCode || '', disabled: true }) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '客户名称'), - React.createElement(Input, { value: data.customerName || '', disabled: true }) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '对接人'), - React.createElement(Input, { value: data.contactPerson || '', disabled: true }) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '合同签订时间'), - React.createElement(Input, { value: data.signDate || '', disabled: true }) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '客户联系电话'), - React.createElement(Input, { value: data.contactPhone || '', disabled: true }) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '业务部门'), - React.createElement(Input, { value: data.businessDept || '', disabled: true }) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '业务人员'), - React.createElement(Input, { value: data.businessPerson || '', disabled: true }) - ) + React.createElement( + Button, + { type: 'link', style: { padding: 0, flexShrink: 0 }, onClick: function () { setRequirementModalVisible(true); } }, + '查看需求说明' ) ), - React.createElement(Card, { title: '替换车详情', style: cardStyle }, - React.createElement('div', { style: formRowStyle }, - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '替换时间'), - React.createElement(DatePicker, { - style: { width: '100%' }, - format: 'YYYY-MM-DD', - value: data.replaceDate ? (typeof data.replaceDate === 'string' ? window.dayjs && window.dayjs(data.replaceDate) : data.replaceDate) : null, - onChange: function (date, dateStr) { updateDetail('replaceDate', dateStr || ''); }, - placeholder: '请选择日期' - }) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '替换类型'), - React.createElement(Select, { - style: { width: '100%' }, - value: data.replaceType || undefined, - onChange: function (v) { updateDetail('replaceType', v || ''); }, - placeholder: '请选择' - }, React.createElement(Option, { value: '永久替换' }, '永久替换'), React.createElement(Option, { value: '临时替换' }, '临时替换')) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '替换原因'), - React.createElement(Select, { - style: { width: '100%' }, - value: data.replaceReason || undefined, - onChange: function (v) { updateDetail('replaceReason', v || ''); }, - placeholder: '请选择' - }, React.createElement(Option, { value: '客户原因' }, '客户原因'), React.createElement(Option, { value: '车辆原因' }, '车辆原因')) - ), - React.createElement('div', { style: Object.assign({}, formItemStyle, { gridColumn: '1 / -1' }) }, - React.createElement('div', { style: labelStyle }, '替换原因说明'), - React.createElement(Input.TextArea, { - value: data.replaceReasonDesc || '', - onChange: function (e) { updateDetail('replaceReasonDesc', e.target.value); }, - rows: 3, - placeholder: '请说明替换原因', - style: { width: '100%' } - }) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '被替换车牌号'), - React.createElement(Select, { - style: { width: '100%' }, - placeholder: '请选择或搜索(仅显示该合同已交车未还车车辆)', - value: data.originalPlate || undefined, - onChange: function (v) { updateDetail('originalPlate', v || ''); }, - showSearch: true, - allowClear: true, - disabled: !data.projectName, - filterOption: function (input, opt) { return opt && opt.children && String(opt.children).toLowerCase().indexOf((input || '').toLowerCase()) >= 0; } - }, (contractDeliveredVehicles[data.projectName] || []).map(function (v) { return React.createElement(Option, { key: v.plateNo, value: v.plateNo }, v.plateNo); })) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '被替换车识别代码'), - React.createElement(Input, { value: data.originalVin || '', disabled: true }) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '被替换车品牌'), - React.createElement(Input, { value: data.originalBrand || '', disabled: true }) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '被替换车型号'), - React.createElement(Input, { value: data.originalModel || '', disabled: true }) - ), - React.createElement('div', { style: { gridColumn: '1 / -1', width: '100%' } }), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '替换车车牌号'), - React.createElement(Select, { - style: { width: '100%' }, - placeholder: data.projectName ? '请选择或搜索(仅显示已备车车辆)' : '请先选择项目名称', - value: data.replacePlate || undefined, - onChange: function (v) { updateDetail('replacePlate', v || ''); }, - showSearch: true, - allowClear: true, - disabled: !data.projectName, - filterOption: function (input, opt) { return opt && opt.children && String(opt.children).toLowerCase().indexOf((input || '').toLowerCase()) >= 0; } - }, preparedVehicleList.map(function (v) { return React.createElement(Option, { key: v.plateNo, value: v.plateNo }, v.plateNo); })) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '替换车识别代码'), - React.createElement(Input, { value: data.replaceVin || '', disabled: true }) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '替换车品牌'), - React.createElement(Input, { value: data.replaceBrand || '', disabled: true }) - ), - React.createElement('div', { style: formItemStyle }, - React.createElement('div', { style: labelStyle }, '替换车型号'), - React.createElement(Input, { value: data.replaceModel || '', disabled: true }) + React.createElement( + Card, + { + className: 'vr-main-card', + title: React.createElement( + 'span', + null, + '编辑替换车 ', + pairs.length > 0 + ? React.createElement(Tag, { style: { marginLeft: 8, fontWeight: 400 } }, pairs.length + ' 辆车') + : null ) - ), - React.createElement('div', { style: { display: 'flex', gap: 8, marginTop: 24 } }, - React.createElement(Button, { type: 'primary', onClick: handleSubmit }, '提交审核'), - React.createElement(Button, { onClick: handleSave }, '保存'), - React.createElement(Button, { onClick: handleCancel }, '取消') + }, + renderMultiPickSection(), + pairs.length > 0 + ? React.createElement( + 'div', + { className: 'vr-pair-list' }, + pairs.map(function (pair, index) { return renderPairCard(pair, index); }) + ) + : React.createElement( + 'div', + { className: 'vr-pair-list-empty' }, + '请在上方选择被替换车辆车牌号,将自动生成替换明细' + ), + renderProjectPanel(), + React.createElement( + 'div', + { className: 'vr-footer' }, + React.createElement(Button, { type: 'primary', size: 'large', onClick: handleSubmit }, '提交审核'), + React.createElement(Button, { size: 'large', onClick: handleSave }, '保存'), + React.createElement(Button, { size: 'large', onClick: handleCancel }, '取消') ) ), React.createElement(Modal, { - title: '需求说明', + title: '替换车管理 - 编辑 · 需求说明', open: requirementModalVisible[0], onCancel: function () { setRequirementModalVisible(false); }, - width: 720, - footer: React.createElement(Button, { onClick: function () { setRequirementModalVisible(false); } }, '关闭'), - bodyStyle: { maxHeight: '70vh', overflow: 'auto' } - }, React.createElement('div', { style: { padding: '8px 0' } }, - React.createElement('div', { style: { whiteSpace: 'pre-wrap', fontSize: 13, lineHeight: 1.6 } }, requirementContent)) - ) + width: 760, + footer: React.createElement(Button, { type: 'primary', onClick: function () { setRequirementModalVisible(false); } }, '关闭'), + bodyStyle: { maxHeight: '72vh', overflow: 'auto', paddingTop: 8 } + }, renderRequirementDoc()), + React.createElement(Modal, { + title: '取消将会丢失所有已填写内容,是否确认?', + open: cancelModalVisible[0], + onCancel: function () { setCancelModalVisible(false); }, + onOk: confirmCancel, + okText: '确认', + cancelText: '返回' + }) ); }; diff --git a/web端/运维管理/车辆业务/替换车管理.jsx b/web端/运维管理/车辆业务/替换车管理.jsx index eebd363..290c7de 100644 --- a/web端/运维管理/车辆业务/替换车管理.jsx +++ b/web端/运维管理/车辆业务/替换车管理.jsx @@ -17,6 +17,11 @@ const Component = function () { var Modal = antd.Modal; var Input = antd.Input; var message = antd.message; + var Tag = antd.Tag; + var Empty = antd.Empty; + var App = antd.App; + var Badge = antd.Badge; + var Popover = antd.Popover; var RangePicker = DatePicker.RangePicker; @@ -50,8 +55,29 @@ const Component = function () { var _withdrawModalRecord = useState(null); var _toPermanentModalVisible = useState(false); var _toPermanentModalRecord = useState(null); + var _deleteModalVisible = useState(false); + var _deleteModalRecord = useState(null); + var _deletedIds = useState([]); var _requirementModalVisible = useState(false); + function ensureReplaceDateTime(dateStr, fallbackTime) { + if (!dateStr) return ''; + if (dateStr.length > 10) return dateStr; + return dateStr + ' ' + (fallbackTime || '09:00:00'); + } + + function enrichListRow(row, timeSuffix) { + var next = Object.assign({}, row); + next.replaceDate = ensureReplaceDateTime(row.replaceDate, timeSuffix); + if (!row.currentApprover) { + if (row.approvalStatus === '审批中') next.currentApprover = '姚守涛'; + else if (row.approvalStatus === '待审批') next.currentApprover = '业务部主管'; + else if (row.approvalStatus === '审批驳回') next.currentApprover = '尚建华'; + else next.currentApprover = '—'; + } + return next; + } + var replaceTypeOptions = [ { value: '永久替换', label: '永久替换' }, { value: '临时替换', label: '临时替换' } @@ -89,12 +115,12 @@ const Component = function () { ]; // 进行中:未结束、暂存,审批状态为 待审批、审批中、审批驳回、未提交、撤回 - var ongoingList = [ - { id: 'o1', replaceDate: '2025-03-05', replaceType: '临时替换', projectName: '嘉兴氢能示范项目', approvalStatus: '待审批', originalPlate: '浙A12345', originalBrand: '东风', originalModel: 'DFH1180', replacePlate: '浙A67890', replaceBrand: '福田', replaceModel: 'BJ1180', replaceReason: '车辆原因', replaceReasonDesc: '原车维修', creator: '张三', createTime: '2025-03-01 10:00' }, - { id: 'o2', replaceDate: '2025-03-06', replaceType: '永久替换', projectName: '上海物流租赁项目', approvalStatus: '审批中', originalPlate: '浙B11111', originalBrand: '江淮', originalModel: 'HFC1180', replacePlate: '浙B22222', replaceBrand: '重汽', replaceModel: 'ZZ1180', replaceReason: '客户原因', replaceReasonDesc: '客户要求换型', creator: '李四', createTime: '2025-03-01 14:30' }, - { id: 'o3', replaceDate: '2025-03-07', replaceType: '临时替换', projectName: '杭州城配租赁项目', approvalStatus: '审批驳回', originalPlate: '浙C33333', originalBrand: '东风', originalModel: 'DFH1190', replacePlate: '浙C44444', replaceBrand: '福田', replaceModel: 'BJ1190', replaceReason: '车辆原因', replaceReasonDesc: '事故替换', creator: '王五', createTime: '2025-03-02 09:15' }, - { id: 'o4', replaceDate: '2025-03-08', replaceType: '永久替换', projectName: '嘉兴氢能示范项目', approvalStatus: '未提交', originalPlate: '浙A55555', originalBrand: '重汽', originalModel: 'ZZ1160', replacePlate: '浙A66666', replaceBrand: '江淮', replaceModel: 'HFC1190', replaceReason: '车辆原因', replaceReasonDesc: '保养替换', creator: '赵六', createTime: '2025-03-02 16:00' }, - { id: 'o5', replaceDate: '2025-03-09', replaceType: '临时替换', projectName: '上海物流租赁项目', approvalStatus: '撤回', originalPlate: '浙F77777', originalBrand: '福田', originalModel: 'BJ1180', replacePlate: '浙F88888', replaceBrand: '东风', replaceModel: 'DFH1180', replaceReason: '客户原因', replaceReasonDesc: '年检替换', creator: '张三', createTime: '2025-03-03 11:20' }, + var ongoingListRaw = [ + { id: 'o1', replaceDate: '2025-03-05', replaceType: '临时替换', projectName: '嘉兴氢能示范项目', approvalStatus: '待审批', currentApprover: '业务部主管', originalPlate: '浙A12345', originalBrand: '东风', originalModel: 'DFH1180', replacePlate: '浙A67890', replaceBrand: '福田', replaceModel: 'BJ1180', replaceReason: '车辆原因', replaceReasonDesc: '原车维修', creator: '张三', createTime: '2025-03-01 10:00:00' }, + { id: 'o2', replaceDate: '2025-03-06', replaceType: '永久替换', projectName: '上海物流租赁项目', approvalStatus: '审批中', currentApprover: '姚守涛', originalPlate: '浙B11111', originalBrand: '江淮', originalModel: 'HFC1180', replacePlate: '浙B22222', replaceBrand: '重汽', replaceModel: 'ZZ1180', replaceReason: '客户原因', replaceReasonDesc: '客户要求换型', creator: '李四', createTime: '2025-03-01 14:30:00' }, + { id: 'o3', replaceDate: '2025-03-07', replaceType: '临时替换', projectName: '杭州城配租赁项目', approvalStatus: '审批驳回', currentApprover: '尚建华', originalPlate: '浙C33333', originalBrand: '东风', originalModel: 'DFH1190', replacePlate: '浙C44444', replaceBrand: '福田', replaceModel: 'BJ1190', replaceReason: '车辆原因', replaceReasonDesc: '事故替换', creator: '王五', createTime: '2025-03-02 09:15:00' }, + { id: 'o4', replaceDate: '2025-03-08', replaceType: '永久替换', projectName: '嘉兴氢能示范项目', approvalStatus: '未提交', currentApprover: '—', originalPlate: '浙A55555', originalBrand: '重汽', originalModel: 'ZZ1160', replacePlate: '浙A66666', replaceBrand: '江淮', replaceModel: 'HFC1190', replaceReason: '车辆原因', replaceReasonDesc: '保养替换', creator: '赵六', createTime: '2025-03-02 16:00:00' }, + { id: 'o5', replaceDate: '2025-03-09', replaceType: '临时替换', projectName: '上海物流租赁项目', approvalStatus: '撤回', currentApprover: '—', originalPlate: '浙F77777', originalBrand: '福田', originalModel: 'BJ1180', replacePlate: '浙F88888', replaceBrand: '东风', replaceModel: 'DFH1180', replaceReason: '客户原因', replaceReasonDesc: '年检替换', creator: '张三', createTime: '2025-03-03 11:20:00' }, { id: 'o6', replaceDate: '2025-03-10', replaceType: '永久替换', projectName: '杭州城配租赁项目', approvalStatus: '待审批', originalPlate: '浙A11201', originalBrand: '江淮', originalModel: 'HFC1160', replacePlate: '浙A11202', replaceBrand: '东风', replaceModel: 'DFH1160', replaceReason: '车辆原因', replaceReasonDesc: '发动机故障', creator: '李四', createTime: '2025-03-04 08:45' }, { id: 'o7', replaceDate: '2025-03-11', replaceType: '临时替换', projectName: '嘉兴氢能示范项目', approvalStatus: '审批中', originalPlate: '浙B22301', originalBrand: '重汽', originalModel: 'ZZ1160', replacePlate: '浙B22302', replaceBrand: '福田', replaceModel: 'BJ1160', replaceReason: '客户原因', replaceReasonDesc: '临时增运力', creator: '王五', createTime: '2025-03-04 13:00' }, { id: 'o8', replaceDate: '2025-03-12', replaceType: '永久替换', projectName: '上海物流租赁项目', approvalStatus: '审批驳回', originalPlate: '浙C33401', originalBrand: '东风', originalModel: 'DFH1190', replacePlate: '浙C33402', replaceBrand: '江淮', replaceModel: 'HFC1190', replaceReason: '车辆原因', replaceReasonDesc: '底盘大修', creator: '赵六', createTime: '2025-03-05 10:20' }, @@ -109,12 +135,16 @@ const Component = function () { { id: 'o17', replaceDate: '2025-03-21', replaceType: '临时替换', projectName: '上海物流租赁项目', approvalStatus: '审批中', originalPlate: '浙F30301', originalBrand: '福田', originalModel: 'BJ1190', replacePlate: '浙F30302', replaceBrand: '重汽', replaceModel: 'ZZ1190', replaceReason: '客户原因', replaceReasonDesc: '区域调配', creator: '张三', createTime: '2025-03-09 15:00' }, { id: 'o18', replaceDate: '2025-03-22', replaceType: '永久替换', projectName: '杭州城配租赁项目', approvalStatus: '审批驳回', originalPlate: '浙A40401', originalBrand: '江淮', originalModel: 'HFC1160', replacePlate: '浙A40402', replaceBrand: '东风', replaceModel: 'DFH1160', replaceReason: '车辆原因', replaceReasonDesc: '车身锈蚀', creator: '李四', createTime: '2025-03-10 09:45' }, { id: 'o19', replaceDate: '2025-03-23', replaceType: '临时替换', projectName: '嘉兴氢能示范项目', approvalStatus: '未提交', originalPlate: '浙B50501', originalBrand: '重汽', originalModel: 'ZZ1160', replacePlate: '浙B50502', replaceBrand: '福田', replaceModel: 'BJ1160', replaceReason: '客户原因', replaceReasonDesc: '试运行换车', creator: '王五', createTime: '2025-03-10 14:30' }, - { id: 'o20', replaceDate: '2025-03-24', replaceType: '永久替换', projectName: '上海物流租赁项目', approvalStatus: '撤回', originalPlate: '浙C60601', originalBrand: '东风', originalModel: 'DFH1190', replacePlate: '浙C60602', replaceBrand: '江淮', replaceModel: 'HFC1190', replaceReason: '车辆原因', replaceReasonDesc: '排放升级', creator: '赵六', createTime: '2025-03-11 11:15' } + { id: 'o20', replaceDate: '2025-03-24', replaceType: '永久替换', projectName: '上海物流租赁项目', approvalStatus: '撤回', currentApprover: '—', originalPlate: '浙C60601', originalBrand: '东风', originalModel: 'DFH1190', replacePlate: '浙C60602', replaceBrand: '江淮', replaceModel: 'HFC1190', replaceReason: '车辆原因', replaceReasonDesc: '排放升级', creator: '赵六', createTime: '2025-03-11 11:15:00' } ]; + function pad2(n) { return n < 10 ? '0' + n : String(n); } + var ongoingList = ongoingListRaw.map(function (r, i) { + return enrichListRow(r, pad2(8 + (i % 12)) + ':' + pad2((i * 7) % 60) + ':00'); + }); // 历史记录:审批完成,审批状态均为审批完成 - var historyList = [ - { id: 'h1', replaceDate: '2025-02-15', replaceType: '永久替换', projectName: '嘉兴氢能示范项目', approvalStatus: '审批完成', originalPlate: '浙A10001', originalBrand: '东风', originalModel: 'DFH1180', replacePlate: '浙A10002', replaceBrand: '福田', replaceModel: 'BJ1180', replaceReason: '车辆原因', replaceReasonDesc: '原车报废', creator: '张三', createTime: '2025-02-10 09:00' }, + var historyListRaw = [ + { id: 'h1', replaceDate: '2025-02-15', replaceType: '永久替换', projectName: '嘉兴氢能示范项目', approvalStatus: '审批完成', currentApprover: '—', originalPlate: '浙A10001', originalBrand: '东风', originalModel: 'DFH1180', replacePlate: '浙A10002', replaceBrand: '福田', replaceModel: 'BJ1180', replaceReason: '车辆原因', replaceReasonDesc: '原车报废', creator: '张三', createTime: '2025-02-10 09:00:00' }, { id: 'h2', replaceDate: '2025-02-14', replaceType: '临时替换', projectName: '上海物流租赁项目', approvalStatus: '审批完成', originalPlate: '浙B20001', originalBrand: '江淮', originalModel: 'HFC1180', replacePlate: '浙B20002', replaceBrand: '重汽', replaceModel: 'ZZ1180', replaceReason: '客户原因', replaceReasonDesc: '客户临时需求', creator: '李四', createTime: '2025-02-09 14:00' }, { id: 'h3', replaceDate: '2025-02-13', replaceType: '永久替换', projectName: '杭州城配租赁项目', approvalStatus: '审批完成', originalPlate: '浙C30001', originalBrand: '重汽', originalModel: 'ZZ1160', replacePlate: '浙C30002', replaceBrand: '东风', replaceModel: 'DFH1160', replaceReason: '车辆原因', replaceReasonDesc: '使用年限到期', creator: '王五', createTime: '2025-02-08 10:30' }, { id: 'h4', replaceDate: '2025-02-12', replaceType: '临时替换', projectName: '嘉兴氢能示范项目', approvalStatus: '审批完成', originalPlate: '浙A40001', originalBrand: '福田', originalModel: 'BJ1180', replacePlate: '浙A40002', replaceBrand: '江淮', replaceModel: 'HFC1180', replaceReason: '客户原因', replaceReasonDesc: '旺季加车', creator: '赵六', createTime: '2025-02-07 15:20' }, @@ -133,18 +163,24 @@ const Component = function () { { id: 'h17', replaceDate: '2025-01-30', replaceType: '永久替换', projectName: '上海物流租赁项目', approvalStatus: '审批完成', originalPlate: '浙B08001', originalBrand: '东风', originalModel: 'DFH1190', replacePlate: '浙B08002', replaceBrand: '重汽', replaceModel: 'ZZ1190', replaceReason: '车辆原因', replaceReasonDesc: '油耗过高', creator: '张三', createTime: '2025-01-25 14:30' }, { id: 'h18', replaceDate: '2025-01-29', replaceType: '临时替换', projectName: '杭州城配租赁项目', approvalStatus: '审批完成', originalPlate: '浙C09001', originalBrand: '江淮', originalModel: 'HFC1160', replacePlate: '浙C09002', replaceBrand: '福田', replaceModel: 'BJ1160', replaceReason: '客户原因', replaceReasonDesc: '活动保障', creator: '李四', createTime: '2025-01-24 10:20' }, { id: 'h19', replaceDate: '2025-01-28', replaceType: '永久替换', projectName: '嘉兴氢能示范项目', approvalStatus: '审批完成', originalPlate: '浙A10003', originalBrand: '重汽', originalModel: 'ZZ1180', replacePlate: '浙A10004', replaceBrand: '东风', replaceModel: 'DFH1180', replaceReason: '车辆原因', replaceReasonDesc: '配件停产', creator: '王五', createTime: '2025-01-23 16:45' }, - { id: 'h20', replaceDate: '2025-01-27', replaceType: '临时替换', projectName: '上海物流租赁项目', approvalStatus: '审批完成', originalPlate: '浙B11001', originalBrand: '福田', originalModel: 'BJ1190', replacePlate: '浙B11002', replaceBrand: '江淮', replaceModel: 'HFC1190', replaceReason: '客户原因', replaceReasonDesc: '新业务启动', creator: '赵六', createTime: '2025-01-22 08:30' } + { id: 'h20', replaceDate: '2025-01-27', replaceType: '临时替换', projectName: '上海物流租赁项目', approvalStatus: '审批完成', currentApprover: '—', originalPlate: '浙B11001', originalBrand: '福田', originalModel: 'BJ1190', replacePlate: '浙B11002', replaceBrand: '江淮', replaceModel: 'HFC1190', replaceReason: '客户原因', replaceReasonDesc: '新业务启动', creator: '赵六', createTime: '2025-01-22 08:30:00' } ]; + var historyList = historyListRaw.map(function (r, i) { + return enrichListRow(r, pad2(10 + (i % 10)) + ':' + pad2((i * 5) % 60) + ':00'); + }); + var deletedIds = _deletedIds[0]; var appliedFilter = _appliedFilter[0]; var filteredOngoing = useMemo(function () { var list = ongoingList.filter(function (r) { + if (deletedIds.indexOf(r.id) !== -1) return false; if (appliedFilter.replaceDateRange && appliedFilter.replaceDateRange.length === 2) { var start = appliedFilter.replaceDateRange[0] && appliedFilter.replaceDateRange[0].format ? appliedFilter.replaceDateRange[0].format('YYYY-MM-DD') : ''; var end = appliedFilter.replaceDateRange[1] && appliedFilter.replaceDateRange[1].format ? appliedFilter.replaceDateRange[1].format('YYYY-MM-DD') : ''; - if (start && (r.replaceDate || '') < start) return false; - if (end && (r.replaceDate || '') > end) return false; + var rd = (r.replaceDate || '').slice(0, 10); + if (start && rd < start) return false; + if (end && rd > end) return false; } if (appliedFilter.replaceType && r.replaceType !== appliedFilter.replaceType) return false; if (appliedFilter.projectName && r.projectName !== appliedFilter.projectName) return false; @@ -164,15 +200,17 @@ const Component = function () { return true; }); return list; - }, [appliedFilter]); + }, [appliedFilter, deletedIds]); var filteredHistory = useMemo(function () { var list = historyList.filter(function (r) { + if (deletedIds.indexOf(r.id) !== -1) return false; if (appliedFilter.replaceDateRange && appliedFilter.replaceDateRange.length === 2) { var start = appliedFilter.replaceDateRange[0] && appliedFilter.replaceDateRange[0].format ? appliedFilter.replaceDateRange[0].format('YYYY-MM-DD') : ''; var end = appliedFilter.replaceDateRange[1] && appliedFilter.replaceDateRange[1].format ? appliedFilter.replaceDateRange[1].format('YYYY-MM-DD') : ''; - if (start && (r.replaceDate || '') < start) return false; - if (end && (r.replaceDate || '') > end) return false; + var rd = (r.replaceDate || '').slice(0, 10); + if (start && rd < start) return false; + if (end && rd > end) return false; } if (appliedFilter.replaceType && r.replaceType !== appliedFilter.replaceType) return false; if (appliedFilter.projectName && r.projectName !== appliedFilter.projectName) return false; @@ -190,7 +228,7 @@ const Component = function () { return true; }); return list; - }, [appliedFilter]); + }, [appliedFilter, deletedIds]); var handleQuery = useCallback(function () { _appliedFilter[1]({ @@ -240,10 +278,163 @@ const Component = function () { _approvalStatus[1](v); }, []); - var filterLabelStyle = { marginBottom: 6, fontSize: 14, color: 'rgba(0,0,0,0.65)' }; - var filterItemStyle = { marginBottom: 12 }; + var filterLabelStyle = { marginBottom: 6, fontSize: 13, fontWeight: 500, color: '#475569', lineHeight: 1.4 }; + var filterItemStyle = { marginBottom: 0 }; var filterControlStyle = { width: '100%' }; + var pageStyles = + '.vr-list-page{font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif}' + + '.vr-list-page .vr-page-header{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:20px;flex-wrap:wrap}' + + '.vr-list-page .vr-filter-card,.vr-list-page .vr-list-card{border-radius:16px;border:none;box-shadow:0 4px 24px -6px rgba(15,23,42,0.08),0 0 0 1px rgba(15,23,42,0.05);margin-bottom:16px}' + + '.vr-list-page .vr-filter-card>.ant-card-head,.vr-list-page .vr-list-card>.ant-card-head{border-bottom:1px solid #f1f5f9;min-height:auto;padding:14px 20px}' + + '.vr-list-page .vr-filter-card>.ant-card-head .ant-card-head-title,.vr-list-page .vr-list-card>.ant-card-head .ant-card-head-title{font-size:15px;font-weight:600;color:#0f172a;padding:0}' + + '.vr-list-page .vr-filter-card>.ant-card-body{padding:16px 20px 20px}' + + '.vr-list-page .vr-list-card>.ant-card-body{padding:12px 16px 16px}' + + '.vr-list-page .vr-filter-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px 20px;align-items:start}' + + '@media(max-width:900px){.vr-list-page .vr-filter-grid{grid-template-columns:1fr}}' + + '.vr-list-page .vr-filter-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:16px;padding-top:16px;border-top:1px solid #f1f5f9}' + + '.vr-list-page .vr-swap-arrow{color:#94a3b8;font-size:12px;margin:0 4px}' + + '.vr-list-page .vr-reason-text{display:block;max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#475569}' + + '.vr-list-page .vr-list-table .ant-table-thead>tr>th{background:#f8fafc!important;color:#475569;font-weight:600;font-size:13px}' + + '.vr-list-page .vr-list-table .ant-table-tbody>tr:hover>td{background:#f0f9ff!important}' + + '.vr-list-page .vr-list-table .ant-table-thead th,.vr-list-page .vr-list-table .ant-table-tbody td{white-space:nowrap}' + + '.vr-list-page .vr-list-table .ant-table-tbody>tr.ant-table-row-selected>td{background:#eff6ff!important}' + + '.vr-list-page .vr-tabs .ant-tabs-nav{margin-bottom:0}' + + '.vr-list-page .vr-empty{padding:48px 16px}' + + '.vr-approval-flow-popover .ant-popover-inner{padding:14px 16px;border-radius:8px}' + + '.vr-approval-flow{width:300px;max-width:min(340px,92vw)}' + + '.vr-approval-flow__item{display:flex;gap:12px;position:relative;padding-bottom:22px}' + + '.vr-approval-flow__item:last-child{padding-bottom:0}' + + '.vr-approval-flow__item:not(:last-child) .vr-approval-flow__line{position:absolute;left:15px;top:34px;bottom:0;width:2px;background:#e5e7eb}' + + '.vr-approval-flow__avatar-wrap{position:relative;flex-shrink:0;z-index:1}' + + '.vr-approval-flow__avatar{width:32px;height:32px;border-radius:50%;background:#1677ff;color:#fff;font-size:12px;font-weight:600;display:inline-flex;align-items:center;justify-content:center;line-height:1}' + + '.vr-approval-flow__body{flex:1;min-width:0;padding-top:2px}' + + '.vr-approval-flow__head{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:4px}' + + '.vr-approval-flow__role{font-size:14px;font-weight:600;color:rgba(0,0,0,0.88);line-height:1.4}' + + '.vr-approval-flow__meta{font-size:12px;color:rgba(0,0,0,0.45);line-height:1.5}' + + '.vr-list-page .vr-approval-status-trigger{display:inline-flex;cursor:pointer;border-radius:4px;transition:opacity .15s ease}' + + '.vr-list-page .vr-approval-status-trigger:hover{opacity:.88}'; + + function formatFlowTime(timeStr) { + if (!timeStr) return '—'; + var s = String(timeStr).trim(); + if (s.length >= 19) return s.slice(0, 19); + if (/^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}$/.test(s)) return s + ':00'; + return s; + } + + function offsetFlowTime(timeStr, minutes) { + if (!timeStr) return '—'; + var s = String(timeStr).trim().replace(/-/g, '/'); + var d = new Date(s); + if (isNaN(d.getTime())) return formatFlowTime(timeStr); + d.setMinutes(d.getMinutes() + (minutes || 0)); + function p2(n) { return n < 10 ? '0' + n : '' + n; } + return d.getFullYear() + '-' + p2(d.getMonth() + 1) + '-' + p2(d.getDate()) + ' ' + p2(d.getHours()) + ':' + p2(d.getMinutes()) + ':' + p2(d.getSeconds()); + } + + function getApproverRoleTitle(approverName) { + if (approverName === '姚守涛') return '业务部主管'; + if (approverName === '尚建华') return '事业部主管'; + if (approverName === '业务部主管') return '业务部主管'; + return '运维主管'; + } + + function getApprovalFlowSteps(record) { + var creator = record.creator || '张三'; + var createTime = formatFlowTime(record.createTime); + var approver = record.currentApprover || '姚守涛'; + var approverPerson = approver === '业务部主管' ? '姚守涛' : approver; + var roleTitle = getApproverRoleTitle(approver); + var avatarFromName = function (name) { + var n = String(name || '').trim(); + if (!n || n === '—') return '用户'; + return n.length >= 2 ? n.slice(-2) : n; + }; + var steps = [ + { + role: roleTitle, + actionLabel: '审批中', + tagColor: 'processing', + person: approverPerson, + time: '待处理', + avatarText: avatarFromName(approverPerson) + }, + { + role: '发起审批', + actionLabel: '通过', + tagColor: 'success', + person: creator, + time: createTime, + avatarText: avatarFromName(creator) + } + ]; + if (roleTitle === '运维主管') { + steps.splice(1, 0, { + role: '业务部主管', + actionLabel: '通过', + tagColor: 'success', + person: '尚建华', + time: offsetFlowTime(record.createTime, 1), + avatarText: '建华' + }); + } + return steps; + } + + function renderApprovalFlowContent(record) { + var steps = getApprovalFlowSteps(record); + return React.createElement('div', { className: 'vr-approval-flow' }, + steps.map(function (step, idx) { + return React.createElement('div', { key: idx, className: 'vr-approval-flow__item' }, + React.createElement('div', { className: 'vr-approval-flow__avatar-wrap' }, + React.createElement('span', { className: 'vr-approval-flow__avatar', title: step.person }, step.avatarText), + idx < steps.length - 1 ? React.createElement('span', { className: 'vr-approval-flow__line' }) : null + ), + React.createElement('div', { className: 'vr-approval-flow__body' }, + React.createElement('div', { className: 'vr-approval-flow__head' }, + React.createElement('span', { className: 'vr-approval-flow__role' }, step.role), + React.createElement(Tag, { color: step.tagColor, style: { margin: 0, fontSize: 12, lineHeight: '20px' } }, step.actionLabel) + ), + React.createElement('div', { className: 'vr-approval-flow__meta' }, + step.person + ' ' + step.time + ) + ) + ); + }) + ); + } + + function renderApprovalStatusCell(status, record) { + var tag = renderApprovalTag(status); + if (status !== '审批中' || !record) return tag; + return React.createElement(Popover, { + content: renderApprovalFlowContent(record), + trigger: 'hover', + placement: 'rightTop', + overlayClassName: 'vr-approval-flow-popover', + mouseEnterDelay: 0.15, + mouseLeaveDelay: 0.12, + destroyTooltipOnHide: true + }, React.createElement('span', { className: 'vr-approval-status-trigger' }, tag)); + } + + function renderApprovalTag(status) { + var color = 'default'; + if (status === '待审批') color = 'processing'; + else if (status === '审批中') color = 'blue'; + else if (status === '审批驳回') color = 'error'; + else if (status === '未提交') color = 'default'; + else if (status === '撤回') color = 'warning'; + else if (status === '审批完成') color = 'success'; + return React.createElement(Tag, { color: color, style: { margin: 0, fontWeight: 500 } }, status || '—'); + } + + function renderReplaceTypeTag(type) { + var color = type === '永久替换' ? 'geekblue' : type === '临时替换' ? 'gold' : 'default'; + return React.createElement(Tag, { color: color, style: { margin: 0 } }, type || '—'); + } + function getOperationButtons(record, isHistory) { if (isHistory) { var viewBtn = React.createElement(Button, { key: 'view', type: 'link', size: 'small', onClick: function () { message.info('查看(跳转替换车管理-查看)'); } }, '查看'); @@ -258,6 +449,18 @@ const Component = function () { if (['未提交', '审批驳回', '撤回'].indexOf(status) !== -1) { items.push(React.createElement(Button, { key: 'edit', type: 'link', size: 'small', onClick: function () { message.info('编辑(跳转替换车管理-编辑)'); } }, '编辑')); } + if (['撤回', '审批驳回'].indexOf(status) !== -1) { + items.push(React.createElement(Button, { + key: 'delete', + type: 'link', + size: 'small', + danger: true, + onClick: function () { + _deleteModalRecord[1](record); + _deleteModalVisible[1](true); + } + }, '删除')); + } if (status === '审批中') { items.push(React.createElement(Button, { key: 'withdraw', type: 'link', size: 'small', danger: true, onClick: function () { _withdrawModalRecord[1](record); _withdrawModalVisible[1](true); } }, '撤回')); } @@ -265,21 +468,50 @@ const Component = function () { } var tableColumns = [ - { title: '替换日期', dataIndex: 'replaceDate', key: 'replaceDate', width: 110, fixed: 'left' }, - { title: '替换类型', dataIndex: 'replaceType', key: 'replaceType', width: 100, fixed: 'left' }, - { title: '项目名称', dataIndex: 'projectName', key: 'projectName', width: 140, fixed: 'left' }, - { title: '审批状态', dataIndex: 'approvalStatus', key: 'approvalStatus', width: 100 }, - { title: '被替换车车牌号', dataIndex: 'originalPlate', key: 'originalPlate', width: 120 }, - { title: '被替换车品牌', dataIndex: 'originalBrand', key: 'originalBrand', width: 100 }, - { title: '被替换车型号', dataIndex: 'originalModel', key: 'originalModel', width: 110 }, - { title: '替换车车牌号', dataIndex: 'replacePlate', key: 'replacePlate', width: 120 }, - { title: '替换车品牌', dataIndex: 'replaceBrand', key: 'replaceBrand', width: 100 }, - { title: '替换车型号', dataIndex: 'replaceModel', key: 'replaceModel', width: 110 }, + { title: '替换日期', dataIndex: 'replaceDate', key: 'replaceDate', width: 168, fixed: 'left' }, + { + title: '审批状态', + dataIndex: 'approvalStatus', + key: 'approvalStatus', + width: 108, + render: function (v, record) { return renderApprovalStatusCell(v, record); } + }, + { + title: '当前审批人', + dataIndex: 'currentApprover', + key: 'currentApprover', + width: 110, + render: function (v) { + return React.createElement('span', { style: { color: v && v !== '—' ? '#334155' : '#94a3b8' } }, v || '—'); + } + }, + { title: '被替换车(旧车)', dataIndex: 'originalPlate', key: 'originalPlate', width: 130 }, + { title: '品牌', dataIndex: 'originalBrand', key: 'originalBrand', width: 88 }, + { title: '型号', dataIndex: 'originalModel', key: 'originalModel', width: 100 }, + { title: '新车', dataIndex: 'replacePlate', key: 'replacePlate', width: 110 }, + { title: '品牌', dataIndex: 'replaceBrand', key: 'replaceBrandNew', width: 88 }, + { title: '型号', dataIndex: 'replaceModel', key: 'replaceModelNew', width: 100 }, + { + title: '替换类型', + dataIndex: 'replaceType', + key: 'replaceType', + width: 108, + render: function (v) { return renderReplaceTypeTag(v); } + }, { title: '替换原因', dataIndex: 'replaceReason', key: 'replaceReason', width: 100 }, - { title: '替换原因说明', dataIndex: 'replaceReasonDesc', key: 'replaceReasonDesc', width: 120, ellipsis: true }, + { + title: '替换原因说明', + dataIndex: 'replaceReasonDesc', + key: 'replaceReasonDesc', + width: 140, + ellipsis: true, + render: function (v) { + return React.createElement('span', { className: 'vr-reason-text', title: v || '' }, v || '—'); + } + }, { title: '创建人', dataIndex: 'creator', key: 'creator', width: 90 }, - { title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 150 }, - { title: '操作', key: 'action', width: 160, fixed: 'right', render: function (_, record) { return getOperationButtons(record, _activeTab[0] === 'history'); } } + { title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 168 }, + { title: '操作', key: 'action', width: 200, fixed: 'right', render: function (_, record) { return getOperationButtons(record, _activeTab[0] === 'history'); } } ]; var filterItems = [ @@ -296,10 +528,10 @@ const Component = function () { React.createElement('div', { style: filterLabelStyle }, '审批状态'), React.createElement(Select, { mode: 'multiple', placeholder: '请选择', style: filterControlStyle, value: _approvalStatus[0], onChange: handleApprovalStatusChange, options: approvalStatusOptions })), React.createElement('div', { key: 'originalPlate', style: filterItemStyle }, - React.createElement('div', { style: filterLabelStyle }, '被替换车车牌号'), + React.createElement('div', { style: filterLabelStyle }, '被替换车(旧车)'), React.createElement(Select, { placeholder: '请输入或选择车牌号', style: filterControlStyle, value: _originalPlate[0], onChange: function (v) { _originalPlate[1](v); }, allowClear: true, showSearch: true, options: plateOptions, filterOption: function (input, opt) { return (opt.label || '').toString().toLowerCase().indexOf((input || '').toLowerCase()) !== -1; } })), React.createElement('div', { key: 'replacePlate', style: filterItemStyle }, - React.createElement('div', { style: filterLabelStyle }, '替换车车牌号'), + React.createElement('div', { style: filterLabelStyle }, '新车'), React.createElement(Select, { placeholder: '请输入或选择车牌号', style: filterControlStyle, value: _replacePlate[0], onChange: function (v) { _replacePlate[1](v); }, allowClear: true, showSearch: true, options: plateOptions, filterOption: function (input, opt) { return (opt.label || '').toString().toLowerCase().indexOf((input || '').toLowerCase()) !== -1; } })), React.createElement('div', { key: 'replaceReason', style: filterItemStyle }, React.createElement('div', { style: filterLabelStyle }, '替换原因'), @@ -330,12 +562,49 @@ const Component = function () { return currentList.slice(start, start + pageSize); }, [currentList, page, pageSize]); + var listStats = useMemo(function () { + return { + ongoing: filteredOngoing.length, + history: filteredHistory.length, + selected: (_selectedRowKeys[0] || []).length + }; + }, [filteredOngoing.length, filteredHistory.length, _selectedRowKeys[0]]); + var rowSelection = { selectedRowKeys: _selectedRowKeys[0], onChange: function (keys) { _selectedRowKeys[1](keys); }, fixed: true }; + var tablePagination = { + current: page, + pageSize: pageSize, + total: currentList.length, + showSizeChanger: true, + showQuickJumper: true, + showTotal: function (t) { return '共 ' + t + ' 条'; }, + onChange: function (p, ps) { setPage(p); if (ps) setPageSize(ps); } + }; + + function renderTableBody() { + if (displayList.length === 0) { + return React.createElement(Empty, { + className: 'vr-empty', + image: Empty.PRESENTED_IMAGE_SIMPLE, + description: '暂无符合条件的替换车记录,请调整筛选条件后重试' + }); + } + return React.createElement(Table, { + rowKey: 'id', + rowSelection: rowSelection, + columns: tableColumns, + dataSource: displayList, + size: 'small', + scroll: { x: 1900 }, + pagination: tablePagination + }); + } + var requirementContent = `替换车管理(2026年3月3日版本) 一个「数字化资产ONEOS运管平台」中的「运维管理」「车辆业务」「替换车管理」模块 @@ -351,39 +620,27 @@ const Component = function () { 2.2.替换类型:选择器,分为永久替换、临时替换两种方式; 2.3.项目名称:选择器,支持输入框中输入关键内容进行搜索,下拉匹配相应项; 2.4.审批状态:选择器,分为全部、待审批、审批中、审批驳回、未提交、撤回; -2.5.被替换车车牌号:选择器,支持输入框中输入关键内容进行搜索,下拉匹配相应项; -2.6.替换车车牌号:选择器,支持输入框中输入关键内容进行搜索,下拉匹配相应项; +2.5.被替换车(旧车):选择器,支持输入框中输入关键内容进行搜索,下拉匹配相应项; +2.6.新车:选择器,支持输入框中输入关键内容进行搜索,下拉匹配相应项; 2.7.替换原因:选择器,分为全部、客户原因、车辆原因; 2.8.创建人:选择器,下拉选择所有创建人; 2.9.创建时间:日期选择器,支持单输入框内双日历选择开始-结束时间,默认提示文本为:请选择开始时间、请选择结束时间; 3.列表:列表右上角为新增、导出,首列为多选,支持多选后导出对应条目; -列表展示所有替换车记录,分为进行中、历史记录两个tab,字段依次为:替换日期、替换类型、项目名称、审批状态、被替换车车牌号、被替换车品牌、被替换车型号、替换车车牌号、替换车品牌、替换车型号、替换原因、替换原因说明、创建人、创建时间、操作; +列表展示所有替换车记录,分为进行中、历史记录两个tab,字段依次为:替换日期、审批状态、当前审批人、被替换车(旧车)、品牌、型号、新车、品牌、型号、替换类型、替换原因、替换原因说明、创建人、创建时间、操作; 3.1.进行中:显示替换车申请流程未结束、暂存的记录; - 3.1.1.替换日期:显示格式为:YYYY-MM-DD,显示替换车申请表单中设置的替换日期; - 3.1.2.替换类型:分为:临时替换、永久替换两种,根据替换车申请表单中设置的替换类型显示; - 3.1.3.项目名称:显示替换车申请表单中设置的项目名称; - 3.1.4.审批状态:显示替换车申请当前审批状态,分为待审批、审批中、审批驳回、未提交、撤回; - 3.1.4.1.待审批:发起人已提交,但还没有任何流程节点完成审批; - 3.1.4.2.审批中:发起人已提交,已有1个以上节点完成审批,但未完成最终节点审批; - 3.1.4.3.审批驳回:发起人已提交,任意流程节点驳回,该状态下操作列支持编辑和重新提交; - 3.1.4.4.未提交:发起人仅保存,但未提交审批; - 3.1.4.5.撤回:发起人主动撤回审批流程; - 3.1.5.被替换车车牌号:显示替换车申请表单中被替换车车牌号; - 3.1.6.被替换车品牌:显示替换车申请表单中被替换车品牌; - 3.1.7.被替换车型号:显示替换车申请表单中被替换车型号; - 3.1.8.替换车车牌号:显示替换车申请表单中替换车车牌号; - 3.1.9.替换车品牌:显示替换车申请表单中替换车品牌; - 3.1.10.替换车型号:显示替换车申请表单中替换车型号; - 3.1.11.替换原因:显示替换车申请表单中替换原因; - 3.1.12.替换原因说明:显示替换车申请表单中替换原因说明; - 3.1.13.创建人:显示替换车申请表单中创建人; - 3.1.14.创建时间:显示替换车申请表单中创建时间,显示格式为:YYYY-MM-DD HH:MM; - 3.1.15.操作:查看、编辑、撤回; - 3.1.15.1.查看:当「审批状态」为「待审批」「审批中」「审批驳回」「未提交」「撤回」时显示,点击跳转替换车管理-查看页面; - 3.1.15.2.编辑:当「审批状态」为「未提交」「审批驳回」「撤回」时显示,点击跳转替换车管理-编辑页面; - 3.1.15.3.撤回:当「审批状态」为「审批中」时显示,点击撤回合同时进行二次确认,提示语:是否确认撤回该替换车申请; + 3.1.1.替换日期:显示格式为:YYYY-MM-DD HH:MM:SS; + 3.1.2.审批状态:分为待审批、审批中、审批驳回、未提交、撤回; + 3.1.3.当前审批人:显示当前待审批节点审批人,未提交/撤回等为「—」; + 3.1.4.被替换车(旧车)、品牌、型号、新车、品牌、型号:展示申请表单车辆信息; + 3.1.5.替换类型:临时替换、永久替换; + 3.1.6.替换原因、替换原因说明、创建人、创建时间(YYYY-MM-DD HH:MM:SS); + 3.1.7.操作:查看、编辑、撤回、删除(逻辑删除); + 3.1.7.1.查看:审批状态为待审批/审批中/审批驳回/未提交/撤回时显示; + 3.1.7.2.编辑:审批状态为未提交/审批驳回/撤回时显示; + 3.1.7.3.撤回:审批状态为审批中时显示,二次确认; + 3.1.7.4.删除:审批状态为撤回/审批驳回时显示,逻辑删除,二次确认; 3.2.历史记录:显示替换车申请流程已结束的记录; 3.2.1.替换日期:显示格式为:YYYY-MM-DD,显示替换车申请表单中设置的替换日期; @@ -406,88 +663,99 @@ const Component = function () { 列表右下方为分页符。`; - var layoutStyle = { padding: '16px 24px', background: '#f5f5f5', minHeight: '100vh' }; - var reqTitleStyle = { fontSize: 18, fontWeight: 600, marginBottom: 16, color: 'rgba(0,0,0,0.85)' }; - var reqSectionStyle = { fontSize: 15, fontWeight: 600, marginTop: 16, marginBottom: 8, color: 'rgba(0,0,0,0.85)' }; - var reqItemStyle = { fontSize: 13, marginLeft: 32, marginTop: 4, marginBottom: 2, lineHeight: 1.6, color: 'rgba(0,0,0,0.75)' }; + var activeTab = _activeTab[0]; + var selectedCount = (_selectedRowKeys[0] || []).length; - return React.createElement('div', { style: layoutStyle }, - React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 } }, - React.createElement(Breadcrumb, { - items: [ - { title: '运维管理' }, - { title: '车辆业务' }, - { title: '替换车管理' } - ] - }), - React.createElement(Button, { type: 'link', style: { padding: 0 }, onClick: function () { _requirementModalVisible[1](true); } }, '查看需求说明') - ), - React.createElement(Card, { style: { marginBottom: 16 } }, - React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '16px 24px', alignItems: 'start' } }, filterNodes), - React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 16 } }, - React.createElement(Button, { onClick: handleReset }, '重置'), - React.createElement(Button, { type: 'primary', onClick: handleQuery }, '查询'), - React.createElement(Button, { type: 'link', size: 'small', onClick: function () { _filterExpanded[1](!_filterExpanded[0]); } }, _filterExpanded[0] ? '收起' : '展开') - ) - ), - React.createElement(Card, null, - React.createElement('div', { style: { marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' } }, - React.createElement(Tabs, { - activeKey: _activeTab[0], - onChange: function (k) { _activeTab[1](k); _selectedRowKeys[1]([]); setPage(1); }, + return React.createElement(App, null, + React.createElement('div', { className: 'vr-list-page', style: { minHeight: '100vh', padding: '20px 24px 32px', background: 'linear-gradient(180deg,#f8fafc 0%,#f1f5f9 100%)' } }, + React.createElement('style', null, pageStyles), + React.createElement('div', { className: 'vr-page-header' }, + React.createElement(Breadcrumb, { items: [ - { key: 'ongoing', label: '进行中' }, - { key: 'history', label: '历史记录' } + { title: '运维管理' }, + { title: '车辆业务' }, + { title: '替换车管理' } ] }), - React.createElement('div', { style: { display: 'flex', gap: 8 } }, - React.createElement(Button, { type: 'primary', onClick: function () { message.info('新增替换车申请(原型)'); } }, '新增'), - React.createElement(Button, { onClick: function () { message.info('导出选中记录(原型)'); } }, '导出') + React.createElement(Button, { type: 'link', style: { padding: 0, color: '#2563eb', fontWeight: 500 }, onClick: function () { _requirementModalVisible[1](true); } }, '查看需求说明') + ), + React.createElement(Card, { className: 'vr-filter-card', title: '筛选条件' }, + React.createElement('div', { className: 'vr-filter-grid' }, filterNodes), + React.createElement('div', { className: 'vr-filter-actions' }, + React.createElement(Button, { onClick: handleReset }, '重置'), + React.createElement(Button, { type: 'primary', onClick: handleQuery }, '查询'), + React.createElement(Button, { type: 'link', size: 'small', onClick: function () { _filterExpanded[1](!_filterExpanded[0]); } }, _filterExpanded[0] ? '收起' : '展开') ) ), - React.createElement(Table, { - rowKey: 'id', - rowSelection: rowSelection, - columns: tableColumns, - dataSource: displayList, - size: 'small', - scroll: { x: 1600 }, - pagination: { - current: page, - pageSize: pageSize, - total: currentList.length, - showSizeChanger: true, - showQuickJumper: true, - showTotal: function (t) { return '共 ' + t + ' 条'; }, - onChange: function (p, ps) { setPage(p); if (ps) setPageSize(ps); } - } - }) - ), - React.createElement(Modal, { - title: '是否确认撤回该替换车申请', - open: _withdrawModalVisible[0], - onCancel: function () { _withdrawModalVisible[1](false); _withdrawModalRecord[1](null); }, - onOk: function () { message.success('已撤回(原型)'); _withdrawModalVisible[1](false); _withdrawModalRecord[1](null); }, - okText: '确定', - cancelText: '取消' - }), - React.createElement(Modal, { - title: '是否确认转永久替换', - open: _toPermanentModalVisible[0], - onCancel: function () { _toPermanentModalVisible[1](false); _toPermanentModalRecord[1](null); }, - onOk: function () { message.success('已转为永久替换(原型)'); _toPermanentModalVisible[1](false); _toPermanentModalRecord[1](null); }, - okText: '提交', - cancelText: '取消' - }), - React.createElement(Modal, { - title: '需求说明', - open: _requirementModalVisible[0], - onCancel: function () { _requirementModalVisible[1](false); }, - width: 720, - footer: React.createElement(Button, { onClick: function () { _requirementModalVisible[1](false); } }, '关闭'), - bodyStyle: { maxHeight: '70vh', overflow: 'auto' } - }, React.createElement('div', { style: { padding: '8px 0' } }, - React.createElement('div', { style: { whiteSpace: 'pre-wrap', fontSize: 13, lineHeight: 1.6 } }, requirementContent)) + React.createElement(Card, { className: 'vr-list-card', title: '替换车列表' }, + React.createElement('div', { className: 'vr-list-table' }, + React.createElement(Tabs, { + className: 'vr-tabs', + activeKey: activeTab, + onChange: function (k) { _activeTab[1](k); _selectedRowKeys[1]([]); setPage(1); }, + tabBarExtraContent: React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' } }, + React.createElement(Button, { type: 'primary', onClick: function () { message.info('新增替换车申请(原型)'); } }, '新增'), + selectedCount > 0 + ? React.createElement(Badge, { count: selectedCount, size: 'small', offset: [-4, 4] }, + React.createElement(Button, { onClick: function () { message.info('导出选中 ' + selectedCount + ' 条(原型)'); } }, '导出') + ) + : React.createElement(Button, { onClick: function () { message.info('请先勾选需要导出的记录'); } }, '导出') + ), + destroyInactiveTabPane: true, + items: [ + { key: 'ongoing', label: '进行中 (' + listStats.ongoing + ')', children: activeTab === 'ongoing' ? renderTableBody() : null }, + { key: 'history', label: '历史记录 (' + listStats.history + ')', children: activeTab === 'history' ? renderTableBody() : null } + ] + }) + ) + ), + React.createElement(Modal, { + title: '是否确认撤回该替换车申请', + open: _withdrawModalVisible[0], + onCancel: function () { _withdrawModalVisible[1](false); _withdrawModalRecord[1](null); }, + onOk: function () { message.success('已撤回(原型)'); _withdrawModalVisible[1](false); _withdrawModalRecord[1](null); }, + okText: '确定', + cancelText: '取消' + }), + React.createElement(Modal, { + title: '是否确认转永久替换', + open: _toPermanentModalVisible[0], + onCancel: function () { _toPermanentModalVisible[1](false); _toPermanentModalRecord[1](null); }, + onOk: function () { message.success('已转为永久替换(原型)'); _toPermanentModalVisible[1](false); _toPermanentModalRecord[1](null); }, + okText: '提交', + cancelText: '取消' + }), + React.createElement(Modal, { + title: '是否确认逻辑删除该替换车申请?', + open: _deleteModalVisible[0], + onCancel: function () { _deleteModalVisible[1](false); _deleteModalRecord[1](null); }, + onOk: function () { + var rec = _deleteModalRecord[0]; + if (rec && rec.id) { + _deletedIds[1](function (prev) { + if (prev.indexOf(rec.id) !== -1) return prev; + return prev.concat(rec.id); + }); + _selectedRowKeys[1](function (prev) { return prev.filter(function (k) { return k !== rec.id; }); }); + } + message.success('已逻辑删除(原型)'); + _deleteModalVisible[1](false); + _deleteModalRecord[1](null); + }, + okText: '确定', + cancelText: '取消', + okButtonProps: { danger: true } + }), + React.createElement(Modal, { + title: '需求说明', + open: _requirementModalVisible[0], + onCancel: function () { _requirementModalVisible[1](false); }, + width: 720, + footer: React.createElement(Button, { onClick: function () { _requirementModalVisible[1](false); } }, '关闭'), + bodyStyle: { maxHeight: '70vh', overflow: 'auto' } + }, React.createElement('div', { style: { padding: '8px 0' } }, + React.createElement('div', { style: { whiteSpace: 'pre-wrap', fontSize: 13, lineHeight: 1.6, color: '#334155' } }, requirementContent)) + ) ) ); }; diff --git a/web端/运维管理/车辆业务/查看故障.jsx b/web端/运维管理/车辆业务/查看故障.jsx new file mode 100644 index 0000000..677eee3 --- /dev/null +++ b/web端/运维管理/车辆业务/查看故障.jsx @@ -0,0 +1,226 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// ONEOS-web - 运维管理 - 车辆业务 - 查看故障 + +const Component = function () { + var useState = React.useState; + + var antd = window.antd; + var Button = antd.Button; + var Input = antd.Input; + var Select = antd.Select; + var DatePicker = antd.DatePicker; + var Form = antd.Form; + var Row = antd.Row; + var Col = antd.Col; + var Breadcrumb = antd.Breadcrumb; + var Layout = antd.Layout; + var message = antd.message; + var Card = antd.Card; + var Upload = antd.Upload; + + var _form = Form.useForm(); + var faultForm = _form[0]; + + // 故障等级枚举 + var faultLevelOptions = [ + { label: '特急', value: '特急' }, + { label: '紧急', value: '紧急' }, + { label: '一般', value: '一般' }, + { label: '提示', value: '提示' } + ]; + + // 故障类型枚举 + var faultTypeOptions = [ + { label: '底盘故障', value: '底盘故障' }, + { label: '三电故障', value: '三电故障' }, + { label: '整车系统', value: '整车系统' }, + { label: '燃料电池系统故障', value: '燃料电池系统故障' }, + { label: '供氢系统故障', value: '供氢系统故障' }, + { label: '空调系统故障', value: '空调系统故障' }, + { label: '冷机故障', value: '冷机故障' }, + { label: '其他故障', value: '其他故障' } + ]; + + // 故障来源枚举 + var faultSourceOptions = [ + { label: '客户报告', value: '客户报告' }, + { label: '定期保养', value: '定期保养' }, + { label: '司机操作问题', value: '司机操作问题' } + ]; + + // 解决情况枚举 + var resolveStatusOptions = [ + { label: '未解决', value: '未解决' }, + { label: '临时排故', value: '临时排故' }, + { label: '已解决', value: '已解决' } + ]; + + var plateOptions = [ + { label: '沪A12345', value: '沪A12345', brand: '一汽解放', model: 'J6P', company: '上海羚牛', vin: 'LNW1234567890ABCD' }, + { label: '浙B88888', value: '浙B88888', brand: '东风商用车', model: '天龙', company: '浙江羚牛', vin: 'LNW0987654321EFGH' }, + { label: '苏C66666', value: '苏C66666', brand: '福田欧曼', model: 'EST', company: '苏州冷链速运有限公司', vin: 'LNW1357924680IJKL' }, + { label: '沪D99999', value: '沪D99999', brand: '陕汽重卡', model: '德龙', company: '上海城配物流有限公司', vin: 'LNW2468013579MNOP' } + ]; + + var handleBack = function () { + message.info('返回列表'); + }; + + // 模拟数据加载 + React.useEffect(function() { + var mockData = { key: '1', code: 'F-2024-001', plate: '沪A12345', brand: '一汽解放', model: 'J6P', company: '上海羚牛', type: '底盘故障', level: '紧急', source: '客户报告', status: '未解决', reportTime: '2024-05-10 08:30:00', lastOperator: '王婷婷', lastOperationTime: '2024-05-10 09:00:00', desc: '车辆重载下制动踏板偏软,制动距离明显变长' }; + if (mockData.reportTime) { + if (typeof window.dayjs === 'function') { + mockData.reportTime = window.dayjs(mockData.reportTime); + } else if (typeof window.moment === 'function') { + mockData.reportTime = window.moment(mockData.reportTime); + } + } + faultForm.setFieldsValue(mockData); + }, []); + + return React.createElement(Layout, { className: 'arco-theme-overrides', style: { minHeight: '100vh', background: '#f2f3f5', fontFamily: 'Inter, Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif' } }, + React.createElement('style', null, ` + .arco-theme-overrides .ant-btn { border-radius: 4px; } + .arco-theme-overrides .ant-btn-primary { background-color: #165dff; border-color: #165dff; } + .arco-theme-overrides .ant-btn-primary:hover { background-color: #4080ff; border-color: #4080ff; } + + .arco-grouped-form-page { display: flex; flex-direction: column; min-height: 100vh; } + .arco-grouped-form-page-content { flex: 1; padding: 16px 20px 24px; } + .arco-grouped-form-page .ant-card { margin-bottom: 16px; border-radius: 4px; border: none; box-shadow: 0 1px 2px rgba(0,0,0,0.06); } + .arco-grouped-form-page .ant-card-head { border-bottom: none; padding: 20px 24px 0; min-height: auto; } + .arco-grouped-form-page .ant-card-head-title { font-size: 16px; font-weight: 500; color: #1d2129; padding: 0; } + .arco-grouped-form-page .ant-card-body { padding: 24px; } + .arco-grouped-form-page .ant-form-vertical .ant-form-item-label { padding-bottom: 8px; height: auto; line-height: 1.5715; } + .arco-grouped-form-page .ant-form-item { margin-bottom: 24px; } + + .arco-grouped-form-page .ant-input, + .arco-grouped-form-page .ant-select-selector, + .arco-grouped-form-page .ant-picker, + .arco-grouped-form-page .ant-input-affix-wrapper { background-color: #f2f3f5; border: 1px solid #e5e6eb; border-radius: 2px; transition: all 0.1s cubic-bezier(0, 0, 1, 1); } + + .arco-grouped-form-page .ant-input[disabled], + .arco-grouped-form-page .ant-select-disabled .ant-select-selector, + .arco-grouped-form-page .ant-picker-disabled { color: #86909c; background-color: #f2f3f5; border-color: #e5e6eb; cursor: not-allowed; } + .arco-grouped-form-page .ant-input-affix-wrapper[disabled] { background-color: #f2f3f5; border-color: #e5e6eb; cursor: not-allowed; } + .arco-grouped-form-page .ant-input-affix-wrapper > input.ant-input { background-color: transparent; } + + .arco-grouped-form-footer { background: #fff; padding: 16px 24px; border-top: 1px solid #e5e6eb; display: flex; justify-content: flex-end; align-items: center; gap: 12px; position: sticky; bottom: 0; z-index: 100; box-shadow: 0 -2px 10px rgba(0,0,0,0.05); } + .arco-grouped-form-footer .ant-btn { border-radius: 5px; height: 32px; padding: 4px 16px; font-size: 14px; } + + .arco-theme-overrides .ant-breadcrumb { color: #86909c; font-size: 14px; white-space: nowrap; flex-shrink: 0; margin-bottom: 20px; } + .arco-theme-overrides .ant-breadcrumb a { color: #4e5969; } + .arco-theme-overrides .ant-breadcrumb a:hover { color: #165dff; background-color: transparent; } + .arco-theme-overrides .ant-form-item-label > label { color: #4e5969; white-space: nowrap; } + .arco-theme-overrides .ant-form-item-label > label::after { display: none !important; content: "" !important; margin: 0 !important; } + `), + React.createElement('div', { className: 'arco-grouped-form-page' }, + React.createElement('div', { className: 'arco-grouped-form-page-content' }, + React.createElement(Breadcrumb, { + separator: React.createElement('span', { style: { color: '#c9cdd4' } }, '/'), + items: [ + { title: '首页' }, + { title: '运维管理' }, + { title: '车辆业务' }, + { title: React.createElement('a', { onClick: function(e) { e.preventDefault(); handleBack(); } }, '故障管理') }, + { title: React.createElement('span', { style: { color: '#1d2129' } }, '查看故障单') } + ] + }), + + React.createElement(Form, { form: faultForm, layout: 'vertical', disabled: true }, + // 车辆信息 + React.createElement(Card, { title: '车辆信息', bordered: false }, + React.createElement(Row, { gutter: 24 }, + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '车牌号', name: 'plate', rules: [{ required: true, message: '请选择车牌号' }] }, + React.createElement(Select, { + placeholder: '请选择车牌号', + options: plateOptions, + showSearch: true + }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '车辆品牌', name: 'brand' }, + React.createElement(Input, { placeholder: '自动带入', disabled: true, style: { backgroundColor: '#f2f3f5', color: '#86909c' } }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '车辆型号', name: 'model' }, + React.createElement(Input, { placeholder: '自动带入', disabled: true, style: { backgroundColor: '#f2f3f5', color: '#86909c' } }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '运营公司', name: 'company' }, + React.createElement(Input, { placeholder: '自动带入', disabled: true, style: { backgroundColor: '#f2f3f5', color: '#86909c' } }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '车辆识别代码', name: 'vin' }, + React.createElement(Input, { placeholder: '自动带入', disabled: true, style: { backgroundColor: '#f2f3f5', color: '#86909c' } }) + ) + ) + ) + ), + + // 故障信息 + React.createElement(Card, { title: '故障信息', bordered: false }, + React.createElement(Row, { gutter: 24 }, + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '故障类型', name: 'type', rules: [{ required: true, message: '请选择故障类型' }] }, + React.createElement(Select, { placeholder: '请选择故障类型', options: faultTypeOptions }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '故障来源', name: 'source', rules: [{ required: true, message: '请选择故障来源' }] }, + React.createElement(Select, { placeholder: '请选择故障来源', options: faultSourceOptions }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '故障等级', name: 'level', rules: [{ required: true, message: '请选择故障等级' }] }, + React.createElement(Select, { placeholder: '请选择故障等级', options: faultLevelOptions }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '故障上报时间', name: 'reportTime', rules: [{ required: true, message: '请选择故障上报时间' }] }, + React.createElement(DatePicker, { style: { width: '100%' }, placeholder: '请选择上报时间', showTime: true }) + ) + ), + React.createElement(Col, { span: 8 }, + React.createElement(Form.Item, { label: '解决情况', name: 'status', rules: [{ required: true, message: '请选择故障解决情况' }] }, + React.createElement(Select, { placeholder: '请选择故障解决情况', options: resolveStatusOptions }) + ) + ) + ) + ), + + // 故障证据与描述 + React.createElement(Card, { title: '故障证据与描述', bordered: false }, + React.createElement(Row, { gutter: 24 }, + React.createElement(Col, { span: 24 }, + React.createElement(Form.Item, { label: '故障描述', name: 'desc', rules: [{ required: true, message: '请填写故障描述' }] }, + React.createElement(Input.TextArea, { + placeholder: '在何种状态下,产生何种现象,导致何种事故', + style: { height: 80, minHeight: 80, resize: 'none' } + }) + ) + ), + React.createElement(Col, { span: 24 }, + React.createElement(Form.Item, { label: '故障证据', name: 'evidence' }, + React.createElement(Upload, { listType: 'picture-card' }), + React.createElement('div', { style: { fontSize: 12, color: '#86909c', marginTop: 8 } }, '支持上传照片、视频、录音') + ) + ) + ) + ) + ) + ), + // 底部操作栏 + React.createElement('div', { className: 'arco-grouped-form-footer' }, + React.createElement(Button, { onClick: handleBack, style: { borderRadius: 5 } }, '返回列表') + ) + ) + ); +}; + +if (typeof module !== 'undefined' && module.exports) module.exports = Component; diff --git a/web端/需求说明.zip b/web端/需求说明.zip new file mode 100644 index 0000000..e8c2f89 Binary files /dev/null and b/web端/需求说明.zip differ