diff --git a/web端/工作台.jsx b/web端/工作台.jsx index 25b9383..70b2c29 100644 --- a/web端/工作台.jsx +++ b/web端/工作台.jsx @@ -1,5 +1,5 @@ // 【重要】必须使用 const Component 作为组件变量名 -// 数字化资产 ONEOS 运管平台 - 工作台(需求见文件内「查看需求说明」) +// 数字化资产 ONEOS 运管平台 - 工作台(参照原型布局 + Dashboard 风格) const Component = function () { var useState = React.useState; @@ -12,127 +12,378 @@ const Component = function () { var Col = antd.Col; var Card = antd.Card; var Statistic = antd.Statistic; - var List = antd.List; - var Tag = antd.Tag; var Tabs = antd.Tabs; var Badge = antd.Badge; + var Breadcrumb = antd.Breadcrumb; var Button = antd.Button; + var Dropdown = antd.Dropdown; var Space = antd.Space; - var Divider = antd.Divider; var Modal = antd.Modal; - var Table = antd.Table; var Popover = antd.Popover; + var Table = antd.Table; + var Select = antd.Select; + var DatePicker = antd.DatePicker; + var Tag = antd.Tag; var Typography = antd.Typography; var message = antd.message; + var Tooltip = antd.Tooltip; var Text = Typography.Text; + var Title = Typography.Title; - var layoutStyle = { padding: '16px 20px 24px', background: '#f0f2f5', minHeight: '100vh' }; - var cardStyle = { borderRadius: 8, boxShadow: '0 1px 2px rgba(0,0,0,0.06)' }; + var pageBg = '#f5f7fa'; + var cardRadius = 12; + var cardShadow = '0 1px 2px rgba(0,0,0,0.04), 0 4px 12px rgba(0,0,0,0.06)'; + var cardStyle = { borderRadius: cardRadius, boxShadow: cardShadow, border: '1px solid rgba(0,0,0,0.04)' }; + var accentBlue = '#1677ff'; + var accentPurple = '#722ed1'; + var accentCyan = '#13c2c2'; + var accentOrange = '#fa8c16'; + var accentGeekblue = '#2f54eb'; + + // 工作台卡片内待办 / 通知默认展示条数 + var wbDashPreviewMax = 5; + var wbChartHeight = 168; + var wbBarTrackH = 72; + var wbBarWrapH = 96; function protoNav(hint) { message.info('跳转「' + hint + '」(原型,联调配置路由)'); } - var requirementOpenState = useState(false); - var requirementOpen = requirementOpenState[0]; - var setRequirementOpen = requirementOpenState[1]; + function formatWorkbenchFinanceYuan(amount) { + if (amount == null || typeof amount !== 'number' || isNaN(amount)) return '—'; + return amount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' 元'; + } - var auditModalState = useState(false); - var auditModalOpen = auditModalState[0]; - var setAuditModalOpen = auditModalState[1]; + function formatNoticeNow() { + var d = new Date(); + var pad = function (n) { return n < 10 ? '0' + n : '' + n; }; + return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()); + } + + // 原型:管理员催办后写入操作员「通知中心」;联调时管理员姓名取当前登录用户 + var mockAdminDisplayName = '张明'; + // 原型用例:工作台问候展示名;联调时替换为当前登录用户姓名(如 JWT / 用户信息里的 displayName) + var mockOperatorDisplayName = '陈思远'; var noticeModalState = useState(false); var noticeModalOpen = noticeModalState[0]; var setNoticeModalOpen = noticeModalState[1]; + var reportTabState = useState('task'); + var reportTab = reportTabState[0]; + var setReportTab = reportTabState[1]; + var roleTabState = useState('ops'); var roleTab = roleTabState[0]; var setRoleTab = roleTabState[1]; - var requirementDoc = [ - '一个「数字化资产ONEOS运管平台」中的「工作台」模块', - '#面包屑:工作台', - '', - '1.顶部为关键指标,以多个指标卡片分别展示,分别为:', - '1.1.待办工作:显示待办工作数量,包括以下任务:', - ' 1.1.1.运维侧:交车任务、调拨任务、异动任务、年审任务;', - ' 1.1.2.业管侧:商业险到期、租赁账单生成、提车应收款、还车应结款;', - ' 1.1.3.业管-能源部侧:氢费审核;', - ' 权限分配规则:业管、运维基层员工:仅显示当前登录人作为处理人的任务;业管、运维主管:显示所属部门所有未完成任务;', - '1.2.已完成工作:显示已完成工作数量,包括当前用户所有已完成的待办工作;', - '1.3.待审批任务:显示所有待审批任务数量,点击弹出卡片,列表显示流程到达时间、流程类型、发起时间、发起人、操作(去处理);', - '1.4.已审批任务:显示所有已审批任务数量,包括当前用户所有已完成的审批任务;', - '1.5.通知消息:显示所有通知消息数量,点击弹出卡片,列表显示时间、通知类型、内容;(含运维/业管/审批侧各类提醒格式,见需求原文)', - '', - '2.中间分为几个单独卡片:', - '2.1.我的待办清单:左侧;2.2.数据统计(车辆+合同);2.3.我的通知清单:右侧;', - '2.4.底部快速入口:按角色 Tab(业管、业管-能源部、运维、财务、安全、法务),图标+名称。', - '', - '(原型:数据为示例;权限与接口联调时对接。)' - ].join('\n'); + var todoMoreModalState = useState(false); + var todoMoreModalOpen = todoMoreModalState[0]; + var setTodoMoreModalOpen = todoMoreModalState[1]; - var todoPopoverContent = React.createElement('div', { style: { maxWidth: 360, fontSize: 12, lineHeight: 1.6 } }, - React.createElement(Text, { strong: true }, '包含任务类型'), - React.createElement('div', { style: { marginTop: 8 } }, - React.createElement('div', null, '【运维】交车、调拨、异动、年审'), - React.createElement('div', null, '【业管】商业险到期、租赁账单生成、提车应收款、还车应结款'), - React.createElement('div', null, '【业管-能源】氢费审核') - ), - React.createElement(Divider, { style: { margin: '8px 0' } }), - React.createElement(Text, { type: 'secondary' }, '基层员工:仅本人待办;主管:本部门全部未完成。') - ); + var overdueDeliveryModalState = useState(false); + var overdueDeliveryModalOpen = overdueDeliveryModalState[0]; + var setOverdueDeliveryModalOpen = overdueDeliveryModalState[1]; - var topMetrics = useMemo(function () { + var overdueReturnModalState = useState(false); + var overdueReturnModalOpen = overdueReturnModalState[0]; + var setOverdueReturnModalOpen = overdueReturnModalState[1]; + + // 业管-能源部 · 独立卡片「本日导入加氢明细条数」:0 条时卡片内显示提示文案(联调接接口) + var energyH2ImportTodayState = useState(0); + var energyH2ImportTodayCount = energyH2ImportTodayState[0]; + + // 财务部 · 独立卡片「能源账户充值金额」:元,null 表示无充值记录示意(联调接接口) + var financeEnergyRechargeYuanState = useState(null); + var financeEnergyRechargeYuan = financeEnergyRechargeYuanState[0]; + + var todoMoreTypeState = useState(undefined); + var todoMoreTaskType = todoMoreTypeState[0]; + var setTodoMoreTaskType = todoMoreTypeState[1]; + + var todoMoreDateStartState = useState(''); + var todoMoreDateStart = todoMoreDateStartState[0]; + var setTodoMoreDateStart = todoMoreDateStartState[1]; + + var todoMoreDateEndState = useState(''); + var todoMoreDateEnd = todoMoreDateEndState[0]; + var setTodoMoreDateEnd = todoMoreDateEndState[1]; + + var todoMoreStatusState = useState(undefined); + var todoMoreStatus = todoMoreStatusState[0]; + var setTodoMoreStatus = todoMoreStatusState[1]; + + var todoBoardFilterState = useState('pending'); + var todoBoardFilter = todoBoardFilterState[0]; + var setTodoBoardFilter = todoBoardFilterState[1]; + + var warningDeptState = useState('ops'); + var warningDeptKey = warningDeptState[0]; + var setWarningDeptKey = warningDeptState[1]; + + // 顶部警告卡片:按部门切换(逻辑见需求脑图「警告卡片」,联调接接口) + var warningDeptOrder = useMemo(function () { return [ - { key: 'todo', title: '待办工作', value: 18, color: '#722ed1', pop: true }, - { key: 'done', title: '已完成工作', value: 126, color: '#52c41a', pop: false }, - { key: 'auditPending', title: '待审批任务', value: 7, color: '#f5222d', pop: false, modal: 'audit' }, - { key: 'auditDone', title: '已审批任务', value: 89, color: '#1677ff', pop: false }, - { key: 'notice', title: '通知消息', value: 12, color: '#fa8c16', pop: false, modal: 'notice' } + { key: 'ops', label: '运维' }, + { key: 'business', label: '业管' }, + { key: 'energy', label: '业管-能源部' }, + { key: 'safety', label: '安全部' }, + { key: 'finance', label: '财务部' }, + { key: 'public', label: '流程审批' } ]; }, []); - var myTodoList = useMemo(function () { + var warningDeptTabItems = useMemo(function () { + return warningDeptOrder.map(function (d) { + return { key: d.key, label: d.label }; + }); + }, [warningDeptOrder]); + + var warningCardsByDept = useMemo(function () { + var g1 = 'linear-gradient(135deg,#fff1f0,#ffccc7)'; + var g2 = 'linear-gradient(135deg,#fff7e6,#ffd591)'; + var g3 = 'linear-gradient(135deg,#f9f0ff,#efdbff)'; + var g4 = 'linear-gradient(135deg,#e6f4ff,#bae0ff)'; + var g5 = 'linear-gradient(135deg,#e6fffb,#b5f5ec)'; + return { + ops: [ + { key: 'w_ops_delivery', title: '超期未交车', value: 5, iconBg: g1, icon: '交', color: '#f5222d' }, + { key: 'w_ops_return_cnt', title: '超期未还车数量', value: 3, iconBg: g2, icon: '还', color: accentOrange }, + { key: 'w_ops_inspect', title: '年审/等级评定', value: 2, iconBg: g3, icon: '审', color: accentPurple }, + { key: 'w_ops_maint', title: '超期未保养', value: 7, iconBg: g4, icon: '保', color: accentGeekblue }, + { key: 'w_ops_settle_om', title: '超期未核对还车应结款', value: 1, iconBg: g5, icon: '款', color: accentCyan } + ], + business: [ + { key: 'w_biz_lease', title: '超期未核对租赁账单', value: 4, iconBg: g3, icon: '租', color: accentPurple }, + { key: 'w_biz_pickup', title: '超期未核对提车应收款', value: 2, iconBg: g4, icon: '提', color: accentGeekblue }, + { key: 'w_biz_return_bs', title: '超期未核对还车应结款', value: 3, iconBg: g1, icon: '结', color: '#f5222d' }, + { key: 'w_biz_h2', title: '超期未核对氢费账单', value: 1, iconBg: g5, icon: '氢', color: accentCyan } + ], + energy: [ + { key: 'w_en_h2order', title: '超期未核对加氢订单', value: 6, iconBg: g5, icon: '氢', color: accentCyan }, + { key: 'w_en_h2_import_today', title: '本日导入加氢明细条数', value: 0, iconBg: g5, icon: '录', color: accentCyan }, + { key: 'w_en_return', title: '超期未核对还车应结款', value: 2, iconBg: g1, icon: '结', color: '#f5222d' } + ], + safety: [ + { key: 'w_safe_return', title: '超期未核对还车应结款', value: 1, iconBg: g1, icon: '结', color: '#f5222d' } + ], + finance: [ + { key: 'w_fin_lease_od', title: '租赁账单超期未收到款', value: 3, iconBg: g3, icon: '租', color: accentPurple }, + { key: 'w_fin_lease_part', title: '租赁账单未收全款', value: 2, iconBg: g2, icon: '全', color: accentOrange }, + { key: 'w_fin_pickup_od', title: '提车应收款超期未收到款', value: 2, iconBg: g4, icon: '提', color: accentGeekblue }, + { key: 'w_fin_pickup_part', title: '提车应收款未收全款', value: 1, iconBg: g2, icon: '款', color: accentOrange }, + { key: 'w_fin_return_od', title: '还车应结款超期未收到款', value: 4, iconBg: g1, icon: '逾', color: '#f5222d' }, + { key: 'w_fin_return_part', title: '还车应结款未收全款', value: 2, iconBg: g1, icon: '结', color: '#cf1322' }, + { key: 'w_fin_h2_od', title: '氢费账单超期未收到款', value: 1, iconBg: g5, icon: '氢', color: accentCyan }, + { key: 'w_fin_energy_recharge', title: '能源账户充值金额', value: 0, iconBg: g5, icon: '充', color: accentCyan }, + { key: 'w_fin_h2_part', title: '氢费账单未收全款', value: 1, iconBg: g5, icon: '费', color: '#13c2c2' } + ], + public: [ + { key: 'w_pub_audit', title: '超期未审核', value: 9, iconBg: g4, icon: '审', color: accentGeekblue } + ] + }; + }, []); + + var warningCardsVisible = useMemo(function () { + return warningCardsByDept[warningDeptKey] || warningCardsByDept.ops; + }, [warningCardsByDept, warningDeptKey]); + + // 顶部警告卡标题旁提示:脑图「警告卡片」末级说明原文(与 assets 脑图截图一致) + var workbenchWarningMetricHintByKey = useMemo(function () { + return { + w_ops_delivery: '超过交车结束时间未交', + w_ops_return_cnt: '合同过期车辆未还', + w_ops_inspect: '30天内未处理', + w_ops_maint: '超过保养项维护里程\n超过保养项维护时间', + w_ops_settle_om: '还车应结款超过15天未完成运维部还车应结款提交', + w_biz_lease: '租赁账单生成7天以上未完成费用明细填报', + w_biz_pickup: '提车应收款生成7天以上未完成费用明细填报', + w_biz_return_bs: '还车应结款生成15天以上未完成业务服务部还车应结款提交', + w_biz_h2: '租赁合同氢费为月结算时,超过30天间隔未生成新氢费账单', + w_en_h2order: '加氢记录形成后1天以上未核对', + w_en_h2_import_today: '展示当日已导入的加氢明细条数;为0时表示今日尚未导入,请确认是否有加氢明细需要导入。', + w_en_return: '还车应结款生成15天以上未完成能源采购部还车应结款提交', + w_safe_return: '还车应结款生成15天以上未完成安全部还车应结款提交', + w_fin_lease_od: '租赁账单审核后到财务部7天以上未收到款项', + w_fin_lease_part: '租赁账单审核后到账金额 < 应收金额', + w_fin_pickup_od: '提车应收款审核后到财务部7天以上未收到款项', + w_fin_pickup_part: '提车应收款审核后到账金额 < 应收金额', + w_fin_return_od: '还车应结款为正数时超期未收到款', + w_fin_return_part: '还车应结款审核后到账金额 < 应收金额', + w_fin_h2_od: '氢费账单审核后到财务部7天以上未收到款项', + w_fin_energy_recharge: '展示能源账户已充值金额;无记录时请确认是否有能源账户充值记录。', + w_fin_h2_part: '氢费账单审核后到账金额 < 应收金额', + w_pub_audit: '流程在当前流程超过24小时以上未审核' + }; + }, []); + + // 工作台-超期未交车弹窗:预计交车结束日早于当前日期的示意数据(联调接接口) + var overdueDeliveryMockRows = useMemo(function () { + var pad = function (n) { return n < 10 ? '0' + n : '' + n; }; + var fmt = 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; + }; + var d1 = addDays(-9); + var d2s = addDays(-24); + var d2e = addDays(-17); + var d3 = addDays(-5); + var d4s = addDays(-40); + var d4e = addDays(-32); + var d5 = addDays(-2); return [ - { id: '1', name: '交车任务 · 粤A12345 待确认交车单', time: '2026-02-27 09:00', path: 'web端/运维管理/车辆业务/交车管理.jsx' }, - { id: '2', name: '调拨任务 · 调拨单 DB-2026-009 待接收', time: '2026-02-26 15:20', path: 'web端/运维管理/车辆业务/调拨管理.jsx' }, - { id: '3', name: '异动任务 · 异动单待结束登记', time: '2026-02-26 11:00', path: 'web端/运维管理/车辆业务/异动管理-结束异动.jsx' }, - { id: '4', name: '年审任务 · 粤B11111 年审材料待上传', time: '2026-02-25 10:30', path: 'web端/运维管理/车辆业务/异动管理.jsx' }, - { id: '5', name: '商业险到期 · 粤C22334 续保跟进', time: '2026-02-24 14:00', path: 'web端/车辆管理.jsx' }, - { id: '6', name: '租赁账单生成 · 项目「华南物流」2月账单', time: '2026-02-24 09:00', path: 'web端/业务管理/租赁账单.jsx' }, - { id: '7', name: '提车应收款 · TK-2026-018 待提交', time: '2026-02-23 16:00', path: 'web端/财务管理/提车应收款.jsx' }, - { id: '8', name: '还车应结款 · HC-2026-006 待核对', time: '2026-02-23 11:00', path: 'web端/财务管理/还车应结款.jsx' }, - { id: '9', name: '氢费审核 · 加氢订单待审核', time: '2026-02-22 08:30', path: 'web端/加氢站管理/加氢订单.jsx' } + { id: 'wb_od_1', expectedDate: fmt(d1), contractCode: 'HT-ZL-2026-011', projectName: '嘉兴氢能示范项目', customerName: '嘉兴某某物流有限公司', deliveryRegion: '浙江省-嘉兴市', deliveryAddress: '嘉兴市南湖区科技大道1号', deliveryCount: 2, vehicleList: [{ vehicleType: '厢式货车', brand: '东风', model: 'DFH1180', plateNo: '-', deliveryTime: '-', deliveryPerson: '-' }, { vehicleType: '平板货车', brand: '福田', model: 'BJ1180', plateNo: '-', deliveryTime: '-', deliveryPerson: '-' }], createTime: fmt(addDays(-28)) + ' 09:00', createBy: '系统', lastModifyTime: fmt(addDays(-11)) + ' 14:30', lastModifyBy: '李四', assignedDeliveryPerson: '张三' }, + { id: 'wb_od_2', expectedDate: fmt(d2s) + ' 至 ' + fmt(d2e), contractCode: 'HT-ZL-2026-012', projectName: '上海物流租赁项目', customerName: '上海某某运输公司', deliveryRegion: '上海市-上海市', deliveryAddress: '浦东新区张江高科技园区', deliveryCount: 1, vehicleList: [{ vehicleType: '厢式货车', brand: '江淮', model: 'HFC1180', plateNo: '-', deliveryTime: '-', deliveryPerson: '-' }], createTime: fmt(addDays(-30)) + ' 10:30', createBy: '王五', lastModifyTime: fmt(addDays(-20)) + ' 11:00', lastModifyBy: '王五', assignedDeliveryPerson: '李四' }, + { id: 'wb_od_3', expectedDate: fmt(d3), contractCode: 'HT-ZL-2026-013', projectName: '杭州城配租赁项目', customerName: '杭州某某租赁有限公司', deliveryRegion: '浙江省-杭州市', deliveryAddress: '余杭区未来科技城', deliveryCount: 3, vehicleList: [{ vehicleType: '栏板货车', brand: '重汽', model: 'ZZ1180', plateNo: '-', deliveryTime: '-', deliveryPerson: '-' }, { vehicleType: '厢式货车', brand: '东风', model: 'DFH1190', plateNo: '-', deliveryTime: '-', deliveryPerson: '-' }, { vehicleType: '平板货车', brand: '福田', model: 'BJ1190', plateNo: '-', deliveryTime: '-', deliveryPerson: '-' }], createTime: fmt(addDays(-26)) + ' 14:00', createBy: '李四', lastModifyTime: fmt(addDays(-6)) + ' 09:15', lastModifyBy: '王五', assignedDeliveryPerson: '张三' }, + { id: 'wb_od_4', expectedDate: fmt(d4s) + ' 至 ' + fmt(d4e), contractCode: 'HT-ZL-2026-014', projectName: '嘉兴氢能示范项目', customerName: '嘉兴某某物流有限公司', deliveryRegion: '浙江省-嘉兴市', deliveryAddress: '嘉兴市秀洲区洪兴西路288号', deliveryCount: 1, vehicleList: [{ vehicleType: '厢式货车', brand: '重汽', model: 'ZZ1160', plateNo: '-', deliveryTime: '-', deliveryPerson: '-' }], createTime: fmt(addDays(-45)) + ' 08:15', createBy: '张三', lastModifyTime: fmt(addDays(-33)) + ' 16:40', lastModifyBy: '张三', assignedDeliveryPerson: '李四' }, + { id: 'wb_od_5', expectedDate: fmt(d5), contractCode: 'HT-ZL-2026-015', projectName: '上海物流租赁项目', customerName: '上海某某运输公司', deliveryRegion: '上海市-上海市', deliveryAddress: '闵行区莘庄工业区申富路669号', deliveryCount: 2, vehicleList: [{ vehicleType: '平板货车', brand: '福田', model: 'BJ1180', plateNo: '-', deliveryTime: '-', deliveryPerson: '-' }, { vehicleType: '栏板货车', brand: '东风', model: 'DFH1160', plateNo: '-', deliveryTime: '-', deliveryPerson: '-' }], createTime: fmt(addDays(-18)) + ' 11:20', createBy: '王五', lastModifyTime: fmt(addDays(-3)) + ' 10:05', lastModifyBy: '王五', assignedDeliveryPerson: '王五' } ]; }, []); - var noticeList = useMemo(function () { + var openOverdueDeliveryModal = useCallback(function () { + setOverdueDeliveryModalOpen(true); + }, []); + + // 工作台-超期未还车:与还车管理-待处理列表字段一致的示意数据(联调接接口) + var overdueReturnMockRows = 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 fmtDt = function (d, hh, mm) { + return fmtD(d) + ' ' + pad(hh) + ':' + pad(mm); + }; + 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; + }; + var a = addDays(-52); + var b = addDays(-38); + var c = addDays(-25); return [ - { id: 'n1', time: '2026-02-27 08:00', type: '商业险到期提醒', content: '「粤A12345」商业险将在2026-04-15到期,请尽快处理' }, - { id: 'n2', time: '2026-02-26 18:00', type: '营运证到期提醒', content: '「粤B88888」营运证还有90天到期,请尽快更新营运证' }, - { id: 'n3', time: '2026-02-26 10:00', type: '行驶证到期提醒', content: '「浙A11111」行驶证还有90天到期,请尽快进行年审' }, - { id: 'n4', time: '2026-02-25 14:00', type: '租赁合同到期提醒', content: '「HT-2025-088」「某某产业园项目」还有30天到期,请尽快处理' }, - { id: 'n5', time: '2026-02-25 09:00', type: '租赁账单生成提醒', content: '「HT-2025-088」「某某产业园项目」「ZD-202602-031」已生成,请尽快处理' }, - { id: 'n6', time: '2026-02-24 11:00', type: '氢费余额不足提醒', content: '「某某物流」「华南干线项目」氢费余额已不足500元,请尽快通知客户处理' }, - { id: 'n7', time: '2026-02-24 08:30', type: '审批流程提醒', content: '「李四」「异动审核」审批节点已到达,请进行审批' } + { id: 'wb_or_1', deliveryTime: fmtDt(a, 9, 30), deliveryPerson: '张三', plateNo: '京A12345', vehicleType: '重型厢式货车', brand: '东风', model: 'DFH1180', vin: 'LGHXCAE28M1234567', contractCode: 'HT-ZL-2026-001', customerName: '嘉兴某某物流有限公司', projectName: '嘉兴氢能示范项目', dept: '华东业务部', bizOwner: '李经理', vehicleArrived: false }, + { id: 'wb_or_2', deliveryTime: fmtDt(b, 14, 0), deliveryPerson: '李四', plateNo: '沪B20001', vehicleType: '厢式货车', brand: '福田', model: 'BJ1180', vin: 'LGHXCAE28M7654321', contractCode: 'HT-ZL-2026-002', customerName: '上海某某运输公司', projectName: '上海物流租赁项目', dept: '华东业务部', bizOwner: '王经理', vehicleArrived: true }, + { id: 'wb_or_3', deliveryTime: fmtDt(c, 10, 15), deliveryPerson: '王五', plateNo: '浙A88888', vehicleType: '轻型厢式货车', brand: '江淮', model: 'HFC1180', vin: 'LGHXCAE28M8888888', contractCode: 'HT-ZL-2026-003', customerName: '杭州某某租赁有限公司', projectName: '杭州城配租赁项目', dept: '浙江业务部', bizOwner: '赵经理', vehicleArrived: false } ]; }, []); - var pendingAuditList = useMemo(function () { + var openOverdueReturnModal = useCallback(function () { + setOverdueReturnModalOpen(true); + }, []); + + // 待办任务表(原型:任务类型、任务名称、状态、生成时间、操作) + var dashboardTodoRows = useMemo(function () { return [ - { id: 'a1', arriveAt: '2026-02-27 09:15', flowType: '异动审核(运维)', startAt: '2026-02-26 14:00', starter: '张三', path: 'web端/运维管理/车辆业务/异动管理.jsx' }, - { id: 'a2', arriveAt: '2026-02-27 08:40', flowType: '调拨审核(运维)', startAt: '2026-02-25 10:00', starter: '王五', path: 'web端/运维管理/车辆业务/调拨管理.jsx' }, - { id: 'a3', arriveAt: '2026-02-26 16:00', flowType: '替换车审核(运维)', startAt: '2026-02-26 09:30', starter: '赵六', path: 'web端/运维管理/车辆业务/替换车管理.jsx' }, - { id: 'a4', arriveAt: '2026-02-26 11:20', flowType: '租赁账单审核(财务)', startAt: '2026-02-25 17:00', starter: '业管-陈七', path: 'web端/财务管理/租赁账单.jsx' }, - { id: 'a5', arriveAt: '2026-02-25 15:00', flowType: '提车应收款审核(财务)', startAt: '2026-02-24 11:00', starter: '业管-周八', path: 'web端/财务管理/提车应收款.jsx' }, - { id: 'a6', arriveAt: '2026-02-25 10:00', flowType: '还车应结款审核(财务)', startAt: '2026-02-23 16:00', starter: '业管-吴九', path: 'web端/财务管理/还车应结款.jsx' }, - { id: 'a7', arriveAt: '2026-02-24 14:00', flowType: '氢费账单审核(财务)', startAt: '2026-02-23 09:00', starter: '能源-郑十', path: 'web端/财务管理/氢费账单.jsx' }, - { id: 'a8', arriveAt: '2026-02-24 09:00', flowType: '租赁合同审核(法务·附件)', startAt: '2026-02-22 10:00', starter: '业管-钱一', path: 'web端/车辆租赁合同/车辆租赁合同.jsx' }, - { id: 'a9', arriveAt: '2026-02-23 11:00', flowType: 'CEO · 提车应收款', startAt: '2026-02-20 15:00', starter: '财务-孙二', path: 'web端/财务管理/提车应收款.jsx' } + { id: '1', taskType: '交车', taskName: '交车任务 · 粤A12345 待确认交车单', genDate: '2026-02-27', path: 'web端/运维管理/车辆业务/交车管理.jsx', status: 'pending' }, + { id: '2', taskType: '调拨', taskName: '调拨任务 · 调拨单 DB-2026-009 待接收', genDate: '2026-02-26', path: 'web端/运维管理/车辆业务/调拨管理.jsx', status: 'overdue' }, + { id: '3', taskType: '异动', taskName: '异动任务 · 异动单待结束登记', genDate: '2026-02-26', path: 'web端/运维管理/车辆业务/异动管理-结束异动.jsx', status: 'pending' }, + { id: '4', taskType: '年审', taskName: '年审任务 · 粤B11111 年审材料待上传', genDate: '2026-02-25', path: 'web端/运维管理/车辆业务/异动管理.jsx', status: 'overdue' }, + { id: '5', taskType: '保险', taskName: '商业险到期 · 粤C22334 续保跟进', genDate: '2026-02-24', path: 'web端/车辆管理.jsx', status: 'pending' }, + { id: '6', taskType: '租赁账单', taskName: '租赁账单生成 · 项目「华南物流」2月账单', genDate: '2026-02-24', path: 'web端/业务管理/租赁账单.jsx', status: 'pending' }, + { id: '7', taskType: '审批中心', taskName: '提车应收款 · TK-2026-018 待提交', genDate: '2026-02-23', path: 'web端/财务管理/提车应收款.jsx', status: 'done' }, + { id: '8', taskType: '审批中心', taskName: '租赁合同审核 · HT-2025-088 法务附件', genDate: '2026-02-22', path: 'web端/车辆租赁合同/车辆租赁合同.jsx', status: 'done' } ]; }, []); + var todoSummary = useMemo(function () { + var p = 0, o = 0, d = 0; + dashboardTodoRows.forEach(function (r) { + if (r.status === 'pending') p++; + else if (r.status === 'overdue') o++; + else if (r.status === 'done') d++; + }); + return { pending: p, overdue: o, done: d }; + }, [dashboardTodoRows]); + + var dashboardTodoBoardRows = useMemo(function () { + return dashboardTodoRows.filter(function (r) { + return r.status === todoBoardFilter; + }); + }, [dashboardTodoRows, todoBoardFilter]); + + var dashboardTodoBoardPreviewRows = useMemo(function () { + return dashboardTodoBoardRows.slice(0, wbDashPreviewMax); + }, [dashboardTodoBoardRows]); + + // 全部待办弹窗:任务类型下拉展示业务全量类型(与示意数据并集,联调可改为接口枚举) + var todoMoreTaskTypeFilterOptions = useMemo(function () { + var catalog = [ + '交车', '调拨', '异动', '年审', '保险', '租赁账单', '审批中心', + '还车', '备车', '提车应收', '替换车', '违章', '事故', '充电', 'ETC', '能源账户', '氢费', '电费' + ]; + var seen = {}; + catalog.forEach(function (t) { seen[t] = true; }); + dashboardTodoRows.forEach(function (r) { + if (r.taskType) seen[r.taskType] = true; + }); + return Object.keys(seen).sort(function (a, b) { return a.localeCompare(b, 'zh-CN'); }).map(function (t) { + return { value: t, label: t }; + }); + }, [dashboardTodoRows]); + + var todoMoreStatusFilterOptions = useMemo(function () { + return [ + { value: 'pending', label: '待处理' }, + { value: 'overdue', label: '已超时' }, + { value: 'done', label: '已完成' } + ]; + }, []); + + var noticeListState = useState(function () { + return [ + { id: 'n1', time: '2026-02-27 08:00', type: '商业险到期提醒', content: '「粤A12345」商业险将在2026-04-15到期,请尽快处理', read: false }, + { id: 'n2', time: '2026-02-26 18:00', type: '营运证到期提醒', content: '「粤B88888」营运证还有90天到期,请尽快更新营运证', read: false }, + { id: 'n3', time: '2026-02-26 10:00', type: '行驶证到期提醒', content: '「浙A11111」行驶证还有90天到期,请尽快进行年审', read: false }, + { id: 'n4', time: '2026-02-25 14:00', type: '租赁合同到期提醒', content: '「HT-2025-088」「某某产业园项目」还有30天到期,请尽快处理', read: false }, + { id: 'n5', time: '2026-02-25 09:00', type: '租赁账单生成提醒', content: '「HT-2025-088」「某某产业园项目」「ZD-202602-031」已生成,请尽快处理', read: false }, + { id: 'n6', time: '2026-02-24 11:00', type: '氢费余额不足提醒', content: '「某某物流」「华南干线项目」氢费余额已不足500元,请尽快通知客户处理', read: false }, + { id: 'n7', time: '2026-02-24 08:30', type: '审批流程提醒', content: '「李四」「异动审核」审批节点已到达,请进行审批', read: false } + ]; + }); + var noticeList = noticeListState[0]; + var setNoticeList = noticeListState[1]; + + var noticeNewCount = useMemo(function () { + var n = 0; + (noticeList || []).forEach(function (it) { + if (!it.read) n++; + }); + return n; + }, [noticeList]); + + var markAllNoticesRead = useCallback(function () { + setNoticeList(function (prev) { + return (prev || []).map(function (n) { + return Object.assign({}, n, { read: true }); + }); + }); + }, []); + + var openNoticeModal = useCallback(function () { + markAllNoticesRead(); + setNoticeModalOpen(true); + }, [markAllNoticesRead]); + + var pushUrgeNotice = useCallback(function (taskRow) { + var adminName = mockAdminDisplayName; + var content = '(' + adminName + ')已对(' + taskRow.taskName + ')进行催办,请尽快处理'; + setNoticeList(function (prev) { + return [{ id: 'urge-' + Date.now(), time: formatNoticeNow(), type: '催办提醒', content: content, read: false }].concat(prev || []); + }); + message.success('催办提醒已推送至通知列表'); + }, []); + var vehicleMonthBars = useMemo(function () { return [ { m: '3月', op: 62, idle: 18 }, { m: '4月', op: 65, idle: 16 }, { m: '5月', op: 68, idle: 15 }, @@ -144,31 +395,438 @@ const Component = function () { var contractMonthBars = useMemo(function () { return [ - { m: '3月', lease: 8, self: 3 }, { m: '4月', lease: 9, self: 3 }, { m: '5月', lease: 10, self: 4 }, - { m: '6月', lease: 10, self: 4 }, { m: '7月', lease: 11, self: 4 }, { m: '8月', lease: 12, self: 5 }, - { m: '9月', lease: 12, self: 5 }, { m: '10月', lease: 13, self: 5 }, { m: '11月', lease: 14, self: 6 }, - { m: '12月', lease: 14, self: 6 }, { m: '1月', lease: 15, self: 6 }, { m: '2月', lease: 16, self: 7 } + { m: '3月', lease: 8, self: 3, renew: 2 }, { m: '4月', lease: 9, self: 3, renew: 2 }, { m: '5月', lease: 10, self: 4, renew: 3 }, + { m: '6月', lease: 10, self: 4, renew: 3 }, { m: '7月', lease: 11, self: 4, renew: 4 }, { m: '8月', lease: 12, self: 5, renew: 3 }, + { m: '9月', lease: 12, self: 5, renew: 4 }, { m: '10月', lease: 13, self: 5, renew: 5 }, { m: '11月', lease: 14, self: 6, renew: 4 }, + { m: '12月', lease: 14, self: 6, renew: 5 }, { m: '1月', lease: 15, self: 6, renew: 5 }, { m: '2月', lease: 16, self: 7, renew: 6 } ]; }, []); - function renderBarGroup(items, k1, k2, c1, c2) { + // 任务处理情况:横轴员工、纵轴任务数(示意,联调接接口) + var taskEmployeeBars = useMemo(function () { + return [ + { m: '陈思远', done: 18, overdue: 2 }, + { m: '张明', done: 22, overdue: 1 }, + { m: '李小琳', done: 15, overdue: 4 }, + { m: '王强', done: 9, overdue: 3 }, + { m: '赵敏', done: 24, overdue: 0 }, + { m: '周杰', done: 11, overdue: 5 } + ]; + }, []); + + // 合同到账:应收 / 已收(元,示意;联调接接口) + var contractPaymentDonut = useMemo(function () { + return { receivable: 128500000, received: 94200000 }; + }, []); + + function renderBarGroup(items, k1, k2, c1, c2, labelMinWidth) { var max = 1; items.forEach(function (it) { max = Math.max(max, it[k1] + it[k2]); }); - return React.createElement('div', { style: { display: 'flex', alignItems: 'flex-end', gap: 4, height: 140, paddingTop: 8, overflowX: 'auto' } }, + var colMinW = labelMinWidth != null ? labelMinWidth : 28; + return React.createElement('div', { style: { display: 'flex', alignItems: 'flex-end', gap: 5, height: wbBarWrapH, paddingTop: 8, overflowX: 'auto' } }, items.map(function (it) { var h1 = Math.round((it[k1] / max) * 100); var h2 = Math.round((it[k2] / max) * 100); - return React.createElement('div', { key: it.m, style: { display: 'flex', flexDirection: 'column', alignItems: 'center', minWidth: 26 } }, - React.createElement('div', { style: { display: 'flex', alignItems: 'flex-end', gap: 2, height: 108 } }, - React.createElement('div', { style: { width: 9, height: h1, background: c1, borderRadius: 2 } }), - React.createElement('div', { style: { width: 9, height: h2, background: c2, borderRadius: 2 } }) + return React.createElement('div', { key: it.m, style: { display: 'flex', flexDirection: 'column', alignItems: 'center', minWidth: colMinW } }, + React.createElement('div', { style: { display: 'flex', alignItems: 'flex-end', gap: 3, height: wbBarTrackH } }, + React.createElement('div', { style: { width: 10, height: Math.max(h1, 4), background: c1, borderRadius: 3 } }), + React.createElement('div', { style: { width: 10, height: Math.max(h2, 4), background: c2, borderRadius: 3 } }) ), - React.createElement('span', { style: { fontSize: 10, color: 'rgba(0,0,0,0.45)', marginTop: 4 } }, it.m) + React.createElement('span', { style: { fontSize: 11, color: 'rgba(0,0,0,0.45)', marginTop: 6, textAlign: 'center', maxWidth: colMinW + 24, wordBreak: 'break-all', lineHeight: 1.2 } }, it.m) ); }) ); } + // 合同情况:每月三组柱 — 租赁 / 自营 / 续签(数量);纵轴刻度按全局最大单柱值 + function renderBarGroupTriple(items, k1, k2, k3, c1, c2, c3, labelMinWidth) { + var max = 1; + items.forEach(function (it) { + max = Math.max(max, it[k1] || 0, it[k2] || 0, it[k3] || 0); + }); + var colMinW = labelMinWidth != null ? labelMinWidth : 40; + var barW = 8; + return React.createElement('div', { style: { display: 'flex', alignItems: 'flex-end', gap: 6, height: wbBarWrapH, paddingTop: 8, overflowX: 'auto' } }, + items.map(function (it) { + var h1 = Math.round(((it[k1] || 0) / max) * 100); + var h2 = Math.round(((it[k2] || 0) / max) * 100); + var h3 = Math.round(((it[k3] || 0) / max) * 100); + return React.createElement('div', { key: it.m, style: { display: 'flex', flexDirection: 'column', alignItems: 'center', minWidth: colMinW } }, + React.createElement('div', { style: { display: 'flex', alignItems: 'flex-end', gap: 2, height: wbBarTrackH } }, + React.createElement('div', { style: { width: barW, height: Math.max(h1, (it[k1] || 0) > 0 ? 4 : 0), background: c1, borderRadius: 2 } }), + React.createElement('div', { style: { width: barW, height: Math.max(h2, (it[k2] || 0) > 0 ? 4 : 0), background: c2, borderRadius: 2 } }), + React.createElement('div', { style: { width: barW, height: Math.max(h3, (it[k3] || 0) > 0 ? 4 : 0), background: c3, borderRadius: 2 } }) + ), + React.createElement('span', { style: { fontSize: 11, color: 'rgba(0,0,0,0.45)', marginTop: 6, textAlign: 'center', maxWidth: colMinW + 20, wordBreak: 'break-all', lineHeight: 1.2 } }, it.m) + ); + }) + ); + } + + // 任务处理情况:双柱在统计卡片内随 Tab 区域伸缩;含 Y/X 轴刻度与悬浮数据标签 + function renderTaskEmployeeBarChart(items, k1, k2, c1, c2, labelMinWidth) { + var maxVal = 1; + items.forEach(function (it) { maxVal = Math.max(maxVal, it[k1] + it[k2]); }); + var midVal = Math.max(0, Math.round(maxVal / 2)); + var colMinW = labelMinWidth != null ? labelMinWidth : 40; + var axisMuted = '#707d8f'; + var axisLine = 'rgba(0,0,0,0.12)'; + + function wrapColTooltip(it, rowKey, colNode) { + var tipTitle = React.createElement('div', { style: { fontSize: 12, lineHeight: 1.6 } }, + React.createElement('div', { style: { fontWeight: 600, marginBottom: 4, color: 'rgba(0,0,0,0.88)' } }, it.m), + React.createElement('div', { style: { color: c1 } }, '完成任务:' + it[k1]), + React.createElement('div', { style: { color: c2 } }, '超时任务:' + it[k2]) + ); + if (Tooltip) { + return React.createElement(Tooltip, { key: rowKey, title: tipTitle, placement: 'top', mouseEnterDelay: 0.05 }, colNode); + } + return React.cloneElement(colNode, { key: rowKey }); + } + + var barColumns = items.map(function (it) { + var p1 = maxVal > 0 ? Math.round((it[k1] / maxVal) * 1000) / 10 : 0; + var p2 = maxVal > 0 ? Math.round((it[k2] / maxVal) * 1000) / 10 : 0; + var colInner = React.createElement('div', { + style: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + flex: '1 1 0', + minWidth: colMinW, + minHeight: 0, + overflow: 'hidden', + cursor: 'default' + } + }, + React.createElement('div', { + className: 'workbench-task-bar-track', + style: { + flex: 1, + width: '100%', + minHeight: 48, + display: 'flex', + alignItems: 'flex-end', + justifyContent: 'center', + gap: 'clamp(2px, 6%, 8px)', + boxSizing: 'border-box', + padding: '4px 2px 0', + position: 'relative', + background: 'linear-gradient(to top, transparent 0%, transparent calc(50% - 0.5px), rgba(0,0,0,0.05) calc(50%), transparent calc(50% + 0.5px), transparent 100%)' + } + }, + React.createElement('div', { + className: 'workbench-task-bar-pillar', + style: { + width: 'clamp(7px, 32%, 16px)', + flex: '0 0 auto', + height: it[k1] > 0 ? p1 + '%' : 0, + minHeight: it[k1] > 0 ? 4 : 0, + background: c1, + borderRadius: 3, + alignSelf: 'flex-end', + transition: 'opacity .15s' + } + }), + React.createElement('div', { + className: 'workbench-task-bar-pillar', + style: { + width: 'clamp(7px, 32%, 16px)', + flex: '0 0 auto', + height: it[k2] > 0 ? p2 + '%' : 0, + minHeight: it[k2] > 0 ? 4 : 0, + background: c2, + borderRadius: 3, + alignSelf: 'flex-end', + transition: 'opacity .15s' + } + }) + ), + React.createElement('span', { + style: { + flexShrink: 0, + fontSize: 11, + color: 'rgba(0,0,0,0.45)', + marginTop: 6, + textAlign: 'center', + maxWidth: '100%', + wordBreak: 'break-all', + lineHeight: 1.2, + paddingBottom: 2 + } + }, it.m) + ); + return wrapColTooltip(it, it.m, colInner); + }); + + return React.createElement('div', { className: 'workbench-task-chart-axes-wrap' }, + React.createElement('div', { className: 'workbench-task-chart-plot-row' }, + React.createElement('div', { + className: 'workbench-task-y-title', + style: { + width: 22, + flexShrink: 0, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + fontSize: 11, + color: axisMuted, + fontWeight: 400, + writingMode: 'vertical-rl', + transform: 'rotate(180deg)', + letterSpacing: 1, + userSelect: 'none' + } + }, '任务数量'), + React.createElement('div', { + className: 'workbench-task-y-ticks', + style: { + width: 30, + flexShrink: 0, + display: 'flex', + flexDirection: 'column', + justifyContent: 'space-between', + alignItems: 'flex-end', + padding: '6px 6px 6px 0', + borderRight: '1px solid ' + axisLine, + fontSize: 10, + color: axisMuted, + lineHeight: 1.2, + boxSizing: 'border-box', + userSelect: 'none' + } + }, + React.createElement('span', null, maxVal), + React.createElement('span', null, midVal), + React.createElement('span', null, '0') + ), + React.createElement('div', { + style: { + flex: 1, + minWidth: 0, + display: 'flex', + flexDirection: 'column', + minHeight: 0 + } + }, + React.createElement('div', { className: 'workbench-task-bar-chart' }, barColumns), + React.createElement('div', { + className: 'workbench-task-x-axis', + style: { + flexShrink: 0, + borderTop: '1px solid ' + axisLine, + textAlign: 'center', + fontSize: 11, + color: axisMuted, + fontWeight: 400, + paddingTop: 6, + marginTop: 2, + userSelect: 'none' + } + }, '员工') + ) + ) + ); + } + + // 合同到账情况:环形图 — 全环面积对应应收款;分色为已收/未收;中心为已收款金额;扇区悬浮标签 + function formatContractMoneyBrief(n) { + if (n == null || isNaN(n)) return '-'; + var v = Number(n); + if (Math.abs(v) >= 100000000) return (v / 100000000).toFixed(2) + ' 亿元'; + if (Math.abs(v) >= 10000) { + var w = v / 10000; + var s = w % 1 === 0 ? w.toFixed(0) : w.toFixed(1); + return s + ' 万元'; + } + return v.toLocaleString('zh-CN') + ' 元'; + } + + function renderContractPaymentDonutChart(d) { + var rec = Math.max(0, d.receivable || 0); + var got = Math.max(0, Math.min(d.received || 0, rec)); + var pend = Math.max(0, rec - got); + var ratio = rec > 0 ? got / rec : 0; + var angleReceived = ratio * 360; + var cReceived = '#1677ff'; + var cPending = '#d9d9d9'; + + function donutSeg(cx, cy, r0, r1, a0, a1) { + if (a1 - a0 < 0.04) return ''; + var rad = function (deg) { return (deg - 90) * Math.PI / 180; }; + var x1 = cx + r1 * Math.cos(rad(a0)); + var y1 = cy + r1 * Math.sin(rad(a0)); + var x2 = cx + r1 * Math.cos(rad(a1)); + var y2 = cy + r1 * Math.sin(rad(a1)); + var x3 = cx + r0 * Math.cos(rad(a1)); + var y3 = cy + r0 * Math.sin(rad(a1)); + var x4 = cx + r0 * Math.cos(rad(a0)); + var y4 = cy + r0 * Math.sin(rad(a0)); + var large = (a1 - a0) > 180 ? 1 : 0; + return 'M' + x1 + ',' + y1 + ' A' + r1 + ',' + r1 + ' 0 ' + large + ' 1 ' + x2 + ',' + y2 + ' L' + x3 + ',' + y3 + ' A' + r0 + ',' + r0 + ' 0 ' + large + ' 0 ' + x4 + ',' + y4 + ' Z'; + } + + var cx = 100; + var cy = 100; + var r0 = 54; + var r1 = 90; + var pathReceived = + angleReceived >= 359.95 ? donutSeg(cx, cy, r0, r1, 0, 360) : (angleReceived > 0.08 ? donutSeg(cx, cy, r0, r1, 0, angleReceived) : ''); + var pathPending = + angleReceived <= 0.05 ? donutSeg(cx, cy, r0, r1, 0, 360) : (angleReceived < 359.92 ? donutSeg(cx, cy, r0, r1, angleReceived, 360) : ''); + + var pctGot = rec > 0 ? ((got / rec) * 100).toFixed(1) : '0'; + var pctPend = rec > 0 ? ((pend / rec) * 100).toFixed(1) : '0'; + + var tipReceived = React.createElement('div', { style: { fontSize: 12, lineHeight: 1.65 } }, + React.createElement('div', { style: { fontWeight: 600 } }, '已收款'), + React.createElement('div', null, '金额:' + formatContractMoneyBrief(got)), + React.createElement('div', { style: { color: 'rgba(0,0,0,0.55)' } }, '占应收款:' + pctGot + '%') + ); + var tipPending = React.createElement('div', { style: { fontSize: 12, lineHeight: 1.65 } }, + React.createElement('div', { style: { fontWeight: 600 } }, '未到账'), + React.createElement('div', null, '金额:' + formatContractMoneyBrief(pend)), + React.createElement('div', { style: { color: 'rgba(0,0,0,0.55)' } }, '占应收款:' + pctPend + '%') + ); + + var pathRecvEl = pathReceived + ? React.createElement('path', { + d: pathReceived, + fill: cReceived, + stroke: '#fff', + strokeWidth: 1.5, + style: { cursor: 'pointer' } + }) + : null; + var pathPendEl = pathPending + ? React.createElement('path', { + d: pathPending, + fill: cPending, + stroke: '#fff', + strokeWidth: 1.5, + style: { cursor: 'pointer' } + }) + : null; + + if (Tooltip && pathRecvEl) { + pathRecvEl = React.createElement(Tooltip, { title: tipReceived, placement: 'top', mouseEnterDelay: 0.05 }, pathRecvEl); + } + if (Tooltip && pathPendEl) { + pathPendEl = React.createElement(Tooltip, { title: tipPending, placement: 'top', mouseEnterDelay: 0.05 }, pathPendEl); + } + + return React.createElement('div', { className: 'workbench-payment-donut-hold' }, + React.createElement('div', { + className: 'workbench-payment-donut-inner', + style: { + position: 'relative', + width: 'min(280px, 78%)', + maxWidth: 300, + aspectRatio: '1', + flexShrink: 0 + } + }, + React.createElement('svg', { + viewBox: '0 0 200 200', + width: '100%', + height: '100%', + style: { display: 'block' } + }, + React.createElement('g', null, pathPendEl, pathRecvEl) + ), + React.createElement('div', { + style: { + position: 'absolute', + left: 0, + right: 0, + top: 0, + bottom: 0, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + pointerEvents: 'none', + padding: '0 12%', + textAlign: 'center' + } + }, + React.createElement('span', { style: { fontSize: 12, color: 'rgba(0,0,0,0.45)', lineHeight: 1.4 } }, '已收款金额'), + React.createElement('span', { style: { fontSize: 20, fontWeight: 600, color: 'rgba(0,0,0,0.88)', lineHeight: 1.35, marginTop: 4 } }, formatContractMoneyBrief(got)), + React.createElement('span', { style: { fontSize: 11, color: 'rgba(0,0,0,0.38)', marginTop: 6 } }, '应收款 ' + formatContractMoneyBrief(rec)) + ) + ), + React.createElement('div', { + style: { + display: 'flex', + flexWrap: 'wrap', + justifyContent: 'center', + gap: '16px 24px', + marginTop: 14, + fontSize: 12, + color: 'rgba(0,0,0,0.55)' + } + }, + React.createElement('span', null, + React.createElement('span', { style: { display: 'inline-block', width: 8, height: 8, borderRadius: 2, background: cReceived, marginRight: 6, verticalAlign: 'middle' } }), + '已收款 ', + React.createElement('span', { style: { color: 'rgba(0,0,0,0.75)', fontWeight: 500 } }, formatContractMoneyBrief(got)) + ), + React.createElement('span', null, + React.createElement('span', { style: { display: 'inline-block', width: 8, height: 8, borderRadius: 2, background: cPending, marginRight: 6, verticalAlign: 'middle' } }), + '未到账 ', + React.createElement('span', { style: { fontWeight: 500, color: 'rgba(0,0,0,0.75)' } }, formatContractMoneyBrief(pend)) + ) + ) + ); + } + + // 原型「图示」区:仿 Dashboard 面积图占位 + function renderChartPlaceholder(caption, chartH) { + var h = chartH != null ? chartH : wbChartHeight; + var svgH = Math.max(72, Math.round(h * 0.55)); + return React.createElement('div', { + style: { + position: 'relative', + height: h, + borderRadius: 8, + overflow: 'hidden', + background: 'linear-gradient(180deg, rgba(22,119,255,0.08) 0%, rgba(22,119,255,0.02) 45%, #fff 100%)', + border: '1px solid rgba(0,0,0,0.06)' + } + }, + React.createElement('svg', { viewBox: '0 0 800 200', preserveAspectRatio: 'none', style: { position: 'absolute', left: 0, right: 0, bottom: 28, height: svgH, width: '100%' } }, + React.createElement('defs', null, + React.createElement('linearGradient', { id: 'areaGrad', x1: '0', y1: '0', x2: '0', y2: '1' }, + React.createElement('stop', { offset: '0%', stopColor: '#1677ff', stopOpacity: 0.35 }), + React.createElement('stop', { offset: '100%', stopColor: '#1677ff', stopOpacity: 0.02 }) + ) + ), + React.createElement('path', { + fill: 'url(#areaGrad)', + d: 'M0,140 Q120,100 200,120 T400,80 T600,95 T800,70 L800,200 L0,200 Z' + }), + React.createElement('path', { + fill: 'none', + stroke: '#1677ff', + strokeWidth: 2.5, + d: 'M0,140 Q120,100 200,120 T400,80 T600,95 T800,70' + }) + ), + React.createElement('div', { + style: { + position: 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%,-50%)', + fontSize: 14, + color: 'rgba(0,0,0,0.25)', + pointerEvents: 'none', + fontWeight: 500 + } + }, caption || '图示 · 联调接入图表') + ); + } + var quickByRole = useMemo(function () { return { ye: { @@ -184,13 +842,15 @@ const Component = function () { { 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' } ] }, yeEnergy: { label: '业管-能源部', items: [ - { t: '加氢订单管理', p: 'web端/加氢站管理/加氢订单.jsx' } + { t: '加氢订单管理', p: 'web端/加氢站管理/加氢订单.jsx' }, + { t: '意见建议', p: 'web端/意见建议.jsx' } ] }, ops: { @@ -204,7 +864,8 @@ const Component = function () { { t: '替换车管理', p: 'web端/运维管理/车辆业务/替换车管理.jsx' }, { t: '调拨管理', p: 'web端/运维管理/车辆业务/调拨管理.jsx' }, { t: '异动管理', p: 'web端/运维管理/车辆业务/异动管理.jsx' }, - { t: '备件库管理', p: 'web端/运维管理/备件管理/备件库存.jsx' } + { t: '备件库管理', p: 'web端/运维管理/备件管理/备件库存.jsx' }, + { t: '意见建议', p: 'web端/意见建议.jsx' } ] }, finance: { @@ -214,7 +875,8 @@ const Component = function () { { t: '租赁账单', p: 'web端/财务管理/租赁账单.jsx' }, { t: '还车应结款', p: 'web端/财务管理/还车应结款.jsx' }, { t: '审批中心', p: 'web端/审批中心.jsx' }, - { t: '充值单管理', p: 'web端/财务管理/充值单管理.jsx' } + { t: '充值单管理', p: 'web端/财务管理/充值单管理.jsx' }, + { t: '意见建议', p: 'web端/意见建议.jsx' } ] }, safety: { @@ -224,35 +886,20 @@ const Component = function () { { t: '事故管理', p: 'web端/安全管理/事故管理.jsx' }, { t: '司机管理', p: 'web端/安全管理/司机管理.jsx' }, { t: '安全培训资料', p: 'web端/安全管理/安全培训资料.jsx' }, - { t: '安全培训记录', p: 'web端/安全管理/安全培训记录.jsx' } + { t: '安全培训记录', p: 'web端/安全管理/安全培训记录.jsx' }, + { t: '意见建议', p: 'web端/意见建议.jsx' } ] }, legal: { label: '法务', items: [ - { t: '审批中心', p: 'web端/审批中心.jsx' } + { t: '审批中心', p: 'web端/审批中心.jsx' }, + { t: '意见建议', p: 'web端/意见建议.jsx' } ] } }; }, []); - var auditColumns = useMemo(function () { - return [ - { title: '流程到达时间', dataIndex: 'arriveAt', key: 'arriveAt', width: 150 }, - { title: '流程类型', dataIndex: 'flowType', key: 'flowType', ellipsis: true }, - { title: '发起时间', dataIndex: 'startAt', key: 'startAt', width: 140 }, - { title: '发起人', dataIndex: 'starter', key: 'starter', width: 100 }, - { - title: '操作', - key: 'act', - width: 88, - render: function (_, r) { - return React.createElement(Button, { type: 'link', size: 'small', onClick: function () { protoNav(r.path); } }, '去处理'); - } - } - ]; - }, []); - var noticeColumns = useMemo(function () { return [ { title: '时间', dataIndex: 'time', key: 'time', width: 150 }, @@ -261,102 +908,866 @@ const Component = function () { ]; }, []); - function renderMetricCard(m) { - var inner = React.createElement(Card, { - size: 'small', - bordered: false, - style: Object.assign({}, cardStyle, { minHeight: 108, width: '100%', flex: 1, display: 'flex', flexDirection: 'column' }), - bodyStyle: { - padding: '16px 12px', - cursor: m.modal || m.pop ? 'pointer' : 'default', - flex: 1, - display: 'flex', - alignItems: 'flex-start', - justifyContent: 'center' - }, - onClick: function () { - if (m.modal === 'audit') setAuditModalOpen(true); - else if (m.modal === 'notice') setNoticeModalOpen(true); - } - }, - React.createElement(Statistic, { - title: React.createElement('span', { style: { fontSize: 13 } }, m.title), - value: m.value, - valueStyle: { color: m.color, fontWeight: 600, fontSize: 24 } - }) - ); + var overdueDeliveryPopoverTableStyle = { width: '100%', borderCollapse: 'collapse', fontSize: 12 }; + var overdueDeliveryPopoverThStyle = { padding: '6px 8px', textAlign: 'left', borderBottom: '1px solid #f0f0f0', backgroundColor: '#fafafa', fontWeight: 600 }; + var overdueDeliveryPopoverTdStyle = { padding: '6px 8px', borderBottom: '1px solid #f0f0f0' }; - if (m.pop) { - return React.createElement(Popover, { content: todoPopoverContent, title: '待办工作说明', trigger: 'click' }, - React.createElement('div', { style: { display: 'flex', flex: 1, width: '100%', minWidth: 0 } }, inner)); - } - return inner; - } - - var quickTabItems = useMemo(function () { - return ['ye', 'yeEnergy', 'ops', 'finance', 'safety', 'legal'].map(function (k) { - var g = quickByRole[k]; - return { - key: k, - label: g.label, - children: React.createElement(Row, { gutter: [12, 12] }, - g.items.map(function (it) { - return React.createElement(Col, { xs: 8, sm: 6, md: 4, lg: 3, key: it.t }, - React.createElement(Card, { - size: 'small', - hoverable: true, - bodyStyle: { padding: '12px 8px', textAlign: 'center' }, - onClick: function () { protoNav(it.p); } - }, - React.createElement('div', { - style: { - width: 40, height: 40, margin: '0 auto 8px', borderRadius: 8, - background: 'linear-gradient(135deg,#e6f4ff,#bae0ff)', lineHeight: '40px', fontSize: 18 - } - }, it.t.charAt(0)), - React.createElement('div', { style: { fontSize: 12, fontWeight: 500 } }, it.t) - ) + function renderOverdueDeliveryQuantity(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: overdueDeliveryPopoverTableStyle }, + React.createElement('thead', null, + React.createElement('tr', null, + React.createElement('th', { style: overdueDeliveryPopoverThStyle }, '车辆类型'), + React.createElement('th', { style: overdueDeliveryPopoverThStyle }, '品牌'), + React.createElement('th', { style: overdueDeliveryPopoverThStyle }, '型号'), + React.createElement('th', { style: overdueDeliveryPopoverThStyle }, '车牌号'), + React.createElement('th', { style: overdueDeliveryPopoverThStyle }, '交车时间'), + React.createElement('th', { style: overdueDeliveryPopoverThStyle }, '交车人员') + ) + ), + React.createElement('tbody', null, + list.map(function (v, i) { + return React.createElement('tr', { key: i }, + React.createElement('td', { style: overdueDeliveryPopoverTdStyle }, v.vehicleType || '-'), + React.createElement('td', { style: overdueDeliveryPopoverTdStyle }, v.brand || '-'), + React.createElement('td', { style: overdueDeliveryPopoverTdStyle }, v.model || '-'), + React.createElement('td', { style: overdueDeliveryPopoverTdStyle }, v.plateNo || '-'), + React.createElement('td', { style: overdueDeliveryPopoverTdStyle }, (v.deliveryTime && v.deliveryTime !== '-') ? v.deliveryTime : '-'), + React.createElement('td', { style: overdueDeliveryPopoverTdStyle }, v.deliveryPerson || '-') ); }) ) - }; + ) + ); + return React.createElement(Popover, { content: content, title: null }, + React.createElement('span', { style: { color: '#1677ff', cursor: 'pointer', fontWeight: 600 } }, record.deliveryCount + ' 辆') + ); + } + + var overdueReturnModalColumns = [ + { title: '交车时间', dataIndex: 'deliveryTime', key: 'deliveryTime', width: 160 }, + { title: '交车人', dataIndex: 'deliveryPerson', key: 'deliveryPerson', width: 90 }, + { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 100 }, + { title: '车辆类型', dataIndex: 'vehicleType', key: 'vehicleType', width: 120, ellipsis: true }, + { title: '品牌', dataIndex: 'brand', key: 'brand', width: 80 }, + { title: '型号', dataIndex: 'model', key: 'model', width: 110, ellipsis: true }, + { title: '车辆识别代码', dataIndex: 'vin', key: 'vin', width: 160, ellipsis: true }, + { title: '合同编码', dataIndex: 'contractCode', key: 'contractCode', width: 130, ellipsis: true }, + { title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 160, ellipsis: true }, + { title: '项目名称', dataIndex: 'projectName', key: 'projectName', width: 140, ellipsis: true }, + { title: '业务部门', dataIndex: 'dept', key: 'dept', width: 110, ellipsis: true }, + { title: '业务负责人', dataIndex: 'bizOwner', key: 'bizOwner', width: 100 }, + { + title: '操作', + key: 'action', + width: 160, + fixed: 'right', + render: function (_, record) { + return React.createElement('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap' } }, + record.vehicleArrived + ? React.createElement(Button, { type: 'link', size: 'small', style: { padding: 0 }, onClick: function () { message.info('跳转还车管理-还车页(原型)'); } }, '还车') + : 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 }, + { 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 renderOverdueDeliveryQuantity(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: 88, fixed: 'right', render: function () { + return React.createElement(Button, { type: 'link', size: 'small', onClick: function () { message.info('跳转交车管理-交车单(原型)'); } }, '交车单'); + } } + ]; + + function renderTodoStatusCell(_, r) { + if (r.status === 'pending') return React.createElement(Tag, { color: 'processing' }, '待处理'); + if (r.status === 'overdue') return React.createElement(Tag, { color: 'error' }, '已超时'); + if (r.status === 'done') return React.createElement(Tag, { color: 'success' }, '已完成'); + return React.createElement(Tag, null, r.status); + } + + function renderTodoStatChip(filterKey, labelText, count, pillBg, numColor) { + var selected = todoBoardFilter === filterKey; + return React.createElement('span', { + role: 'button', + tabIndex: 0, + onClick: function () { setTodoBoardFilter(filterKey); }, + onKeyDown: function (e) { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + setTodoBoardFilter(filterKey); + } + }, + style: { + display: 'inline-flex', + alignItems: 'center', + gap: 4, + padding: '3px 10px', + borderRadius: 6, + fontSize: 12, + cursor: 'pointer', + background: pillBg, + boxShadow: selected ? '0 0 0 2px ' + numColor : undefined, + outline: 'none' + } + }, + React.createElement('span', { style: { color: 'rgba(0,0,0,0.5)' } }, labelText), + React.createElement('span', { style: { fontWeight: 600, color: numColor } }, count) + ); + } + + var todoTableColumns = useMemo(function () { + return [ + { title: '任务类型', dataIndex: 'taskType', key: 'taskType', width: 100 }, + { title: '任务名称', dataIndex: 'taskName', key: 'taskName', ellipsis: true }, + { + title: '状态', + key: 'status', + width: 88, + render: renderTodoStatusCell + }, + { + title: '生成时间', + dataIndex: 'genDate', + key: 'genDate', + width: 120, + showSorterTooltip: false, + sorter: function (a, b) { + return String(a.genDate || '').localeCompare(String(b.genDate || '')); + } + }, + { + title: '操作', + key: 'op', + width: 128, + align: 'right', + render: function (_, r) { + var isDone = r.status === 'done'; + var urgeBtn = !isDone + ? React.createElement(Button, { + type: 'link', + size: 'small', + style: { padding: 0 }, + onClick: function () { pushUrgeNotice(r); } + }, '催办') + : null; + var primaryLabel = isDone ? '查看' : '处理'; + return React.createElement(Space, { size: 4 }, + React.createElement(Button, { type: 'link', size: 'small', style: { padding: 0 }, onClick: function () { protoNav(r.path); } }, primaryLabel), + urgeBtn + ); + } + } + ]; + }, [pushUrgeNotice]); + + var todoMoreModalColumns = useMemo(function () { + return [ + { title: '任务类型', dataIndex: 'taskType', key: 'taskType', width: 100 }, + { title: '任务名称', dataIndex: 'taskName', key: 'taskName', ellipsis: true }, + { + title: '状态', + key: 'status', + width: 88, + render: renderTodoStatusCell + }, + { + title: '生成时间', + dataIndex: 'genDate', + key: 'genDate', + width: 120, + showSorterTooltip: false, + sorter: function (a, b) { + return String(a.genDate || '').localeCompare(String(b.genDate || '')); + } + }, + { + title: '操作', + key: 'op', + width: 128, + align: 'right', + render: function (_, r) { + var isDone = r.status === 'done'; + var urgeBtn = !isDone + ? React.createElement(Button, { + type: 'link', + size: 'small', + style: { padding: 0 }, + onClick: function () { pushUrgeNotice(r); } + }, '催办') + : null; + var primaryLabel = isDone ? '查看' : '处理'; + return React.createElement(Space, { size: 4 }, + React.createElement(Button, { type: 'link', size: 'small', style: { padding: 0 }, onClick: function () { protoNav(r.path); } }, primaryLabel), + urgeBtn + ); + } + } + ]; + }, [pushUrgeNotice]); + + var todoMoreFilteredRows = useMemo(function () { + return dashboardTodoRows.filter(function (r) { + if (todoMoreTaskType && r.taskType !== todoMoreTaskType) return false; + if (todoMoreStatus && r.status !== todoMoreStatus) return false; + if (todoMoreDateStart && r.genDate < todoMoreDateStart) return false; + if (todoMoreDateEnd && r.genDate > todoMoreDateEnd) return false; + return true; }); - }, [quickByRole]); + }, [dashboardTodoRows, todoMoreTaskType, todoMoreStatus, todoMoreDateStart, todoMoreDateEnd]); + + var openTodoMoreModal = useCallback(function () { + setTodoMoreTaskType(undefined); + setTodoMoreStatus(undefined); + setTodoMoreDateStart(''); + setTodoMoreDateEnd(''); + setTodoMoreModalOpen(true); + }, []); + + var todoMoreRangeValue = useMemo(function () { + var ds = todoMoreDateStart; + var de = todoMoreDateEnd; + if (!ds && !de) return null; + var dayjs = typeof window !== 'undefined' ? window.dayjs : null; + var moment = typeof window !== 'undefined' ? window.moment : null; + if (dayjs) { + return [ + ds ? dayjs(ds, 'YYYY-MM-DD') : null, + de ? dayjs(de, 'YYYY-MM-DD') : null + ]; + } + if (moment) { + return [ + ds ? moment(ds, 'YYYY-MM-DD') : null, + de ? moment(de, 'YYYY-MM-DD') : null + ]; + } + return null; + }, [todoMoreDateStart, todoMoreDateEnd]); + + function renderWorkbenchMetricHintIcon(cardKey) { + var hint = workbenchWarningMetricHintByKey[cardKey]; + if (!hint) return null; + var titleNode = React.createElement('div', { style: { maxWidth: 368, lineHeight: 1.65, fontSize: 12, textAlign: 'left', whiteSpace: 'pre-line' } }, hint); + var stopBubble = function (e) { + if (e && e.stopPropagation) e.stopPropagation(); + }; + return React.createElement(Tooltip, { + title: titleNode, + placement: 'top', + mouseEnterDelay: 0.08, + destroyTooltipOnHide: true + }, + React.createElement('span', { + className: 'workbench-metric-hint-icon', + role: 'img', + 'aria-label': '指标说明', + tabIndex: 0, + style: { + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + marginLeft: 2, + cursor: 'help', + color: 'rgba(0,0,0,0.4)', + flexShrink: 0, + verticalAlign: 'middle', + lineHeight: 1 + }, + onClick: stopBubble, + onMouseDown: stopBubble, + onKeyDown: function (e) { + if (e.key === 'Enter' || e.key === ' ') stopBubble(e); + } + }, + React.createElement('svg', { width: 14, height: 14, viewBox: '0 0 1024 1024', fill: 'currentColor', 'aria-hidden': true }, + React.createElement('path', { d: 'M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z' }), + React.createElement('path', { d: 'M464 336a48 48 0 1 0 96 0 48 48 0 1 0-96 0zm72 112h-48c-4.4 0-8 3.6-8 8v272c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V456c0-4.4-3.6-8-8-8z' }) + ) + ) + ); + } + + function renderVehicleMetricCard(s) { + var onClick = s.onClick; + var mw = s.minWidth != null ? s.minWidth : 128; + var trailing = s.trailing; + var valueRow = s.valueRow; + var minCardH = 84; + if (trailing || valueRow) minCardH = 96; + var cardStyleMerged = Object.assign({}, cardStyle, { + flex: 1, + minWidth: mw, + minHeight: minCardH, + display: 'flex', + flexDirection: 'column', + height: '100%' + }); + if (onClick) { + cardStyleMerged.cursor = 'pointer'; + } + return React.createElement(Card, { + key: s.key, + className: 'workbench-warning-metric-card', + size: 'small', + bordered: false, + style: cardStyleMerged, + bodyStyle: { + padding: '12px 12px', + flex: 1, + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + minHeight: 0 + }, + onClick: onClick, + onKeyDown: onClick + ? function (e) { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + onClick(e); + } + } + : undefined, + tabIndex: onClick ? 0 : undefined, + role: onClick ? 'button' : undefined + }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 10, width: '100%', boxSizing: 'border-box' } }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 10, flex: 1, minWidth: 0 } }, + React.createElement('div', { + style: { + width: 40, + height: 40, + borderRadius: 8, + background: s.iconBg, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + fontSize: 16, + fontWeight: 600, + color: s.color, + flexShrink: 0 + } + }, s.icon), + React.createElement('div', { style: { minWidth: 0 } }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 2, flexWrap: 'wrap', marginBottom: 0 } }, + React.createElement('span', { style: { fontSize: 12, color: 'rgba(0,0,0,0.55)', lineHeight: 1.35 } }, s.title), + renderWorkbenchMetricHintIcon(s.key) + ), + valueRow != null + ? valueRow + : React.createElement('div', { style: { fontSize: 22, fontWeight: 600, color: accentOrange, lineHeight: 1.2 } }, s.value) + ) + ), + trailing || null + ) + ); + } + + function renderWarningMetricCardItem(s) { + var minW = 152; + if (s.key === 'w_en_h2_import_today') minW = 280; + if (s.key === 'w_fin_energy_recharge') minW = 260; + var cardProps = { + key: s.key, + title: s.title, + value: s.key === 'w_en_h2_import_today' ? energyH2ImportTodayCount : s.value, + iconBg: s.iconBg, + icon: s.icon, + color: s.color, + minWidth: minW, + onClick: function () { + if (s.key === 'w_ops_delivery') { + openOverdueDeliveryModal(); + return; + } + if (s.key === 'w_ops_return_cnt') { + openOverdueReturnModal(); + return; + } + message.info('「' + s.title + '」明细(原型,联调接口后打开列表)'); + } + }; + if (s.key === 'w_en_h2_import_today') { + cardProps.valueRow = energyH2ImportTodayCount > 0 + ? React.createElement('div', { style: { fontSize: 22, fontWeight: 600, color: accentOrange, lineHeight: 1.2 } }, energyH2ImportTodayCount) + : React.createElement('div', { style: { fontSize: 11, lineHeight: 1.45, display: 'block', color: accentOrange, fontWeight: 500 } }, + '今日还未导入任何加氢记录,请确认是否有加氢明细需要导入' + ); + } + if (s.key === 'w_fin_energy_recharge') { + cardProps.value = financeEnergyRechargeYuan; + cardProps.valueRow = financeEnergyRechargeYuan != null + ? React.createElement('div', { style: { fontSize: 22, fontWeight: 600, color: accentOrange, lineHeight: 1.2 } }, formatWorkbenchFinanceYuan(financeEnergyRechargeYuan)) + : React.createElement('div', { style: { fontSize: 11, lineHeight: 1.45, display: 'block', color: accentOrange, fontWeight: 500 } }, + '请确认是否有能源账户充值记录' + ); + } + return renderVehicleMetricCard(cardProps); + } + + function renderNoticePanelCard() { + var preview = (noticeList || []).slice(0, wbDashPreviewMax); + var titleNode = React.createElement(Space, { size: 8, wrap: true, align: 'center' }, + React.createElement('span', { className: 'workbench-dash-pair-head-title' }, '通知'), + noticeNewCount > 0 + ? React.createElement(Badge, { count: noticeNewCount, style: { backgroundColor: '#fa8c16' } }) + : null + ); + var lines = preview.map(function (n, idx) { + var isLast = idx === preview.length - 1; + return React.createElement('div', { + key: n.id, + className: 'workbench-notice-detail-line', + style: { + padding: '6px 0', + borderBottom: isLast ? 'none' : '1px solid rgba(0,0,0,0.06)' + } + }, + React.createElement('div', { style: { fontSize: 11, color: 'rgba(0,0,0,0.45)', marginBottom: 4, lineHeight: 1.3 } }, + n.time, + React.createElement('span', { style: { margin: '0 6px', color: 'rgba(0,0,0,0.25)' } }, '·'), + n.type, + !n.read + ? React.createElement(Tag, { color: 'warning', style: { marginLeft: 6, fontSize: 11, lineHeight: '18px', padding: '0 6px' } }, '未读') + : null + ), + React.createElement(Text, { + ellipsis: { tooltip: true }, + style: { fontSize: 12, color: 'rgba(0,0,0,0.78)', display: 'block', lineHeight: 1.45, width: '100%' } + }, n.content) + ); + }); + return React.createElement(Card, { + key: 'notice-panel', + className: 'workbench-notice-panel workbench-dash-pair-head', + bordered: false, + title: titleNode, + style: Object.assign({}, cardStyle, { flex: 1, width: '100%', minHeight: 0, display: 'flex', flexDirection: 'column' }), + bodyStyle: { flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0, overflow: 'hidden' }, + extra: React.createElement(Button, { type: 'link', size: 'small', style: { padding: 0 }, onClick: openNoticeModal }, '更多') + }, + React.createElement('div', { className: 'workbench-dash-card-body-center' }, + preview.length === 0 + ? React.createElement('div', { className: 'workbench-dash-card-body-inner workbench-dash-card-body-empty' }, + React.createElement(Text, { type: 'secondary', style: { fontSize: 12 } }, '暂无通知') + ) + : React.createElement('div', { className: 'workbench-dash-card-body-inner workbench-notice-detail-scroll' }, lines) + ) + ); + } + + var reportTabItems = useMemo(function () { + return [ + { + key: 'task', + label: '任务处理情况', + children: React.createElement('div', { className: 'workbench-task-report-pane' }, + React.createElement('div', { style: { flexShrink: 0, fontSize: 12, color: 'rgba(0,0,0,0.45)', marginBottom: 8, lineHeight: 1.5 } }, + '横轴:员工 · 纵轴:任务数量 · ', + React.createElement('span', { style: { color: '#52c41a', fontWeight: 500 } }, '■'), + ' 完成任务 ', + React.createElement('span', { style: { color: '#f5222d', fontWeight: 500 } }, '■'), + ' 超时任务(示意)' + ), + renderTaskEmployeeBarChart(taskEmployeeBars, 'done', 'overdue', '#52c41a', '#ff7875', 52) + ) + }, + { + key: 'payment', + label: '合同到账情况', + children: React.createElement('div', { className: 'workbench-payment-report-pane' }, + React.createElement('div', { style: { flexShrink: 0, fontSize: 12, color: 'rgba(0,0,0,0.45)', marginBottom: 8, lineHeight: 1.5 } }, + '环形合计对应应收款金额;蓝色为已收款,灰色为未到账(示意,联调接接口)' + ), + renderContractPaymentDonutChart(contractPaymentDonut) + ) + }, + { + key: 'contract', + label: '合同情况', + children: React.createElement('div', { style: { padding: '8px 0 0' } }, + React.createElement('div', { style: { fontSize: 12, color: 'rgba(0,0,0,0.45)', marginBottom: 8, lineHeight: 1.5 } }, + '横轴:月份 · 纵轴:合同数量 · 近12个月 · ', + React.createElement('span', { style: { color: '#722ed1', fontWeight: 500 } }, '■'), + ' 租赁合同 ', + React.createElement('span', { style: { color: '#b37feb', fontWeight: 500 } }, '■'), + ' 自营合同 ', + React.createElement('span', { style: { color: '#fa8c16', fontWeight: 500 } }, '■'), + ' 续签合同(示意)' + ), + renderBarGroupTriple(contractMonthBars, 'lease', 'self', 'renew', '#722ed1', '#b37feb', '#fa8c16', 44) + ) + }, + { + key: 'vehicle', + label: '车辆运营情况', + children: React.createElement('div', { style: { padding: '8px 0 0' } }, + React.createElement('div', { style: { fontSize: 12, color: 'rgba(0,0,0,0.45)', marginBottom: 8 } }, '近12个月 · 运营车辆 / 闲置车辆'), + renderBarGroup(vehicleMonthBars, 'op', 'idle', '#1677ff', '#91caff') + ) + } + ]; + }, [vehicleMonthBars, contractMonthBars, taskEmployeeBars, contractPaymentDonut]); + + var quickItems = quickByRole[roleTab] ? quickByRole[roleTab].items : []; + + var quickRoleKeys = ['ye', 'yeEnergy', 'ops', 'finance', 'safety', 'legal']; + + var roleSwitchMenuProps = useMemo(function () { + return { + items: quickRoleKeys.map(function (k) { + return { key: k, label: quickByRole[k].label }; + }), + selectable: true, + selectedKeys: [roleTab], + onClick: function (info) { + if (info && info.key) setRoleTab(info.key); + } + }; + }, [quickByRole, roleTab]); + + var oneScreenCss = + '.workbench-one-screen .workbench-report-tabs.ant-tabs{height:100%;display:flex;flex-direction:column;min-height:0}' + + '.workbench-one-screen .workbench-report-tabs .ant-tabs-content-holder{flex:1;min-height:0;overflow:hidden;display:flex;flex-direction:column}' + + '.workbench-one-screen .workbench-report-tabs .ant-tabs-content{flex:1;min-height:0;overflow:hidden;display:flex;flex-direction:column}' + + '.workbench-one-screen .workbench-report-tabs .ant-tabs-content .ant-tabs-tabpane.ant-tabs-tabpane-active{flex:1;min-height:0!important;display:flex!important;flex-direction:column;overflow:hidden}' + + '.workbench-one-screen .workbench-report-tabs .ant-tabs-content .ant-tabs-tabpane.ant-tabs-tabpane-hidden{display:none!important}' + + '.workbench-task-report-pane{flex:1;min-height:0;display:flex;flex-direction:column;height:100%;padding:8px 0 0;box-sizing:border-box}' + + '.workbench-task-chart-axes-wrap{flex:1;min-height:0;width:100%;display:flex;flex-direction:column;box-sizing:border-box}' + + '.workbench-task-chart-plot-row{flex:1;min-height:0;display:flex;flex-direction:row;align-items:stretch;overflow:hidden;box-sizing:border-box}' + + '.workbench-task-bar-chart{flex:1;min-height:80px;width:100%;display:flex;align-items:stretch;gap:6px;overflow-x:auto;overflow-y:hidden;box-sizing:border-box}' + + '.workbench-payment-report-pane{flex:1;min-height:0;display:flex;flex-direction:column;height:100%;padding:8px 0 0;box-sizing:border-box;overflow:hidden}' + + '.workbench-payment-donut-hold{flex:1;min-height:0;width:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;overflow:auto;box-sizing:border-box}' + + '.workbench-main-split{display:flex!important;flex-direction:column!important;gap:12px;flex:1;min-height:0;width:100%;overflow:hidden}' + + '.workbench-main-split>.ant-row.workbench-todo-notice-row,.workbench-main-split>.ant-row.workbench-report-quick-row{flex:1 1 0!important;min-height:0!important;height:auto!important;max-height:none!important;margin:0!important;overflow:hidden}' + + '.workbench-todo-notice-row>.ant-col,.workbench-report-quick-row>.ant-col{display:flex!important;flex-direction:column;min-height:0}' + + '.workbench-notice-slot,.workbench-todo-slot,.workbench-report-slot{min-height:0}' + + '.workbench-dash-pair-head.ant-card .ant-card-head{min-height:56px;padding:0 16px;display:flex;align-items:center;border-bottom:1px solid rgba(0,0,0,0.06)}' + + '.workbench-dash-pair-head.ant-card .ant-card-head-title,.workbench-dash-pair-head-title{font-size:16px;font-weight:600;line-height:24px;color:rgba(0,0,0,0.88)}' + + '.workbench-dash-pair-head.ant-card .ant-card-extra{padding:0}' + + '.workbench-dash-pair-head.ant-card .ant-card-body{padding-top:10px!important}' + + '.workbench-dash-pair-head-sub{font-size:12px!important;line-height:20px!important}' + + '.workbench-notice-panel.ant-card{height:100%;min-height:0;display:flex!important;flex-direction:column}' + + '.workbench-notice-panel .ant-card-body{flex:1;min-height:0!important;display:flex!important;flex-direction:column}' + + '.workbench-dash-card-body-center{flex:1;min-height:0;display:flex;flex-direction:column;justify-content:flex-start;align-items:stretch;overflow:hidden;width:100%}' + + '.workbench-dash-card-body-inner{width:100%;max-height:100%;overflow-x:auto;overflow-y:auto;flex:0 1 auto;min-height:0}' + + '.workbench-dash-card-body-empty{display:flex;align-items:center;justify-content:center;min-height:120px}' + + '.workbench-todo-slot .ant-card,.workbench-report-slot .ant-card{height:100%;min-height:0}' + + '.workbench-report-tabs .ant-tabs-nav{margin-bottom:8px;}' + + '.workbench-notice-inline-hint{font-size:11px;font-weight:500;color:#ad4e00;cursor:pointer;user-select:none;white-space:nowrap}' + + '.workbench-notice-inline-hint:hover{color:#d46b08;text-decoration:underline}' + + '.workbench-todo-table .ant-table-thead>tr>th{color:#707d8f!important;font-weight:400!important;font-size:12px!important;background:#f7f8fa!important;border-bottom:1px solid rgba(0,0,0,0.06)!important;padding:12px 12px!important}' + + '.workbench-todo-table .ant-table-thead>tr>th::before{display:none!important}' + + '.workbench-todo-table .ant-table-column-sorter{color:#98a1b0!important}' + + '.workbench-todo-table .ant-table-column-sorter-up.active,.workbench-todo-table .ant-table-column-sorter-down.active{color:#707d8f!important}' + + '.workbench-todo-table.ant-table-small .ant-table-thead>tr>th{padding:10px 12px!important}' + + '.workbench-one-screen .workbench-quick-card.ant-card{display:flex;flex-direction:column;min-height:0;height:100%}' + + '.workbench-one-screen .workbench-quick-card .ant-card-body{padding:0;flex:1;min-height:0;display:flex;flex-direction:column}' + + '.workbench-quick-scroll{flex:1;min-height:0;overflow:auto;-webkit-overflow-scrolling:touch}' + + '.workbench-quick-item{display:flex;align-items:center;gap:6px;width:100%;box-sizing:border-box;padding:5px 6px;border-radius:6px;border:1px solid rgba(0,0,0,0.06);background:#fafafa;cursor:pointer;transition:border-color .15s,background .15s,box-shadow .15s;text-align:left;outline:none}' + + '.workbench-quick-item:hover{border-color:rgba(22,119,255,0.22);background:#f3f8ff;box-shadow:0 1px 2px rgba(22,119,255,0.06)}' + + '.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-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}' + + '.workbench-header-warning-dept-tabs .ant-tabs-nav::before{border-bottom:none!important}' + + '.workbench-header-warning-dept-tabs .ant-tabs-tab{padding:4px 8px!important;font-size:12px}' + + '.workbench-header-warning-dept-tabs .ant-tabs-nav-wrap{justify-content:flex-end}' + + '.workbench-header-warning-dept-tabs .ant-tabs-content-holder{display:none!important}' + + '.workbench-overdue-delivery-modal .ant-table-tbody>tr>td{color:#a8071a!important;background-color:#fff2f0!important;border-color:#ffccc7!important}' + + '.workbench-overdue-delivery-modal .ant-table-tbody>tr>td.ant-table-cell-fix-left,.workbench-overdue-delivery-modal .ant-table-tbody>tr>td.ant-table-cell-fix-right{background-color:#fff2f0!important}' + + '.workbench-overdue-delivery-modal .ant-table-tbody>tr:hover>td{background-color:#ffccc7!important;color:#a8071a!important}' + + '.workbench-overdue-delivery-modal .ant-table-tbody>tr:hover>td.ant-table-cell-fix-left,.workbench-overdue-delivery-modal .ant-table-tbody>tr:hover>td.ant-table-cell-fix-right{background-color:#ffccc7!important}' + + '.workbench-overdue-return-modal .ant-table-thead>tr>th,.workbench-overdue-return-modal .ant-table-tbody>tr>td{white-space:nowrap}' + + '.workbench-overdue-return-modal .ant-table-tbody>tr>td{color:#ad4e00!important;background-color:#fff7e6!important;border-color:#ffd591!important}' + + '.workbench-overdue-return-modal .ant-table-tbody>tr>td.ant-table-cell-fix-left,.workbench-overdue-return-modal .ant-table-tbody>tr>td.ant-table-cell-fix-right{background-color:#fff7e6!important}' + + '.workbench-overdue-return-modal .ant-table-tbody>tr:hover>td{background-color:#ffe7ba!important;color:#ad4e00!important}' + + '.workbench-overdue-return-modal .ant-table-tbody>tr:hover>td.ant-table-cell-fix-left,.workbench-overdue-return-modal .ant-table-tbody>tr:hover>td.ant-table-cell-fix-right{background-color:#ffe7ba!important}' + + '.workbench-overdue-return-modal .ant-btn-link{color:#1677ff!important}' + + '.workbench-warning-metric-card.ant-card{display:flex!important;flex-direction:column!important;height:100%!important;min-height:inherit}' + + '.workbench-warning-metric-card.ant-card .ant-card-body{flex:1!important;display:flex!important;flex-direction:column!important;justify-content:center!important;min-height:0!important}' + + '.workbench-metric-hint-icon:hover{color:rgba(22,119,255,0.9)!important}' + + '.workbench-metric-hint-icon:focus-visible{outline:2px solid rgba(22,119,255,0.35);outline-offset:2px;border-radius:4px}'; return React.createElement(App, null, - React.createElement('div', { style: layoutStyle }, - React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 8, marginBottom: 8 } }, - React.createElement(Space, null, - React.createElement('span', { style: { fontSize: 20, fontWeight: 600, color: 'rgba(0,0,0,0.85)' } }, '工作台'), - React.createElement(Tag, { color: 'default' }, '工作台') + React.createElement('div', { + className: 'workbench-one-screen', + style: { + boxSizing: 'border-box', + height: '100vh', + padding: '12px 16px 12px', + background: pageBg, + overflow: 'hidden', + display: 'flex', + flexDirection: 'column' + } + }, + React.createElement('style', null, oneScreenCss), + + React.createElement('div', { style: { flexShrink: 0, marginBottom: 10 } }, + React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8, flexWrap: 'wrap', gap: 8 } }, + React.createElement(Breadcrumb, { items: [{ title: '运管平台' }, { title: '工作台' }] }), + React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', justifyContent: 'flex-end', minWidth: 0 } }, + React.createElement(Tabs, { + className: 'workbench-header-warning-dept-tabs', + activeKey: warningDeptKey, + onChange: setWarningDeptKey, + size: 'small', + items: warningDeptTabItems, + tabBarGutter: 4 + }) + ) ), - React.createElement(Button, { type: 'link', style: { padding: 0 }, onClick: function () { setRequirementOpen(true); } }, '查看需求说明') + React.createElement(Title, { level: 5, style: { margin: 0, fontWeight: 600 } }, '欢迎回来,' + mockOperatorDisplayName) + ), + React.createElement('div', { style: { flexShrink: 0, marginBottom: 10, boxSizing: 'border-box' } }, + React.createElement('div', { className: 'workbench-top-metrics', style: { display: 'flex', flexWrap: 'wrap', gap: 10, alignItems: 'stretch' } }, + React.createElement('div', { + className: 'workbench-warning-cards' + (warningDeptKey === 'finance' ? ' workbench-warning-cards--finance' : ''), + style: warningDeptKey === 'finance' + ? { flex: '1 1 100%', display: 'flex', flexDirection: 'column', gap: 10, alignItems: 'stretch', minWidth: 0, width: '100%' } + : { flex: '1 1 280px', display: 'flex', flexWrap: 'wrap', gap: 10, alignItems: 'stretch', minWidth: 0 } + }, + warningDeptKey === 'finance' + ? [ + React.createElement('div', { + key: 'wb-fin-row1', + style: { display: 'flex', flexWrap: 'wrap', gap: 10, width: '100%', alignItems: 'stretch' } + }, warningCardsVisible.slice(0, 5).map(function (s) { return renderWarningMetricCardItem(s); })), + React.createElement('div', { + key: 'wb-fin-row2', + style: { display: 'flex', flexWrap: 'wrap', gap: 10, width: '100%', alignItems: 'stretch' } + }, warningCardsVisible.slice(5).map(function (s) { return renderWarningMetricCardItem(s); })) + ] + : warningCardsVisible.map(function (s) { return renderWarningMetricCardItem(s); }) + ) + ) + ), + React.createElement(Row, { gutter: [12, 12], align: 'stretch', style: { flex: 1, minHeight: 0, overflow: 'hidden' } }, + React.createElement(Col, { + xs: 24, + lg: 24, + xl: 24, + className: 'workbench-main-split', + style: { flex: 1, minHeight: 0, minWidth: 0, height: '100%' } + }, + React.createElement(Row, { + className: 'workbench-todo-notice-row', + gutter: [12, 12], + align: 'stretch', + wrap: true, + style: { minHeight: 0, flex: '1 1 0', margin: 0 } + }, + React.createElement(Col, { + xs: 24, + lg: 16, + xl: 16, + style: { display: 'flex', flexDirection: 'column', minHeight: 0, minWidth: 0 } + }, + React.createElement('div', { className: 'workbench-todo-slot', style: { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden', width: '100%' } }, + React.createElement(Card, { + className: 'workbench-dash-pair-head', + title: React.createElement(Space, { size: 8, wrap: true, align: 'center' }, + React.createElement('span', { className: 'workbench-dash-pair-head-title' }, '待办任务'), + React.createElement(Badge, { count: todoSummary.pending + todoSummary.overdue, style: { backgroundColor: accentBlue } }), + renderTodoStatChip('pending', '待处理', todoSummary.pending, 'rgba(22,119,255,0.06)', accentBlue), + renderTodoStatChip('overdue', '已超时', todoSummary.overdue, 'rgba(245,34,45,0.06)', '#f5222d'), + renderTodoStatChip('done', '已完成', todoSummary.done, 'rgba(82,196,26,0.08)', '#52c41a') + ), + bordered: false, + style: Object.assign({}, cardStyle, { flex: 1, width: '100%', minHeight: 0, display: 'flex', flexDirection: 'column' }), + bodyStyle: { flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0, overflow: 'hidden' }, + extra: React.createElement(Button, { type: 'link', size: 'small', style: { padding: 0 }, onClick: openTodoMoreModal }, '更多') + }, + React.createElement('div', { className: 'workbench-dash-card-body-center' }, + React.createElement('div', { className: 'workbench-dash-card-body-inner' }, + React.createElement(Table, { + className: 'workbench-todo-table', + size: 'small', + rowKey: 'id', + columns: todoTableColumns, + dataSource: dashboardTodoBoardPreviewRows, + pagination: false, + scroll: { x: 704 } + }) + ) + ) + ) + ) + ), + React.createElement(Col, { + xs: 24, + lg: 8, + xl: 8, + style: { display: 'flex', flexDirection: 'column', minHeight: 0, minWidth: 0 } + }, + React.createElement('div', { className: 'workbench-notice-slot', style: { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', width: '100%' } }, + renderNoticePanelCard() + ) + ) + ), + React.createElement(Row, { + className: 'workbench-report-quick-row', + gutter: [12, 12], + align: 'stretch', + wrap: true, + style: { minHeight: 0, flex: '1 1 0', margin: 0 } + }, + React.createElement(Col, { + xs: 24, + lg: 16, + xl: 16, + style: { display: 'flex', flexDirection: 'column', minHeight: 0, minWidth: 0 } + }, + React.createElement('div', { className: 'workbench-report-slot', style: { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden', width: '100%' } }, + React.createElement(Card, { + title: '统计报表', + bordered: false, + style: Object.assign({}, cardStyle, { flex: 1, width: '100%', minHeight: 0, display: 'flex', flexDirection: 'column' }), + bodyStyle: { paddingTop: 4, paddingBottom: 8, flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' } + }, + React.createElement(Tabs, { + className: 'workbench-report-tabs', + activeKey: reportTab, + onChange: setReportTab, + items: reportTabItems, + tabBarStyle: { marginBottom: 0 }, + style: { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' } + }) + ) + ) + ), + React.createElement(Col, { + xs: 24, + lg: 8, + xl: 8, + style: { display: 'flex', flexDirection: 'column', minHeight: 0, minWidth: 0 } + }, + React.createElement(Card, { + className: 'workbench-quick-card', + title: '快速入口', + bordered: false, + style: Object.assign({}, cardStyle, { flex: 1, width: '100%', minHeight: 0, display: 'flex', flexDirection: 'column' }), + bodyStyle: { flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0, overflow: 'hidden', padding: 0 }, + extra: React.createElement(Dropdown, { + menu: roleSwitchMenuProps, + trigger: ['click'], + placement: 'bottomRight' + }, + React.createElement(Button, { type: 'link', size: 'small', style: { padding: 0 } }, '切换角色') + ) + }, + React.createElement('div', { className: 'workbench-quick-scroll', style: { padding: '6px 6px 4px' } }, + React.createElement(Row, { gutter: [6, 6], style: { marginBottom: 0 } }, + quickItems.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); }, + onKeyDown: function (e) { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + protoNav(it.p); + } + } + }, + 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) + ) + ); + }) + ) + ) + ) + ) + ) + ) ), React.createElement(Modal, { - title: '需求说明', - open: requirementOpen, - width: 760, - onCancel: function () { setRequirementOpen(false); }, - footer: React.createElement(Button, { onClick: function () { setRequirementOpen(false); } }, '关闭'), - bodyStyle: { maxHeight: '72vh', overflow: 'auto' } - }, React.createElement('div', { style: { whiteSpace: 'pre-wrap', fontSize: 13, lineHeight: 1.65, color: 'rgba(0,0,0,0.85)' } }, requirementDoc)), - - React.createElement(Modal, { - title: '待审批任务', - open: auditModalOpen, - width: 900, - onCancel: function () { setAuditModalOpen(false); }, - footer: React.createElement(Button, { onClick: function () { setAuditModalOpen(false); } }, '关闭'), + title: '全部待办任务', + open: todoMoreModalOpen, + width: 960, + onCancel: function () { setTodoMoreModalOpen(false); }, + footer: React.createElement(Button, { onClick: function () { setTodoMoreModalOpen(false); } }, '关闭'), destroyOnClose: true }, - React.createElement(Table, { - size: 'small', - rowKey: 'id', - columns: auditColumns, - dataSource: pendingAuditList, - pagination: { pageSize: 8, showSizeChanger: false } - }) + React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 12 } }, + React.createElement(Space, { wrap: true, align: 'center', size: [8, 8] }, + React.createElement(Text, { type: 'secondary', style: { fontSize: 12 } }, '任务类型'), + React.createElement(Select, { + allowClear: true, + placeholder: '全部类型', + style: { width: 160 }, + options: todoMoreTaskTypeFilterOptions, + value: todoMoreTaskType, + onChange: setTodoMoreTaskType, + showSearch: true, + optionFilterProp: 'label' + }), + React.createElement(Text, { type: 'secondary', style: { fontSize: 12 } }, '任务状态'), + React.createElement(Select, { + allowClear: true, + placeholder: '全部状态', + style: { width: 140 }, + options: todoMoreStatusFilterOptions, + value: todoMoreStatus, + onChange: setTodoMoreStatus + }), + React.createElement(Text, { type: 'secondary', style: { fontSize: 12 } }, '生成时间'), + React.createElement(DatePicker.RangePicker, { + style: { width: 140 }, + format: 'YYYY-MM-DD', + placeholder: ['开始', '结束'], + allowClear: true, + value: todoMoreRangeValue, + onChange: function (_dates, dateStrings) { + if (!dateStrings || (!dateStrings[0] && !dateStrings[1])) { + setTodoMoreDateStart(''); + setTodoMoreDateEnd(''); + return; + } + setTodoMoreDateStart(dateStrings[0] || ''); + setTodoMoreDateEnd(dateStrings[1] || ''); + } + }), + React.createElement(Button, { + size: 'small', + onClick: function () { + setTodoMoreTaskType(undefined); + setTodoMoreStatus(undefined); + setTodoMoreDateStart(''); + setTodoMoreDateEnd(''); + } + }, '重置') + ), + React.createElement(Table, { + className: 'workbench-todo-table', + size: 'small', + rowKey: 'id', + columns: todoMoreModalColumns, + dataSource: todoMoreFilteredRows, + pagination: { pageSize: 10, showSizeChanger: false, showTotal: function (t) { return '共 ' + t + ' 条'; } }, + scroll: { x: 780, y: 360 } + }) + ) ), React.createElement(Modal, { @@ -376,111 +1787,47 @@ const Component = function () { }) ), - React.createElement('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 16, marginBottom: 16, alignItems: 'stretch' } }, - topMetrics.map(function (m) { - return React.createElement('div', { key: m.key, style: { flex: '1 1 160px', minWidth: 148, maxWidth: '100%', display: 'flex' } }, renderMetricCard(m)); + React.createElement(Modal, { + title: '超期未交车', + open: overdueDeliveryModalOpen, + width: 1280, + onCancel: function () { setOverdueDeliveryModalOpen(false); }, + footer: React.createElement(Button, { onClick: function () { setOverdueDeliveryModalOpen(false); } }, '关闭'), + destroyOnClose: true + }, + React.createElement(Text, { type: 'secondary', style: { fontSize: 12, display: 'block', marginBottom: 10 } }, + '以下任务当前日期已超过预计交车结束日期(含区间结束日),示意数据标红;列表字段与交车管理-待处理一致,联调接接口。' + ), + React.createElement(Table, { + className: 'workbench-overdue-delivery-modal', + size: 'small', + rowKey: 'id', + columns: overdueDeliveryModalColumns, + dataSource: overdueDeliveryMockRows, + pagination: false, + scroll: { x: 1620, y: 400 } }) ), - React.createElement(Row, { gutter: [16, 16], style: { marginBottom: 16 } }, - React.createElement(Col, { xs: 24, xl: 7 }, - React.createElement(Card, { - title: React.createElement(Space, null, '我的待办清单', React.createElement(Badge, { count: myTodoList.length, style: { backgroundColor: '#1677ff' } })), - size: 'small', - bordered: false, - style: cardStyle, - bodyStyle: { maxHeight: 520, overflow: 'auto' } - }, - React.createElement(List, { - size: 'small', - dataSource: myTodoList, - locale: { emptyText: '暂无待办' }, - renderItem: function (item) { - return React.createElement(List.Item, { - actions: [ - React.createElement(Button, { type: 'link', key: 'go', size: 'small', onClick: function () { protoNav(item.path); } }, '去处理') - ] - }, - React.createElement(List.Item.Meta, { - title: React.createElement('span', { style: { fontSize: 13 } }, item.name), - description: React.createElement(Text, { type: 'secondary', style: { fontSize: 12 } }, '生成时间:' + item.time) - }) - ); - } - }) - ) - ), - React.createElement(Col, { xs: 24, xl: 10 }, - React.createElement(Card, { title: '数据统计', size: 'small', bordered: false, style: Object.assign({ marginBottom: 16 }, cardStyle) }, - React.createElement('div', { style: { fontWeight: 600, marginBottom: 12, fontSize: 13 } }, '车辆数据统计'), - React.createElement(Row, { gutter: 8 }, - [ - { label: '车辆总数', v: 248 }, - { label: '自营车辆数', v: 32 }, - { label: '库存车辆数', v: 30 }, - { label: '待运营车辆数', v: 12 } - ].map(function (s) { - return React.createElement(Col, { span: 12, key: s.label }, - React.createElement(Card, { size: 'small', bodyStyle: { padding: '10px 8px' } }, - React.createElement(Statistic, { title: s.label, value: s.v, valueStyle: { fontSize: 18 } }) - ) - ); - }) - ), - React.createElement(Divider, { plain: true, style: { margin: '12px 0 8px', fontSize: 12 } }, '近12个月 · 运营车辆 / 闲置车辆'), - React.createElement('div', { style: { fontSize: 11, color: 'rgba(0,0,0,0.45)', marginBottom: 6 } }, '蓝色:运营 浅蓝:闲置(原型双柱示意)'), - renderBarGroup(vehicleMonthBars, 'op', 'idle', '#1677ff', '#91caff') - ), - React.createElement(Card, { title: '合同数据统计', size: 'small', bordered: false, style: cardStyle }, - React.createElement(Row, { gutter: 16 }, - React.createElement(Col, { span: 12 }, React.createElement(Statistic, { title: '租赁合同总数', value: 58, valueStyle: { color: '#722ed1' } })), - React.createElement(Col, { span: 12 }, React.createElement(Statistic, { title: '自营合同总数', value: 14, valueStyle: { color: '#52c41a' } })) - ), - React.createElement(Divider, { plain: true, style: { margin: '12px 0 8px', fontSize: 12 } }, '近12个月 · 租赁合同 / 自营合同'), - React.createElement('div', { style: { fontSize: 11, color: 'rgba(0,0,0,0.45)', marginBottom: 6 } }, '紫色:租赁 浅紫:自营(原型双柱示意)'), - renderBarGroup(contractMonthBars, 'lease', 'self', '#722ed1', '#b37feb') - ) - ), - React.createElement(Col, { xs: 24, xl: 7 }, - React.createElement(Card, { - title: React.createElement(Space, null, '我的通知清单', React.createElement(Badge, { count: noticeList.length })), - size: 'small', - bordered: false, - style: cardStyle, - extra: React.createElement(Button, { type: 'link', size: 'small', style: { padding: 0 }, onClick: function () { setNoticeModalOpen(true); } }, '全部') - }, - React.createElement('div', { style: { maxHeight: 480, overflow: 'auto' } }, - React.createElement(List, { - size: 'small', - dataSource: noticeList.slice(0, 8), - renderItem: function (n) { - return React.createElement(List.Item, { style: { padding: '10px 0' } }, - React.createElement(List.Item.Meta, { - title: React.createElement(Space, { wrap: true, size: 4 }, - React.createElement(Text, { type: 'secondary', style: { fontSize: 12 } }, n.time), - React.createElement(Tag, null, n.type) - ), - description: React.createElement('span', { style: { fontSize: 12, color: 'rgba(0,0,0,0.75)' } }, n.content) - }) - ); - } - }) - ) - ) - ) - ), - - React.createElement(Card, { - title: '快速入口(按角色)', - size: 'small', - bordered: false, - style: cardStyle, - extra: React.createElement(Text, { type: 'secondary', style: { fontSize: 12 } }, '多角色时 Tab 切换;联调按权限过滤') + React.createElement(Modal, { + title: '超期未还车数量', + open: overdueReturnModalOpen, + width: 1320, + onCancel: function () { setOverdueReturnModalOpen(false); }, + footer: React.createElement(Button, { onClick: function () { setOverdueReturnModalOpen(false); } }, '关闭'), + destroyOnClose: true }, - React.createElement(Tabs, { - activeKey: roleTab, - onChange: setRoleTab, - items: quickTabItems + React.createElement(Text, { type: 'secondary', style: { fontSize: 12, display: 'block', marginBottom: 10 } }, + '以下车辆已超过合同应还期限仍未完成还车流程,示意数据与还车管理-待处理列表字段一致(标橙提示);联调接接口。' + ), + React.createElement(Table, { + className: 'workbench-overdue-return-modal', + size: 'small', + rowKey: 'id', + columns: overdueReturnModalColumns, + dataSource: overdueReturnMockRows, + pagination: false, + scroll: { x: 1780, y: 360 } }) ) ) diff --git a/web端/需求说明/工作台 b/web端/需求说明/工作台 new file mode 100644 index 0000000..8940481 --- /dev/null +++ b/web端/需求说明/工作台 @@ -0,0 +1,52 @@ +一个「数字化资产ONEOS运管平台」中的「工作台」模块 +#面包屑:工作台 + +1.顶部为关键指教,以多个指标卡片分别展示,分别为: +1.1.待办工作:显示待办工作数量,包括以下任务: + 1.1.1.运维侧:交车任务、调拨任务、异动任务、年审任务; + 1.1.2.业管侧:商业险到期、租赁账单生成、提车应收款、还车应结款; + 1.1.3.业管-能源部侧:氢费审核; + 权限分配规则: + 业管、运维基层员工:仅显示当前登录人作为处理人的任务; + 业管、运维主管:显示所属部门所有未完成任务; + +1.2.已完成工作:显示已完成工作数量,包括当前用户所有已完成的待办工作; + +1.3.待审批任务:显示所有待审批任务数量,点击弹出卡片,卡片中列表显示流程到达时间、流程类型、发起时间、发起人、操作(去处理,点击跳转); + 包括以下任务: + 1.3.1.运维侧:替换车审核、调拨审核、异动审核; + 1.3.1.财务侧:租赁账单审核、提车应收款审核、还车应结款审核、氢费账单审核; + 1.3.2.法务侧:租赁合同审核(附件上传); + 1.3.3.CEO侧:提车应收款、还车应结款、氢费账单; + +1.4.已审批任务:显示所有已审批任务数量,包括当前用户所有已完成的审批任务; + +1.3.通知消息:显示所有通知消息数量,点击弹出卡片,卡片中列表显示时间、通知类型、内容; + 通知消息包括: + 1.3.1.运维侧: + 1.3.1.1.保险到期提醒:格式为:“车牌号”商业险将在"YYYY-MM-DD"到期,请尽快处理; + 1.3.1.2.营运证到期提醒:格式为:“车牌号”营运证还有90天到期,请尽快更新营运证; + 1.3.1.3.行驶证到期提醒:格式为:“车牌号”行驶证还有90天到期,请尽快进行年审; + 1.3.1.4.加氢证到期提醒:格式为:“车牌号”加氢证还有90天到期,请尽快处理; + 1.3.1.5.安全阀到期提醒:格式为:“车牌号”安全阀还有90天到期,请尽快处理; + 1.3.1.6.压力表到期提醒:格式为:“车牌号”压力表还有90天到期,请尽快处理; + 1.3.2.业管侧: + 1.3.2.1.租赁合同到期提醒:格式为:“租赁合同编码”“项目名称”还有30天到期,请尽快处理; + 1.3.2.2.租赁账单生成提醒:格式为:“租赁合同编码”“项目名称”“账单编号”已生成,请尽快处理; + 1.3.2.3.氢费余额不足提醒:格式为:“客户名称”“项目名称”氢费余额已不足500元,请尽快通知客户处理; + 1.3.3.审批侧: + 1.3.3.1.审批流程提醒:格式为:“发起人姓名”“流程名称”审批节点已到达,请进行审批; + +2.中间分为几个单独卡片,包括: +2.1.我的待办清单:左侧,显示所有待办工作列表,列表显示待办任务名称、任务生成时间、后方为去处理,点击去处理,跳转相应模块并打开该任务; +2.2.数据统计模块:包括车辆数据统计、合同数据统计; + 2.2.1.车辆数据统计:上方为统计卡片,包括:车辆总数、自营车辆数、库存车辆数、待运营车辆数,下方为柱状图:近12个月运营车辆、闲置车辆双柱状图; + 2.2.2.合同数据统计:上方为统计卡片,包括:租赁合同总数、自营合同总数,下方为柱状图:近12个月租赁合同、自营合同双柱状图; +2.3.我的通知清单:右侧,显示所有通知列表,列表显示通知时间、通知类型、通知内容; +2.4.底部为快速入口,根据人员角色显示,如当前用户有多个角色,则显示tab切换(运维、业管、安全、能源、财务、安全、法务),入口以上方图标,下方名称显示,分别为: + 2.4.1.业管:租赁合同、交车任务、提车应收款、租赁账单、还车应结款、能源账户、氢费账单、电费账单、ETC账单、保险管理、审批中心; + 2.4.2.业管-能源部:加氢订单管理; + 2.4.3.运维:车辆管理、证照管理、备车管理、交车管理、还车管理、替换车管理、调拨管理、异动管理、备件库管理; + 2.4.4.财务:提车应收款、租赁账单、还车应结款、审批中心、充值单管理; + 2.4.5.安全:违章管理、事故管理、司机管理、安全培训资料、安全培训记录; + 2.4.6.法务:审批中心; \ No newline at end of file diff --git a/web端/需求说明/运维管理-仓库管理/备件库存 b/web端/需求说明/运维管理-仓库管理/备件库存 index 946c021..bddabea 100644 --- a/web端/需求说明/运维管理-仓库管理/备件库存 +++ b/web端/需求说明/运维管理-仓库管理/备件库存 @@ -7,14 +7,73 @@ 1.3.备件名称:选择器,支持输入框内输入模糊搜索,下拉匹配正确项; 1.4.适配车型:选择器,支持多选,显示所有型号; -2.列表;右侧为新增、导出按钮 +2.列表;右侧为新增备件信息、入库、领用出库、批量导入、导出按钮 +#列表: 2.1.序号:1.2.3.以此类推; 2.2.仓库编码:显示仓库编码; 2.3.仓库名称:显示仓库名称; 2.4.备件编码:显示备件编码; 2.5.备件名称:显示备件名称; -2.6.适配车型:显示适配车型,一个备件可能有多个车型; +2.6.适配车型:显示适配车型,一个备件可能有多个车型,非必填; 2.7.库存数量:显示库存数量; 2.8.累积入库数量:显示该备件入库数量总和; 2.9.累积出库数量:显示该备件出库数量总和; -2.10.右下角为分页功能,支持选择单页显示数据条数; +2.10.操作:备件明细、入库记录、出库记录; + 2.10.1.备件明细:点击弹出抽屉,字段为: + 2.10.1.1.备件编码:显示备件编码; + 2.10.1.2.备件名称:显示备件名称; + 2.10.1.3.计算单位:显示备件计算单位; + 2.10.1.4.适配车型:显示备件适配车型; + 2.10.1.5.告警阈值:显示备件告警阈值; + 2.10.2.入库记录:点击弹出抽屉,显示列表,支持导出csv,列表字段为: + 2.10.2.1.备件编码:显示备件编码; + 2.10.2.2.备件名称:显示备件名称; + 2.10.2.3.入库类型:显示入库类型,分为采购、其他; + 2.10.2.4.入库数量:显示入库数量,单位根据备件明细单位显示,格式为:xx(单位); + 2.10.2.5.入库日期:显示入库日期,YYYY-MM-DD; + 2.10.2.6.入库人员:显示入库人员姓名; + 2.10.2.7.单价:显示入库单价,格式为:xx.xx元; + 2.10.2.8.入库照片:显示入库照片,点击可放大预览; + 2.10.2.9.备注:显示备注信息; + 2.10.3.出库记录:点击弹出抽屉,显示列表,支持导出csv,字段为: + 2.10.3.1.备件编码:显示备件编码; + 2.10.3.2.备件名称:显示备件名称; + 2.10.3.3.出库类型:显示出库类型; + 2.10.3.4.出库数量:显示出库数量,单位根据备件明细单位显示,格式为:xx(单位); + 2.10.3.5.出库日期:显示出库日期,YYYY-MM-DD; + 2.10.3.6.出库人员:显示出库人员姓名; + 2.10.3.7.备注:显示备注信息; +2.11.右下角为分页功能,支持选择单页显示数据条数; + +3.新增备件信息 +点击弹出抽屉,字段为: +3.1.备件编码:输入框,默认提示为请输入备件编码; +3.2.备件名称:输入框,默认提示为请输入备件名称; +3.3.计算单位:输入框,默认提示为请输入计量单位; +3.4.适配车型:非必填,选择器,支持多选,选项来自型号表,默认提示:请选择车型; +3.5.告警阈值:输入框,支持正数整数输入,支持输入框上下按钮调整数量,默认提示为请设置备件告警阈值; + +4.入库: +点击弹出抽屉,字段为: +4.1.仓库名称:选择器,选项来自于仓库管理; +4.2.备件名称:选择器,选项来自于备件管理; +4.3.入库类型:选择器,选项为:采购、其他; +4.4.入库数量:输入框,支持正数整数输入;支持点击增加减少变动数量; +4.5.入库日期:日期选择器,精确至天,默认为当日; +4.6.单价:输入框,支持2位小数输入,后缀为元; +4.7.入库照片:照片上传按钮,点击上传本地文件,上传后,支持预览和点击右上角删除按钮删除; +4.8.备注:文本域,支持自定义输入; + +5.领用出库: +点击弹出抽屉,字段为: +5.1.仓库名称:选择器,选项来自于仓库管理; +5.2.备件名称:选择器,选项来自于备件管理; +5.3.出库类型:选择器,选项为:维修、保养、其他; +5.4.出库数量:输入框,支持正数整数输入;支持点击增加减少变动数量; +5.5.出库日期:日期选择器,精确至天,默认为当天; +5.6.备注:文本域,支持自定义输入; + +6.批量导入: +根据入库字段,生成csv模板; + +7.导出:根据筛选条件自动导出csv文件; \ No newline at end of file