diff --git a/web端/运维管理/车辆业务/交车管理.jsx b/web端/运维管理/车辆业务/交车管理.jsx index 218128f..0f97fb0 100644 --- a/web端/运维管理/车辆业务/交车管理.jsx +++ b/web端/运维管理/车辆业务/交车管理.jsx @@ -22,7 +22,12 @@ var DV_KPI_STYLE = '' + '.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;}'; + + '.lc-alert-card-active{box-shadow:0 0 0 2px rgba(22,93,255,.2)!important;border-color:#165dff!important;}' + + '.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;}'; 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' }, @@ -119,6 +124,61 @@ function isDeliverySignedStatus(status) { return status === '客户已签章' || status === '已签章'; } +function isDeliveryPendingCustomerSignStatus(status) { + return status === '待客户签章'; +} + +/** 待客户签章 / 客户已签章 使用统一 Tag 样式 */ +var DV_CUSTOMER_SIGN_STATUS_BG = '#2563eb'; + +function isCustomerSignRelatedStatus(status) { + return isDeliveryPendingCustomerSignStatus(status) || isDeliverySignedStatus(status); +} + +function getAuthorizedListFromRecord(record) { + var list = record && record.authorizedList; + if (!Array.isArray(list)) return []; + return list.filter(function (a) { + return a && ((a.name && String(a.name).trim()) || (a.phone && String(a.phone).trim())); + }); +} + +function buildCustomerSignPendingPopoverContent(record) { + var list = getAuthorizedListFromRecord(record); + var boxStyle = { minWidth: 168, fontSize: 13, lineHeight: 1.65 }; + var labelStyle = { color: '#64748b' }; + var valStyle = { color: '#334155', fontWeight: 600 }; + if (!list.length) { + return React.createElement('div', { style: boxStyle }, + React.createElement('div', null, + React.createElement('span', { style: labelStyle }, '授权人姓名:'), + React.createElement('span', { style: valStyle }, '-') + ), + React.createElement('div', { style: { marginTop: 4 } }, + React.createElement('span', { style: labelStyle }, '授权人手机号:'), + React.createElement('span', { style: valStyle }, '-') + ) + ); + } + var children = []; + list.forEach(function (item, idx) { + if (idx > 0) { + children.push(React.createElement('div', { key: 'sep-' + idx, style: { borderTop: '1px solid #f1f5f9', margin: '8px 0' } })); + } + children.push( + React.createElement('div', { key: 'name-' + idx }, + React.createElement('span', { style: labelStyle }, '授权人姓名:'), + React.createElement('span', { style: valStyle }, (item.name && String(item.name).trim()) || '-') + ), + React.createElement('div', { key: 'phone-' + idx, style: { marginTop: 4 } }, + React.createElement('span', { style: labelStyle }, '授权人手机号:'), + React.createElement('span', { style: valStyle }, (item.phone && String(item.phone).trim()) || '-') + ) + ); + }); + return React.createElement('div', { style: boxStyle }, children); +} + function buildDeliverySignFileName(record) { var plate = (record.plateNo && String(record.plateNo).trim()) ? String(record.plateNo).trim() : '车牌待选'; var orderId = record.orderId != null ? String(record.orderId) : 'unknown'; @@ -279,6 +339,7 @@ function DeliveryEditDrawer(props) { var Modal = antd.Modal; var Table = antd.Table; var Tag = antd.Tag; + var Popover = antd.Popover; var message = antd.message; function RequiredLabel(text) { @@ -1087,16 +1148,15 @@ function DeliveryEditDrawer(props) { (function () { var status = record.deliveryStatus || '未开始'; var signed = isDeliverySignedStatus(status); - return React.createElement(Tag, { + var pendingSign = isDeliveryPendingCustomerSignStatus(status); + var signRelated = isCustomerSignRelatedStatus(status); + var tag = 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 + background: signRelated ? DV_CUSTOMER_SIGN_STATUS_BG : '#f1f5f9', + color: signRelated ? '#fff' : '#475569', + fontWeight: signRelated ? 600 : 400 }, title: signed ? '点击下载签章文件' : undefined, role: signed ? 'button' : undefined, @@ -1104,6 +1164,17 @@ function DeliveryEditDrawer(props) { 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); + if (pendingSign) { + return React.createElement(Popover, { + content: buildCustomerSignPendingPopoverContent(record), + trigger: 'hover', + placement: 'topLeft', + mouseEnterDelay: 0.15, + mouseLeaveDelay: 0.1, + destroyTooltipOnHide: true + }, tag); + } + return tag; })() ), React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(2,minmax(0,1fr))', gap: '8px 16px' } }, @@ -1399,209 +1470,371 @@ if (typeof window !== 'undefined') { } -function getDeliveryManageRequirementDoc() { +var DV_REQ_SCROLL_STYLE = { fontSize: 13, color: '#334155', lineHeight: 1.75, maxHeight: '68vh', overflowY: 'auto', paddingRight: 4 }; +var DV_REQ_TH_STYLE = { border: '1px solid #e2e8f0', padding: '8px 10px', textAlign: 'left', background: '#f8fafc', fontWeight: 600 }; +var DV_REQ_TD_STYLE = { border: '1px solid #e2e8f0', padding: '8px 10px', verticalAlign: 'top' }; + +function dvReqTitle(text) { + return React.createElement('p', { style: { margin: '0 0 8px', fontWeight: 700, color: '#0f172a', fontSize: 14 } }, text); +} + +function dvReqSubtitle(text) { + return React.createElement('p', { style: { margin: '14px 0 6px', fontWeight: 600, color: '#334155' } }, text); +} + +function dvReqHint(text) { + return React.createElement('p', { style: { margin: '4px 0 8px', color: '#64748b', fontSize: 12 } }, text); +} + +function dvReqStrong(label, text) { + return React.createElement(React.Fragment, null, React.createElement('strong', null, label), text); +} + +function dvReqUl(items) { + return React.createElement('ul', { style: { paddingLeft: 20, margin: '6px 0 14px' } }, + items.map(function (item, idx) { + return React.createElement('li', { key: idx, style: { marginBottom: 4 } }, item); + }) + ); +} + +function dvReqOl(items) { + return React.createElement('ol', { style: { paddingLeft: 20, margin: '6px 0 14px' } }, + items.map(function (item, idx) { + return React.createElement('li', { key: idx, style: { marginBottom: 4 } }, item); + }) + ); +} + +function dvReqTable(headers, rows) { + return React.createElement('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: 12, margin: '8px 0 14px' } }, + React.createElement('thead', null, + React.createElement('tr', null, + headers.map(function (h, i) { + return React.createElement('th', { key: i, style: DV_REQ_TH_STYLE }, h); + }) + ) + ), + React.createElement('tbody', null, + rows.map(function (row, ri) { + return React.createElement('tr', { key: ri }, + row.map(function (cell, ci) { + return React.createElement('td', { key: ci, style: DV_REQ_TD_STYLE }, cell); + }) + ); + }) + ) + ); +} + +function renderDeliveryRequirementPrdTab(Alert) { + return React.createElement('div', { style: DV_REQ_SCROLL_STYLE }, + React.createElement(Alert, { + type: 'info', + showIcon: true, + style: { marginBottom: 14, borderRadius: 10 }, + message: '模块定位', + description: '交车管理面向运维人员,按「单车一行」跟进交车任务进度。运维在列表抽屉内完成交车单录入与提交;客户被授权人通过 E 签宝完成签章后,交车流程完结。本文从产品经理视角描述业务规则与页面行为,不涉及表结构、接口字段及实现细节。' + }), + + dvReqTitle('一、业务对象说明'), + dvReqUl([ + dvReqStrong('交车任务:', '一次交车业务单元,通常对应一份车辆租赁合同或替换车场景,可包含多台车。'), + dvReqStrong('交车单(单车):', '交车任务下某一台车的交车记录,列表以「一车一行」展示。'), + dvReqStrong('运维交车:', '运维人员现场验车、拍照、录入里程/电量/氢量等,保存或提交交车单。'), + dvReqStrong('客户签章:', '运维提交后,客户侧被授权人在 E 签宝完成电子签章,状态由「待客户签章」变为「客户已签章」。'), + dvReqStrong('被授权人:', '取自关联租赁合同的被授权人信息;本模块只读展示姓名与手机号,供运维跟进签章。') + ]), + + dvReqTitle('二、页面结构'), + dvReqUl([ + '面包屑:运维管理 / 车辆业务 / 交车管理', + '右上角「查看需求说明」:打开本说明文档', + '页面自上而下:筛选区 → KPI 统计卡片 → 列表区(含导出)' + ]), + + dvReqTitle('三、数据统计(KPI 卡片)'), + dvReqHint('三张可点击卡片替代原 Tab,统计范围均为「当前筛选条件命中后的全部车辆行」,不受分页影响。'), + dvReqTable( + ['卡片', '统计口径', '默认'], + [ + ['全部交车任务', '进行中 + 已完成', '—'], + ['进行中的交车任务', '交车状态为「未开始」「已保存」「待客户签章」', React.createElement('strong', null, '默认选中')], + ['已完成的交车任务', '交车状态为「客户已签章」', '—'] + ] + ), + dvReqUl([ + '卡片右上角问号:悬停展示指标说明', + '点击卡片切换列表数据,选中卡片高亮' + ]), + + dvReqTitle('四、筛选区'), + dvReqSubtitle('4.1 通用规则'), + dvReqUl([ + '默认展示首行 4 项;点击「展开」显示全部 16 项,「收起」恢复默认', + '多条件之间为「且」关系;修改筛选项后须点击「搜索」才生效', + '「重置」清空全部筛选并恢复默认' + ]), + dvReqSubtitle('4.2 车辆批量筛选(第一项)'), + dvReqHint('交互体验与「保险采购 · 比价单选车」一致。'), + dvReqOl([ + '点击输入框弹出批量录入面板,支持车牌号、车辆识别代码(VIN)', + '每行一条;可从 Excel 等批量复制粘贴;同一行内亦可用逗号、顿号、分号分隔', + '面板内「确定」仅保存录入内容;须再点筛选区「搜索」后列表才刷新', + '面板内「清空」或触发器一键清除:立即清除该条件并刷新列表', + '已生效时触发器展示「已选 N 辆车」', + dvReqStrong('匹配规则:', '命中任一输入值即展示(OR);车牌/VIN 均支持精确或包含匹配') + ]), + dvReqSubtitle('4.3 其余筛选项'), + dvReqUl([ + '合同编号、项目名称、客户名称:可搜索下拉', + '交车区域:省-市二级联动', + '完成交车时间:日期段,精确至天', + '交车人、车辆识别代码、车辆类型、品牌、型号', + '业务部门、业务负责人、任务来源、业务类型、是否延期', + dvReqStrong('说明:', '「车辆」批量筛选与「车辆识别代码」单项筛选可同时使用,结果为且关系') + ]), + + dvReqTitle('五、列表(一车一行)'), + dvReqSubtitle('5.1 数据粒度'), + dvReqUl([ + '一个交车任务含多车时,按车辆拆分为独立行展示', + '列表数据范围受运维人员区域权限约束(详见第十二节)' + ]), + dvReqSubtitle('5.2 列说明'), + dvReqTable( + ['列', '展示与交互'], + [ + ['车辆信息', '三行:车牌号、品牌-型号、VIN;车牌未选时橙色「车牌待选」,VIN 显示 -;点击车牌号打开查看抽屉'], + ['合同信息', '客户名称 + 业务类型 Tag、合同编号(可跳转)、项目名称(可跳转)'], + ['业务负责人', '业务部门、业务负责人(两行)'], + ['任务来源', '实心 Tag;来源为「替换车」时悬停展示 旧车 → 新车'], + ['交车地点', '第一行交车区域(省-市),第二行停车场(车牌待选时显示 -)'], + ['交车状态', '详见第六节'], + ['完成交车时间', '单行展示'], + ['交车人', '单行展示'], + ['是否归还', '未交车显示 -;已交车未还车显示「未归还」;已还车显示「已归还」且悬停展示还车时间、还车人'], + ['交车记录', '交车里程(km)、交车氢量(%或MPa)、交车电量(%) 三行合并'], + ['创建时间 / 创建人', '交车任务维度字段'], + ['操作', '「查看」始终可用;「编辑」仅「未开始」「已保存」时展示'] + ] + ), + dvReqSubtitle('5.3 其他规则'), + dvReqUl([ + '合同信息列宽支持拖拽调整', + '分页:默认 10 条/页,可选 10 / 20 / 50' + ]), + + dvReqTitle('六、交车状态与客户签章'), + dvReqSubtitle('6.1 状态定义'), + dvReqTable( + ['状态', '含义', 'KPI 归属'], + [ + ['未开始', '交车单尚未保存有效数据', '进行中'], + ['已保存', '运维已保存但未正式提交', '进行中'], + ['待客户签章', '运维已提交,等待客户被授权人 E 签宝签章', '进行中'], + ['客户已签章', '客户被授权人已完成签章,流程完结', '已完成'] + ] + ), + dvReqSubtitle('6.2 Tag 视觉与交互规则'), + dvReqUl([ + dvReqStrong('统一视觉:', '「待客户签章」与「客户已签章」使用相同 Tag 样式(蓝色实心),不做颜色或下划线区分'), + dvReqStrong('待客户签章:', '悬停 Popover 展示租赁合同被授权人姓名、手机号;多人逐条展示;无数据显示 -'), + dvReqStrong('客户已签章:', '点击 Tag 下载 E 签宝签章文件;文件名建议含交车单标识、车辆序号、车牌号'), + '抽屉摘要卡中的状态 Tag 遵循与列表相同的视觉与交互规则' + ]), + dvReqSubtitle('6.3 被授权人数据来源'), + dvReqUl([ + '取自交车任务关联的车辆租赁合同「被授权人信息」', + '交车管理侧只读展示,不在本模块维护被授权人', + '已提交/已签章交车单以提交当时合同快照为准(具体以业务归档规则为准)' + ]), + + dvReqTitle('七、交车状态流转'), + React.createElement('div', { style: { padding: '10px 14px', background: '#f8fafc', borderRadius: 8, border: '1px solid #e2e8f0', margin: '8px 0 14px', fontSize: 13, fontWeight: 600, color: '#334155' } }, + '未开始 → 已保存(保存)→ 待客户签章(提交)→ 客户已签章(客户 E 签宝完成)' + ), + dvReqUl([ + '仅「未开始」「已保存」可编辑;任意状态均可查看' + ]), + + dvReqTitle('八、编辑交车单(抽屉)'), + dvReqSubtitle('8.1 入口与布局'), + dvReqUl([ + '入口:列表操作列「编辑」(仅未开始/已保存)', + '顶部摘要卡:客户名称、合同编号、交车状态、项目、交车地点、品牌型号、任务来源', + '左侧目录锚点:交车车辆 · 车辆信息 · 交车数据 · 交车检查单 · 交车照片', + '底部操作:取消、保存、提交' + ]), + dvReqSubtitle('8.2 交车车辆'), + dvReqUl([ + '车牌号(必填):可搜索选择停车场内「已备车」车辆(详见第十二节)', + '车牌待选时 VIN 显示 -;车辆类型/品牌/型号/VIN 只读,随车牌联动' + ]), + dvReqSubtitle('8.3 车辆信息'), + dvReqHint('备车数据同步 — 打开抽屉或选择车牌后自动同步,页面不单独提示。'), + dvReqUl([ + '同步:车身广告及照片、尾板、备胎照片/胎纹深度、车辆基础信息', + '不同步:驾驶培训相关(提车码、司机证照等),须在交车环节单独上传识别', + '车身广告开关与备车「后装设备-车身广告」双向同步', + '车身广告及放大字(开关必填;开启后各 1 张照片必填)', + '尾板(开关必填);备胎照片/胎纹深度(选填,支持 OCR)', + '驾驶培训(必填):上传提车码 → 识别成功后加载司机证照;失败提示重新选择' + ]), + dvReqSubtitle('8.4 交车数据'), + dvReqUl([ + '交车里程(必填,km)、交车电量(必填,%)、交车氢量(必填,% 或 MPa)', + '送车服务费(选填,元)', + dvReqStrong('里程自动取值:', '优先车机当前里程 → 其次最近一次异动/调拨/还车结束里程 → 均不可用则手工录入') + ]), + dvReqSubtitle('8.5 交车检查单'), + dvReqUl([ + '列:类别(同类合并)、检查项目、检查情况、备注', + '覆盖车灯、仪表盘、驾驶室、轮胎、液位、外观、随车工具/证件、燃料电池、冷机、制动等' + ]), + dvReqSubtitle('8.6 交车照片'), + dvReqUl([ + '分模块:车辆、底盘、轮胎、瑕疵、其他', + '车辆/底盘/轮胎各 6 个必传点位;瑕疵、其他不限张数', + '单张:jpg/jpeg/png/gif/webp,不超过 5MB;支持预览、删除' + ]), + dvReqSubtitle('8.7 保存与提交校验'), + dvReqTable( + ['业务动作', '须满足的条件'], + [ + [React.createElement('strong', null, '保存'), React.createElement('ul', { style: { margin: 0, paddingLeft: 18 } }, + React.createElement('li', null, '通过当前已填字段校验'), + React.createElement('li', null, '状态变为「已保存」,抽屉不关闭') + )], + [React.createElement('strong', null, '提交'), React.createElement('ul', { style: { margin: 0, paddingLeft: 18 } }, + React.createElement('li', null, '车牌已选择'), + React.createElement('li', null, '车身广告(若开启)及放大字照片已上传'), + React.createElement('li', null, '提车码识别成功,驾驶培训证照齐全'), + React.createElement('li', null, '交车里程、电量、氢量已填写'), + React.createElement('li', null, '车辆/底盘/轮胎全部必传照片点已上传'), + React.createElement('li', null, '二次确认后状态变为「待客户签章」,写入完成交车时间/交车人,关闭抽屉') + )] + ] + ), + + dvReqTitle('九、查看交车单(抽屉)'), + dvReqUl([ + '入口:列表点击车牌号,或操作列「查看」', + '布局与编辑页一致;全部只读,底部仅「关闭」', + '「客户已签章」时额外展示「E签宝签章文件」卡片:文件名、签章时间、签章方;支持预览与下载', + '已提交交车单展示完整交车照片;驾驶培训区展示状态与证照图片' + ]), + + dvReqTitle('十、导出'), + dvReqUl([ + '范围:当前 KPI 卡片选中项 + 全部筛选条件(含车辆批量筛选)下的全部命中行,不受分页限制', + '导出列与业务导出样表一致;交车里程、氢量、电量为独立三列(非列表合并列)', + '列表区展示当前 KPI 名称及导出范围说明' + ]), + + dvReqTitle('十一、指标单位约定'), + dvReqUl([ + '交车里程:km', + '交车电量:%', + '交车氢量:% 或 MPa(按车辆/合同配置)', + '列表「交车记录」合并展示;导出仍为三列' + ]), + + dvReqTitle('十二、区域权限与车牌选择'), + dvReqSubtitle('12.1 交车任务可见范围'), + dvReqUl([ + '以交车单「交车区域」(省-市)为任务所属区域', + '运维人员须具备该区域或其上级区域权限,方可查看并执行', + '示例:交车区域「浙江省-嘉兴市」→ 具备「浙江省」或「嘉兴市」权限可见;仅「杭州市」权限不可见', + '无权限时整条交车任务(含其下所有车辆行)均不可见' + ]), + dvReqSubtitle('12.2 停车场车牌可选范围'), + dvReqUl([ + '下拉仅展示车辆状态为「已备车」的车辆', + '在「已备车」前提下,停车场区域须落在当前运维权限覆盖范围内', + '省级权限:可选该省各停车场内已备车车辆;市级权限:仅可选该市', + '下拉建议展示:车牌号 + 停车场名称' + ]), + + dvReqTitle('十三、关联模块与数据依赖'), + dvReqTable( + ['关联模块', '提供的数据 / 能力'], + [ + ['车辆租赁合同', '合同编号、客户、项目、交车区域/地点、被授权人信息'], + ['备车管理', '已备车车辆、车身广告、备胎等同步数据'], + ['提车码 / 司机培训', '驾驶培训识别与证照数据'], + ['E 签宝', '客户签章状态与签章文件'], + ['合同管理', '列表点击合同编号/项目名称跳转详情'], + ['替换车', '任务来源为「替换车」时展示旧车 → 新车对照'] + ] + ) + ); +} + +function renderDeliveryRequirementManualTab(Alert, Table) { + return React.createElement('div', { style: Object.assign({}, DV_REQ_SCROLL_STYLE, { maxHeight: '62vh' }) }, + React.createElement(Alert, { + type: 'success', + showIcon: true, + style: { marginBottom: 14, borderRadius: 10 }, + message: '交车全流程(简版)', + description: '选车 → 录入交车数据 → 拍照 → 保存/提交 → 跟进客户签章。运维侧以「提交」为节点,客户签章在 E 签宝侧完成。' + }), + React.createElement(Table, { + size: 'small', + pagination: false, + bordered: true, + style: { marginBottom: 14 }, + columns: [ + { title: '步骤', dataIndex: 'step', width: 56 }, + { title: '操作', dataIndex: 'action' }, + { title: '要点', dataIndex: 'tip', width: 220 } + ], + dataSource: [ + { key: '1', step: '①', action: '筛选定位车辆', tip: 'KPI 卡片切换进行中/已完成;车辆批量筛选粘贴车牌' }, + { key: '2', step: '②', action: '编辑交车单', tip: '仅「未开始/已保存」可编辑;选择已备车车牌' }, + { key: '3', step: '③', action: '填写车辆信息', tip: '广告/尾板/备胎;上传提车码完成驾驶培训' }, + { key: '4', step: '④', action: '录入交车数据', tip: '里程/电量/氢量必填;里程可自动带入后修改' }, + { key: '5', step: '⑤', action: '完成检查单与拍照', tip: '检查单逐项确认;车辆/底盘/轮胎 18 点位必传' }, + { key: '6', step: '⑥', action: '保存或提交', tip: '保存→已保存;提交→待客户签章' }, + { key: '7', step: '⑦', action: '跟进客户签章', tip: '悬停「待客户签章」查看被授权人联系方式' }, + { key: '8', step: '⑧', action: '签章完成', tip: '状态变「客户已签章」;点击 Tag 下载签章文件' } + ] + }), + dvReqTitle('提交前自查'), + dvReqUl([ + '车牌已从「已备车」列表中选择', + '提车码识别成功,司机证照已加载', + '交车里程、电量、氢量均已填写', + '车辆 / 底盘 / 轮胎全部必传照片点已上传', + '车身广告若开启,广告图与放大字图均已上传' + ]), + dvReqTitle('常见情况'), + dvReqUl([ + '车牌待选:VIN 不展示,交车地点停车场行显示 -', + '待客户签章:运维不可再编辑,可悬停查看被授权人手机号跟进', + '客户已签章:归入 KPI「已完成」,可下载签章 PDF', + '替换车任务:任务来源列悬停可查看旧车 → 新车对照', + '导出:与当前 KPI + 筛选条件一致,不受分页限制' + ]) + ); +} + +function createDeliveryRequirementModalItems(antd) { + var Alert = antd.Alert; + var Table = antd.Table; 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'); + { + key: 'prd', + label: '需求说明', + children: renderDeliveryRequirementPrdTab(Alert) + }, + { + key: 'manual', + label: '操作手册', + children: renderDeliveryRequirementManualTab(Alert, Table) + } + ]; } const Component = function () { @@ -1622,20 +1855,63 @@ const Component = function () { var Tooltip = antd.Tooltip; var Popover = antd.Popover; var Modal = antd.Modal; + var Tabs = antd.Tabs; var message = antd.message; + var Input = antd.Input; var RangePicker = DatePicker.RangePicker; + function parseMultiPlates(text) { + var raw = (text || '').trim(); + if (!raw) return []; + var lines = raw.split(/\r?\n/).map(function (line) { return line.trim(); }).filter(Boolean); + var expanded = []; + lines.forEach(function (line) { + if (/[,,、;;]/.test(line)) { + line.split(/[,,、;;]+/).forEach(function (s) { + var t = s.trim(); + if (t) expanded.push(t); + }); + } else { + expanded.push(line); + } + }); + var seen = {}; + var out = []; + expanded.forEach(function (s) { + var key = s.toUpperCase(); + if (!seen[key]) { + seen[key] = true; + out.push(key); + } + }); + return out; + } + + function rowMatchesVehicleFilter(row, vehicleText) { + var tokens = parseMultiPlates(vehicleText); + if (!tokens.length) return true; + var plateRaw = (row.plateNo || '').trim(); + var plate = (!plateRaw || plateRaw === '-') ? '' : plateRaw.toUpperCase(); + var vin = (row.vin || '').trim().toUpperCase(); + for (var i = 0; i < tokens.length; i++) { + var token = tokens[i]; + if (plate && (plate === token || plate.indexOf(token) !== -1)) return true; + if (vin && (vin === token || vin.indexOf(token) !== -1)) return true; + } + return false; + } + function createEmptyFilters() { return { - contractCode: undefined, - projectName: undefined, - customerName: undefined, - deliveryRegion: undefined, - dateStart: '', - dateEnd: '', + contractCode: undefined, + projectName: undefined, + customerName: undefined, + deliveryRegion: undefined, + dateStart: '', + dateEnd: '', deliveryPerson: undefined, - plateNo: undefined, + plateNos: '', vin: undefined, vehicleType: undefined, brand: undefined, @@ -1665,6 +1941,12 @@ const Component = function () { var filterExpandedState = useState(false); var filterExpanded = filterExpandedState[0]; var setFilterExpanded = filterExpandedState[1]; + var multiPlateOpenState = useState(false); + var multiPlateOpen = multiPlateOpenState[0]; + var setMultiPlateOpen = multiPlateOpenState[1]; + var multiPlateDraftState = useState(''); + var multiPlateDraft = multiPlateDraftState[0]; + var setMultiPlateDraft = multiPlateDraftState[1]; /** total | inProgress | completed */ var kpiFilterState = useState('inProgress'); @@ -1678,7 +1960,9 @@ const Component = function () { var requirementModalOpen = useState(false); var setRequirementModalOpen = requirementModalOpen[1]; - var requirementDocContent = getDeliveryManageRequirementDoc(); + var requirementModalItems = useMemo(function () { + return createDeliveryRequirementModalItems(antd); + }, []); // 交车区域:省-市 二级 var regionOptions = [ @@ -1756,6 +2040,7 @@ const Component = function () { 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: '赵小峰', + authorizedList: [{ name: '周授权', phone: '13805731234', idCard: '330402199001011234' }], 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 } @@ -1763,6 +2048,7 @@ const Component = function () { }, { 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: '何苗苗', + authorizedList: [{ name: '陈明', phone: '13800138088', idCard: '420102199002022345' }], 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 } @@ -1770,12 +2056,17 @@ const Component = function () { }, { 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: '童军林', + authorizedList: [{ name: '黄签章', phone: '13600136066', idCard: '441900199003033456' }], 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: '何苗苗', + authorizedList: [ + { name: '李签章', phone: '13900139099', idCard: '210102199004044567' }, + { name: '王签章', phone: '13700137077', idCard: '210103199005055678' } + ], 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 } @@ -1783,6 +2074,7 @@ const Component = function () { }, { 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: '系统', + authorizedList: [{ name: '张授权', phone: '13500135055', idCard: '330402199006066789' }], 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 } @@ -1790,6 +2082,7 @@ const Component = function () { }, { 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: '李四', + authorizedList: [{ name: '赵授权', phone: '13300133033', idCard: '330106199007077890' }], 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 }, @@ -1855,6 +2148,7 @@ const Component = function () { createTime: order.createTime, createBy: order.createBy, expectedDate: order.expectedDate, + authorizedList: Array.isArray(order.authorizedList) ? order.authorizedList.slice() : [], vehicleReturned: returnFields.vehicleReturned, returnTime: returnFields.returnTime, returnPerson: returnFields.returnPerson @@ -1935,7 +2229,9 @@ const Component = function () { 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 (parseMultiPlates(f.plateNos).length) { + list = list.filter(function (r) { return rowMatchesVehicleFilter(r, f.plateNos); }); + } 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; }); @@ -1971,7 +2267,6 @@ const Component = function () { } var dynamicFilterOptions = useMemo(function () { - var plates = []; var vins = []; var vehicleTypes = []; var brands = []; @@ -1979,7 +2274,6 @@ const Component = function () { 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); @@ -1988,7 +2282,6 @@ const Component = function () { if (r.businessOwner && r.businessOwner !== '-') owners.push(r.businessOwner); }); return { - plateNoOptions: buildSelectOptions(plates), vinOptions: buildSelectOptions(vins), vehicleTypeOptions: buildSelectOptions(vehicleTypes), brandOptions: buildSelectOptions(brands), @@ -2112,23 +2405,57 @@ const Component = function () { return regionVal; } - var handleQuery = useCallback(function () { - var next = patchFilters(filters, { deliveryRegion: normalizeRegionFilter(filters.deliveryRegion) }); + var appliedMultiVehicles = useMemo(function () { + return parseMultiPlates(appliedFilters.plateNos); + }, [appliedFilters.plateNos]); + + var vehicleFilterTriggerText = appliedMultiVehicles.length + ? ('已选 ' + appliedMultiVehicles.length + ' 辆车') + : ''; + + var handleMultiPlateOpenChange = useCallback(function (open) { + setMultiPlateOpen(open); + if (open) setMultiPlateDraft(filters.plateNos || ''); + }, [filters.plateNos]); + + var handleMultiPlateClear = useCallback(function () { + setMultiPlateDraft(''); + setMultiPlateOpen(false); + var next = patchFilters(filters, { plateNos: '' }); setFilters(next); setAppliedFilters(patchFilters(next, {})); setPage(1); }, [filters]); + var handleMultiPlateApply = useCallback(function () { + var trimmed = multiPlateDraft.trim(); + setFilters(function (f) { return patchFilters(f, { plateNos: trimmed }); }); + setMultiPlateOpen(false); + }, [multiPlateDraft]); + + var handleQuery = useCallback(function () { + var plateText = (multiPlateDraft.trim() || (filters.plateNos || '')).trim(); + var next = patchFilters(filters, { + deliveryRegion: normalizeRegionFilter(filters.deliveryRegion), + plateNos: plateText + }); + setFilters(next); + setAppliedFilters(patchFilters(next, {})); + setMultiPlateDraft(plateText); + setMultiPlateOpen(false); + setPage(1); + var tokens = parseMultiPlates(plateText); + if (tokens.length) { + message.success('已按 ' + tokens.length + ' 辆车筛选'); + } + }, [filters, multiPlateDraft]); + var handleReset = useCallback(function () { 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 }); }); + setMultiPlateDraft(''); + setMultiPlateOpen(false); setPage(1); }, []); @@ -2212,18 +2539,13 @@ const Component = function () { function renderDeliveryStatus(status, record) { var bg = '#8c8c8c'; - if (status === '客户已签章' || status === '已签章') bg = '#16a34a'; - else if (status === '待客户签章') bg = '#2563eb'; + if (isCustomerSignRelatedStatus(status)) bg = DV_CUSTOMER_SIGN_STATUS_BG; else if (status === '已保存') bg = '#ea580c'; else if (status === '未开始') bg = '#64748b'; var signed = isDeliverySignedStatus(status); + var pendingSign = isDeliveryPendingCustomerSignStatus(status); var style = Object.assign({}, solidTagBaseStyle, { backgroundColor: bg || '#64748b' }); - if (signed) { - style.cursor = 'pointer'; - style.textDecoration = 'underline'; - style.textUnderlineOffset = '2px'; - } - return React.createElement(Tag, { + var tag = React.createElement(Tag, { style: style, title: signed ? '点击下载签章文件' : undefined, role: signed ? 'button' : undefined, @@ -2231,6 +2553,17 @@ const Component = function () { 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); + if (pendingSign) { + return React.createElement(Popover, { + content: buildCustomerSignPendingPopoverContent(record), + trigger: 'hover', + placement: 'topLeft', + mouseEnterDelay: 0.15, + mouseLeaveDelay: 0.1, + destroyTooltipOnHide: true + }, tag); + } + return tag; } function renderBizTypeTag(bizType) { @@ -2670,86 +3003,130 @@ const Component = function () { }; var filterItems = [ + React.createElement('div', { key: 'plateNos', style: filterItemStyle }, + React.createElement('div', { style: filterLabelStyle }, '车辆'), + React.createElement(Popover, { + open: multiPlateOpen, + onOpenChange: handleMultiPlateOpenChange, + trigger: 'click', + placement: 'bottomLeft', + overlayClassName: 'lc-multi-plate-popover', + content: React.createElement('div', { className: 'lc-multi-plate-pop' }, + React.createElement('div', { className: 'lc-multi-plate-pop-hint' }, + '支持多辆车车牌号、车辆识别代码,每行一条;可从 Excel 等批量复制粘贴,点击「确定」后于筛选区点击「搜索」生效。' + ), + React.createElement(Input.TextArea, { + value: multiPlateDraft, + onChange: function (e) { setMultiPlateDraft(e.target.value); }, + placeholder: '浙F80088\n粤AGP4598\nLNBSCPKB9RR223402', + autoSize: { minRows: 5, maxRows: 10 }, + style: { borderRadius: 8, fontFamily: 'monospace', fontSize: 13 } + }), + React.createElement('div', { className: 'lc-multi-plate-pop-actions' }, + React.createElement(Button, { size: 'small', onClick: handleMultiPlateClear }, '清空'), + React.createElement(Button, { size: 'small', type: 'primary', onClick: handleMultiPlateApply }, '确定') + ) + ) + }, + React.createElement(Input, { + className: 'lc-multi-plate-trigger', + readOnly: true, + allowClear: !!vehicleFilterTriggerText, + placeholder: '支持多辆车车牌号、车辆识别代码,每行一条', + value: vehicleFilterTriggerText, + onClick: function () { setMultiPlateOpen(true); }, + onClear: function (e) { + if (e && e.stopPropagation) e.stopPropagation(); + handleMultiPlateClear(); + }, + style: Object.assign({ borderRadius: 8 }, filterControlStyle), + suffix: React.createElement('svg', { + width: 14, height: 14, viewBox: '0 0 24 24', fill: 'none', stroke: '#94a3b8', strokeWidth: 2, + style: { pointerEvents: 'none' } + }, React.createElement('polyline', { points: '6 9 12 15 18 9' })) + }) + ) + ), React.createElement('div', { key: 'contractCode', style: filterItemStyle }, React.createElement('div', { style: filterLabelStyle }, '合同编号'), - React.createElement(Select, { + React.createElement(Select, { placeholder: '请输入或选择合同编号', - allowClear: true, - showSearch: true, - optionFilterProp: 'label', + allowClear: true, + showSearch: true, + optionFilterProp: 'label', style: filterControlStyle, - value: filters.contractCode, + value: filters.contractCode, onChange: function (v) { setFilters(function (f) { return patchFilters(f, { contractCode: v }); }); }, - options: contractCodeOptions - }) - ), + options: contractCodeOptions + }) + ), React.createElement('div', { key: 'projectName', style: filterItemStyle }, React.createElement('div', { style: filterLabelStyle }, '项目名称'), - React.createElement(Select, { - placeholder: '请输入或选择项目名称', - allowClear: true, - showSearch: true, - optionFilterProp: 'label', + React.createElement(Select, { + placeholder: '请输入或选择项目名称', + allowClear: true, + showSearch: true, + optionFilterProp: 'label', style: filterControlStyle, - value: filters.projectName, + value: filters.projectName, onChange: function (v) { setFilters(function (f) { return patchFilters(f, { projectName: v }); }); }, - options: projectNameOptions - }) - ), + options: projectNameOptions + }) + ), React.createElement('div', { key: 'customerName', style: filterItemStyle }, React.createElement('div', { style: filterLabelStyle }, '客户名称'), - React.createElement(Select, { - placeholder: '请输入或选择客户名称', - allowClear: true, - showSearch: true, - optionFilterProp: 'label', + React.createElement(Select, { + placeholder: '请输入或选择客户名称', + allowClear: true, + showSearch: true, + optionFilterProp: 'label', style: filterControlStyle, - value: filters.customerName, + value: filters.customerName, onChange: function (v) { setFilters(function (f) { return patchFilters(f, { customerName: v }); }); }, - options: customerNameOptions - }) - ), + options: customerNameOptions + }) + ), React.createElement('div', { key: 'deliveryRegion', style: filterItemStyle }, React.createElement('div', { style: filterLabelStyle }, '交车区域'), - React.createElement(Cascader, { - options: regionOptions, - placeholder: '请选择省-市', - allowClear: true, + 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; } + 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(' / ') : ''; } - }) - ), + }, + displayRender: function (labels) { return labels && labels.length ? labels.join(' / ') : ''; } + }) + ), React.createElement('div', { key: 'completedTime', style: filterItemStyle }, React.createElement('div', { style: filterLabelStyle }, '完成交车时间'), - React.createElement(RangePicker, { + React.createElement(RangePicker, { style: filterControlStyle, placeholder: ['请选择开始时间', '请选择结束时间'], - value: dateRangeValue, - onChange: onDateRangeChange - }) - ), + value: dateRangeValue, + onChange: onDateRangeChange + }) + ), React.createElement('div', { key: 'deliveryPerson', style: filterItemStyle }, React.createElement('div', { style: filterLabelStyle }, '交车人'), - React.createElement(Select, { + React.createElement(Select, { placeholder: '请输入或选择交车人', - allowClear: true, - showSearch: true, - optionFilterProp: 'label', + allowClear: true, + showSearch: true, + optionFilterProp: 'label', style: filterControlStyle, - value: filters.deliveryPerson, + value: filters.deliveryPerson, onChange: function (v) { setFilters(function (f) { return patchFilters(f, { deliveryPerson: v }); }); }, - options: deliveryPersonOptions - }) + options: deliveryPersonOptions + }) ), React.createElement('div', { key: 'vin', style: filterItemStyle }, React.createElement('div', { style: filterLabelStyle }, '车辆识别代码'), @@ -2881,7 +3258,11 @@ const Component = function () { 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(Button, { + type: 'default', + style: { borderRadius: 8, border: '1px solid #cbd5e1', fontWeight: 600, color: '#475569' }, + onClick: function () { setRequirementModalOpen(true); } + }, '查看需求说明') ), React.createElement(Card, { style: { marginBottom: 16 } }, React.createElement('div', { style: styles.cardBody }, @@ -2914,21 +3295,8 @@ const Component = function () { React.createElement(Card, null, 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 - }) - ), - React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', marginLeft: 'auto' } }, + React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', alignItems: 'center', marginBottom: 12, flexWrap: 'wrap', gap: 12 } }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' } }, React.createElement('span', { style: { fontSize: 13, color: '#64748b' } }, '当前标签:', React.createElement('span', { style: { color: '#334155', fontWeight: 600 } }, kpiExportLabelMap[kpiFilter] || '-'), @@ -2940,22 +3308,27 @@ const Component = function () { React.createElement(Table, { columns: listColumns, dataSource: displayList, - rowKey: 'id', - pagination: tablePagination, + rowKey: 'id', + pagination: tablePagination, tableLayout: 'fixed', scroll: { x: 1760 }, - size: 'middle' - }) + size: 'middle' + }) ) ), React.createElement(Modal, { - title: '需求说明', + title: '交车管理 — 说明文档', open: requirementModalOpen[0], onCancel: function () { setRequirementModalOpen(false); }, - footer: React.createElement(Button, { onClick: function () { setRequirementModalOpen(false); } }, '关闭'), - width: 800, + footer: null, + width: 880, + centered: true, destroyOnClose: true - }, React.createElement('div', { style: { maxHeight: '72vh', overflowY: 'auto', whiteSpace: 'pre-wrap', lineHeight: 1.65, fontSize: 13, color: '#334155' } }, requirementDocContent)), + }, React.createElement(Tabs, { + defaultActiveKey: 'prd', + size: 'small', + items: requirementModalItems + })), React.createElement(DeliveryEditDrawer, { open: editDrawer.open, record: editDrawer.record,