From 2a3b5386851466b5ba6292d3eb9e0cbc389a939c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=86=95?= Date: Mon, 15 Jun 2026 09:33:54 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=8A=A0=E6=B0=A2=E7=AB=99?= =?UTF-8?q?=E7=AB=99=E7=82=B9=E4=BF=A1=E6=81=AF=E4=B8=8E=E5=8A=A0=E6=B0=A2?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E9=A1=B5=E9=9D=A2=E5=8E=9F=E5=9E=8B=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增加氢记录模块并完善站点信息管理,支撑建站、绑号与台账对账相关交互。 Co-authored-by: Cursor --- web端/加氢站管理/加氢记录.jsx | 1807 +++++++++ web端/加氢站管理/站点信息.jsx | 6657 ++++++++++++++++++++++++++++----- 2 files changed, 7576 insertions(+), 888 deletions(-) create mode 100644 web端/加氢站管理/加氢记录.jsx diff --git a/web端/加氢站管理/加氢记录.jsx b/web端/加氢站管理/加氢记录.jsx new file mode 100644 index 0000000..9909154 --- /dev/null +++ b/web端/加氢站管理/加氢记录.jsx @@ -0,0 +1,1807 @@ +// 【重要】必须使用 const Component 作为组件变量名 +// 加氢站管理 - 加氢记录(列表 + 筛选 + 预约加氢弹窗 + 卡片式新增/编辑 + 导出) + +function hrSvgIcon(paths, size) { + return React.createElement('svg', { + viewBox: '0 0 24 24', + width: size || 18, + height: size || 18, + fill: 'none', + stroke: 'currentColor', + strokeWidth: 2, + strokeLinecap: 'round', + strokeLinejoin: 'round', + 'aria-hidden': true + }, paths.map(function (p, i) { + if (p.tag === 'circle') return React.createElement('circle', { key: i, cx: p.cx, cy: p.cy, r: p.r }); + if (p.tag === 'line') return React.createElement('line', { key: i, x1: p.x1, y1: p.y1, x2: p.x2, y2: p.y2 }); + return React.createElement('path', { key: i, d: p.d }); + })); +} + +var HR_ICONS = { + back: hrSvgIcon([{ tag: 'line', x1: 19, y1: 12, x2: 5, y2: 12 }, { d: 'M12 19l-7-7 7-7' }], 16), + fuel: hrSvgIcon([{ d: 'M3 22h12M5 22V10l7-4v16M19 22V8l-4-2v16' }, { tag: 'circle', cx: 8.5, cy: 17.5, r: 1.5 }], 18), + user: hrSvgIcon([{ d: 'M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2' }, { tag: 'circle', cx: 12, cy: 7, r: 4 }], 16), + calendar: hrSvgIcon([{ d: 'M19 4H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2z' }, { tag: 'line', x1: 16, y1: 2, x2: 16, y2: 6 }, { tag: 'line', x1: 8, y1: 2, x2: 8, y2: 6 }, { tag: 'line', x1: 3, y1: 10, x2: 21, y2: 10 }], 16), + empty: hrSvgIcon([{ tag: 'circle', cx: 12, cy: 12, r: 10 }, { tag: 'line', x1: 8, y1: 12, x2: 16, y2: 12 }], 40) +}; + +/** @see web端/styles/oneos-ant-table-global-fix.js */ +var ONEOS_ANT_TABLE_GLOBAL_FIX = [ + '.ant-table-container .ant-table-header { margin-bottom: 0 !important; }', + '.ant-table-container .ant-table-body { margin-top: 0 !important; }', + '.ant-table-container .ant-table-body > table, .ant-table-content table { margin-top: 0 !important; }', + '.ant-table-tbody > tr.ant-table-measure-row, .ant-table-tbody > tr.ant-table-measure-row > td, .ant-table-tbody > tr.ant-table-measure-row > th { display: none !important; height: 0 !important; max-height: 0 !important; min-height: 0 !important; padding: 0 !important; margin: 0 !important; border: none !important; line-height: 0 !important; font-size: 0 !important; overflow: hidden !important; visibility: hidden !important; pointer-events: none !important; }' +]; + +var HR_PAGE_STYLE = ONEOS_ANT_TABLE_GLOBAL_FIX.concat([ + '.h2-station-page { --h2-form-row-gap: 20px; --h2-form-col-gap: 24px; --h2-form-label-gap: 8px; padding: 24px 24px 80px; height: 100vh; display: flex; flex-direction: column; background: linear-gradient(165deg, #f1f5f9 0%, #f8fafc 50%, #f1f5f9 100%); overflow: hidden; box-sizing: border-box; }', + '.h2-station-page.h2-station-page--create { padding: 0 0 96px; height: auto; min-height: 100dvh; overflow: auto; }', + '.h2-station-page--create .h2-create-shell { width: 100%; max-width: none; margin: 0; padding: 24px 24px 0; box-sizing: border-box; }', + '.h2-station-page--create .h2-create-topbar { display: flex; align-items: center; min-height: 40px; margin-bottom: 16px; position: relative; }', + '.h2-station-page--create .h2-create-topbar-title { position: absolute; left: 50%; transform: translateX(-50%); margin: 0; font-size: 18px; font-weight: 700; color: #0f172a; line-height: 1.35; pointer-events: none; white-space: nowrap; }', + '.h2-station-page--create .h2-create-back-btn { display: inline-flex !important; align-items: center; gap: 6px; height: 32px; padding: 0 12px !important; border-radius: 8px !important; font-weight: 500; color: #475569 !important; border: 1px solid #e2e8f0 !important; background: #fff !important; }', + '.h2-station-page--create .h2-create-back-btn:hover { color: #059669 !important; border-color: #10b981 !important; background: #f0fdf4 !important; }', + '.h2-station-page .h2-card-title-bar--step::before { display: none !important; }', + '.h2-station-page .h2-card-step-badge { width: 28px; height: 28px; border-radius: 50%; background: linear-gradient(145deg, #10b981 0%, #059669 100%); color: #fff; font-size: 14px; font-weight: 700; display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; box-shadow: 0 2px 8px rgba(16, 185, 129, 0.32); }', + '.h2-station-page .h2-card-title-text { font-size: 15px; font-weight: 700; color: #0f172a; }', + '.h2-station-page--create .h2-create-form > .h2-create-card + .h2-create-card { margin-top: 16px; }', + '.h2-station-page--create .h2-create-footer-inner { justify-content: flex-end; }', + '@media (prefers-reduced-motion: no-preference) { .h2-station-page--create .h2-create-card { animation: hrCreateCardIn 0.35s ease-out backwards; } .h2-station-page--create .h2-create-card + .h2-create-card { animation-delay: 0.06s; } }', + '@keyframes hrCreateCardIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }', + '.h2-station-page .h2-create-card.ant-card { border-radius: 16px !important; border: 1px solid #e2e8f0 !important; box-shadow: 0 4px 24px -6px rgba(15, 23, 42, 0.06) !important; margin-bottom: 16px !important; }', + '.h2-station-page .h2-create-card > .ant-card-head { border-bottom: 1px solid #f1f5f9 !important; min-height: auto; padding: 16px 22px !important; background: linear-gradient(180deg, #fafbfc 0%, #fff 100%); }', + '.h2-station-page .h2-create-card > .ant-card-head .ant-card-head-title { font-size: 15px !important; font-weight: 700 !important; color: #0f172a !important; padding: 0 !important; }', + '.h2-station-page .h2-create-card > .ant-card-body { padding: 20px 24px 24px !important; }', + '.h2-station-page .h2-card-title-bar { display: inline-flex; align-items: center; gap: 10px; }', + '.h2-station-page .h2-card-title-bar::before { content: ""; width: 3px; height: 16px; border-radius: 2px; background: linear-gradient(180deg, #10b981, #34d399); flex-shrink: 0; }', + '.h2-station-page .h2-card-title-icon { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 10px; background: #ecfdf5; color: #059669; flex-shrink: 0; }', + '.h2-station-page--create .h2-create-form--grid .h2-create-form-grid { width: 100%; }', + '.h2-station-page--create .h2-create-form--grid .ant-row { width: 100% !important; margin-left: 0 !important; margin-right: 0 !important; }', + '.h2-station-page--create .h2-create-form--grid .ant-col { min-width: 0; }', + '.h2-station-page--create .h2-create-form--grid .ant-form-item { margin-bottom: 0; width: 100%; }', + '.h2-station-page--create .h2-create-form--grid .ant-form-item-label { padding: 0 0 var(--h2-form-label-gap); min-height: 22px; }', + '.h2-station-page--create .h2-create-form--grid .ant-form-item-label > label { font-size: 13px; font-weight: 500; color: #475569; }', + '.h2-station-page--create .h2-create-form--grid .ant-col > .ant-form-item { display: flex; flex-direction: column; }', + '.h2-station-page--create .h2-create-form--grid .ant-form-item-control { flex: 1; width: 100%; }', + '.h2-station-page--create .h2-create-form--grid .ant-form-item-control-input, .h2-station-page--create .h2-create-form--grid .ant-form-item-control-input-content { width: 100%; max-width: 100%; }', + '.h2-station-page--create .h2-create-form--grid .h2-create-input, .h2-station-page--create .h2-create-form--grid .ant-select, .h2-station-page--create .h2-create-form--grid .ant-picker, .h2-station-page--create .h2-create-form--grid .ant-input-number { width: 100% !important; max-width: 100% !important; }', + '.h2-station-page--create .h2-create-form--list .ant-input:not(.ant-input-disabled), .h2-station-page--create .h2-create-form--list .ant-select:not(.ant-select-disabled) .ant-select-selector, .h2-station-page--create .h2-create-form--list .ant-picker:not(.ant-picker-disabled), .h2-station-page--create .h2-create-form--list .ant-input-number:not(.ant-input-number-disabled) { border-radius: 8px !important; border: 1px solid #e2e8f0 !important; min-height: 32px !important; height: 32px !important; font-size: 13px !important; background: #fff !important; color: #0f172a !important; width: 100% !important; }', + '.h2-station-page--create .h2-create-subsection-title { margin: 4px 0 0; padding: 12px 0 10px; font-size: 13px; font-weight: 600; color: #475569; border-bottom: 1px solid #f1f5f9; width: 100%; box-sizing: border-box; }', + '.h2-station-page .h2-create-footer { position: fixed; bottom: 0; left: 0; right: 0; z-index: 100; background: rgba(255, 255, 255, 0.92); backdrop-filter: blur(10px); border-top: 1px solid #e2e8f0; box-shadow: 0 -8px 24px rgba(15, 23, 42, 0.06); }', + '.h2-station-page .h2-create-footer-inner { width: 100%; padding: 14px 24px; display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; box-sizing: border-box; }', + '.h2-station-page .h2-create-footer-hint { font-size: 13px; color: #64748b; display: flex; align-items: center; gap: 10px; min-width: 0; }', + '.h2-station-page .h2-create-footer-progress { flex: 1; min-width: 120px; max-width: 200px; }', + '.h2-station-page .h2-create-footer-progress .ant-progress-text { font-size: 12px !important; font-weight: 700; color: #059669 !important; }', + '.h2-station-page .h2-create-footer-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-left: auto; }', + '.h2-station-page .h2-create-footer-actions .ant-btn { min-height: 44px; padding: 0 20px; border-radius: 10px; font-weight: 600; }', + '.h2-station-page .h2-create-footer-actions .ant-btn-primary { min-width: 120px; box-shadow: 0 4px 14px -2px rgba(16, 185, 129, 0.45); }', + '.h2-station-page .lc-filter-card.ant-card { border-radius: 16px !important; border: 1px solid #e2e8f0 !important; box-shadow: 0 4px 20px -4px rgba(15, 23, 42, 0.03) !important; margin-bottom: 16px; }', + '.h2-station-page .lc-filter-card > .ant-card-head { border-bottom: 1px solid #f1f5f9 !important; min-height: auto; padding: 12px 20px !important; }', + '.h2-station-page .lc-filter-card > .ant-card-head .ant-card-head-title { font-size: 15px !important; font-weight: 700 !important; color: #0f172a !important; padding: 0 !important; }', + '.h2-station-page .lc-filter-card > .ant-card-body { padding: 16px 20px 20px !important; }', + '.h2-station-page .lc-filter-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px 24px; align-items: center; }', + '@media (max-width: 1280px) { .h2-station-page .lc-filter-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }', + '@media (max-width: 640px) { .h2-station-page .lc-filter-grid { grid-template-columns: 1fr; } }', + '.h2-station-page .lc-filter-field { display: flex; align-items: center; gap: 12px; min-width: 0; min-height: 32px; }', + '.h2-station-page .lc-filter-field-label { flex: 0 0 88px; text-align: right; font-size: 13px; font-weight: 500; color: #475569; line-height: 32px; white-space: nowrap; }', + '.h2-station-page .lc-filter-field-control { flex: 1; min-width: 0; }', + '.h2-station-page .lc-filter-field-control .ant-input, .h2-station-page .lc-filter-field-control .ant-input-affix-wrapper, .h2-station-page .lc-filter-field-control .ant-select .ant-select-selector, .h2-station-page .lc-filter-field-control .ant-picker, .h2-station-page .lc-filter-field-control .ant-picker-range { width: 100%; height: 32px !important; min-height: 32px !important; border-radius: 8px !important; box-sizing: border-box; }', + '.h2-station-page .lc-filter-field-control .ant-input-affix-wrapper { display: inline-flex; align-items: center; padding-top: 0 !important; padding-bottom: 0 !important; }', + '.h2-station-page .lc-filter-field-control .ant-select-single .ant-select-selector { display: flex; align-items: center; }', + '.h2-station-page .lc-filter-field-control .ant-select-single .ant-select-selector .ant-select-selection-item, .h2-station-page .lc-filter-field-control .ant-select-single .ant-select-selector .ant-select-selection-placeholder { line-height: 30px !important; }', + '.h2-station-page .lc-filter-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; padding-top: 16px; border-top: 1px solid #f1f5f9; }', + '.h2-station-page .lc-table-section { margin-bottom: 0; flex: 1; display: flex; flex-direction: column; min-height: 0; }', + '.h2-station-page .lc-table-toolbar { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px 16px; margin-bottom: 8px; min-height: 32px; }', + '.h2-station-page .lc-table-toolbar-left { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }', + '.h2-station-page .lc-table-toolbar-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-left: auto; }', + '.h2-station-page .lc-table-card { background: #fff; border-radius: 16px; border: 1px solid #e2e8f0; box-shadow: 0 4px 20px -4px rgba(15, 23, 42, 0.03); overflow: hidden; flex: 1; overflow-y: auto; display: flex; flex-direction: column; }', + '.h2-station-page .lc-list-table .ant-table-wrapper, .h2-station-page .lc-list-table .ant-table { width: 100% !important; }', + '.h2-station-page .lc-table-card .ant-table-thead > tr > th { background: #f8fafc !important; color: #475569 !important; font-weight: 700 !important; font-size: 13px !important; border-bottom: 1px solid #e2e8f0 !important; padding: 12px 16px !important; }', + '.h2-station-page .lc-table-card .ant-table-tbody > tr.ant-table-measure-row, .h2-station-page .lc-table-card .ant-table-tbody > tr.ant-table-measure-row > td { height: 0 !important; max-height: 0 !important; padding: 0 !important; border: none !important; visibility: hidden !important; }', + '.h2-station-page .lc-table-card .ant-table-tbody > tr:not(.ant-table-measure-row) > td { padding: 12px 16px !important; font-size: 13px; }', + '.h2-station-page .lc-table-card .ant-table-tbody > tr:not(.ant-table-measure-row):hover > td { background: #f8fafc !important; }', + '.h2-station-page .lc-table-card .ant-pagination { margin: 0 !important; padding: 12px 16px !important; border-top: 1px solid #f1f5f9; }', + '.h2-station-page .lc-action-btn { font-weight: 600 !important; color: #10b981 !important; padding: 0 !important; }', + '.h2-station-page .lc-action-btn-danger { color: #ef4444 !important; }', + '.h2-station-page .h2-row-actions { display: inline-flex; align-items: center; gap: 4px; }', + '.h2-station-page .hr-refuel-plate { font-weight: 700; color: #0f172a; font-variant-numeric: tabular-nums; }', + '.h2-station-page .hr-refuel-kg { font-weight: 700; color: #059669; font-variant-numeric: tabular-nums; }', + '.h2-station-page .hr-refuel-money { font-weight: 600; color: #4f46e5; font-variant-numeric: tabular-nums; }', + '.h2-station-page .hr-refuel-km { font-variant-numeric: tabular-nums; color: #334155; }', + '.h2-station-page .hr-refuel-time { font-variant-numeric: tabular-nums; color: #334155; font-size: 13px; white-space: nowrap; }', + '.h2-station-page .hr-appointment-driver { font-weight: 600; color: #0f172a; }', + '.h2-station-page .hr-appointment-contact { font-variant-numeric: tabular-nums; color: #475569; }', + '.h2-station-page .hr-appointment-reject-reason { font-size: 12px; color: #94a3b8; margin-top: 4px; max-width: 220px; }', + '.h2-refuel-drill-modal .ant-modal-content { border-radius: 16px !important; overflow: hidden; box-shadow: 0 24px 48px -12px rgba(15, 23, 42, 0.18) !important; }', + '.h2-refuel-drill-modal .ant-modal-header { padding: 18px 24px 14px !important; border-bottom: 1px solid #f1f5f9 !important; margin-bottom: 0 !important; }', + '.h2-refuel-drill-modal .ant-modal-title { font-size: 17px !important; font-weight: 700 !important; color: #0f172a !important; }', + '.h2-refuel-drill-modal .ant-modal-body { padding: 16px 24px 20px !important; background: #f8fafc; }', + '.h2-refuel-drill-modal .ant-modal-footer { padding: 12px 24px 18px !important; border-top: 1px solid #f1f5f9 !important; background: #fff; }', + '.h2-refuel-drill-modal .h2-refuel-drill-panel { display: flex; flex-direction: column; gap: 14px; }', + '.h2-refuel-drill-modal .h2-refuel-drill-station-card { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; padding: 12px 16px; background: linear-gradient(135deg, #eff6ff 0%, #ecfdf5 50%, #f8fafc 100%); border: 1px solid #bfdbfe; border-radius: 12px; }', + '.h2-refuel-drill-modal .h2-refuel-drill-station-card__name { font-size: 15px; font-weight: 700; color: #0f172a; line-height: 1.35; }', + '.h2-refuel-drill-modal .h2-refuel-drill-station-card__meta { font-size: 12px; color: #64748b; font-variant-numeric: tabular-nums; white-space: nowrap; }', + '.h2-refuel-drill-modal .h2-refuel-drill-stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; }', + '@media (max-width: 860px) { .h2-refuel-drill-modal .h2-refuel-drill-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); } }', + '@media (max-width: 520px) { .h2-refuel-drill-modal .h2-refuel-drill-stats { grid-template-columns: 1fr; } }', + '.h2-refuel-drill-modal .h2-refuel-drill-stat { display: flex; flex-direction: column; justify-content: center; min-height: 78px; padding: 12px 14px; border-radius: 12px; border: 1px solid #e2e8f0; background: #fff; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06); min-width: 0; box-sizing: border-box; }', + '.h2-refuel-drill-modal .h2-refuel-drill-stat--count { border-left: 4px solid #3b82f6; }', + '.h2-refuel-drill-modal .h2-refuel-drill-stat--kg { border-left: 4px solid #10b981; background: linear-gradient(180deg, #fff 0%, #f0fdf4 100%); }', + '.h2-refuel-drill-modal .h2-refuel-drill-stat--cost { border-left: 4px solid #f97316; }', + '.h2-refuel-drill-modal .h2-refuel-drill-stat--customer { border-left: 4px solid #6366f1; }', + '.h2-refuel-drill-modal .h2-refuel-drill-stat__label { font-size: 12px; font-weight: 600; color: #64748b; margin-bottom: 8px; line-height: 1.2; }', + '.h2-refuel-drill-modal .h2-refuel-drill-stat__value { font-size: 20px; font-weight: 800; font-variant-numeric: tabular-nums; line-height: 1.2; color: #0f172a; word-break: break-all; }', + '.h2-refuel-drill-modal .h2-refuel-drill-stat__value--count { color: #2563eb; }', + '.h2-refuel-drill-modal .h2-refuel-drill-stat__value--kg { color: #059669; }', + '.h2-refuel-drill-modal .h2-refuel-drill-stat__value--cost { color: #ea580c; }', + '.h2-refuel-drill-modal .h2-refuel-drill-stat__value--customer { color: #4f46e5; }', + '.h2-refuel-drill-modal .h2-refuel-drill-table-wrap { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; overflow: hidden; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04); }', + '.h2-refuel-drill-modal .h2-refuel-drill-table-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 14px; border-bottom: 1px solid #f1f5f9; background: #fafbfc; }', + '.h2-refuel-drill-modal .h2-refuel-drill-table-head__left { display: flex; align-items: center; gap: 10px; min-width: 0; }', + '.h2-refuel-drill-modal .h2-refuel-drill-table-head__title { font-size: 13px; font-weight: 700; color: #334155; }', + '.h2-refuel-drill-modal .h2-refuel-drill-table-head__count { font-size: 12px; color: #64748b; font-variant-numeric: tabular-nums; }', + '.h2-refuel-drill-modal .h2-refuel-record-table { border-radius: 0 !important; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .ant-table { background: transparent !important; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .ant-table-container { border: none !important; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .ant-table-header { margin-bottom: 0 !important; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .ant-table-body { margin-top: 0 !important; overflow-y: auto !important; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .ant-table-body table { margin-top: 0 !important; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .ant-table-tbody > tr.ant-table-measure-row, .h2-refuel-drill-modal .h2-refuel-record-table .ant-table-tbody > tr.ant-table-measure-row > td { height: 0 !important; max-height: 0 !important; padding: 0 !important; margin: 0 !important; border: none !important; line-height: 0 !important; font-size: 0 !important; overflow: hidden !important; visibility: hidden !important; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .ant-table-thead > tr > th { background: #f8fafc !important; font-size: 12px !important; font-weight: 700 !important; color: #475569 !important; padding: 10px 12px !important; border-bottom: 1px solid #e2e8f0 !important; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .ant-table-tbody > tr:not(.ant-table-measure-row) > td { font-size: 13px !important; padding: 10px 12px !important; vertical-align: middle !important; border-bottom: 1px solid #f8fafc !important; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .ant-table-tbody > tr:not(.ant-table-measure-row):last-child > td { border-bottom: none !important; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .ant-table-tbody > tr:hover > td { background: #eff6ff !important; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .h2-refuel-drill-seq { display: inline-flex; align-items: center; justify-content: center; min-width: 24px; height: 24px; border-radius: 6px; background: #f1f5f9; color: #64748b; font-size: 12px; font-weight: 700; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .h2-refuel-drill-time { color: #334155; font-variant-numeric: tabular-nums; font-size: 12px; white-space: nowrap; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .h2-refuel-drill-plate { font-weight: 700; color: #0f172a; font-variant-numeric: tabular-nums; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .h2-refuel-drill-kg { font-weight: 700; color: #059669; font-variant-numeric: tabular-nums; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .h2-refuel-drill-contact { font-variant-numeric: tabular-nums; color: #475569; font-size: 13px; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .h2-refuel-drill-driver { font-weight: 600; color: #0f172a; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .hr-appointment-reject-reason { font-size: 12px; color: #94a3b8; margin-top: 4px; max-width: 220px; }', + '.h2-station-page .hr-daily-stats-section { margin-bottom: 16px; }', + '.h2-station-page .hr-daily-stats-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 10px; }', + '.h2-station-page .hr-daily-stats-head__title { font-size: 15px; font-weight: 700; color: #0f172a; }', + '.h2-station-page .hr-daily-stats-head__meta { font-size: 12px; color: #64748b; font-variant-numeric: tabular-nums; }', + '.h2-station-page .hr-daily-stats-carousel { display: flex; align-items: stretch; gap: 8px; }', + '.h2-station-page .hr-daily-stats-nav-btn { flex: 0 0 36px; width: 36px !important; min-width: 36px !important; height: auto !important; min-height: 88px !important; padding: 0 !important; border-radius: 10px !important; font-size: 20px !important; font-weight: 700 !important; color: #475569 !important; border: 1px solid #e2e8f0 !important; background: #fff !important; display: inline-flex !important; align-items: center !important; justify-content: center !important; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06); }', + '.h2-station-page .hr-daily-stats-nav-btn:hover:not(:disabled) { color: #059669 !important; border-color: #10b981 !important; background: #f0fdf4 !important; }', + '.h2-station-page .hr-daily-stats-nav-btn:disabled { opacity: 0.35; cursor: not-allowed; box-shadow: none; }', + '.h2-station-page .hr-daily-stats-track { flex: 1; min-width: 0; display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 8px; }', + '.h2-station-page .hr-daily-stat-card { display: flex; flex-direction: column; gap: 8px; min-height: 88px; padding: 12px 10px; border-radius: 12px; border: 1px solid #e2e8f0; background: #fff; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06); box-sizing: border-box; min-width: 0; }', + '.h2-station-page .hr-daily-stat-card--today { border-color: #86efac; background: linear-gradient(180deg, #fff 0%, #f0fdf4 100%); box-shadow: 0 2px 8px rgba(16, 185, 129, 0.12); }', + '.h2-station-page .hr-daily-stat-card__date { font-size: 12px; font-weight: 700; color: #0f172a; font-variant-numeric: tabular-nums; padding-bottom: 8px; border-bottom: 1px solid #f1f5f9; display: flex; align-items: center; justify-content: space-between; gap: 4px; }', + '.h2-station-page .hr-daily-stat-card__today-tag { font-size: 10px; font-weight: 700; color: #059669; background: #ecfdf5; padding: 1px 6px; border-radius: 999px; flex-shrink: 0; }', + '.h2-station-page .hr-daily-stat-card__row { display: flex; align-items: center; justify-content: space-between; gap: 8px; font-size: 12px; }', + '.h2-station-page .hr-daily-stat-card__label { color: #64748b; font-weight: 600; white-space: nowrap; }', + '.h2-station-page .hr-daily-stat-card__value { font-weight: 800; font-variant-numeric: tabular-nums; }', + '.h2-station-page .hr-daily-stat-card__value--count { color: #2563eb; }', + '.h2-station-page .hr-daily-stat-card__value--kg { color: #059669; }', + '.h2-station-page--create .hr-batch-create-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 12px; }', + '.h2-station-page--create .hr-batch-create-toolbar__count { font-size: 13px; color: #64748b; font-variant-numeric: tabular-nums; }', + '.h2-station-page--create .hr-batch-create-table-wrap { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; overflow: hidden; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04); }', + '.h2-station-page--create .hr-batch-create-table .ant-table-thead > tr > th { background: #f8fafc !important; font-size: 12px !important; font-weight: 700 !important; color: #475569 !important; padding: 10px 12px !important; }', + '.h2-station-page--create .hr-batch-create-table .ant-table-tbody > tr > td { padding: 8px 10px !important; vertical-align: middle !important; }', + '.h2-station-page--create .hr-batch-create-table .ant-input, .h2-station-page--create .hr-batch-create-table .ant-input-number, .h2-station-page--create .hr-batch-create-table .ant-select .ant-select-selector, .h2-station-page--create .hr-batch-create-table .ant-picker { border-radius: 8px !important; font-size: 13px !important; }', + '.h2-station-page--create .hr-batch-create-seq { display: inline-flex; align-items: center; justify-content: center; min-width: 24px; height: 24px; border-radius: 6px; background: #f1f5f9; color: #64748b; font-size: 12px; font-weight: 700; }' +]).join('\n'); + +var HR_DAILY_STATS_WINDOW = 28; +var HR_DAILY_STATS_PAGE_SIZE = 7; +var HR_DAILY_STATS_MAX_PAGE = Math.ceil(HR_DAILY_STATS_WINDOW / HR_DAILY_STATS_PAGE_SIZE) - 1; + +var HR_PRIMARY_BTN_STYLE = { + borderRadius: 8, + fontWeight: 600, + background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)', + border: 'none' +}; + +var HR_MOCK_FILLERS = ['张三', '李四', '王静', '赵敏', '陈浩']; + +var HR_SETTLEMENT_STATUS_OPTIONS = [ + { value: 'customer', label: '客户承担' }, + { value: 'internal', label: '我司承担' }, + { value: 'customer_self', label: '客户自行结算' } +]; + +var HR_SETTLEMENT_STATUS_MAP = { + customer: { label: '客户承担', color: 'blue' }, + internal: { label: '我司承担', color: 'green' }, + customer_self: { label: '客户自行结算', color: 'orange' } +}; + +function hrSettlementStatusLabel(value) { + var info = HR_SETTLEMENT_STATUS_MAP[value]; + return info ? info.label : '—'; +} + +var HR_INITIAL_RECORDS = [ + { id: 'hr-1', hydrogenTime: '2026-05-28 10:21:08', plateNo: '浙A12345F', hydrogenKg: 12.5, customerAmount: 562.5, mileageKm: 128560, fillerName: '张三', settlementStatus: 'customer' }, + { id: 'hr-2', hydrogenTime: '2026-05-26 14:08:33', plateNo: '浙A67890F', hydrogenKg: 10.0, customerAmount: 450.0, mileageKm: 95230, fillerName: '李四', settlementStatus: 'internal' }, + { id: 'hr-3', hydrogenTime: '2026-05-22 09:15:00', plateNo: '浙A88888F', hydrogenKg: 18.3, customerAmount: 823.5, mileageKm: 201880, fillerName: '王静', settlementStatus: 'customer_self' }, + { id: 'hr-4', hydrogenTime: '2026-05-18 16:42:11', plateNo: '浙A03561F', hydrogenKg: 15.6, customerAmount: 702.0, mileageKm: 167420, fillerName: '张三', settlementStatus: 'customer' }, + { id: 'hr-5', hydrogenTime: '2026-05-30 09:30:22', plateNo: '浙B23456F', hydrogenKg: 15.3, customerAmount: 703.8, mileageKm: 143200, fillerName: '赵敏', settlementStatus: 'internal' }, + { id: 'hr-6', hydrogenTime: '2026-05-27 18:10:05', plateNo: '浙B99999F', hydrogenKg: 18.2, customerAmount: 837.2, mileageKm: 189650, fillerName: '陈浩', settlementStatus: 'customer_self' }, + { id: 'hr-7', hydrogenTime: '2026-05-24 11:05:40', plateNo: '浙B58888F', hydrogenKg: 11.8, customerAmount: 542.8, mileageKm: 110340, fillerName: '李四', settlementStatus: 'customer' }, + { id: 'hr-8', hydrogenTime: '2026-04-20 16:45:18', plateNo: '沪A88888F', hydrogenKg: 8.0, customerAmount: 376.0, mileageKm: 88420, fillerName: '王静', settlementStatus: 'internal' }, + { id: 'hr-9', hydrogenTime: '2026-04-08 09:12:55', plateNo: '沪BDB9161F', hydrogenKg: 9.5, customerAmount: 446.5, mileageKm: 76500, fillerName: '张三', settlementStatus: 'customer_self' }, + { id: 'hr-10', hydrogenTime: '2026-03-15 10:00:00', plateNo: '苏E33333F', hydrogenKg: 6.2, customerAmount: 272.8, mileageKm: 45210, fillerName: '赵敏', settlementStatus: 'customer' } +]; + +var HR_APPOINTMENT_STATUS_MAP = { + pending: { label: '待处理', color: 'processing' }, + accepted: { label: '已接受', color: 'success' }, + rejected: { label: '已拒绝', color: 'error' } +}; + +var HR_INITIAL_APPOINTMENTS = [ + { id: 'ap-1', plateNo: '浙A12345F', appointmentTime: '2026-06-10 09:00:00', appointmentKg: 15.0, driverName: '刘建国', contact: '13800138011', status: 'pending' }, + { id: 'ap-2', plateNo: '浙B23456F', appointmentTime: '2026-06-10 11:30:00', appointmentKg: 12.5, driverName: '陈师傅', contact: '13900139022', status: 'pending' }, + { id: 'ap-3', plateNo: '浙A67890F', appointmentTime: '2026-06-09 14:00:00', appointmentKg: 10.0, driverName: '王磊', contact: '13700137033', status: 'pending' }, + { id: 'ap-4', plateNo: '沪A88888F', appointmentTime: '2026-06-08 16:20:00', appointmentKg: 8.5, driverName: '张强', contact: '13600136044', status: 'accepted', handledTime: '2026-06-08 10:15:00' }, + { id: 'ap-5', plateNo: '浙B99999F', appointmentTime: '2026-06-07 08:45:00', appointmentKg: 18.0, driverName: '赵明', contact: '13500135055', status: 'rejected', rejectReason: '预约时段站点设备检修,请改约其他时间', handledTime: '2026-06-06 17:30:00' } +]; + +function hrFormatKg(v) { + var n = typeof v === 'number' ? v : parseFloat(v); + return isNaN(n) ? '0.00' : n.toFixed(2); +} + +function hrFormatYuan(v) { + var n = typeof v === 'number' ? v : parseFloat(v); + return isNaN(n) ? '0.00' : n.toFixed(2); +} + +function hrFormatYuanSymbol(v) { + if (v == null || v === '') return '—'; + var n = typeof v === 'number' ? v : parseFloat(v); + if (isNaN(n)) return '—'; + return '¥' + n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); +} + +function hrFormatKm(v) { + if (v == null || v === '') return '—'; + var n = typeof v === 'number' ? v : parseFloat(String(v).replace(/,/g, '')); + if (isNaN(n)) return '—'; + return n.toLocaleString('zh-CN', { maximumFractionDigits: 0 }) + ' km'; +} + +function hrCsvEscape(v) { + var s = v == null ? '' : String(v); + if (/[",\n\r]/.test(s)) return '"' + s.replace(/"/g, '""') + '"'; + return s; +} + +function hrDownloadCsv(filename, headers, rows) { + var lines = [headers.map(hrCsvEscape).join(',')].concat( + (rows || []).map(function (r) { return r.map(hrCsvEscape).join(','); }) + ); + var blob = new Blob(['\uFEFF' + lines.join('\n')], { type: 'text/csv;charset=utf-8;' }); + var a = document.createElement('a'); + a.href = URL.createObjectURL(blob); + a.download = filename; + a.click(); + URL.revokeObjectURL(a.href); +} + +function hrExtractDateKey(timeStr) { + if (!timeStr) return ''; + var s = String(timeStr).trim(); + return s.length >= 10 ? s.slice(0, 10) : s; +} + +function hrPad2(n) { + return n < 10 ? '0' + n : String(n); +} + +function hrDateToKey(d) { + return d.getFullYear() + '-' + hrPad2(d.getMonth() + 1) + '-' + hrPad2(d.getDate()); +} + +function hrBuildRecentDailyWindow(list, dayjs, windowDays) { + windowDays = windowDays || HR_DAILY_STATS_WINDOW; + var statsMap = {}; + (list || []).forEach(function (r) { + var day = hrExtractDateKey(r.hydrogenTime); + if (!day) return; + if (!statsMap[day]) statsMap[day] = { count: 0, totalKg: 0 }; + statsMap[day].count += 1; + var kg = parseFloat(r.hydrogenKg); + if (!isNaN(kg)) statsMap[day].totalKg += kg; + }); + + var days = []; + var i; + for (i = 0; i < windowDays; i++) { + var key; + var shortLabel; + if (dayjs) { + var d = dayjs().startOf('day').subtract(i, 'day'); + key = d.format('YYYY-MM-DD'); + shortLabel = d.format('MM-DD'); + } else { + var native = new Date(); + native.setHours(0, 0, 0, 0); + native.setDate(native.getDate() - i); + key = hrDateToKey(native); + shortLabel = hrPad2(native.getMonth() + 1) + '-' + hrPad2(native.getDate()); + } + var stat = statsMap[key]; + days.push({ + date: key, + dateLabel: shortLabel, + isToday: i === 0, + count: stat ? stat.count : 0, + totalKg: stat ? Math.round(stat.totalKg * 100) / 100 : 0 + }); + } + return days; +} + +function hrInTimeRange(timeStr, range, dayjs) { + if (!range || range.length < 2 || !range[0] || !range[1]) return true; + if (!timeStr) return false; + if (dayjs) { + var t = dayjs(timeStr); + var start = dayjs(range[0]); + var end = dayjs(range[1]); + if (!t.isValid() || !start.isValid() || !end.isValid()) return true; + return (t.isAfter(start) || t.isSame(start)) && (t.isBefore(end) || t.isSame(end)); + } + var tKey = String(timeStr).replace(/[-:\s]/g, ''); + var sKey = range[0].format ? range[0].format('YYYYMMDDHHmmss') : ''; + var eKey = range[1].format ? range[1].format('YYYYMMDDHHmmss') : ''; + if (!sKey || !eKey) return true; + return tKey >= sKey && tKey <= eKey; +} + +function hrEmptyForm() { + return { + hydrogenTime: '', + plateNo: '', + hydrogenKg: '', + customerAmount: '', + mileageKm: '', + fillerName: undefined, + settlementStatus: undefined + }; +} + +function hrNextId() { + return 'hr-' + Date.now() + '-' + Math.floor(Math.random() * 1000); +} + +function hrNextBatchRowId() { + return 'hr-row-' + Date.now() + '-' + Math.floor(Math.random() * 10000); +} + +function hrEmptyBatchRow() { + return { + _rowId: hrNextBatchRowId(), + hydrogenTime: '', + plateNo: '', + hydrogenKg: '', + customerAmount: '', + mileageKm: '', + fillerName: undefined, + settlementStatus: undefined + }; +} + +function hrIsBatchRowEmpty(row) { + if (!row) return true; + if ((row.hydrogenTime || '').trim()) return false; + if ((row.plateNo || '').trim()) return false; + if ((row.hydrogenKg || '').trim()) return false; + if ((row.customerAmount || '').trim()) return false; + if ((row.mileageKm || '').trim()) return false; + if (row.fillerName) return false; + return true; +} + +function hrBatchRowDirty(row) { + return !hrIsBatchRowEmpty(row); +} + +function hrBatchRowsDirty(rows) { + return (rows || []).some(hrBatchRowDirty); +} + +function hrValidateBatchRow(row, rowIndex) { + var label = '第 ' + rowIndex + ' 行'; + if (!(row.hydrogenTime || '').trim()) return label + ':请填写加氢时间'; + if (!(row.plateNo || '').trim()) return label + ':请填写车牌号'; + var kg = parseFloat(row.hydrogenKg); + if (isNaN(kg) || kg <= 0) return label + ':请填写有效的加氢量(kg)'; + var amount = parseFloat(row.customerAmount); + if (isNaN(amount) || amount < 0) return label + ':请填写有效的加氢金额'; + var km = parseFloat(row.mileageKm); + if (isNaN(km) || km < 0) return label + ':请填写有效的公里数'; + if (!row.fillerName) return label + ':请选择充装员'; + if (!row.settlementStatus) return label + ':请选择承担方式'; + return null; +} + +function hrBatchRowToPayload(row) { + return { + hydrogenTime: (row.hydrogenTime || '').trim(), + plateNo: (row.plateNo || '').trim().toUpperCase(), + hydrogenKg: Math.round(parseFloat(row.hydrogenKg) * 100) / 100, + customerAmount: Math.round(parseFloat(row.customerAmount) * 100) / 100, + mileageKm: Math.round(parseFloat(row.mileageKm)), + fillerName: row.fillerName, + settlementStatus: row.settlementStatus + }; +} + +function hrCardTitleWithStep(step, text) { + return React.createElement('span', { className: 'h2-card-title-bar h2-card-title-bar--step' }, + React.createElement('span', { className: 'h2-card-step-badge', 'aria-hidden': true }, step), + React.createElement('span', { className: 'h2-card-title-text' }, text) + ); +} + +function hrFormDirty(form) { + if ((form.hydrogenTime || '').trim()) return true; + if ((form.plateNo || '').trim()) return true; + if ((form.hydrogenKg || '').trim()) return true; + if ((form.customerAmount || '').trim()) return true; + if ((form.mileageKm || '').trim()) return true; + if (form.fillerName) return true; + if (form.settlementStatus) return true; + return false; +} + +const Component = function () { + var useState = React.useState; + var useMemo = React.useMemo; + var useCallback = React.useCallback; + + var antd = window.antd; + var Breadcrumb = antd.Breadcrumb; + var Card = antd.Card; + var Table = antd.Table; + var Button = antd.Button; + var Select = antd.Select; + var Input = antd.Input; + var InputNumber = antd.InputNumber; + var DatePicker = antd.DatePicker; + var Modal = antd.Modal; + var Form = antd.Form; + var Row = antd.Row; + var Col = antd.Col; + var Tag = antd.Tag; + var Badge = antd.Badge; + var message = antd.message; + var dayjs = window.dayjs; + + var RangePicker = DatePicker.RangePicker; + + var subViewState = useState('list'); + var subView = subViewState[0]; + var setSubView = subViewState[1]; + + var formModeState = useState('create'); + var formMode = formModeState[0]; + var setFormMode = formModeState[1]; + + var editingIdState = useState(null); + var editingId = editingIdState[0]; + var setEditingId = editingIdState[1]; + + var listDataState = useState(function () { + return HR_INITIAL_RECORDS.map(function (r) { return Object.assign({}, r); }); + }); + var listData = listDataState[0]; + var setListData = listDataState[1]; + + var draftFiltersState = useState({ timeRange: [], plateNo: undefined, fillerName: undefined }); + var draftFilters = draftFiltersState[0]; + var setDraftFilters = draftFiltersState[1]; + + var appliedFiltersState = useState({ timeRange: [], plateNo: undefined, fillerName: undefined }); + var appliedFilters = appliedFiltersState[0]; + var setAppliedFilters = appliedFiltersState[1]; + + var pageState = useState(1); + var page = pageState[0]; + var setPage = pageState[1]; + var pageSizeState = useState(10); + var pageSize = pageSizeState[0]; + var setPageSize = pageSizeState[1]; + + var formState = useState(hrEmptyForm()); + var form = formState[0]; + var setForm = formState[1]; + + var batchRowsState = useState([]); + var batchRows = batchRowsState[0]; + var setBatchRows = batchRowsState[1]; + + var formSubmittingState = useState(false); + var formSubmitting = formSubmittingState[0]; + var setFormSubmitting = formSubmittingState[1]; + + var deleteModalState = useState({ open: false, record: null }); + var deleteModal = deleteModalState[0]; + var setDeleteModal = deleteModalState[1]; + + var appointmentModalOpenState = useState(false); + var appointmentModalOpen = appointmentModalOpenState[0]; + var setAppointmentModalOpen = appointmentModalOpenState[1]; + + var appointmentDataState = useState(function () { + return HR_INITIAL_APPOINTMENTS.map(function (r) { return Object.assign({}, r); }); + }); + var appointmentData = appointmentDataState[0]; + var setAppointmentData = appointmentDataState[1]; + + var appointmentPageState = useState(1); + var appointmentPage = appointmentPageState[0]; + var setAppointmentPage = appointmentPageState[1]; + var appointmentPageSizeState = useState(10); + var appointmentPageSize = appointmentPageSizeState[0]; + var setAppointmentPageSize = appointmentPageSizeState[1]; + + var rejectModalState = useState({ open: false, record: null, reason: '' }); + var rejectModal = rejectModalState[0]; + var setRejectModal = rejectModalState[1]; + + var dailyStatsPageState = useState(0); + var dailyStatsPage = dailyStatsPageState[0]; + var setDailyStatsPage = dailyStatsPageState[1]; + + var plateOptions = useMemo(function () { + var map = {}; + listData.forEach(function (r) { + if (r.plateNo) map[r.plateNo] = true; + }); + return Object.keys(map).sort().map(function (p) { + return { value: p, label: p }; + }); + }, [listData]); + + var fillerOptions = useMemo(function () { + var map = {}; + HR_MOCK_FILLERS.forEach(function (n) { map[n] = true; }); + listData.forEach(function (r) { + if (r.fillerName) map[r.fillerName] = true; + }); + return Object.keys(map).sort().map(function (n) { + return { value: n, label: n }; + }); + }, [listData]); + + var pendingAppointmentCount = useMemo(function () { + var count = 0; + var i; + for (i = 0; i < appointmentData.length; i++) { + if (appointmentData[i].status === 'pending') count += 1; + } + return count; + }, [appointmentData]); + + var sortedAppointments = useMemo(function () { + return appointmentData.slice().sort(function (a, b) { + return String(b.appointmentTime || '').localeCompare(String(a.appointmentTime || '')); + }); + }, [appointmentData]); + + var appointmentSummary = useMemo(function () { + var pending = 0; + var accepted = 0; + var rejected = 0; + var totalKg = 0; + var i; + for (i = 0; i < appointmentData.length; i++) { + var item = appointmentData[i]; + var kg = parseFloat(item.appointmentKg); + if (!isNaN(kg)) totalKg += kg; + if (item.status === 'pending') pending += 1; + else if (item.status === 'accepted') accepted += 1; + else if (item.status === 'rejected') rejected += 1; + } + return { pending: pending, accepted: accepted, rejected: rejected, totalKg: totalKg, total: appointmentData.length }; + }, [appointmentData]); + + var filteredList = useMemo(function () { + var list = listData.slice(); + if (appliedFilters.plateNo) { + list = list.filter(function (r) { return r.plateNo === appliedFilters.plateNo; }); + } + if (appliedFilters.fillerName) { + list = list.filter(function (r) { return r.fillerName === appliedFilters.fillerName; }); + } + if (appliedFilters.timeRange && appliedFilters.timeRange.length === 2) { + list = list.filter(function (r) { + return hrInTimeRange(r.hydrogenTime, appliedFilters.timeRange, dayjs); + }); + } + return list.sort(function (a, b) { + return String(b.hydrogenTime || '').localeCompare(String(a.hydrogenTime || '')); + }); + }, [listData, appliedFilters, dayjs]); + + var dailyStatsWindow = useMemo(function () { + return hrBuildRecentDailyWindow(filteredList, dayjs, HR_DAILY_STATS_WINDOW); + }, [filteredList, dayjs]); + + var dailyStatsPageItems = useMemo(function () { + var start = dailyStatsPage * HR_DAILY_STATS_PAGE_SIZE; + var chunk = dailyStatsWindow.slice(start, start + HR_DAILY_STATS_PAGE_SIZE); + return chunk.slice().reverse(); + }, [dailyStatsWindow, dailyStatsPage]); + + var renderFilterField = useCallback(function (label, control) { + return React.createElement('div', { className: 'lc-filter-field' }, + React.createElement('span', { className: 'lc-filter-field-label' }, label), + React.createElement('div', { className: 'lc-filter-field-control' }, control) + ); + }, []); + + var handleFilterQuery = useCallback(function () { + setAppliedFilters({ + timeRange: (draftFilters.timeRange || []).slice(), + plateNo: draftFilters.plateNo, + fillerName: draftFilters.fillerName + }); + setPage(1); + setDailyStatsPage(0); + }, [draftFilters]); + + var handleFilterReset = useCallback(function () { + var empty = { timeRange: [], plateNo: undefined, fillerName: undefined }; + setDraftFilters(empty); + setAppliedFilters(empty); + setPage(1); + setDailyStatsPage(0); + }, []); + + var resetFormView = useCallback(function () { + setSubView('list'); + setForm(hrEmptyForm()); + setBatchRows([]); + setEditingId(null); + }, []); + + var handleFormPageBack = useCallback(function () { + if (formSubmitting) return; + var dirty = formMode === 'create' ? hrBatchRowsDirty(batchRows) : hrFormDirty(form); + if (dirty) { + Modal.confirm({ + title: '确认离开', + content: '当前有未保存内容,确定返回列表吗?', + okText: '离开', + cancelText: '继续编辑', + centered: true, + onOk: resetFormView + }); + return; + } + resetFormView(); + }, [form, batchRows, formMode, formSubmitting, resetFormView]); + + var openCreate = useCallback(function () { + setForm(hrEmptyForm()); + setBatchRows([hrEmptyBatchRow()]); + setFormMode('create'); + setEditingId(null); + setSubView('form'); + }, []); + + var updateBatchRow = useCallback(function (rowId, patch) { + setBatchRows(function (prev) { + return prev.map(function (r) { + if (r._rowId !== rowId) return r; + return Object.assign({}, r, patch); + }); + }); + }, []); + + var addBatchRow = useCallback(function () { + setBatchRows(function (prev) { return prev.concat([hrEmptyBatchRow()]); }); + }, []); + + var removeBatchRow = useCallback(function (rowId) { + setBatchRows(function (prev) { + if (prev.length <= 1) return [hrEmptyBatchRow()]; + return prev.filter(function (r) { return r._rowId !== rowId; }); + }); + }, []); + + var openEdit = useCallback(function (record) { + setForm({ + hydrogenTime: record.hydrogenTime || '', + plateNo: record.plateNo || '', + hydrogenKg: record.hydrogenKg != null ? String(record.hydrogenKg) : '', + customerAmount: record.customerAmount != null ? String(record.customerAmount) : '', + mileageKm: record.mileageKm != null ? String(record.mileageKm) : '', + fillerName: record.fillerName, + settlementStatus: record.settlementStatus + }); + setFormMode('edit'); + setEditingId(record.id); + setSubView('form'); + }, []); + + var validateForm = useCallback(function () { + if (!(form.hydrogenTime || '').trim()) { + message.warning('请填写加氢时间'); + return false; + } + if (!(form.plateNo || '').trim()) { + message.warning('请填写车牌号'); + return false; + } + var kg = parseFloat(form.hydrogenKg); + if (isNaN(kg) || kg <= 0) { + message.warning('请填写有效的加氢量(kg)'); + return false; + } + var amount = parseFloat(form.customerAmount); + if (isNaN(amount) || amount < 0) { + message.warning('请填写有效的加氢金额'); + return false; + } + var km = parseFloat(form.mileageKm); + if (isNaN(km) || km < 0) { + message.warning('请填写有效的公里数'); + return false; + } + if (!form.fillerName) { + message.warning('请选择充装员'); + return false; + } + if (!form.settlementStatus) { + message.warning('请选择承担方式'); + return false; + } + return true; + }, [form]); + + var handleFormSubmit = useCallback(function () { + if (formSubmitting) return; + if (!validateForm()) return; + setFormSubmitting(true); + var payload = { + hydrogenTime: (form.hydrogenTime || '').trim(), + plateNo: (form.plateNo || '').trim().toUpperCase(), + hydrogenKg: Math.round(parseFloat(form.hydrogenKg) * 100) / 100, + customerAmount: Math.round(parseFloat(form.customerAmount) * 100) / 100, + mileageKm: Math.round(parseFloat(form.mileageKm)), + fillerName: form.fillerName, + settlementStatus: form.settlementStatus + }; + if (formMode === 'edit' && editingId) { + setListData(function (prev) { + return prev.map(function (r) { + if (r.id !== editingId) return r; + return Object.assign({}, r, payload); + }); + }); + message.success('加氢记录已更新'); + } + window.setTimeout(function () { + setFormSubmitting(false); + resetFormView(); + }, 280); + }, [form, formMode, editingId, formSubmitting, validateForm, resetFormView]); + + var handleBatchSubmit = useCallback(function () { + if (formSubmitting) return; + var filledRows = batchRows.filter(function (r) { return !hrIsBatchRowEmpty(r); }); + if (!filledRows.length) { + message.warning('请至少填写一行加氢记录'); + return; + } + var i; + for (i = 0; i < filledRows.length; i++) { + var rowIndex = 0; + var j; + for (j = 0; j < batchRows.length; j++) { + if (batchRows[j]._rowId === filledRows[i]._rowId) { + rowIndex = j + 1; + break; + } + } + var err = hrValidateBatchRow(filledRows[i], rowIndex); + if (err) { + message.warning(err); + return; + } + } + setFormSubmitting(true); + var newRecords = filledRows.map(function (row) { + return Object.assign({ id: hrNextId() }, hrBatchRowToPayload(row)); + }); + setListData(function (prev) { return newRecords.concat(prev); }); + message.success('已成功新增 ' + newRecords.length + ' 条加氢记录'); + window.setTimeout(function () { + setFormSubmitting(false); + resetFormView(); + }, 280); + }, [batchRows, formSubmitting, resetFormView]); + + var handleAcceptAppointment = useCallback(function (record) { + if (!record || record.status !== 'pending') return; + Modal.confirm({ + title: '接受预约', + content: '确认接受车牌「' + (record.plateNo || '') + '」的加氢预约?预约时间:' + (record.appointmentTime || '—'), + okText: '接受预约', + cancelText: '取消', + centered: true, + okButtonProps: { style: HR_PRIMARY_BTN_STYLE }, + onOk: function () { + var now = dayjs ? dayjs().format('YYYY-MM-DD HH:mm:ss') : ''; + setAppointmentData(function (prev) { + return prev.map(function (r) { + if (r.id !== record.id) return r; + return Object.assign({}, r, { status: 'accepted', handledTime: now, rejectReason: '' }); + }); + }); + message.success('已接受预约'); + } + }); + }, [dayjs]); + + var openRejectAppointment = useCallback(function (record) { + if (!record || record.status !== 'pending') return; + setRejectModal({ open: true, record: record, reason: '' }); + }, []); + + var closeRejectModal = useCallback(function () { + setRejectModal({ open: false, record: null, reason: '' }); + }, []); + + var handleConfirmRejectAppointment = useCallback(function () { + if (!rejectModal.record) return; + var reason = (rejectModal.reason || '').trim(); + if (!reason) { + message.warning('请填写拒绝原因'); + return; + } + var recordId = rejectModal.record.id; + var now = dayjs ? dayjs().format('YYYY-MM-DD HH:mm:ss') : ''; + setAppointmentData(function (prev) { + return prev.map(function (r) { + if (r.id !== recordId) return r; + return Object.assign({}, r, { status: 'rejected', rejectReason: reason, handledTime: now }); + }); + }); + message.success('已拒绝预约'); + closeRejectModal(); + }, [rejectModal, dayjs, closeRejectModal]); + + var handleExport = useCallback(function () { + if (!filteredList.length) { + message.warning('当前筛选条件下暂无数据可导出'); + return; + } + var headers = ['序号', '加氢时间', '车牌号', '加氢量(kg)', '加氢金额(元)', '公里数(km)', '承担方式', '充装员']; + var rows = filteredList.map(function (r, index) { + return [ + index + 1, + r.hydrogenTime || '', + r.plateNo || '', + hrFormatKg(r.hydrogenKg), + hrFormatYuan(r.customerAmount), + r.mileageKm != null ? r.mileageKm : '', + hrSettlementStatusLabel(r.settlementStatus), + r.fillerName || '' + ]; + }); + hrDownloadCsv('加氢记录_' + new Date().getTime() + '.csv', headers, rows); + message.success('已导出 ' + filteredList.length + ' 条加氢记录'); + }, [filteredList]); + + var columns = useMemo(function () { + return [ + { + title: '加氢时间', + dataIndex: 'hydrogenTime', + key: 'hydrogenTime', + width: 168, + render: function (v) { + return React.createElement('span', { className: 'hr-refuel-time' }, v || '—'); + } + }, + { + title: '车牌号', + dataIndex: 'plateNo', + key: 'plateNo', + width: 120, + render: function (v) { + return React.createElement('span', { className: 'hr-refuel-plate' }, v || '—'); + } + }, + { + title: '加氢量(kg)', + dataIndex: 'hydrogenKg', + key: 'hydrogenKg', + width: 120, + align: 'right', + render: function (v) { + return React.createElement('span', { className: 'hr-refuel-kg' }, hrFormatKg(v)); + } + }, + { + title: '加氢金额', + dataIndex: 'customerAmount', + key: 'customerAmount', + width: 120, + align: 'right', + render: function (v) { + return React.createElement('span', { className: 'hr-refuel-money' }, hrFormatYuanSymbol(v)); + } + }, + { + title: '公里数', + dataIndex: 'mileageKm', + key: 'mileageKm', + width: 120, + align: 'right', + render: function (v) { + return React.createElement('span', { className: 'hr-refuel-km' }, hrFormatKm(v)); + } + }, + { + title: '承担方式', + dataIndex: 'settlementStatus', + key: 'settlementStatus', + width: 120, + render: function (v) { + var info = HR_SETTLEMENT_STATUS_MAP[v]; + if (!info) return '—'; + return React.createElement(Tag, { color: info.color, style: { margin: 0, borderRadius: 6, fontWeight: 600 } }, info.label); + } + }, + { + title: '充装员', + dataIndex: 'fillerName', + key: 'fillerName', + width: 100, + ellipsis: true + }, + { + title: '操作', + key: 'actions', + width: 140, + fixed: 'right', + render: function (_, record) { + return React.createElement('div', { className: 'h2-row-actions' }, + React.createElement(Button, { + type: 'link', + size: 'small', + className: 'lc-action-btn', + onClick: function () { openEdit(record); } + }, '编辑'), + React.createElement(Button, { + type: 'link', + size: 'small', + className: 'lc-action-btn lc-action-btn-danger', + onClick: function () { setDeleteModal({ open: true, record: record }); } + }, '删除') + ); + } + } + ]; + }, [openEdit]); + + var appointmentColumns = useMemo(function () { + return [ + { + title: '序号', + key: 'seq', + width: 52, + align: 'center', + render: function (_, __, index) { + return React.createElement('span', { className: 'h2-refuel-drill-seq' }, index + 1); + } + }, + { + title: '车牌号', + dataIndex: 'plateNo', + key: 'plateNo', + width: 120, + render: function (v) { + return React.createElement('span', { className: 'h2-refuel-drill-plate' }, v || '—'); + } + }, + { + title: '预约时间', + dataIndex: 'appointmentTime', + key: 'appointmentTime', + width: 168, + render: function (v) { + return React.createElement('span', { className: 'h2-refuel-drill-time' }, v || '—'); + } + }, + { + title: '预约加氢量(kg)', + dataIndex: 'appointmentKg', + key: 'appointmentKg', + width: 140, + align: 'right', + render: function (v) { + return React.createElement('span', { className: 'h2-refuel-drill-kg' }, hrFormatKg(v)); + } + }, + { + title: '预约司机', + dataIndex: 'driverName', + key: 'driverName', + width: 110, + render: function (v) { + return React.createElement('span', { className: 'h2-refuel-drill-driver' }, v || '—'); + } + }, + { + title: '联系方式', + dataIndex: 'contact', + key: 'contact', + width: 130, + render: function (v) { + return React.createElement('span', { className: 'h2-refuel-drill-contact' }, v || '—'); + } + }, + { + title: '状态', + dataIndex: 'status', + key: 'status', + width: 100, + render: function (v) { + var info = HR_APPOINTMENT_STATUS_MAP[v] || { label: v || '—', color: 'default' }; + return React.createElement(Tag, { color: info.color, style: { margin: 0, borderRadius: 6, fontWeight: 600 } }, info.label); + } + }, + { + title: '操作', + key: 'actions', + width: 200, + fixed: 'right', + render: function (_, record) { + if (record.status !== 'pending') { + return record.status === 'rejected' && record.rejectReason + ? React.createElement('div', null, + React.createElement('span', { style: { color: '#94a3b8', fontSize: 12 } }, '已处理'), + React.createElement('div', { className: 'hr-appointment-reject-reason', title: record.rejectReason }, '拒绝原因:' + record.rejectReason) + ) + : React.createElement('span', { style: { color: '#94a3b8', fontSize: 12 } }, '已处理'); + } + return React.createElement('div', { className: 'h2-row-actions' }, + React.createElement(Button, { + type: 'link', + size: 'small', + className: 'lc-action-btn', + onClick: function () { handleAcceptAppointment(record); } + }, '接受预约'), + React.createElement(Button, { + type: 'link', + size: 'small', + className: 'lc-action-btn lc-action-btn-danger', + onClick: function () { openRejectAppointment(record); } + }, '拒绝预约') + ); + } + } + ]; + }, [handleAcceptAppointment, openRejectAppointment]); + + var formTimeValue = null; + if (dayjs && form.hydrogenTime) { + var ft = dayjs(form.hydrogenTime); + if (ft.isValid()) formTimeValue = ft; + } + + var inputCls = 'h2-create-input'; + var H2_CREATE_GUTTER = [24, 20]; + + var formItem = function (label, required, node) { + return React.createElement(Form.Item, { + label: required + ? React.createElement('span', null, React.createElement('span', { style: { color: '#ef4444', marginRight: 4 } }, '*'), label) + : label + }, node); + }; + + var formRow = function () { + return React.createElement.apply(null, [Row, { gutter: H2_CREATE_GUTTER, align: 'stretch' }].concat(Array.prototype.slice.call(arguments))); + }; + + var col24 = function (node) { + return React.createElement(Col, { xs: 24, span: 24 }, node); + }; + + var col12 = function (node) { + return React.createElement(Col, { xs: 24, md: 12 }, node); + }; + + var col8 = function (node) { + return React.createElement(Col, { xs: 24, lg: 8 }, node); + }; + + var gridBlock = function () { + var rows = Array.prototype.slice.call(arguments, 0); + return React.createElement('div', { className: 'h2-create-form-grid' }, rows); + }; + + var formPageTitle = formMode === 'edit' ? '编辑加氢记录' : '批量新增加氢记录'; + + var batchFilledCount = useMemo(function () { + var n = 0; + var i; + for (i = 0; i < batchRows.length; i++) { + if (!hrIsBatchRowEmpty(batchRows[i])) n += 1; + } + return n; + }, [batchRows]); + + var batchCreateColumns = useMemo(function () { + return [ + { + title: '序号', + width: 56, + align: 'center', + render: function (_, __, index) { + return React.createElement('span', { className: 'hr-batch-create-seq' }, index + 1); + } + }, + { + title: React.createElement('span', null, React.createElement('span', { style: { color: '#ef4444', marginRight: 4 } }, '*'), '加氢时间'), + width: 200, + render: function (_, record) { + var timeVal = null; + if (dayjs && record.hydrogenTime) { + var t = dayjs(record.hydrogenTime); + if (t.isValid()) timeVal = t; + } + return dayjs && DatePicker + ? React.createElement(DatePicker, { + showTime: true, + style: { width: '100%' }, + format: 'YYYY-MM-DD HH:mm:ss', + placeholder: '加氢时间', + value: timeVal, + onChange: function (v) { + updateBatchRow(record._rowId, { + hydrogenTime: v && v.isValid && v.isValid() ? v.format('YYYY-MM-DD HH:mm:ss') : '' + }); + } + }) + : React.createElement(Input, { + placeholder: 'YYYY-MM-DD HH:mm:ss', + value: record.hydrogenTime || '', + onChange: function (e) { updateBatchRow(record._rowId, { hydrogenTime: e.target.value }); } + }); + } + }, + { + title: React.createElement('span', null, React.createElement('span', { style: { color: '#ef4444', marginRight: 4 } }, '*'), '车牌号'), + width: 130, + render: function (_, record) { + return React.createElement(Input, { + placeholder: '车牌号', + value: record.plateNo || '', + maxLength: 16, + onChange: function (e) { updateBatchRow(record._rowId, { plateNo: e.target.value }); } + }); + } + }, + { + title: React.createElement('span', null, React.createElement('span', { style: { color: '#ef4444', marginRight: 4 } }, '*'), '加氢量(kg)'), + width: 110, + render: function (_, record) { + return React.createElement(InputNumber, { + style: { width: '100%' }, + min: 0.01, + step: 0.1, + precision: 2, + placeholder: 'kg', + value: record.hydrogenKg === '' ? null : parseFloat(record.hydrogenKg), + onChange: function (v) { updateBatchRow(record._rowId, { hydrogenKg: v == null ? '' : String(v) }); } + }); + } + }, + { + title: React.createElement('span', null, React.createElement('span', { style: { color: '#ef4444', marginRight: 4 } }, '*'), '加氢金额(元)'), + width: 120, + render: function (_, record) { + return React.createElement(InputNumber, { + style: { width: '100%' }, + min: 0, + step: 0.01, + precision: 2, + placeholder: '金额', + value: record.customerAmount === '' ? null : parseFloat(record.customerAmount), + onChange: function (v) { updateBatchRow(record._rowId, { customerAmount: v == null ? '' : String(v) }); } + }); + } + }, + { + title: React.createElement('span', null, React.createElement('span', { style: { color: '#ef4444', marginRight: 4 } }, '*'), '公里数(km)'), + width: 110, + render: function (_, record) { + return React.createElement(InputNumber, { + style: { width: '100%' }, + min: 0, + step: 1, + precision: 0, + placeholder: 'km', + value: record.mileageKm === '' ? null : parseFloat(record.mileageKm), + onChange: function (v) { updateBatchRow(record._rowId, { mileageKm: v == null ? '' : String(v) }); } + }); + } + }, + { + title: React.createElement('span', null, React.createElement('span', { style: { color: '#ef4444', marginRight: 4 } }, '*'), '承担方式'), + width: 130, + render: function (_, record) { + return React.createElement(Select, { + placeholder: '承担方式', + value: record.settlementStatus, + options: HR_SETTLEMENT_STATUS_OPTIONS, + style: { width: '100%' }, + onChange: function (v) { updateBatchRow(record._rowId, { settlementStatus: v }); }, + dropdownStyle: { borderRadius: 8 } + }); + } + }, + { + title: React.createElement('span', null, React.createElement('span', { style: { color: '#ef4444', marginRight: 4 } }, '*'), '充装员'), + width: 120, + render: function (_, record) { + return React.createElement(Select, { + showSearch: true, + placeholder: '充装员', + optionFilterProp: 'label', + value: record.fillerName, + options: fillerOptions, + style: { width: '100%' }, + onChange: function (v) { updateBatchRow(record._rowId, { fillerName: v }); }, + dropdownStyle: { borderRadius: 8 } + }); + } + }, + { + title: '操作', + width: 72, + fixed: 'right', + render: function (_, record) { + return React.createElement(Button, { + type: 'link', + size: 'small', + className: 'lc-action-btn-danger', + disabled: batchRows.length <= 1, + onClick: function () { removeBatchRow(record._rowId); } + }, '删除'); + } + } + ]; + }, [batchRows.length, dayjs, DatePicker, Input, InputNumber, Select, fillerOptions, updateBatchRow, removeBatchRow]); + + var emptyNode = React.createElement('div', { style: { padding: '40px 0', textAlign: 'center' } }, + HR_ICONS.empty, + React.createElement('div', { style: { color: '#94a3b8', marginTop: 12 } }, '暂无符合检索条件的加氢记录') + ); + + var renderAppointmentPanel = function () { + var records = sortedAppointments; + var summary = appointmentSummary; + var stats = [ + { key: 'pending', label: '待处理预约', value: String(summary.pending || 0), mod: 'count', valueMod: 'count' }, + { key: 'accepted', label: '已接受', value: String(summary.accepted || 0), mod: 'kg', valueMod: 'kg' }, + { key: 'rejected', label: '已拒绝', value: String(summary.rejected || 0), mod: 'cost', valueMod: 'cost' }, + { key: 'totalKg', label: '预约加氢量合计(kg)', value: hrFormatKg(summary.totalKg), mod: 'customer', valueMod: 'customer' } + ]; + var tablePagination = records.length > 8 + ? { + current: appointmentPage, + pageSize: appointmentPageSize, + total: records.length, + showSizeChanger: true, + pageSizeOptions: ['5', '10', '20', '50'], + size: 'small', + showTotal: function (t) { return '共 ' + t + ' 条'; }, + onChange: function (p, ps) { setAppointmentPage(p); setAppointmentPageSize(ps); } + } + : false; + return React.createElement('div', { className: 'h2-refuel-drill-panel' }, + React.createElement('div', { className: 'h2-refuel-drill-station-card' }, + React.createElement('div', { className: 'h2-refuel-drill-station-card__name' }, '预约加氢管理'), + React.createElement('div', { className: 'h2-refuel-drill-station-card__meta' }, + '司机预约记录 · 待处理 ' + (summary.pending || 0) + ' 条 · 共 ' + records.length + ' 条' + ) + ), + React.createElement('div', { className: 'h2-refuel-drill-stats', role: 'group', 'aria-label': '预约统计' }, + stats.map(function (item) { + return React.createElement('div', { + key: item.key, + className: 'h2-refuel-drill-stat h2-refuel-drill-stat--' + item.mod, + role: 'article', + 'aria-label': item.label + ' ' + item.value + }, + React.createElement('div', { className: 'h2-refuel-drill-stat__label' }, item.label), + React.createElement('div', { + className: 'h2-refuel-drill-stat__value h2-refuel-drill-stat__value--' + item.valueMod + }, item.value) + ); + }) + ), + React.createElement('div', { className: 'h2-refuel-drill-table-wrap' }, + React.createElement('div', { className: 'h2-refuel-drill-table-head' }, + React.createElement('div', { className: 'h2-refuel-drill-table-head__left' }, + React.createElement('span', { className: 'h2-refuel-drill-table-head__title' }, '预约明细'), + React.createElement('span', { className: 'h2-refuel-drill-table-head__count' }, '共 ' + records.length + ' 条') + ), + summary.pending + ? React.createElement(Tag, { color: 'processing', style: { margin: 0, borderRadius: 6, fontWeight: 600 } }, summary.pending + ' 条待处理') + : React.createElement(Tag, { style: { margin: 0, borderRadius: 6, color: '#64748b' } }, '暂无待处理') + ), + React.createElement(Table, { + className: 'h2-refuel-record-table', + size: 'small', + bordered: false, + rowKey: 'id', + columns: appointmentColumns, + dataSource: records, + pagination: tablePagination, + locale: { emptyText: '暂无预约记录' }, + scroll: records.length > 8 ? { x: 'max-content', y: 320 } : { x: 'max-content' } + }) + ) + ); + }; + + var batchCreateViewNode = subView === 'form' && formMode === 'create' ? React.createElement('div', { className: 'h2-create-shell' }, + React.createElement('div', { className: 'h2-create-topbar' }, + React.createElement(Button, { + className: 'h2-create-back-btn', + type: 'default', + icon: HR_ICONS.back, + onClick: handleFormPageBack, + disabled: formSubmitting, + style: { borderRadius: 8 } + }, '返回'), + React.createElement('h1', { className: 'h2-create-topbar-title' }, formPageTitle) + ), + React.createElement(Card, { + className: 'h2-create-card', + id: 'hr-batch-create-card', + title: React.createElement('span', { className: 'h2-card-title-bar' }, + React.createElement('span', { className: 'h2-card-title-icon' }, HR_ICONS.fuel), + React.createElement('span', { className: 'h2-card-title-text' }, '加氢记录明细') + ), + bordered: false + }, + React.createElement('div', { className: 'hr-batch-create-toolbar' }, + React.createElement('span', { className: 'hr-batch-create-toolbar__count' }, + '共 ' + batchRows.length + ' 行 · 已填写 ' + batchFilledCount + ' 条' + ), + React.createElement(Button, { + type: 'dashed', + style: { borderRadius: 8, fontWeight: 600, borderColor: '#10b981', color: '#059669' }, + onClick: addBatchRow, + 'aria-label': '添加一行加氢记录' + }, '+ 添加一行') + ), + React.createElement('div', { className: 'hr-batch-create-table-wrap' }, + React.createElement(Table, { + className: 'hr-batch-create-table', + size: 'small', + bordered: false, + rowKey: '_rowId', + columns: batchCreateColumns, + dataSource: batchRows, + pagination: false, + locale: { emptyText: '请点击「添加一行」开始录入' }, + scroll: { x: 1240 } + }) + ) + ), + React.createElement('footer', { className: 'h2-create-footer' }, + React.createElement('div', { className: 'h2-create-footer-inner' }, + React.createElement('div', { className: 'h2-create-footer-hint' }, + '将提交 ' + batchFilledCount + ' 条有效记录' + ), + React.createElement('div', { className: 'h2-create-footer-actions' }, + React.createElement(Button, { + onClick: handleFormPageBack, + disabled: formSubmitting, + style: { borderRadius: 8 }, + 'aria-label': '取消并返回列表' + }, '取消'), + React.createElement(Button, { + type: 'primary', + style: HR_PRIMARY_BTN_STYLE, + onClick: handleBatchSubmit, + loading: formSubmitting, + disabled: batchFilledCount === 0, + 'aria-label': '批量提交加氢记录' + }, '批量提交') + ) + ) + ) + ) : null; + + var editFormViewNode = subView === 'form' && formMode === 'edit' ? React.createElement('div', { className: 'h2-create-shell' }, + React.createElement('div', { className: 'h2-create-topbar' }, + React.createElement(Button, { + className: 'h2-create-back-btn', + type: 'default', + icon: HR_ICONS.back, + onClick: handleFormPageBack, + disabled: formSubmitting, + style: { borderRadius: 8 } + }, '返回'), + React.createElement('h1', { className: 'h2-create-topbar-title' }, formPageTitle) + ), + React.createElement(Form, { + layout: 'vertical', + requiredMark: false, + className: 'h2-create-form h2-create-form--list h2-create-form--grid' + }, + React.createElement(Card, { + className: 'h2-create-card', + id: 'hr-create-basic-card', + title: hrCardTitleWithStep(1, '基本信息'), + bordered: false + }, + gridBlock( + formRow( + col12(formItem('加氢时间', true, dayjs && DatePicker + ? React.createElement(DatePicker, { + className: inputCls, + showTime: true, + style: { width: '100%' }, + format: 'YYYY-MM-DD HH:mm:ss', + placeholder: '请选择加氢时间', + value: formTimeValue, + onChange: function (v) { + setForm(function (f) { + return Object.assign({}, f, { + hydrogenTime: v && v.isValid && v.isValid() ? v.format('YYYY-MM-DD HH:mm:ss') : '' + }); + }); + } + }) + : React.createElement(Input, { + className: inputCls, + placeholder: 'YYYY-MM-DD HH:mm:ss', + value: form.hydrogenTime, + onChange: function (e) { setForm(function (f) { return Object.assign({}, f, { hydrogenTime: e.target.value }); }); } + }) + )), + col12(formItem('车牌号', true, React.createElement(Input, { + className: inputCls, + placeholder: '请输入车牌号', + value: form.plateNo || '', + maxLength: 16, + onChange: function (e) { setForm(function (f) { return Object.assign({}, f, { plateNo: e.target.value }); }); } + }))) + ) + ) + ), + React.createElement(Card, { + className: 'h2-create-card', + id: 'hr-create-data-card', + title: hrCardTitleWithStep(2, '加氢数据'), + bordered: false + }, + gridBlock( + formRow( + col8(formItem('加氢量(kg)', true, React.createElement(InputNumber, { + className: inputCls, + style: { width: '100%' }, + min: 0.01, + step: 0.1, + precision: 2, + placeholder: '请输入加氢量', + value: form.hydrogenKg === '' ? null : parseFloat(form.hydrogenKg), + onChange: function (v) { setForm(function (f) { return Object.assign({}, f, { hydrogenKg: v == null ? '' : String(v) }); }); } + }))), + col8(formItem('加氢金额(元)', true, React.createElement(InputNumber, { + className: inputCls, + style: { width: '100%' }, + min: 0, + step: 0.01, + precision: 2, + placeholder: '请输入加氢金额', + value: form.customerAmount === '' ? null : parseFloat(form.customerAmount), + onChange: function (v) { setForm(function (f) { return Object.assign({}, f, { customerAmount: v == null ? '' : String(v) }); }); } + }))), + col8(formItem('公里数(km)', true, React.createElement(InputNumber, { + className: inputCls, + style: { width: '100%' }, + min: 0, + step: 1, + precision: 0, + placeholder: '请输入公里数', + value: form.mileageKm === '' ? null : parseFloat(form.mileageKm), + onChange: function (v) { setForm(function (f) { return Object.assign({}, f, { mileageKm: v == null ? '' : String(v) }); }); } + }))) + ) + ) + ), + React.createElement(Card, { + className: 'h2-create-card', + id: 'hr-create-operator-card', + title: hrCardTitleWithStep(3, '操作人员'), + bordered: false + }, + gridBlock( + formRow( + col12(formItem('充装员', true, React.createElement(Select, { + className: inputCls, + showSearch: true, + placeholder: '请选择充装员', + optionFilterProp: 'label', + value: form.fillerName, + options: fillerOptions, + onChange: function (v) { setForm(function (f) { return Object.assign({}, f, { fillerName: v }); }); }, + dropdownStyle: { borderRadius: 8 } + }))), + col12(formItem('承担方式', true, React.createElement(Select, { + className: inputCls, + placeholder: '请选择承担方式', + value: form.settlementStatus, + options: HR_SETTLEMENT_STATUS_OPTIONS, + onChange: function (v) { setForm(function (f) { return Object.assign({}, f, { settlementStatus: v }); }); }, + dropdownStyle: { borderRadius: 8 } + }))) + ) + ) + ) + ), + React.createElement('footer', { className: 'h2-create-footer' }, + React.createElement('div', { className: 'h2-create-footer-inner' }, + React.createElement('div', { className: 'h2-create-footer-actions' }, + React.createElement(Button, { + onClick: handleFormPageBack, + disabled: formSubmitting, + style: { borderRadius: 8 }, + 'aria-label': '取消并返回列表' + }, '取消'), + React.createElement(Button, { + type: 'primary', + style: HR_PRIMARY_BTN_STYLE, + onClick: handleFormSubmit, + loading: formSubmitting, + 'aria-label': '保存编辑加氢记录' + }, '保存修改') + ) + ) + ) + ) : null; + + return React.createElement('div', { + className: 'h2-station-page lc-edit-page' + (subView === 'form' ? ' h2-station-page--create' : '') + }, + React.createElement('style', null, HR_PAGE_STYLE), + batchCreateViewNode, + editFormViewNode, + subView === 'list' ? React.createElement('div', { style: { display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' } }, + React.createElement('div', { style: { marginBottom: 16 } }, + React.createElement(Breadcrumb, { items: [{ title: '加氢站管理' }, { title: '加氢记录' }] }) + ), + + React.createElement(Card, { className: 'lc-filter-card', title: '筛选条件', bordered: false }, + React.createElement('div', { className: 'lc-filter-grid' }, + renderFilterField('加氢时间', React.createElement(RangePicker, { + showTime: true, + style: { width: '100%' }, + placeholder: ['开始时间', '结束时间'], + value: draftFilters.timeRange && draftFilters.timeRange.length ? draftFilters.timeRange : null, + onChange: function (v) { + setDraftFilters(function (p) { return Object.assign({}, p, { timeRange: v || [] }); }); + } + })), + renderFilterField('车牌号', React.createElement(Select, { + placeholder: '请选择车牌号', + allowClear: true, + showSearch: true, + optionFilterProp: 'label', + value: draftFilters.plateNo, + options: plateOptions, + onChange: function (v) { setDraftFilters(function (p) { return Object.assign({}, p, { plateNo: v }); }); }, + style: { width: '100%' }, + dropdownStyle: { borderRadius: 8 } + })), + renderFilterField('充装员', React.createElement(Select, { + placeholder: '请选择充装员', + allowClear: true, + showSearch: true, + optionFilterProp: 'label', + value: draftFilters.fillerName, + options: fillerOptions, + onChange: function (v) { setDraftFilters(function (p) { return Object.assign({}, p, { fillerName: v }); }); }, + style: { width: '100%' }, + dropdownStyle: { borderRadius: 8 } + })) + ), + React.createElement('div', { className: 'lc-filter-actions' }, + React.createElement(Button, { onClick: handleFilterReset, style: { borderRadius: 8 } }, '重置'), + React.createElement(Button, { type: 'primary', onClick: handleFilterQuery, style: HR_PRIMARY_BTN_STYLE }, '查询') + ) + ), + + React.createElement('div', { className: 'hr-daily-stats-section' }, + React.createElement('div', { className: 'hr-daily-stats-head' }, + React.createElement('span', { className: 'hr-daily-stats-head__title' }, '每日统计'), + React.createElement('span', { className: 'hr-daily-stats-head__meta' }, + (function () { + var rangeStart = dailyStatsPageItems[0] ? dailyStatsPageItems[0].date : '—'; + var rangeEnd = dailyStatsPageItems.length ? dailyStatsPageItems[dailyStatsPageItems.length - 1].date : '—'; + return '近28天(含今天)· 第 ' + (dailyStatsPage + 1) + '/' + (HR_DAILY_STATS_MAX_PAGE + 1) + ' 组 · ' + + rangeStart + ' 至 ' + rangeEnd + ' · 筛选记录 ' + filteredList.length + ' 笔'; + })() + ) + ), + React.createElement('div', { className: 'hr-daily-stats-carousel' }, + React.createElement(Button, { + type: 'default', + className: 'hr-daily-stats-nav-btn', + disabled: dailyStatsPage >= HR_DAILY_STATS_MAX_PAGE, + onClick: function () { setDailyStatsPage(function (p) { return Math.min(HR_DAILY_STATS_MAX_PAGE, p + 1); }); }, + 'aria-label': '向左翻页,查看更早的7天' + }, '‹'), + React.createElement('div', { className: 'hr-daily-stats-track', role: 'group', 'aria-label': '每日加氢统计' }, + dailyStatsPageItems.map(function (item) { + return React.createElement('div', { + key: item.date, + className: 'hr-daily-stat-card' + (item.isToday ? ' hr-daily-stat-card--today' : ''), + role: 'article', + 'aria-label': item.date + ' 加氢 ' + item.count + ' 次,加氢量 ' + hrFormatKg(item.totalKg) + ' kg' + }, + React.createElement('div', { className: 'hr-daily-stat-card__date' }, + React.createElement('span', null, item.dateLabel), + item.isToday ? React.createElement('span', { className: 'hr-daily-stat-card__today-tag' }, '今天') : null + ), + React.createElement('div', { className: 'hr-daily-stat-card__row' }, + React.createElement('span', { className: 'hr-daily-stat-card__label' }, '加氢次数'), + React.createElement('span', { className: 'hr-daily-stat-card__value hr-daily-stat-card__value--count' }, String(item.count)) + ), + React.createElement('div', { className: 'hr-daily-stat-card__row' }, + React.createElement('span', { className: 'hr-daily-stat-card__label' }, '加氢量(kg)'), + React.createElement('span', { className: 'hr-daily-stat-card__value hr-daily-stat-card__value--kg' }, hrFormatKg(item.totalKg)) + ) + ); + }) + ), + React.createElement(Button, { + type: 'default', + className: 'hr-daily-stats-nav-btn', + disabled: dailyStatsPage <= 0, + onClick: function () { setDailyStatsPage(function (p) { return Math.max(0, p - 1); }); }, + 'aria-label': '向右翻页,查看更近的7天' + }, '›') + ) + ), + + React.createElement('div', { className: 'lc-table-section' }, + React.createElement('div', { className: 'lc-table-toolbar' }, + React.createElement('div', { className: 'lc-table-toolbar-left' }, + React.createElement(Badge, { count: pendingAppointmentCount, offset: [8, 0], size: 'small' }, + React.createElement(Button, { + type: 'default', + icon: HR_ICONS.calendar, + style: { borderRadius: 8, fontWeight: 600, borderColor: '#3b82f6', color: '#2563eb' }, + onClick: function () { setAppointmentModalOpen(true); }, + 'aria-label': '预约加氢' + }, '预约加氢') + ) + ), + React.createElement('div', { className: 'lc-table-toolbar-actions' }, + React.createElement(Button, { + type: 'default', + style: { borderRadius: 8, fontWeight: 600, borderColor: '#10b981', color: '#059669' }, + onClick: handleExport, + 'aria-label': '导出加氢记录' + }, '导出'), + React.createElement(Button, { + type: 'primary', + style: HR_PRIMARY_BTN_STYLE, + onClick: openCreate, + 'aria-label': '新增加氢记录' + }, '新增') + ) + ), + React.createElement('div', { className: 'lc-table-card' }, + React.createElement(Table, { + className: 'lc-list-table', + rowKey: 'id', + columns: columns, + dataSource: filteredList, + size: 'middle', + scroll: { x: 1100 }, + pagination: { + current: page, + pageSize: pageSize, + total: filteredList.length, + showSizeChanger: true, + pageSizeOptions: ['5', '10', '20', '50'], + showTotal: function (t) { return '共 ' + t + ' 条'; }, + onChange: function (p, ps) { setPage(p); setPageSize(ps); } + }, + locale: { emptyText: emptyNode } + }) + ) + ) + ) : null, + + React.createElement(Modal, { + className: 'h2-refuel-drill-modal', + title: '预约加氢', + open: appointmentModalOpen, + onCancel: function () { setAppointmentModalOpen(false); }, + footer: React.createElement(Button, { + type: 'primary', + onClick: function () { setAppointmentModalOpen(false); }, + style: HR_PRIMARY_BTN_STYLE + }, '知道了'), + width: 980, + centered: true, + destroyOnClose: true, + styles: { body: { maxHeight: '78vh', overflow: 'auto' } } + }, + renderAppointmentPanel() + ), + + React.createElement(Modal, { + title: '确认删除', + open: deleteModal.open, + centered: true, + onCancel: function () { setDeleteModal({ open: false, record: null }); }, + onOk: function () { + if (deleteModal.record) { + setListData(function (prev) { + return prev.filter(function (r) { return r.id !== deleteModal.record.id; }); + }); + message.success('已删除'); + } + setDeleteModal({ open: false, record: null }); + }, + okButtonProps: { danger: true }, + okText: '删除', + cancelText: '取消' + }, '确定删除该条加氢记录吗?车牌号「' + ((deleteModal.record && deleteModal.record.plateNo) || '') + '」,此操作不可撤销。'), + + React.createElement(Modal, { + title: '拒绝预约', + open: rejectModal.open, + centered: true, + width: 480, + onCancel: closeRejectModal, + onOk: handleConfirmRejectAppointment, + okText: '确认拒绝', + cancelText: '取消', + okButtonProps: { danger: true }, + destroyOnClose: true + }, + React.createElement('div', { style: { marginBottom: 12, fontSize: 13, color: '#475569', lineHeight: 1.6 } }, + '车牌号:', + React.createElement('strong', { style: { color: '#0f172a' } }, (rejectModal.record && rejectModal.record.plateNo) || '—'), + ' · 预约时间:', + React.createElement('span', { style: { fontVariantNumeric: 'tabular-nums' } }, (rejectModal.record && rejectModal.record.appointmentTime) || '—') + ), + React.createElement(Form, { layout: 'vertical', requiredMark: false }, + React.createElement(Form.Item, { + label: React.createElement('span', null, React.createElement('span', { style: { color: '#ef4444', marginRight: 4 } }, '*'), '拒绝原因') + }, + React.createElement(Input.TextArea, { + placeholder: '请填写拒绝原因,将通知预约司机', + value: rejectModal.reason, + rows: 4, + maxLength: 200, + showCount: true, + style: { borderRadius: 8 }, + onChange: function (e) { + setRejectModal(function (m) { return Object.assign({}, m, { reason: e.target.value }); }); + } + }) + ) + ) + ) + ); +}; diff --git a/web端/加氢站管理/站点信息.jsx b/web端/加氢站管理/站点信息.jsx index 14b9267..4b8ab0a 100644 --- a/web端/加氢站管理/站点信息.jsx +++ b/web端/加氢站管理/站点信息.jsx @@ -1,5 +1,5 @@ // 【重要】必须使用 const Component 作为组件变量名 -// 加氢站管理 - 站点信息(列表 + KPI 分类 + 独立新建页 / 编辑查看抽屉) +// 加氢站管理 - 站点信息(列表 + KPI 分类 + 独立新建/编辑页 + 使用说明书) var H2_REGION_CASCADER_OPTIONS = [ { value: '浙江省', label: '浙江省', children: [{ value: '嘉兴市', label: '嘉兴市' }, { value: '杭州市', label: '杭州市' }, { value: '宁波市', label: '宁波市' }] }, @@ -10,14 +10,14 @@ var H2_REGION_CASCADER_OPTIONS = [ var H2_STATION_KPI_CARDS = [ { key: 'all', type: 'total', title: '全部加氢站', desc: '纳入台账管理的全部站点' }, - { key: 'high', type: 'normal', title: '高频加氢站点', desc: '加氢次数 ≥ 3 次的站点' }, - { key: 'low', type: 'warning', title: '低频加氢站点', desc: '加氢次数 1~2 次的站点' }, + { key: 'balanceAlert', type: 'warning', title: '预付余额预警站点', desc: '预付余额低于所设提醒阈值的站点' }, + { key: 'arrears', type: 'unuploaded', title: '已欠费站点', desc: '预付余额小于 0 的站点' }, { key: 'none', type: 'unuploaded', title: '无加氢站点', desc: '暂无加氢记录的站点' } ]; var H2_SIGNED_FILTER_CARDS = [ - { key: 'yes', type: 'normal', title: '已签约站点', desc: '已签约的加氢站点,点击快捷筛选' }, - { key: 'no', type: 'unuploaded', title: '未签约站点', desc: '尚未签约的加氢站点,点击快捷筛选' } + { key: 'yes', type: 'normal', title: '签约站点', desc: '已签约的加氢站点,点击快捷筛选' }, + { key: 'no', type: 'unuploaded', title: '普通站点', desc: '尚未签约的加氢站点,点击快捷筛选' } ]; var H2_BUSINESS_STATUS_OPTIONS = [ @@ -26,7 +26,15 @@ var H2_BUSINESS_STATUS_OPTIONS = [ { value: '停止营业', label: '停止营业' } ]; -var H2_PAGE_STYLE = [ +/** @see web端/styles/oneos-ant-table-global-fix.js — 与 ant-table-global-fix.css 保持同步 */ +var ONEOS_ANT_TABLE_GLOBAL_FIX = [ + '.ant-table-container .ant-table-header { margin-bottom: 0 !important; }', + '.ant-table-container .ant-table-body { margin-top: 0 !important; }', + '.ant-table-container .ant-table-body > table, .ant-table-content table { margin-top: 0 !important; }', + '.ant-table-tbody > tr.ant-table-measure-row, .ant-table-tbody > tr.ant-table-measure-row > td, .ant-table-tbody > tr.ant-table-measure-row > th { display: none !important; height: 0 !important; max-height: 0 !important; min-height: 0 !important; padding: 0 !important; margin: 0 !important; border: none !important; line-height: 0 !important; font-size: 0 !important; overflow: hidden !important; visibility: hidden !important; pointer-events: none !important; }' +]; + +var H2_PAGE_STYLE = ONEOS_ANT_TABLE_GLOBAL_FIX.concat([ '.h2-station-page { --h2-form-row-gap: 20px; --h2-form-col-gap: 24px; --h2-form-label-gap: 8px; padding: 24px 24px 80px; height: 100vh; display: flex; flex-direction: column; background: linear-gradient(165deg, #f1f5f9 0%, #f8fafc 50%, #f1f5f9 100%); overflow: hidden; box-sizing: border-box; }', '.h2-station-page .lc-filter-card.ant-card { border-radius: 16px !important; border: 1px solid #e2e8f0 !important; box-shadow: 0 4px 20px -4px rgba(15, 23, 42, 0.03) !important; margin-bottom: 16px; }', '.h2-station-page .lc-filter-card > .ant-card-head { border-bottom: 1px solid #f1f5f9 !important; min-height: auto; padding: 12px 20px !important; }', @@ -38,31 +46,29 @@ var H2_PAGE_STYLE = [ '.h2-station-page .lc-filter-field { display: flex; align-items: center; gap: 12px; min-width: 0; min-height: 32px; }', '.h2-station-page .lc-filter-field-label { flex: 0 0 88px; text-align: right; font-size: 13px; font-weight: 500; color: #475569; line-height: 32px; white-space: nowrap; }', '.h2-station-page .lc-filter-field-control { flex: 1; min-width: 0; }', - '.h2-station-page .lc-filter-field-control .ant-input, .h2-station-page .lc-filter-field-control .ant-input-affix-wrapper, .h2-station-page .lc-filter-field-control .ant-select .ant-select-selector, .h2-station-page .lc-filter-field-control .ant-cascader .ant-select-selector { width: 100%; min-height: 32px !important; border-radius: 8px !important; }', + '.h2-station-page .lc-filter-field-control .ant-input, .h2-station-page .lc-filter-field-control .ant-input-affix-wrapper, .h2-station-page .lc-filter-field-control .ant-select .ant-select-selector, .h2-station-page .lc-filter-field-control .ant-cascader .ant-select-selector { width: 100%; height: 32px !important; min-height: 32px !important; border-radius: 8px !important; box-sizing: border-box; }', + '.h2-station-page .lc-filter-field-control .ant-input-affix-wrapper { display: inline-flex; align-items: center; padding-top: 0 !important; padding-bottom: 0 !important; }', + '.h2-station-page .lc-filter-field-control .ant-input-affix-wrapper > input.ant-input { height: 30px; line-height: 30px; padding-top: 0; padding-bottom: 0; }', + '.h2-station-page .lc-filter-field-control .ant-select-single .ant-select-selector { display: flex; align-items: center; }', + '.h2-station-page .lc-filter-field-control .ant-select-single .ant-select-selector .ant-select-selection-search-input { height: 30px; }', + '.h2-station-page .lc-filter-field-control .ant-select-single .ant-select-selector .ant-select-selection-item, .h2-station-page .lc-filter-field-control .ant-select-single .ant-select-selector .ant-select-selection-placeholder { line-height: 30px !important; }', + '.h2-station-page .lc-filter-field-control .ant-cascader .ant-select-selector { display: flex; align-items: center; }', '.h2-station-page .lc-filter-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; padding-top: 16px; border-top: 1px solid #f1f5f9; }', '.h2-station-page .lc-alert-stats-row { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 12px; margin-bottom: 16px; }', '@media (max-width: 1400px) { .h2-station-page .lc-alert-stats-row { grid-template-columns: repeat(3, minmax(0, 1fr)); } }', '@media (max-width: 900px) { .h2-station-page .lc-alert-stats-row { grid-template-columns: repeat(2, minmax(0, 1fr)); } }', '@media (max-width: 640px) { .h2-station-page .lc-alert-stats-row { grid-template-columns: 1fr; } }', - '.h2-station-page .lc-alert-card { display: flex; align-items: flex-start; gap: 12px; padding: 14px 30px 14px 16px; border-radius: 12px; border: 1px solid #e2e8f0; background: #fff; position: relative; overflow: hidden; min-width: 0; min-height: 44px; }', - '.h2-station-page .lc-alert-card-main { flex: 1; min-width: 0; }', - '.h2-station-page .lc-alert-card-icon { flex-shrink: 0; width: 40px; height: 40px; border-radius: 10px; display: flex; align-items: center; justify-content: center; }', - '.h2-station-page .lc-alert-card-val { font-size: 26px; font-weight: 800; line-height: 1.1; color: #0f172a; font-variant-numeric: tabular-nums; }', - '.h2-station-page .lc-alert-card-title { font-size: 13px; font-weight: 600; color: #334155; margin-top: 2px; }', + '.h2-station-page .lc-alert-card { display: flex; align-items: center; gap: 14px; padding: 16px 30px 16px 16px; border-radius: 12px; border: 1px solid #e2e8f0; background: #fff; position: relative; overflow: hidden; min-width: 0; min-height: 72px; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04); }', + '.h2-station-page .lc-alert-card-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }', + '.h2-station-page .lc-alert-card-icon { flex-shrink: 0; width: 48px; height: 48px; border-radius: 50%; display: flex; align-items: center; justify-content: center; background: linear-gradient(145deg, #0d7a63 0%, #065f4a 100%); color: #fff; box-shadow: 0 4px 12px rgba(6, 95, 74, 0.28); }', + '.h2-station-page .lc-alert-card-icon svg { display: block; flex-shrink: 0; }', + '.h2-station-page .lc-alert-card-val { font-size: 24px; font-weight: 800; line-height: 1.1; color: #0f172a; font-variant-numeric: tabular-nums; }', + '.h2-station-page .lc-alert-card-title { font-size: 13px; font-weight: 500; color: #64748b; line-height: 1.3; }', '.h2-station-page .lc-alert-card-tip-anchor { position: absolute; top: 8px; right: 8px; z-index: 2; line-height: 0; }', '.h2-station-page .lc-alert-card-tip { width: 18px; height: 18px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; color: #94a3b8; background: rgba(255,255,255,0.92); border: 1px solid #e2e8f0; cursor: help; line-height: 0; }', '.h2-station-page .lc-alert-card-tip:hover { color: #64748b; border-color: #cbd5e1; background: #fff; }', - '.h2-station-page .lc-alert-card--total { background: linear-gradient(135deg, #f8fafc 0%, #fff 100%); }', - '.h2-station-page .lc-alert-card--total .lc-alert-card-icon { background: #e2e8f0; color: #475569; }', - '.h2-station-page .lc-alert-card--normal { background: linear-gradient(135deg, #ecfdf5 0%, #fff 55%); border-color: #bbf7d0; }', - '.h2-station-page .lc-alert-card--normal .lc-alert-card-icon { background: #d1fae5; color: #059669; }', - '.h2-station-page .lc-alert-card--normal .lc-alert-card-val { color: #047857; }', - '.h2-station-page .lc-alert-card--warning { background: linear-gradient(135deg, #fff7ed 0%, #fff 55%); border-color: #fed7aa; }', - '.h2-station-page .lc-alert-card--warning .lc-alert-card-icon { background: #ffedd5; color: #ea580c; }', - '.h2-station-page .lc-alert-card--warning .lc-alert-card-val { color: #c2410c; }', - '.h2-station-page .lc-alert-card--unuploaded { background: linear-gradient(135deg, #f8fafc 0%, #fff 55%); }', - '.h2-station-page .lc-alert-card--unuploaded .lc-alert-card-icon { background: #f1f5f9; color: #64748b; }', - '.h2-station-page .lc-alert-card--unuploaded .lc-alert-card-val { color: #64748b; }', + '.h2-station-page .lc-alert-card--total, .h2-station-page .lc-alert-card--normal, .h2-station-page .lc-alert-card--warning, .h2-station-page .lc-alert-card--unuploaded { background: #fff; border-color: #e2e8f0; }', + '.h2-station-page .lc-alert-card--normal .lc-alert-card-val, .h2-station-page .lc-alert-card--warning .lc-alert-card-val, .h2-station-page .lc-alert-card--unuploaded .lc-alert-card-val { color: #0f172a; }', '.h2-station-page .lc-alert-card-clickable { cursor: pointer; transition: box-shadow 0.2s ease, border-color 0.2s ease, transform 0.2s ease; }', '.h2-station-page .lc-alert-card-clickable:hover { box-shadow: 0 4px 14px rgba(15, 23, 42, 0.08); }', '.h2-station-page .lc-alert-card-clickable:focus-visible { outline: 2px solid #10b981; outline-offset: 2px; }', @@ -88,8 +94,10 @@ var H2_PAGE_STYLE = [ '.h2-station-page .h2-action-more-btn:focus-visible { outline: 2px solid #10b981; outline-offset: 2px; }', '.h2-station-page .h2-row-actions { display: inline-flex; align-items: center; gap: 4px; }', '.h2-station-page .lc-station-name { font-weight: 700; color: #0f172a; font-size: 13px; }', + '.h2-station-page .lc-station-name-cell { min-width: 0; }', '.h2-station-page .lc-station-name-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-width: 0; }', '.h2-station-page .lc-station-name-row .lc-station-name { min-width: 0; }', + '.h2-station-page .lc-station-address-line { margin-top: 4px; font-size: 12px; color: #64748b; line-height: 1.4; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }', '.h2-station-page .lc-station-signed-tag { margin: 0 !important; border-radius: 6px !important; font-weight: 600 !important; flex-shrink: 0; line-height: 20px !important; }', '.h2-station-page .lc-refuel-kg-row { display: flex; align-items: center; justify-content: flex-end; gap: 8px; flex-wrap: wrap; min-width: 0; }', '.h2-station-page .lc-refuel-freq-tag { margin: 0 !important; border-radius: 6px !important; font-weight: 600 !important; flex-shrink: 0; line-height: 20px !important; }', @@ -113,10 +121,24 @@ var H2_PAGE_STYLE = [ '.h2-station-page .h2-contract-file-btns { display: flex; align-items: center; gap: 4px; flex-shrink: 0; }', '.h2-station-page--create .h2-create-form .h2-create-upload-btn.ant-upload-wrapper { width: auto; }', '.h2-station-page--create .h2-create-form .h2-region-address { gap: 12px; margin-top: 0; }', - '.h2-station-page .h2-business-hours { display: flex; flex-direction: column; gap: 12px; width: 100%; }', - '.h2-station-page .h2-business-hours-range { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; width: 100%; }', - '.h2-station-page .h2-business-hours-range-item { display: flex; flex-direction: column; gap: 6px; min-width: 0; }', - '.h2-station-page .h2-business-hours-range-label { font-size: 12px; color: #64748b; line-height: 1.2; }', + '.h2-station-page .h2-option-btn-group, .h2-business-status-modal .h2-option-btn-group { display: flex; flex-wrap: wrap; gap: 8px; }', + '.h2-station-page .h2-option-btn, .h2-business-status-modal .h2-option-btn { display: inline-flex; align-items: center; justify-content: center; min-height: 36px; padding: 0 16px; border-radius: 8px; border: 1px solid #e2e8f0; background: #fff; color: #475569; font-size: 13px; font-weight: 600; cursor: pointer; transition: border-color 0.2s ease, background 0.2s ease, color 0.2s ease, box-shadow 0.2s ease; }', + '.h2-station-page .h2-option-btn:hover:not(:disabled), .h2-business-status-modal .h2-option-btn:hover:not(:disabled) { border-color: #86efac; color: #047857; }', + '.h2-station-page .h2-option-btn:focus-visible, .h2-business-status-modal .h2-option-btn:focus-visible { outline: 2px solid #10b981; outline-offset: 2px; }', + '.h2-station-page .h2-option-btn:disabled, .h2-business-status-modal .h2-option-btn:disabled { opacity: 0.55; cursor: not-allowed; }', + '.h2-station-page .h2-option-btn--active, .h2-business-status-modal .h2-option-btn--active { box-shadow: 0 1px 3px rgba(15, 23, 42, 0.08); }', + '.h2-station-page .h2-option-btn--active.h2-option-btn--open, .h2-business-status-modal .h2-option-btn--active.h2-option-btn--open { border-color: #10b981; background: #ecfdf5; color: #047857; }', + '.h2-station-page .h2-option-btn--active.h2-option-btn--pause, .h2-business-status-modal .h2-option-btn--active.h2-option-btn--pause { border-color: #f97316; background: #fff7ed; color: #c2410c; }', + '.h2-station-page .h2-option-btn--active.h2-option-btn--stop, .h2-business-status-modal .h2-option-btn--active.h2-option-btn--stop { border-color: #ef4444; background: #fef2f2; color: #b91c1c; }', + '.h2-station-page .h2-option-btn--active.h2-option-btn--allday, .h2-business-status-modal .h2-option-btn--active.h2-option-btn--allday { border-color: #10b981; background: #ecfdf5; color: #047857; }', + '.h2-station-page .h2-option-btn--active.h2-option-btn--custom, .h2-business-status-modal .h2-option-btn--active.h2-option-btn--custom { border-color: #3b82f6; background: #eff6ff; color: #1d4ed8; }', + '.h2-station-page .h2-business-hours, .h2-business-status-modal .h2-business-hours { display: flex; flex-direction: column; gap: 12px; width: 100%; }', + '.h2-business-status-modal .h2-business-hours-custom-panel { margin-top: 4px; }', + '.h2-station-page .h2-business-hours-custom-panel, .h2-business-status-modal .h2-business-hours-custom-panel { padding: 12px 14px; border-radius: 10px; background: #f8fafc; border: 1px solid #e2e8f0; }', + '.h2-station-page .h2-business-hours-custom-panel__title, .h2-business-status-modal .h2-business-hours-custom-panel__title { margin-bottom: 10px; font-size: 12px; font-weight: 700; color: #475569; }', + '.h2-station-page .h2-business-hours-range, .h2-business-status-modal .h2-business-hours-range { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; width: 100%; }', + '.h2-station-page .h2-business-hours-range-item, .h2-business-status-modal .h2-business-hours-range-item { display: flex; flex-direction: column; gap: 6px; min-width: 0; }', + '.h2-station-page .h2-business-hours-range-label, .h2-business-status-modal .h2-business-hours-range-label { font-size: 12px; color: #64748b; line-height: 1.2; }', '.h2-station-page--create .h2-create-form .h2-business-hours .ant-picker { width: 100% !important; height: 32px !important; border-radius: 2px !important; border: 1px solid #e5e6eb !important; background: #fff !important; }', '.h2-station-page--create .h2-create-form .h2-business-hours .ant-picker .ant-picker-input > input { font-size: 14px !important; color: #1d2129 !important; }', '.h2-station-page--create .h2-create-form .h2-business-hours .ant-picker:not(.ant-picker-disabled):hover { border-color: #c9cdd4 !important; background: #fff !important; }', @@ -141,17 +163,17 @@ var H2_PAGE_STYLE = [ '.h2-station-page--create .h2-create-form--grid-4 .h2-create-upload.ant-upload-wrapper .ant-upload-drag { padding: 10px 8px !important; }', '.h2-station-page--create .h2-create-form--grid-4 .h2-create-upload.ant-upload-wrapper .ant-upload-drag .ant-upload-drag-icon { margin-bottom: 4px !important; }', '.h2-station-page--create .h2-create-form--grid-4 .h2-create-upload.ant-upload-wrapper .ant-upload-drag p { font-size: 12px !important; }', - '.h2-station-page .h2-address-paste { display: flex; flex-direction: column; gap: 8px; width: 100%; }', + '.h2-station-page .h2-address-paste { display: flex; flex-direction: column; width: 100%; }', '.h2-station-page .ant-drawer-body .ant-form .ant-form-item { margin-bottom: 20px; }', '.h2-station-page .ant-drawer-body .ant-form .ant-form-item:last-child { margin-bottom: 0; }', '.h2-station-page .ant-drawer-body .ant-form .ant-form-item-label { padding-bottom: var(--h2-form-label-gap, 8px); }', '.h2-station-page .ant-drawer-body .ant-form .ant-form-item-label > label { min-height: 22px; line-height: 22px; }', '.h2-station-page .ant-drawer-body .ant-divider { margin: 4px 0 20px !important; }', '.h2-station-page .h2-address-paste .ant-input-textarea { font-size: 13px !important; line-height: 1.45 !important; resize: vertical; min-height: 56px; }', - '.h2-station-page .h2-address-paste-actions { display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: wrap; }', - '.h2-station-page .h2-address-paste-hint { font-size: 11px; color: #94a3b8; line-height: 1.35; flex: 1; min-width: 0; }', '.h2-station-page .h2-prd-modal .ant-modal-body { max-height: 70vh; overflow: auto; }', '.h2-station-page .h2-prd-content { padding: 8px 0; white-space: pre-wrap; font-size: 13px; line-height: 1.65; color: #475569; }', + '.h2-station-page .h2-req-doc-panel { max-width: 100%; }', + '.h2-station-page .h2-req-doc-panel h1:first-child { margin-top: 0; }', '.h2-station-page .h2-import-template-bar { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; padding: 14px 16px; margin-bottom: 14px; border-radius: 12px; background: linear-gradient(135deg, #ecfdf5 0%, #f8fafc 100%); border: 1px solid #bbf7d0; }', '.h2-station-page .h2-import-template-bar-text { font-size: 13px; color: #475569; line-height: 1.55; flex: 1; min-width: 0; }', '.h2-station-page .h2-import-preview { margin-top: 14px; padding: 12px 14px; border-radius: 10px; background: #f8fafc; border: 1px solid #e2e8f0; font-size: 13px; color: #334155; }', @@ -170,23 +192,359 @@ var H2_PAGE_STYLE = [ '.h2-station-page .h2-ledger-totals-bar__item:last-child { border-right: none; }', '.h2-station-page .h2-ledger-totals-bar__label { font-size: 12px; color: rgba(15, 23, 42, 0.55); font-weight: 500; line-height: 1.2; }', '.h2-station-page .h2-ledger-totals-bar__value { font-size: 16px; font-weight: 700; color: #0f172a; font-variant-numeric: tabular-nums; line-height: 1.3; }', - '.h2-station-page .h2-refuel-record-table .ant-table-thead > tr > th { background: #f8fafc !important; font-size: 12px !important; padding: 8px 12px !important; }', - '.h2-station-page .h2-refuel-record-table .ant-table-tbody > tr > td { font-size: 12px !important; padding: 8px 12px !important; }', + '.h2-refuel-drill-modal .ant-modal-content { border-radius: 16px !important; overflow: hidden; box-shadow: 0 24px 48px -12px rgba(15, 23, 42, 0.18) !important; }', + '.h2-refuel-drill-modal .ant-modal-header { padding: 18px 24px 14px !important; border-bottom: 1px solid #f1f5f9 !important; margin-bottom: 0 !important; }', + '.h2-refuel-drill-modal .ant-modal-title { font-size: 17px !important; font-weight: 700 !important; color: #0f172a !important; }', + '.h2-refuel-drill-modal .ant-modal-body { padding: 16px 24px 20px !important; background: #f8fafc; }', + '.h2-refuel-drill-modal .ant-modal-footer { padding: 12px 24px 18px !important; border-top: 1px solid #f1f5f9 !important; background: #fff; }', + '.h2-refuel-drill-modal .h2-refuel-drill-panel { display: flex; flex-direction: column; gap: 14px; }', + '.h2-refuel-drill-modal .h2-refuel-drill-station-card { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; padding: 12px 16px; background: linear-gradient(135deg, #eff6ff 0%, #ecfdf5 50%, #f8fafc 100%); border: 1px solid #bfdbfe; border-radius: 12px; }', + '.h2-refuel-drill-modal .h2-refuel-drill-station-card__name { font-size: 15px; font-weight: 700; color: #0f172a; line-height: 1.35; }', + '.h2-refuel-drill-modal .h2-refuel-drill-station-card__meta { font-size: 12px; color: #64748b; font-variant-numeric: tabular-nums; white-space: nowrap; }', + '.h2-refuel-drill-modal .h2-refuel-drill-stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; }', + '@media (max-width: 860px) { .h2-refuel-drill-modal .h2-refuel-drill-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); } }', + '@media (max-width: 520px) { .h2-refuel-drill-modal .h2-refuel-drill-stats { grid-template-columns: 1fr; } }', + '.h2-refuel-drill-modal .h2-refuel-drill-stat { display: flex; flex-direction: column; justify-content: center; min-height: 78px; padding: 12px 14px; border-radius: 12px; border: 1px solid #e2e8f0; background: #fff; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06); min-width: 0; box-sizing: border-box; }', + '.h2-refuel-drill-modal .h2-refuel-drill-stat--count { border-left: 4px solid #3b82f6; }', + '.h2-refuel-drill-modal .h2-refuel-drill-stat--kg { border-left: 4px solid #10b981; background: linear-gradient(180deg, #fff 0%, #f0fdf4 100%); }', + '.h2-refuel-drill-modal .h2-refuel-drill-stat--cost { border-left: 4px solid #f97316; }', + '.h2-refuel-drill-modal .h2-refuel-drill-stat--customer { border-left: 4px solid #6366f1; }', + '.h2-refuel-drill-modal .h2-refuel-drill-stat__label { font-size: 12px; font-weight: 600; color: #64748b; margin-bottom: 8px; line-height: 1.2; }', + '.h2-refuel-drill-modal .h2-refuel-drill-stat__value { font-size: 20px; font-weight: 800; font-variant-numeric: tabular-nums; line-height: 1.2; color: #0f172a; word-break: break-all; }', + '.h2-refuel-drill-modal .h2-refuel-drill-stat__value--count { color: #2563eb; }', + '.h2-refuel-drill-modal .h2-refuel-drill-stat__value--kg { color: #059669; }', + '.h2-refuel-drill-modal .h2-refuel-drill-stat__value--cost { color: #ea580c; }', + '.h2-refuel-drill-modal .h2-refuel-drill-stat__value--customer { color: #4f46e5; }', + '.h2-refuel-drill-modal .h2-refuel-drill-table-wrap { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; overflow: hidden; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04); }', + '.h2-refuel-drill-modal .h2-refuel-drill-table-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 14px; border-bottom: 1px solid #f1f5f9; background: #fafbfc; }', + '.h2-refuel-drill-modal .h2-refuel-drill-table-head__left { display: flex; align-items: center; gap: 10px; min-width: 0; }', + '.h2-refuel-drill-modal .h2-refuel-drill-table-head__title { font-size: 13px; font-weight: 700; color: #334155; }', + '.h2-refuel-drill-modal .h2-refuel-drill-table-head__count { font-size: 12px; color: #64748b; font-variant-numeric: tabular-nums; }', + '.h2-refuel-drill-modal .h2-refuel-record-table { border-radius: 0 !important; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .ant-table { background: transparent !important; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .ant-table-container { border: none !important; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .ant-table-header { margin-bottom: 0 !important; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .ant-table-body { margin-top: 0 !important; overflow-y: auto !important; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .ant-table-body table { margin-top: 0 !important; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .ant-table-tbody > tr.ant-table-measure-row, .h2-refuel-drill-modal .h2-refuel-record-table .ant-table-tbody > tr.ant-table-measure-row > td { height: 0 !important; max-height: 0 !important; padding: 0 !important; margin: 0 !important; border: none !important; line-height: 0 !important; font-size: 0 !important; overflow: hidden !important; visibility: hidden !important; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .ant-table-thead > tr > th { background: #f8fafc !important; font-size: 12px !important; font-weight: 700 !important; color: #475569 !important; padding: 10px 12px !important; border-bottom: 1px solid #e2e8f0 !important; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .ant-table-tbody > tr:not(.ant-table-measure-row) > td { font-size: 13px !important; padding: 10px 12px !important; vertical-align: middle !important; border-bottom: 1px solid #f8fafc !important; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .ant-table-tbody > tr:not(.ant-table-measure-row):last-child > td { border-bottom: none !important; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .ant-table-tbody > tr:hover > td { background: #eff6ff !important; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .h2-refuel-drill-seq { display: inline-flex; align-items: center; justify-content: center; min-width: 24px; height: 24px; border-radius: 6px; background: #f1f5f9; color: #64748b; font-size: 12px; font-weight: 700; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .h2-refuel-drill-time { color: #334155; font-variant-numeric: tabular-nums; font-size: 12px; white-space: nowrap; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .h2-refuel-drill-plate { font-weight: 700; color: #0f172a; font-variant-numeric: tabular-nums; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .h2-refuel-drill-kg { font-weight: 700; color: #059669; font-variant-numeric: tabular-nums; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .h2-refuel-drill-money { font-variant-numeric: tabular-nums; white-space: nowrap; font-weight: 600; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .h2-refuel-drill-money--cost { color: #ea580c; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .h2-refuel-drill-money--customer { color: #4f46e5; font-weight: 700; }', + '.h2-refuel-drill-modal .h2-refuel-record-table .h2-refuel-drill-order-no { display: inline-block; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; color: #475569; background: #f1f5f9; border: 1px solid #e2e8f0; padding: 2px 7px; border-radius: 6px; letter-spacing: 0.02em; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }', '.h2-station-page .h2-prepaid-balance-cell { display: flex; align-items: center; justify-content: flex-end; gap: 6px; flex-wrap: nowrap; box-sizing: border-box; }', '.h2-station-page .h2-prepaid-balance-cell .h2-prepaid-balance-amount { flex: 0 0 auto; white-space: nowrap; border: none; background: none; padding: 0; font: inherit; font-weight: 700; cursor: pointer; text-decoration: underline; text-underline-offset: 2px; font-variant-numeric: tabular-nums; }', '.h2-station-page .h2-prepaid-balance-cell .h2-prepaid-balance-amount:focus-visible { outline: 2px solid #10b981; outline-offset: 2px; border-radius: 4px; }', '.h2-station-page .h2-prepaid-balance-cell .lc-station-signed-tag { flex-shrink: 0; }', '.h2-station-page .lc-table-card .ant-table-tbody > tr:not(.ant-table-measure-row) > td.h2-cell-prepaid-balance { overflow: visible !important; }', - '.h2-station-page .h2-balance-record-table .ant-table-thead > tr > th { background: #f8fafc !important; font-size: 12px !important; padding: 8px 12px !important; }', - '.h2-station-page .h2-balance-record-table .ant-table-tbody > tr > td { font-size: 12px !important; padding: 8px 12px !important; }', + '.h2-balance-drill-modal .ant-modal-content { border-radius: 16px !important; overflow: hidden; box-shadow: 0 24px 48px -12px rgba(15, 23, 42, 0.18) !important; }', + '.h2-balance-drill-modal .ant-modal-header { padding: 18px 24px 14px !important; border-bottom: 1px solid #f1f5f9 !important; margin-bottom: 0 !important; }', + '.h2-balance-drill-modal .ant-modal-title { font-size: 17px !important; font-weight: 700 !important; color: #0f172a !important; }', + '.h2-balance-drill-modal .ant-modal-body { padding: 16px 24px 20px !important; background: #f8fafc; }', + '.h2-balance-drill-modal .ant-modal-footer { padding: 12px 24px 18px !important; border-top: 1px solid #f1f5f9 !important; background: #fff; }', + '.h2-balance-drill-modal .h2-balance-drill-panel { display: flex; flex-direction: column; gap: 14px; }', + '.h2-balance-drill-modal .h2-balance-drill-stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }', + '@media (max-width: 860px) { .h2-balance-drill-modal .h2-balance-drill-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); } }', + '@media (max-width: 520px) { .h2-balance-drill-modal .h2-balance-drill-stats { grid-template-columns: 1fr; } }', + '.h2-balance-drill-modal .h2-balance-drill-stat { display: flex; flex-direction: column; justify-content: center; min-height: 78px; padding: 12px 14px; border-radius: 12px; border: 1px solid #e2e8f0; background: #fff; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06); min-width: 0; box-sizing: border-box; }', + '.h2-balance-drill-modal .h2-balance-drill-stat--balance { border-left: 4px solid #10b981; background: linear-gradient(180deg, #fff 0%, #f0fdf4 100%); }', + '.h2-balance-drill-modal .h2-balance-drill-stat--income { border-left: 4px solid #10b981; }', + '.h2-balance-drill-modal .h2-balance-drill-stat--expense { border-left: 4px solid #f97316; }', + '.h2-balance-drill-modal .h2-balance-drill-stat__label { font-size: 12px; font-weight: 600; color: #64748b; margin-bottom: 8px; line-height: 1.2; }', + '.h2-balance-drill-modal .h2-balance-trend-wrap { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; overflow: hidden; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04); }', + '.h2-balance-drill-modal .h2-balance-trend-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 14px; border-bottom: 1px solid #f1f5f9; background: #fafbfc; }', + '.h2-balance-drill-modal .h2-balance-trend-head__title { font-size: 13px; font-weight: 700; color: #334155; }', + '.h2-balance-drill-modal .h2-balance-trend-head__meta { font-size: 12px; color: #64748b; font-variant-numeric: tabular-nums; }', + '.h2-balance-drill-modal .h2-balance-trend-body { padding: 12px 14px 8px; }', + '.h2-balance-drill-modal .h2-balance-trend-chart-wrap { position: relative; }', + '.h2-balance-drill-modal .h2-balance-trend-svg { display: block; width: 100%; height: auto; }', + '.h2-balance-drill-modal .h2-balance-trend-tooltip { position: absolute; z-index: 2; padding: 8px 10px; border-radius: 8px; background: rgba(15, 23, 42, 0.92); color: #f8fafc; font-size: 12px; line-height: 1.45; white-space: nowrap; pointer-events: none; box-shadow: 0 8px 20px rgba(15, 23, 42, 0.18); transform: translate(-50%, calc(-100% - 10px)); }', + '.h2-balance-drill-modal .h2-balance-trend-tooltip__date { color: #cbd5e1; font-size: 11px; margin-bottom: 2px; }', + '.h2-balance-drill-modal .h2-balance-trend-tooltip__balance { font-weight: 700; font-variant-numeric: tabular-nums; color: #fff; }', + '.h2-balance-drill-modal .h2-balance-trend-tooltip__balance--negative { color: #fca5a5; }', + '.h2-balance-drill-modal .h2-balance-trend-hit { fill: transparent; cursor: pointer; }', + '.h2-balance-drill-modal .h2-balance-trend-point--active { r: 5.5; stroke-width: 2.5; }', + '.h2-balance-drill-modal .h2-balance-trend-empty { padding: 36px 16px; text-align: center; font-size: 13px; color: #94a3b8; }', + '.h2-balance-drill-modal .h2-balance-trend-legend { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; padding: 8px 14px 12px; border-top: 1px solid #f8fafc; }', + '.h2-balance-drill-modal .h2-balance-trend-legend__item { display: inline-flex; align-items: center; gap: 6px; font-size: 11px; color: #64748b; }', + '.h2-balance-drill-modal .h2-balance-trend-legend__dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }', + '.h2-balance-drill-modal .h2-balance-trend-legend__dot--line { background: #10b981; }', + '.h2-balance-drill-modal .h2-balance-trend-legend__dot--area { background: rgba(16, 185, 129, 0.35); }', + '.h2-balance-drill-modal .h2-balance-drill-stat__value { font-size: 20px; font-weight: 800; font-variant-numeric: tabular-nums; line-height: 1.2; color: #0f172a; word-break: break-all; }', + '.h2-balance-drill-modal .h2-balance-drill-stat__value--income { color: #059669; }', + '.h2-balance-drill-modal .h2-balance-drill-stat__value--expense { color: #ea580c; }', + '.h2-balance-drill-modal .h2-balance-drill-stat__value--negative { color: #dc2626; }', + '.h2-balance-drill-modal .h2-balance-drill-table-wrap { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; overflow: hidden; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04); }', + '.h2-balance-drill-modal .h2-balance-drill-table-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 14px; border-bottom: 1px solid #f1f5f9; background: #fafbfc; }', + '.h2-balance-drill-modal .h2-balance-drill-table-head__left { display: flex; align-items: center; gap: 10px; min-width: 0; }', + '.h2-balance-drill-modal .h2-balance-drill-table-head__title { font-size: 13px; font-weight: 700; color: #334155; }', + '.h2-balance-drill-modal .h2-balance-drill-table-head__count { font-size: 12px; color: #64748b; font-variant-numeric: tabular-nums; }', + '.h2-balance-drill-modal .h2-balance-record-table { border-radius: 0 !important; }', + '.h2-balance-drill-modal .h2-balance-record-table .ant-table { background: transparent !important; }', + '.h2-balance-drill-modal .h2-balance-record-table .ant-table-container { border: none !important; }', + '.h2-balance-drill-modal .h2-balance-record-table .ant-table-header { margin-bottom: 0 !important; }', + '.h2-balance-drill-modal .h2-balance-record-table .ant-table-body { margin-top: 0 !important; overflow-y: auto !important; }', + '.h2-balance-drill-modal .h2-balance-record-table .ant-table-body table { margin-top: 0 !important; }', + '.h2-balance-drill-modal .h2-balance-record-table .ant-table-tbody > tr.ant-table-measure-row, .h2-balance-drill-modal .h2-balance-record-table .ant-table-tbody > tr.ant-table-measure-row > td { height: 0 !important; max-height: 0 !important; padding: 0 !important; margin: 0 !important; border: none !important; line-height: 0 !important; font-size: 0 !important; overflow: hidden !important; visibility: hidden !important; }', + '.h2-balance-drill-modal .h2-balance-record-table .ant-table-thead > tr > th { background: #f8fafc !important; font-size: 12px !important; font-weight: 700 !important; color: #475569 !important; padding: 10px 12px !important; border-bottom: 1px solid #e2e8f0 !important; }', + '.h2-balance-drill-modal .h2-balance-record-table .ant-table-tbody > tr:not(.ant-table-measure-row) > td { font-size: 13px !important; padding: 10px 12px !important; vertical-align: middle !important; border-bottom: 1px solid #f8fafc !important; }', + '.h2-balance-drill-modal .h2-balance-record-table .ant-table-tbody > tr:not(.ant-table-measure-row):last-child > td { border-bottom: none !important; }', + '.h2-balance-drill-modal .h2-balance-record-table .ant-table-tbody > tr:hover > td { background: #f0fdf4 !important; }', + '.h2-balance-drill-modal .h2-balance-record-table .h2-balance-money { font-variant-numeric: tabular-nums; white-space: nowrap; letter-spacing: -0.01em; }', + '.h2-balance-drill-modal .h2-balance-record-table .h2-balance-money--income { color: #059669; font-weight: 600; }', + '.h2-balance-drill-modal .h2-balance-record-table .h2-balance-money--expense { color: #ea580c; font-weight: 600; }', + '.h2-balance-drill-modal .h2-balance-record-table .h2-balance-money--balance { color: #0f172a; font-weight: 700; }', + '.h2-balance-drill-modal .h2-balance-record-table .h2-balance-money--negative { color: #dc2626 !important; }', + '.h2-balance-drill-modal .h2-balance-record-table .h2-balance-money--muted { color: #cbd5e1; font-weight: 400; }', + '.h2-balance-drill-modal .h2-balance-record-table .h2-balance-order-no { display: inline-block; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; color: #475569; background: #f1f5f9; border: 1px solid #e2e8f0; padding: 2px 7px; border-radius: 6px; letter-spacing: 0.02em; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }', + '.h2-balance-drill-modal .h2-balance-record-table .h2-balance-seq { display: inline-flex; align-items: center; justify-content: center; min-width: 24px; height: 24px; border-radius: 6px; background: #f1f5f9; color: #64748b; font-size: 12px; font-weight: 700; }', + '.h2-station-view-modal .ant-modal-content { border-radius: 16px !important; overflow: hidden; box-shadow: 0 24px 48px -12px rgba(15, 23, 42, 0.18) !important; }', + '.h2-station-view-modal .ant-modal-header { padding: 18px 24px 14px !important; border-bottom: 1px solid #f1f5f9 !important; margin-bottom: 0 !important; }', + '.h2-station-view-modal .ant-modal-title { font-size: 17px !important; font-weight: 700 !important; color: #0f172a !important; }', + '.h2-station-view-modal .ant-modal-body { padding: 16px 24px 20px !important; background: #f8fafc; }', + '.h2-station-view-modal .ant-modal-footer { padding: 12px 24px 18px !important; border-top: 1px solid #f1f5f9 !important; background: #fff; }', + '.h2-station-view-modal .h2-station-view-panel { display: flex; flex-direction: column; gap: 20px; }', + '.h2-station-view-modal .h2-station-view-section__title { margin: 0 0 10px; padding-left: 10px; font-size: 14px; font-weight: 700; color: #334155; line-height: 1.35; border-left: 4px solid #10b981; }', + '.h2-station-view-modal .h2-station-view-section .ant-descriptions { background: #fff; border-radius: 10px; overflow: hidden; }', + '.h2-station-view-modal .h2-station-view-section .ant-descriptions-item-label { width: 132px !important; font-size: 12px !important; font-weight: 600 !important; color: #64748b !important; background: #fafbfc !important; }', + '.h2-station-view-modal .h2-station-view-section .ant-descriptions-item-content { font-size: 13px !important; color: #0f172a !important; font-weight: 500 !important; word-break: break-word; }', + '.h2-station-view-modal .h2-station-view-files { display: flex; flex-wrap: wrap; gap: 8px; }', + '.h2-station-view-modal .h2-station-view-file-link { display: inline-flex; align-items: center; max-width: 100%; padding: 4px 10px; border: 1px solid #e2e8f0; border-radius: 8px; background: #f8fafc; color: #059669; font-size: 13px; font-weight: 600; cursor: pointer; transition: background 0.2s ease, border-color 0.2s ease; }', + '.h2-station-view-modal .h2-station-view-file-link:hover { background: #ecfdf5; border-color: #86efac; }', + '.h2-station-view-modal .h2-station-view-file-link:focus-visible { outline: 2px solid #10b981; outline-offset: 2px; }', + '.h2-station-view-modal .h2-station-view-license-count { font-size: 13px; color: #64748b; margin-bottom: 8px; }', + '.h2-station-view-modal .h2-station-view-license-images { display: flex; flex-wrap: wrap; gap: 10px; }', + '.h2-station-view-modal .h2-station-view-license-thumb { display: block; padding: 0; border: 1px solid #e2e8f0; border-radius: 10px; overflow: hidden; background: #f8fafc; cursor: pointer; transition: box-shadow 0.2s ease, border-color 0.2s ease; }', + '.h2-station-view-modal .h2-station-view-license-thumb:hover { border-color: #86efac; box-shadow: 0 4px 14px rgba(16, 185, 129, 0.15); }', + '.h2-station-view-modal .h2-station-view-license-thumb img { display: block; width: 168px; height: 118px; object-fit: cover; }', + '.h2-station-view-modal .h2-station-view-license-empty { font-size: 13px; color: #94a3b8; }', + '.h2-file-preview-modal .ant-modal-body { padding: 0 !important; background: #0f172a; }', + '.h2-file-preview-modal .h2-file-preview-body { display: flex; align-items: center; justify-content: center; min-height: 360px; max-height: 78vh; overflow: auto; padding: 12px; box-sizing: border-box; }', + '.h2-file-preview-modal .h2-file-preview-body iframe { width: 100%; min-height: 70vh; border: none; background: #fff; border-radius: 8px; }', + '.h2-file-preview-modal .h2-file-preview-body img { max-width: 100%; max-height: 76vh; border-radius: 8px; box-shadow: 0 8px 24px rgba(0,0,0,0.25); }', + '.h2-station-view-modal .h2-station-view-empty { padding: 20px 16px; text-align: center; font-size: 13px; color: #94a3b8; background: #fff; border: 1px dashed #e2e8f0; border-radius: 10px; }', + '.h2-price-config-modal .ant-modal-content { border-radius: 16px !important; overflow: hidden; box-shadow: 0 24px 48px -12px rgba(15, 23, 42, 0.18) !important; }', + '.h2-price-config-modal .ant-modal-header { padding: 18px 24px 14px !important; border-bottom: 1px solid #f1f5f9 !important; margin-bottom: 0 !important; }', + '.h2-price-config-modal .ant-modal-title { font-size: 17px !important; font-weight: 700 !important; color: #0f172a !important; }', + '.h2-price-config-modal .ant-modal-body { padding: 16px 24px 20px !important; background: #f8fafc; }', + '.h2-price-config-modal .ant-modal-footer { padding: 12px 24px 18px !important; border-top: 1px solid #f1f5f9 !important; background: #fff; }', + '.h2-price-config-modal .h2-price-config-panel { display: flex; flex-direction: column; gap: 14px; }', + '.h2-price-config-modal .h2-price-config-station-card { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; padding: 12px 16px; background: linear-gradient(135deg, #fff7ed 0%, #fffbeb 50%, #f8fafc 100%); border: 1px solid #fed7aa; border-radius: 12px; }', + '.h2-price-config-modal .h2-price-config-station-card__name { font-size: 15px; font-weight: 700; color: #0f172a; line-height: 1.35; }', + '.h2-price-config-modal .h2-price-config-station-card__meta { font-size: 12px; color: #64748b; font-variant-numeric: tabular-nums; white-space: nowrap; }', + '.h2-price-config-modal .h2-price-config-stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }', + '@media (max-width: 640px) { .h2-price-config-modal .h2-price-config-stats { grid-template-columns: 1fr; } }', + '.h2-price-config-modal .h2-price-config-stat { display: flex; flex-direction: column; justify-content: center; min-height: 78px; padding: 12px 14px; border-radius: 12px; border: 1px solid #e2e8f0; background: #fff; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06); min-width: 0; box-sizing: border-box; }', + '.h2-price-config-modal .h2-price-config-stat--current { border-left: 4px solid #f97316; background: linear-gradient(180deg, #fff 0%, #fff7ed 100%); }', + '.h2-price-config-modal .h2-price-config-stat--history { border-left: 4px solid #3b82f6; }', + '.h2-price-config-modal .h2-price-config-stat--pending { border-left: 4px solid #6366f1; }', + '.h2-price-config-modal .h2-price-config-stat__label { font-size: 12px; font-weight: 600; color: #64748b; margin-bottom: 8px; line-height: 1.2; }', + '.h2-price-config-modal .h2-price-config-stat__value { font-size: 20px; font-weight: 800; font-variant-numeric: tabular-nums; line-height: 1.2; color: #0f172a; word-break: break-all; }', + '.h2-price-config-modal .h2-price-config-stat__value--current { color: #ea580c; }', + '.h2-price-config-modal .h2-price-config-stat__value--history { color: #2563eb; }', + '.h2-price-config-modal .h2-price-config-stat__value--pending { color: #4f46e5; }', + '.h2-price-config-modal .h2-price-config-form-wrap { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; padding: 14px 16px; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04); }', + '.h2-price-config-modal .h2-price-config-form-wrap .ant-form-item { margin-bottom: 14px; }', + '.h2-price-config-modal .h2-price-config-form-wrap .ant-form-item:last-child { margin-bottom: 0; }', + '.h2-price-config-modal .h2-price-config-form-wrap .ant-form-item-label > label { font-size: 13px; font-weight: 600; color: #334155; }', + '.h2-price-config-modal .h2-price-config-form-wrap .ant-input, .h2-price-config-modal .h2-price-config-form-wrap .ant-picker { width: 100% !important; border-radius: 8px !important; }', + '.h2-price-config-modal .h2-price-config-form-hint { margin-top: 10px; padding: 8px 12px; font-size: 12px; color: #64748b; line-height: 1.5; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; }', + '.h2-price-config-modal .h2-price-config-table-wrap { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; overflow: hidden; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04); }', + '.h2-price-config-modal .h2-price-config-table-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 14px; border-bottom: 1px solid #f1f5f9; background: #fafbfc; }', + '.h2-price-config-modal .h2-price-config-table-head__title { font-size: 13px; font-weight: 700; color: #334155; }', + '.h2-price-config-modal .h2-price-config-table-head__count { font-size: 12px; color: #64748b; font-variant-numeric: tabular-nums; }', + '.h2-price-config-modal .h2-price-config-record-table { border-radius: 0 !important; }', + '.h2-price-config-modal .h2-price-config-record-table .ant-table { background: transparent !important; }', + '.h2-price-config-modal .h2-price-config-record-table .ant-table-container { border: none !important; }', + '.h2-price-config-modal .h2-price-config-record-table .ant-table-header { margin-bottom: 0 !important; }', + '.h2-price-config-modal .h2-price-config-record-table .ant-table-body { margin-top: 0 !important; overflow-y: auto !important; }', + '.h2-price-config-modal .h2-price-config-record-table .ant-table-body table { margin-top: 0 !important; }', + '.h2-price-config-modal .h2-price-config-record-table .ant-table-tbody > tr.ant-table-measure-row, .h2-price-config-modal .h2-price-config-record-table .ant-table-tbody > tr.ant-table-measure-row > td { height: 0 !important; max-height: 0 !important; padding: 0 !important; margin: 0 !important; border: none !important; line-height: 0 !important; font-size: 0 !important; overflow: hidden !important; visibility: hidden !important; }', + '.h2-price-config-modal .h2-price-config-record-table .ant-table-thead > tr > th { background: #f8fafc !important; font-size: 12px !important; font-weight: 700 !important; color: #475569 !important; padding: 10px 12px !important; border-bottom: 1px solid #e2e8f0 !important; }', + '.h2-price-config-modal .h2-price-config-record-table .ant-table-tbody > tr:not(.ant-table-measure-row) > td { font-size: 13px !important; padding: 10px 12px !important; vertical-align: middle !important; border-bottom: 1px solid #f8fafc !important; }', + '.h2-price-config-modal .h2-price-config-record-table .ant-table-tbody > tr:not(.ant-table-measure-row):last-child > td { border-bottom: none !important; }', + '.h2-price-config-modal .h2-price-config-record-table .ant-table-tbody > tr:hover > td { background: #fff7ed !important; }', + '.h2-price-config-modal .h2-price-config-record-table .h2-price-config-money { font-variant-numeric: tabular-nums; white-space: nowrap; font-weight: 600; }', + '.h2-price-config-modal .h2-price-config-record-table .h2-price-config-money--before { color: #64748b; }', + '.h2-price-config-modal .h2-price-config-record-table .h2-price-config-money--after { color: #ea580c; font-weight: 700; }', + '.h2-business-status-modal .ant-modal-content { border-radius: 16px !important; overflow: hidden; box-shadow: 0 24px 48px -12px rgba(15, 23, 42, 0.18) !important; }', + '.h2-business-status-modal .ant-modal-header { padding: 18px 24px 14px !important; border-bottom: 1px solid #f1f5f9 !important; margin-bottom: 0 !important; }', + '.h2-business-status-modal .ant-modal-title { font-size: 17px !important; font-weight: 700 !important; color: #0f172a !important; }', + '.h2-business-status-modal .ant-modal-body { padding: 16px 24px 20px !important; background: #f8fafc; }', + '.h2-business-status-modal .ant-modal-footer { padding: 12px 24px 18px !important; border-top: 1px solid #f1f5f9 !important; background: #fff; }', + '.h2-business-status-modal .h2-business-status-panel { display: flex; flex-direction: column; gap: 14px; }', + '.h2-business-status-modal .h2-business-status-station-card { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; padding: 12px 16px; background: linear-gradient(135deg, #ecfdf5 0%, #f0fdf4 50%, #f8fafc 100%); border: 1px solid #bbf7d0; border-radius: 12px; }', + '.h2-business-status-modal .h2-business-status-station-card__name { font-size: 15px; font-weight: 700; color: #0f172a; line-height: 1.35; }', + '.h2-business-status-modal .h2-business-status-station-card__meta { font-size: 12px; color: #64748b; font-variant-numeric: tabular-nums; white-space: nowrap; }', + '.h2-business-status-modal .h2-business-status-stats { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }', + '@media (max-width: 640px) { .h2-business-status-modal .h2-business-status-stats { grid-template-columns: 1fr; } }', + '.h2-business-status-modal .h2-business-status-stat { display: flex; flex-direction: column; justify-content: center; min-height: 78px; padding: 12px 14px; border-radius: 12px; border: 1px solid #e2e8f0; background: #fff; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06); min-width: 0; box-sizing: border-box; }', + '.h2-business-status-modal .h2-business-status-stat--status { border-left: 4px solid #10b981; background: linear-gradient(180deg, #fff 0%, #f0fdf4 100%); }', + '.h2-business-status-modal .h2-business-status-stat--hours { border-left: 4px solid #3b82f6; }', + '.h2-business-status-modal .h2-business-status-stat__label { font-size: 12px; font-weight: 600; color: #64748b; margin-bottom: 8px; line-height: 1.2; }', + '.h2-business-status-modal .h2-business-status-stat__value { font-size: 16px; font-weight: 700; line-height: 1.35; color: #0f172a; word-break: break-word; }', + '.h2-business-status-modal .h2-business-status-stat__value--hours { font-size: 15px; font-weight: 700; color: #2563eb; font-variant-numeric: tabular-nums; }', + '.h2-business-status-modal .h2-business-status-form-wrap { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; padding: 14px 16px; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04); }', + '.h2-business-status-modal .h2-business-status-form-wrap .ant-form-item { margin-bottom: 14px; }', + '.h2-business-status-modal .h2-business-status-form-wrap .ant-form-item:last-child { margin-bottom: 0; }', + '.h2-business-status-modal .h2-business-status-form-wrap .ant-form-item-label > label { font-size: 13px; font-weight: 600; color: #334155; }', + '.h2-business-status-modal .h2-business-status-form-wrap .ant-picker { width: 100% !important; border-radius: 8px !important; }', + '.h2-business-status-modal .h2-business-status-form-hint { margin-top: 10px; padding: 8px 12px; font-size: 12px; color: #64748b; line-height: 1.5; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; }', + '.h2-business-status-modal .h2-business-status-table-wrap { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; overflow: hidden; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04); }', + '.h2-business-status-modal .h2-business-status-table-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 14px; border-bottom: 1px solid #f1f5f9; background: #fafbfc; }', + '.h2-business-status-modal .h2-business-status-table-head__title { font-size: 13px; font-weight: 700; color: #334155; }', + '.h2-business-status-modal .h2-business-status-table-head__count { font-size: 12px; color: #64748b; font-variant-numeric: tabular-nums; }', + '.h2-business-status-modal .h2-business-status-record-table { border-radius: 0 !important; }', + '.h2-business-status-modal .h2-business-status-record-table .ant-table { background: transparent !important; }', + '.h2-business-status-modal .h2-business-status-record-table .ant-table-container { border: none !important; }', + '.h2-business-status-modal .h2-business-status-record-table .ant-table-header { margin-bottom: 0 !important; }', + '.h2-business-status-modal .h2-business-status-record-table .ant-table-body { margin-top: 0 !important; overflow-y: auto !important; }', + '.h2-business-status-modal .h2-business-status-record-table .ant-table-body table { margin-top: 0 !important; }', + '.h2-business-status-modal .h2-business-status-record-table .ant-table-tbody > tr.ant-table-measure-row, .h2-business-status-modal .h2-business-status-record-table .ant-table-tbody > tr.ant-table-measure-row > td { height: 0 !important; max-height: 0 !important; padding: 0 !important; margin: 0 !important; border: none !important; line-height: 0 !important; font-size: 0 !important; overflow: hidden !important; visibility: hidden !important; }', + '.h2-business-status-modal .h2-business-status-record-table .ant-table-thead > tr > th { background: #f8fafc !important; font-size: 12px !important; font-weight: 700 !important; color: #475569 !important; padding: 10px 12px !important; border-bottom: 1px solid #e2e8f0 !important; }', + '.h2-business-status-modal .h2-business-status-record-table .ant-table-tbody > tr:not(.ant-table-measure-row) > td { font-size: 13px !important; padding: 10px 12px !important; vertical-align: middle !important; border-bottom: 1px solid #f8fafc !important; }', + '.h2-business-status-modal .h2-business-status-record-table .ant-table-tbody > tr:not(.ant-table-measure-row):last-child > td { border-bottom: none !important; }', + '.h2-business-status-modal .h2-business-status-record-table .ant-table-tbody > tr:hover > td { background: #f0fdf4 !important; }', + '.h2-statement-modal .ant-modal-content { border-radius: 16px !important; overflow: hidden; box-shadow: 0 24px 48px -12px rgba(15, 23, 42, 0.18) !important; }', + '.h2-statement-modal .ant-modal-header { padding: 18px 24px 14px !important; border-bottom: 1px solid #f1f5f9 !important; margin-bottom: 0 !important; }', + '.h2-statement-modal .ant-modal-title { font-size: 17px !important; font-weight: 700 !important; color: #0f172a !important; }', + '.h2-statement-modal .ant-modal-body { padding: 16px 24px 20px !important; background: #f8fafc; }', + '.h2-statement-modal .ant-modal-footer { padding: 12px 24px 18px !important; border-top: 1px solid #f1f5f9 !important; background: #fff; }', + '.h2-statement-modal .h2-statement-panel { display: flex; flex-direction: column; gap: 14px; }', + '.h2-statement-modal .h2-statement-station-card { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; padding: 12px 16px; background: linear-gradient(135deg, #f5f3ff 0%, #ede9fe 50%, #f8fafc 100%); border: 1px solid #ddd6fe; border-radius: 12px; }', + '.h2-statement-modal .h2-statement-station-card__name { font-size: 15px; font-weight: 700; color: #0f172a; line-height: 1.35; }', + '.h2-statement-modal .h2-statement-station-card__meta { font-size: 12px; color: #64748b; font-variant-numeric: tabular-nums; white-space: nowrap; }', + '.h2-statement-modal .h2-statement-form-wrap { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; padding: 14px 16px; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04); }', + '.h2-statement-modal .h2-statement-form-wrap .ant-form-item { margin-bottom: 0; }', + '.h2-statement-modal .h2-statement-form-wrap .ant-form-item-label > label { font-size: 13px; font-weight: 600; color: #334155; }', + '.h2-statement-modal .h2-statement-form-wrap .ant-input { border-radius: 8px !important; }', + '.h2-statement-modal .h2-statement-date-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }', + '@media (max-width: 520px) { .h2-statement-modal .h2-statement-date-grid { grid-template-columns: 1fr; } }', + '.h2-statement-modal .h2-statement-form-hint { margin-top: 10px; padding: 8px 12px; font-size: 12px; color: #64748b; line-height: 1.5; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; }', + '.h2-statement-modal .h2-statement-form-hint strong { color: #5b21b6; font-weight: 700; }', + '.h2-statement-modal .h2-statement-stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }', + '@media (max-width: 640px) { .h2-statement-modal .h2-statement-stats { grid-template-columns: 1fr; } }', + '.h2-statement-modal .h2-statement-stat { display: flex; flex-direction: column; justify-content: center; min-height: 78px; padding: 12px 14px; border-radius: 12px; border: 1px solid #e2e8f0; background: #fff; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06); min-width: 0; box-sizing: border-box; }', + '.h2-statement-modal .h2-statement-stat--count { border-left: 4px solid #7c3aed; }', + '.h2-statement-modal .h2-statement-stat--kg { border-left: 4px solid #10b981; background: linear-gradient(180deg, #fff 0%, #f0fdf4 100%); }', + '.h2-statement-modal .h2-statement-stat--cost { border-left: 4px solid #f97316; }', + '.h2-statement-modal .h2-statement-stat__label { font-size: 12px; font-weight: 600; color: #64748b; margin-bottom: 8px; line-height: 1.2; }', + '.h2-statement-modal .h2-statement-stat__value { font-size: 20px; font-weight: 800; font-variant-numeric: tabular-nums; line-height: 1.2; color: #0f172a; word-break: break-all; }', + '.h2-statement-modal .h2-statement-stat__value--count { color: #6d28d9; }', + '.h2-statement-modal .h2-statement-stat__value--kg { color: #059669; }', + '.h2-statement-modal .h2-statement-stat__value--cost { color: #ea580c; }', + '.h2-statement-modal .h2-statement-table-wrap { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; overflow: hidden; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04); }', + '.h2-statement-modal .h2-statement-table-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 14px; border-bottom: 1px solid #f1f5f9; background: #fafbfc; }', + '.h2-statement-modal .h2-statement-table-head__left { display: flex; align-items: center; gap: 10px; min-width: 0; }', + '.h2-statement-modal .h2-statement-table-head__title { font-size: 13px; font-weight: 700; color: #334155; }', + '.h2-statement-modal .h2-statement-table-head__count { font-size: 12px; color: #64748b; font-variant-numeric: tabular-nums; }', + '.h2-statement-modal .h2-statement-record-table { border-radius: 0 !important; }', + '.h2-statement-modal .h2-statement-record-table .ant-table { background: transparent !important; }', + '.h2-statement-modal .h2-statement-record-table .ant-table-container { border: none !important; }', + '.h2-statement-modal .h2-statement-record-table .ant-table-header { margin-bottom: 0 !important; }', + '.h2-statement-modal .h2-statement-record-table .ant-table-body { margin-top: 0 !important; overflow-y: auto !important; }', + '.h2-statement-modal .h2-statement-record-table .ant-table-body table { margin-top: 0 !important; }', + '.h2-statement-modal .h2-statement-record-table .ant-table-tbody > tr.ant-table-measure-row, .h2-statement-modal .h2-statement-record-table .ant-table-tbody > tr.ant-table-measure-row > td { height: 0 !important; max-height: 0 !important; padding: 0 !important; margin: 0 !important; border: none !important; line-height: 0 !important; font-size: 0 !important; overflow: hidden !important; visibility: hidden !important; }', + '.h2-statement-modal .h2-statement-record-table .ant-table-thead > tr > th { background: #f8fafc !important; font-size: 12px !important; font-weight: 700 !important; color: #475569 !important; padding: 10px 12px !important; border-bottom: 1px solid #e2e8f0 !important; }', + '.h2-statement-modal .h2-statement-record-table .ant-table-tbody > tr:not(.ant-table-measure-row) > td { font-size: 13px !important; padding: 10px 12px !important; vertical-align: middle !important; border-bottom: 1px solid #f8fafc !important; }', + '.h2-statement-modal .h2-statement-record-table .ant-table-tbody > tr:not(.ant-table-measure-row):last-child > td { border-bottom: none !important; }', + '.h2-statement-modal .h2-statement-record-table .ant-table-tbody > tr:hover > td { background: #f5f3ff !important; }', + '.h2-statement-modal .h2-statement-last-end { margin-top: 10px; padding: 8px 12px; font-size: 12px; color: #475569; line-height: 1.5; background: #faf5ff; border: 1px solid #e9d5ff; border-radius: 8px; font-variant-numeric: tabular-nums; }', + '.h2-statement-modal .h2-statement-last-end strong { color: #6d28d9; font-weight: 700; }', + '.h2-statement-modal .h2-statement-settlement-wrap { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; padding: 14px 16px; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04); display: flex; flex-direction: column; gap: 14px; }', + '.h2-statement-modal .h2-statement-settlement-wrap .ant-form-item { margin-bottom: 0; }', + '.h2-statement-modal .h2-statement-settlement-wrap .ant-form-item-label > label { font-size: 13px; font-weight: 600; color: #334155; }', + '.h2-statement-modal .h2-statement-receipt-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }', + '@media (max-width: 520px) { .h2-statement-modal .h2-statement-receipt-grid { grid-template-columns: 1fr; } }', + '.h2-statement-modal .h2-statement-receipt-amount-input .ant-input { font-variant-numeric: tabular-nums; }', + '.h2-statement-modal .h2-statement-receipt-amount-input .ant-input-prefix { color: #64748b; font-weight: 600; margin-inline-end: 4px; }', + '.h2-statement-modal .h2-statement-settlement-wrap .ant-picker { width: 100% !important; border-radius: 8px !important; }', + '.h2-statement-modal .h2-statement-invoice-upload .h2-contract-upload-actions { gap: 0; }', + '.h2-statement-modal .h2-statement-invoice-upload .h2-statement-upload-btn.ant-btn { display: inline-flex !important; align-items: center; gap: 6px; height: 32px !important; padding: 0 14px !important; border-radius: 8px !important; font-size: 13px !important; font-weight: 600 !important; border: 1px solid #e2e8f0 !important; color: #475569 !important; background: #fff !important; box-shadow: none !important; }', + '.h2-statement-modal .h2-statement-invoice-upload .h2-statement-upload-btn.ant-btn:hover { border-color: #10b981 !important; color: #059669 !important; background: #f0fdf4 !important; }', + '.h2-statement-modal .h2-statement-invoice-upload .h2-statement-upload-btn.ant-btn:focus-visible { outline: 2px solid #10b981; outline-offset: 2px; }', + '.h2-recharge-modal .ant-modal-content { border-radius: 16px !important; overflow: hidden; box-shadow: 0 24px 48px -12px rgba(15, 23, 42, 0.18) !important; }', + '.h2-recharge-modal .ant-modal-header { padding: 18px 24px 14px !important; border-bottom: 1px solid #f1f5f9 !important; margin-bottom: 0 !important; }', + '.h2-recharge-modal .ant-modal-title { font-size: 17px !important; font-weight: 700 !important; color: #0f172a !important; }', + '.h2-recharge-modal .ant-modal-body { padding: 16px 24px 20px !important; background: #f8fafc; }', + '.h2-recharge-modal .ant-modal-footer { padding: 12px 24px 18px !important; border-top: 1px solid #f1f5f9 !important; background: #fff; }', + '.h2-recharge-modal .h2-recharge-panel { display: flex; flex-direction: column; gap: 14px; }', + '.h2-recharge-modal .h2-recharge-hint { margin: 0; padding: 10px 12px; font-size: 12px; color: #64748b; line-height: 1.55; background: #fff; border: 1px solid #e2e8f0; border-radius: 8px; }', + '.h2-recharge-modal .h2-recharge-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }', + '.h2-recharge-modal .h2-recharge-toolbar__count { font-size: 13px; color: #64748b; font-variant-numeric: tabular-nums; }', + '.h2-recharge-modal .h2-recharge-table-wrap { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; overflow: hidden; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04); }', + '.h2-recharge-modal .h2-recharge-record-table .ant-table-thead > tr > th { background: #f8fafc !important; font-size: 12px !important; font-weight: 700 !important; color: #475569 !important; padding: 10px 12px !important; }', + '.h2-recharge-modal .h2-recharge-record-table .ant-table-tbody > tr > td { font-size: 13px !important; padding: 8px 10px !important; vertical-align: middle !important; }', + '.h2-recharge-modal .h2-recharge-readonly { font-size: 13px; color: #334155; line-height: 1.45; word-break: break-all; }', + '.h2-recharge-modal .h2-recharge-readonly--muted { color: #94a3b8; }', + '.h2-recharge-modal .h2-recharge-readonly-input.ant-input[disabled] { background: #f8fafc !important; color: #334155 !important; cursor: default !important; border: 1px solid #e2e8f0 !important; }', + '.h2-alert-station-modal .ant-modal-content { border-radius: 16px !important; overflow: hidden; box-shadow: 0 24px 48px -12px rgba(15, 23, 42, 0.18) !important; }', + '.h2-alert-station-modal .ant-modal-header { padding: 18px 24px 14px !important; border-bottom: 1px solid #f1f5f9 !important; margin-bottom: 0 !important; }', + '.h2-alert-station-modal .ant-modal-title { font-size: 17px !important; font-weight: 700 !important; color: #0f172a !important; }', + '.h2-alert-station-modal .ant-modal-body { padding: 16px 24px 20px !important; background: #f8fafc; }', + '.h2-alert-station-modal .ant-modal-footer { padding: 12px 24px 18px !important; border-top: 1px solid #f1f5f9 !important; background: #fff; }', + '.h2-alert-station-modal .h2-alert-station-panel { display: flex; flex-direction: column; gap: 14px; }', + '.h2-alert-station-modal .h2-alert-station-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; padding: 12px 16px; background: linear-gradient(135deg, #fff7ed 0%, #fffbeb 50%, #f8fafc 100%); border: 1px solid #fed7aa; border-radius: 12px; }', + '.h2-alert-station-modal .h2-alert-station-head__title { font-size: 14px; font-weight: 700; color: #0f172a; }', + '.h2-alert-station-modal .h2-alert-station-head__count { font-size: 12px; color: #64748b; font-variant-numeric: tabular-nums; }', + '.h2-alert-station-modal .h2-alert-station-table-wrap { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; overflow: hidden; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04); }', + '.h2-statement-modal .h2-statement-detail-receipt-wrap { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; padding: 14px 16px; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04); }', + '.h2-statement-modal .h2-statement-detail-receipt-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 20px; }', + '@media (max-width: 520px) { .h2-statement-modal .h2-statement-detail-receipt-grid { grid-template-columns: 1fr; } }', + '.h2-statement-modal .h2-statement-detail-receipt-item__label { font-size: 12px; font-weight: 600; color: #64748b; margin-bottom: 6px; }', + '.h2-statement-modal .h2-statement-detail-receipt-item__value { font-size: 14px; font-weight: 700; color: #0f172a; font-variant-numeric: tabular-nums; }', + '.h2-statement-modal .h2-statement-detail-receipt-item__value--amount { color: #ea580c; }', + '.h2-statement-modal .h2-statement-detail-receipt-files { margin-top: 12px; padding-top: 12px; border-top: 1px solid #f1f5f9; }', + '.h2-statement-modal .h2-statement-detail-receipt-file-list { display: flex; flex-direction: column; gap: 8px; margin-top: 8px; }', + '.h2-statement-modal .h2-statement-detail-receipt-file-item { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 8px 12px; border-radius: 8px; background: #f8fafc; border: 1px solid #e2e8f0; }', + '.h2-statement-modal .h2-statement-detail-receipt-file-name { font-size: 13px; color: #334155; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }', + '.h2-statement-modal .h2-statement-balance-readonly { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-height: 32px; padding: 6px 12px; border-radius: 8px; background: #f8fafc; border: 1px solid #e2e8f0; }', + '.h2-statement-modal .h2-statement-balance-readonly__amount { font-size: 16px; font-weight: 800; font-variant-numeric: tabular-nums; }', + '.h2-statement-modal .h2-statement-balance-readonly__amount--positive { color: #059669; }', + '.h2-statement-modal .h2-statement-balance-readonly__amount--negative { color: #dc2626; }', + '.h2-statement-history-modal .ant-modal-content { border-radius: 16px !important; overflow: hidden; box-shadow: 0 24px 48px -12px rgba(15, 23, 42, 0.18) !important; }', + '.h2-statement-history-modal .ant-modal-header { padding: 18px 24px 14px !important; border-bottom: 1px solid #f1f5f9 !important; margin-bottom: 0 !important; }', + '.h2-statement-history-modal .ant-modal-title { font-size: 17px !important; font-weight: 700 !important; color: #0f172a !important; }', + '.h2-statement-history-modal .ant-modal-body { padding: 16px 24px 20px !important; background: #f8fafc; }', + '.h2-statement-history-modal .ant-modal-footer { padding: 12px 24px 18px !important; border-top: 1px solid #f1f5f9 !important; background: #fff; }', + '.h2-statement-history-modal .h2-statement-history-panel { display: flex; flex-direction: column; gap: 14px; }', + '.h2-statement-history-modal .h2-statement-history-station-card { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; padding: 12px 16px; background: linear-gradient(135deg, #f5f3ff 0%, #ede9fe 50%, #f8fafc 100%); border: 1px solid #ddd6fe; border-radius: 12px; }', + '.h2-statement-history-modal .h2-statement-history-station-card__name { font-size: 15px; font-weight: 700; color: #0f172a; line-height: 1.35; }', + '.h2-statement-history-modal .h2-statement-history-station-card__meta { font-size: 12px; color: #64748b; font-variant-numeric: tabular-nums; white-space: nowrap; }', + '.h2-statement-history-modal .h2-statement-history-table-wrap { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; overflow: hidden; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04); }', + '.h2-statement-history-modal .h2-statement-history-table-wrap .ant-table-thead > tr > th { background: #f8fafc !important; font-size: 12px !important; font-weight: 700 !important; color: #475569 !important; }', + '.h2-statement-history-modal .h2-statement-history-table-wrap .ant-table-tbody > tr:hover > td { background: #f5f3ff !important; }', + '.h2-statement-history-modal .h2-statement-history-balance { font-weight: 700; font-variant-numeric: tabular-nums; }', + '.h2-statement-history-modal .h2-statement-history-balance--negative { color: #dc2626; }', + '.h2-statement-history-modal .h2-statement-history-balance--positive { color: #059669; }', + '.h2-station-page .h2-ledger-totals-bar__value--income { color: #059669 !important; }', + '.h2-station-page .h2-ledger-totals-bar__value--expense { color: #ea580c !important; }', + '.h2-station-page .h2-ledger-totals-bar__value--negative { color: #dc2626 !important; }', '.h2-station-page.h2-station-page--create { padding: 0 0 96px; height: auto; min-height: 100dvh; overflow: auto; }', '.h2-station-page--create .h2-create-shell { width: 100%; max-width: none; margin: 0; padding: 24px 24px 0; box-sizing: border-box; }', + '.h2-station-page.h2-station-page--edit { padding: 0 0 96px; height: auto; min-height: 100dvh; overflow: auto; }', + '.h2-station-page--edit .h2-create-shell { width: 100%; max-width: none; margin: 0; padding: 24px 24px 0; box-sizing: border-box; }', + '.h2-account-bind-readonly-wrap { padding: 12px 14px; border-radius: 10px; background: #f8fafc; border: 1px solid #e2e8f0; }', + '.h2-account-bind-readonly-item { padding: 10px 12px; border-radius: 8px; background: #fff; border: 1px solid #e2e8f0; margin-bottom: 8px; }', + '.h2-account-bind-readonly-item:last-child { margin-bottom: 0; }', + '.h2-account-bind-readonly-item__name { font-size: 14px; font-weight: 700; color: #0f172a; margin-bottom: 4px; }', + '.h2-account-bind-readonly-item__meta { font-size: 12px; color: #64748b; }', '.h2-station-page--create .h2-create-pagehead { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px; margin-bottom: 16px; }', '.h2-station-page--create .h2-create-pagehead-left { display: flex; align-items: center; gap: 12px; min-width: 0; }', '.h2-station-page--create .h2-create-pagehead-title { margin: 0; font-size: 18px; font-weight: 700; color: #0f172a; line-height: 1.35; }', '.h2-station-page--create .h2-create-pagehead-desc { margin: 2px 0 0; font-size: 13px; color: #64748b; line-height: 1.45; }', '.h2-station-page--create .h2-create-card.ant-card { margin-bottom: 16px !important; }', - '.h2-station-page--create .h2-create-form--grid .h2-create-form-grid { width: 100%; }', + '.h2-station-page--create .h2-create-form--grid .h2-create-form-grid { width: 100%; display: flex; flex-direction: column; gap: var(--h2-form-row-gap); }', + '.h2-station-page--create .h2-create-form--grid .h2-create-form-grid > .ant-row { margin-bottom: 0 !important; }', + '.h2-station-page--create .h2-create-form--grid .ant-form-item-label { padding: 0 0 var(--h2-form-label-gap) !important; min-height: 22px; }', + '.h2-station-page--create .h2-create-form--grid .ant-form-item-label > label { display: inline-flex; align-items: center; min-height: 22px; line-height: 22px; font-size: 13px; font-weight: 500; color: #475569; height: auto; }', '.h2-station-page--create .h2-create-form--grid .ant-row { width: 100% !important; margin-left: 0 !important; margin-right: 0 !important; }', '.h2-station-page--create .h2-create-form--grid .ant-col { min-width: 0; }', '.h2-station-page--create .h2-create-form--grid .ant-form-item { margin-bottom: 0; width: 100%; }', @@ -210,6 +568,42 @@ var H2_PAGE_STYLE = [ '.h2-station-page--create .h2-create-contract-fields { display: grid; grid-template-columns: 1fr; gap: 16px; margin-top: 14px; }', '@media (min-width: 640px) { .h2-station-page--create .h2-create-contract-fields { grid-template-columns: 1fr 1fr; } }', '.h2-station-page--create .h2-supplier-link-panel { padding: 16px 18px; border-radius: 12px; background: linear-gradient(135deg, #eff6ff 0%, #f8fafc 100%); border: 1px solid #bfdbfe; }', + '.h2-station-page--create .h2-account-bind-layout { display: grid; grid-template-columns: minmax(280px, 1fr) minmax(0, 1.65fr); gap: 24px; align-items: start; width: 100%; }', + '@media (max-width: 960px) { .h2-station-page--create .h2-account-bind-layout { grid-template-columns: 1fr; } }', + '.h2-station-page--create .h2-account-bind-side { display: flex; flex-direction: column; gap: 12px; min-width: 0; }', + '.h2-station-page--create .h2-account-bind-side .ant-form-item { margin-bottom: 0 !important; }', + '.h2-station-page--create .h2-account-bind-side__hint { margin: 0; font-size: 12px; color: #94a3b8; line-height: 1.55; }', + '.h2-station-page--create .h2-account-bind-selected-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }', + '.h2-station-page--create .h2-account-bind-selected-label { font-size: 13px; color: #64748b; }', + '.h2-station-page--create .h2-account-bind-selected-count.ant-tag { margin: 0 !important; border-radius: 6px !important; font-weight: 600 !important; line-height: 20px !important; padding: 0 8px !important; }', + '.h2-station-page--create .h2-account-bind-preview-area { min-width: 0; padding: 14px 16px; border-radius: 12px; background: #f8fafc; border: 1px solid #e8ecf0; min-height: 180px; box-sizing: border-box; }', + '.h2-station-page--create .h2-account-bind-preview-title { font-size: 14px; font-weight: 600; color: #334155; margin-bottom: 12px; }', + '.h2-station-page--create .h2-account-bind-preview-cards { display: flex; flex-wrap: wrap; gap: 12px; }', + '.h2-station-page--create .h2-account-bind-preview-empty { font-size: 13px; color: #94a3b8; line-height: 1.5; padding: 24px 8px; text-align: center; }', + '.h2-station-page--create .h2-account-bind-user-card { position: relative; display: flex; align-items: flex-start; gap: 12px; padding: 12px 34px 12px 12px; width: 220px; max-width: 100%; border: 1px solid #e2e8f0; border-radius: 10px; background: #fff; box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04); box-sizing: border-box; }', + '.h2-station-page--create .h2-account-bind-user-card__avatar { flex-shrink: 0; width: 40px; height: 40px; border-radius: 50%; background: linear-gradient(145deg, #10b981 0%, #059669 100%); color: #fff; display: inline-flex; align-items: center; justify-content: center; }', + '.h2-station-page--create .h2-account-bind-user-card__body { flex: 1; min-width: 0; }', + '.h2-station-page--create .h2-account-bind-user-card__name-row { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }', + '.h2-station-page--create .h2-account-bind-user-card__name { font-size: 14px; font-weight: 700; color: #0f172a; line-height: 1.3; }', + '.h2-station-page--create .h2-account-bind-user-card__role.ant-tag { margin: 0 !important; border-radius: 4px !important; font-size: 11px !important; font-weight: 600 !important; line-height: 18px !important; padding: 0 6px !important; }', + '.h2-station-page--create .h2-account-bind-user-card__meta { margin-top: 6px; font-size: 12px; color: #94a3b8; line-height: 1.5; }', + '.h2-station-page--create .h2-account-bind-user-card__meta + .h2-account-bind-user-card__meta { margin-top: 2px; }', + '.h2-station-page--create .h2-account-bind-user-card__remove { position: absolute; top: 8px; right: 8px; width: 20px; height: 20px; border: none; background: transparent; color: #94a3b8; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; border-radius: 4px; padding: 0; font-size: 14px; line-height: 1; }', + '.h2-station-page--create .h2-account-bind-user-card__remove:hover { color: #64748b; background: #f1f5f9; }', + '.h2-station-page--create .h2-account-bind-user-card__remove:focus-visible { outline: 2px solid #10b981; outline-offset: 1px; }', + '.h2-station-page--create .h2-account-bind-hint { margin: 0; font-size: 12px; color: #64748b; line-height: 1.55; }', + '.h2-station-page--create .h2-create-readonly-input.ant-input[disabled] { background: #f8fafc !important; color: #64748b !important; cursor: default !important; border: 1px dashed #e2e8f0 !important; }', + '.h2-station-page--create .h2-station-type-field { display: flex; flex-direction: column; gap: 8px; width: 100%; }', + '.h2-station-page--create .h2-supplier-tabs { width: 100%; }', + '.h2-station-page--create .h2-supplier-tabs__nav { display: flex; align-items: flex-end; gap: 32px; border-bottom: 1px solid #e2e8f0; margin-bottom: var(--h2-form-row-gap); }', + '.h2-station-page--create .h2-supplier-tabs__item { position: relative; border: none; background: transparent; padding: 0 0 12px; font-size: 14px; font-weight: 500; color: #64748b; cursor: pointer; line-height: 1.4; }', + '.h2-station-page--create .h2-supplier-tabs__item:hover { color: #059669; }', + '.h2-station-page--create .h2-supplier-tabs__item--active { color: #059669; font-weight: 600; }', + '.h2-station-page--create .h2-supplier-tabs__item--active::after { content: ""; position: absolute; left: 0; right: 0; bottom: -1px; height: 2px; background: #10b981; border-radius: 1px 1px 0 0; }', + '.h2-station-page--create .h2-supplier-tabs__panel { width: 100%; }', + '.h2-station-page--create .h2-supplier-linked-preview { margin-top: var(--h2-form-row-gap); }', + '.h2-station-page--create .h2-supplier-none-hint { margin: 0; padding: 20px 16px; text-align: center; font-size: 13px; color: #94a3b8; line-height: 1.55; background: #f8fafc; border: 1px dashed #e2e8f0; border-radius: 10px; }', + '.h2-station-page--create .h2-create-upload--readonly.ant-upload-wrapper .ant-upload-drag { cursor: default !important; pointer-events: none; opacity: 0.92; }', '.h2-station-page--create .h2-supplier-new-stack { display: flex; flex-direction: column; gap: 16px; }', '.h2-station-page--create .h2-create-form--visual .ant-form-item-label > label { font-weight: 500; color: #334155; }', '.h2-station-page--create .h2-create-form--visual .h2-create-input.ant-input:not(.ant-input-disabled), .h2-station-page--create .h2-create-form--visual .ant-select:not(.ant-select-disabled) .ant-select-selector, .h2-station-page--create .h2-create-form--visual .ant-cascader:not(.ant-select-disabled) .ant-select-selector, .h2-station-page--create .h2-create-form--visual .h2-create-input.ant-picker:not(.ant-picker-disabled) { border-radius: 10px !important; height: 40px !important; min-height: 40px !important; border-color: #e2e8f0 !important; }', @@ -219,11 +613,10 @@ var H2_PAGE_STYLE = [ '.h2-station-page--create .h2-create-panel .ant-form-item + .ant-form-item, .h2-station-page--create .h2-create-panel .ant-form-item + .ant-row, .h2-station-page--create .h2-create-panel .ant-row + .ant-form-item, .h2-station-page--create .h2-create-panel .ant-form-item + .h2-address-paste, .h2-station-page--create .h2-create-panel .ant-row + .h2-address-paste { margin-top: 12px; }', '.h2-station-page--create .h2-create-form--visual .h2-create-radio-group { min-height: 40px; }', '.h2-station-page--create .h2-create-form--visual .ant-form-item-control-input { max-width: 100%; }', - '.h2-station-page--create .h2-create-topbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 16px; padding: 12px 16px; background: #fff; border-radius: 4px; border: 1px solid #e2e8f0; }', - '.h2-station-page--create .h2-create-back-btn { display: inline-flex !important; align-items: center; gap: 6px; height: 32px; padding: 0 12px !important; border-radius: 4px !important; font-weight: 500; color: #475569 !important; border: 1px solid #e2e8f0 !important; background: #fff !important; }', - '.h2-station-page--create .h2-create-back-btn:hover { color: #059669 !important; border-color: #10b981 !important; background: #f0fdf4 !important; }', - '.h2-station-page--create .h2-create-back-btn:focus-visible { outline: 2px solid #10b981; outline-offset: 2px; }', - '.h2-station-page--create .h2-create-topbar-actions { display: flex; align-items: center; gap: 10px; }', + '.h2-station-page--create .h2-create-back-only, .h2-station-page--edit .h2-create-back-only { display: flex; align-items: center; margin-bottom: 16px; }', + '.h2-station-page--create .h2-create-back-btn, .h2-station-page--edit .h2-create-back-btn { display: inline-flex !important; align-items: center; gap: 6px; height: 32px; padding: 0 12px !important; border-radius: 8px !important; font-weight: 500; color: #475569 !important; border: 1px solid #e2e8f0 !important; background: #fff !important; }', + '.h2-station-page--create .h2-create-back-btn:hover, .h2-station-page--edit .h2-create-back-btn:hover { color: #059669 !important; border-color: #10b981 !important; background: #f0fdf4 !important; }', + '.h2-station-page--create .h2-create-back-btn:focus-visible, .h2-station-page--edit .h2-create-back-btn:focus-visible { outline: 2px solid #10b981; outline-offset: 2px; }', '.h2-station-page .h2-create-card.ant-card { border-radius: 16px !important; border: 1px solid #e2e8f0 !important; box-shadow: 0 4px 24px -6px rgba(15, 23, 42, 0.06) !important; margin-bottom: 0 !important; transition: box-shadow 0.2s ease, border-color 0.2s ease, transform 0.2s ease; }', '.h2-station-page--create .h2-create-card.ant-card:hover { box-shadow: 0 4px 24px -6px rgba(15, 23, 42, 0.06) !important; transform: none; }', '.h2-station-page .h2-create-card > .ant-card-head { border-bottom: 1px solid #f1f5f9 !important; min-height: auto; padding: 16px 22px !important; background: linear-gradient(180deg, #fafbfc 0%, #fff 100%); }', @@ -308,10 +701,24 @@ var H2_PAGE_STYLE = [ '.h2-station-page--create .h2-create-form--list .ant-form-item-label > label { font-size: 13px; font-weight: 500; color: #475569; }', '.h2-station-page--create .h2-create-form--list .ant-radio-wrapper .ant-radio-checked .ant-radio-inner { border-color: #10b981; background-color: #10b981; }', '.h2-station-page--create .h2-create-pagehead { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px; margin-bottom: 16px; padding: 12px 20px; background: #fff; border-radius: 16px; border: 1px solid #e2e8f0; box-shadow: 0 4px 20px -4px rgba(15, 23, 42, 0.03); }', - '.h2-station-page--create .h2-create-card.ant-card { border-radius: 16px !important; }' -].join('\n'); + '.h2-station-page--create .h2-create-card.ant-card { border-radius: 16px !important; }', + '.h2-station-page .h2-card-title-bar--step::before { display: none !important; }', + '.h2-station-page .h2-card-step-badge { width: 28px; height: 28px; border-radius: 50%; background: linear-gradient(145deg, #10b981 0%, #059669 100%); color: #fff; font-size: 14px; font-weight: 700; display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; box-shadow: 0 2px 8px rgba(16, 185, 129, 0.32); }', + '.h2-station-page .h2-card-title-text { font-size: 15px; font-weight: 700; color: #0f172a; }', + '.h2-station-page--create .h2-create-form > .h2-create-card + .h2-create-card, .h2-station-page--edit .h2-create-form > .h2-create-card + .h2-create-card { margin-top: 16px; }', + '.h2-station-page--create .h2-create-footer-inner, .h2-station-page--edit .h2-create-footer-inner { justify-content: flex-end; }' +]).join('\n'); var H2_CURRENT_OPERATOR = '系统管理员'; +/** 原型登录角色:admin 平台管理员可改系统账号;station 加氢站账号不可改 */ +var H2_CURRENT_USER_ROLE = 'admin'; + +function h2IsAdminUser() { + if (typeof window !== 'undefined' && window.H2_STATION_PAGE_USER_ROLE) { + return String(window.H2_STATION_PAGE_USER_ROLE).toLowerCase() === 'admin'; + } + return H2_CURRENT_USER_ROLE === 'admin'; +} function h2OperateTimestamp() { return new Date().toISOString().slice(0, 16).replace('T', ' '); @@ -327,6 +734,110 @@ function h2CreateBusinessStatusLog(beforeStatus, afterStatus, operator) { }; } +function h2CreateCostPriceLog(beforePrice, afterPrice, effectiveTime, operator) { + return { + id: 'cpl-' + Date.now() + '-' + Math.floor(Math.random() * 1000), + operator: operator || H2_CURRENT_OPERATOR, + operateTime: h2OperateTimestamp(), + beforeCostPrice: beforePrice, + afterCostPrice: afterPrice, + effectiveTime: effectiveTime + }; +} + +function h2ParseDateTimeMs(val) { + if (!val) return NaN; + var s = String(val).trim(); + if (!s) return NaN; + return new Date(s.indexOf('T') >= 0 ? s : s.replace(' ', 'T')).getTime(); +} + +function h2ToDateTimeDayjs(text) { + var dayjs = window.dayjs; + if (!dayjs || !text) return null; + var s = String(text).trim(); + if (!s) return null; + var d = dayjs(s.indexOf('T') >= 0 ? s : s.replace(' ', 'T')); + if (!(d.isValid && d.isValid())) d = dayjs(s); + return (d.isValid && d.isValid()) ? d : null; +} + +function h2ToDateDayjs(text) { + var dayjs = window.dayjs; + if (!dayjs || !text) return null; + var s = String(text).trim(); + if (!s) return null; + var d = dayjs(s); + return (d.isValid && d.isValid()) ? d : null; +} + +function h2SanitizeReceiptAmountInput(text) { + var s = String(text || '').replace(/[^\d.]/g, ''); + var parts = s.split('.'); + if (parts.length > 2) s = parts[0] + '.' + parts.slice(1).join(''); + var dot = s.indexOf('.'); + if (dot >= 0 && s.length - dot - 1 > 2) s = s.slice(0, dot + 3); + return s; +} + +function h2FormatReceiptAmountInput(text) { + var s = String(text || '').trim(); + if (!s) return ''; + var n = parseFloat(s); + if (isNaN(n) || n < 0) return ''; + return n.toFixed(2); +} + +function h2ResolveCurrentCostPrice(record) { + var logs = (record && record.costPriceLogs) || []; + var now = Date.now(); + var best = null; + var i; + for (i = 0; i < logs.length; i++) { + var log = logs[i]; + var t = h2ParseDateTimeMs(log.effectiveTime); + if (isNaN(t) || t > now) continue; + if (!best || t >= best.t) { + best = { t: t, price: log.afterCostPrice }; + } + } + if (best && best.price != null) return best.price; + return record && record.costUnitPrice != null ? record.costUnitPrice : null; +} + +function h2ApplyDueCostPriceToRecord(record) { + var resolved = h2ResolveCurrentCostPrice(record); + if (resolved == null) return record; + if (record.costUnitPrice === resolved) return record; + return Object.assign({}, record, { costUnitPrice: resolved }); +} + +function h2ApplyDueCostPricesToList(records) { + var changed = false; + var next = records.map(function (r) { + var updated = h2ApplyDueCostPriceToRecord(r); + if (updated !== r) changed = true; + return updated; + }); + return { records: next, changed: changed }; +} + +function h2FormatCostPriceDisplay(v) { + if (v == null || v === '') return '—'; + return h2FormatYuanNum(v) + ' 元/kg'; +} + +function h2CountPendingCostPriceLogs(logs) { + var now = Date.now(); + var count = 0; + var i; + for (i = 0; i < (logs || []).length; i++) { + var t = h2ParseDateTimeMs(logs[i].effectiveTime); + if (!isNaN(t) && t > now) count += 1; + } + return count; +} + var H2_IMPORT_TEMPLATE_HEADERS = [ '加氢站名称', '省', '市', '详细地址', '是否签约', '签约开始时间', '签约结束时间', '营业状态', '营业时间', '联系人', '联系电话' ]; @@ -519,6 +1030,7 @@ var H2_ICONS = { none: h2SvgIcon([{ d: 'M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z' }, { tag: 'line', x1: 12, y1: 9, x2: 12, y2: 13 }, { tag: 'line', x1: 12, y1: 17, x2: 12.01, y2: 17 }], 16), doc: h2SvgIcon([{ d: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z' }, { d: 'M14 2v6h6' }, { tag: 'line', x1: 16, y1: 13, x2: 8, y2: 13 }, { tag: 'line', x1: 16, y1: 17, x2: 8, y2: 17 }], 14), upload: h2SvgIcon([{ d: 'M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4' }, { d: 'M17 8l-5-5-5 5' }, { tag: 'line', x1: 12, y1: 3, x2: 12, y2: 15 }], 14), + download: h2SvgIcon([{ d: 'M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4' }, { d: 'M7 10l5 5 5-5' }, { tag: 'line', x1: 12, y1: 15, x2: 12, y2: 3 }], 14), empty: h2SvgIcon([{ tag: 'circle', cx: 12, cy: 12, r: 10 }, { tag: 'line', x1: 8, y1: 12, x2: 16, y2: 12 }], 40), back: h2SvgIcon([{ tag: 'line', x1: 19, y1: 12, x2: 5, y2: 12 }, { d: 'M12 19l-7-7 7-7' }], 16), mapPin: h2SvgIcon([{ tag: 'path', d: 'M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z' }, { tag: 'circle', cx: 12, cy: 10, r: 3 }], 16), @@ -551,11 +1063,109 @@ function h2DeriveFrequencyByRefuelCount(refuelCount) { return 'low'; } +function h2ResolveBalanceAlertThreshold(record) { + if (!record || record.balanceAlertThreshold == null || record.balanceAlertThreshold === '') return null; + var n = parseFloat(record.balanceAlertThreshold); + return isNaN(n) || n <= 0 ? null : n; +} + +function h2IsArrearsStation(record) { + return h2NumOrZero(record && record.prepaidBalance) < 0; +} + +function h2IsBalanceAlertStation(record) { + if (h2IsArrearsStation(record)) return false; + var threshold = h2ResolveBalanceAlertThreshold(record); + if (threshold == null) return false; + return h2NumOrZero(record.prepaidBalance) < threshold; +} + +function h2CreateRechargeLineId() { + return 'rcl-' + Date.now() + '-' + Math.floor(Math.random() * 10000); +} + +function h2BuildRechargeTransferPurpose(date) { + var d = date || new Date(); + var y = d.getFullYear(); + var m = String(d.getMonth() + 1).padStart(2, '0'); + var day = String(d.getDate()).padStart(2, '0'); + return '羚牛预付氢费款' + y + '-' + m + '-' + day; +} + +function h2CreateEmptyRechargeLine() { + return { + id: h2CreateRechargeLineId(), + stationId: undefined, + stationName: '', + currentBalance: '', + payAmount: '', + companyName: '', + bankAccount: '', + bankName: '', + transferPurpose: h2BuildRechargeTransferPurpose() + }; +} + +function h2BuildRechargeLineFromStation(record, payAmount) { + var supplier = h2ResolveStationSupplier(record); + return { + id: h2CreateRechargeLineId(), + stationId: record.id, + stationName: record.name || '', + currentBalance: h2FormatYuanNum(record.prepaidBalance), + payAmount: payAmount != null && payAmount !== '' ? payAmount : '', + companyName: supplier ? (supplier.name || '') : '', + bankAccount: supplier ? (supplier.bankAccount || '') : '', + bankName: supplier ? (supplier.bankName || '') : '', + transferPurpose: h2BuildRechargeTransferPurpose() + }; +} + +function h2EnrichStationListRows(records) { + return (records || []).map(function (r) { + var stats = h2CalcRefuelStats(r.name); + return Object.assign({}, r, { + refuelCount: stats.count, + refuelTotalKg: stats.totalKg, + refuelFreqKey: h2DeriveFrequencyByRefuelCount(stats.count) + }); + }); +} + +function h2RenderRechargeAutoField(value, hasStation, placeholder) { + if (!hasStation) { + return React.createElement('span', { className: 'h2-recharge-readonly h2-recharge-readonly--muted' }, '—'); + } + var text = value != null && value !== '' ? String(value) : ''; + if (!text) { + return React.createElement('span', { className: 'h2-recharge-readonly h2-recharge-readonly--muted' }, placeholder || '—'); + } + var InputComp = window.antd && window.antd.Input; + if (!InputComp) { + return React.createElement('span', { className: 'h2-recharge-readonly' }, text); + } + return React.createElement(InputComp, { + className: 'h2-recharge-readonly-input', + value: text, + disabled: true, + style: { width: '100%', borderRadius: 8 } + }); +} + function h2FormatRegion(region) { if (!region || !region.length) return '—'; return region.join('-'); } +function h2FormatStationAddressLine(record) { + var regionText = h2FormatRegion(record && record.region); + var detail = String((record && record.addressDetail) || '').trim(); + if (regionText === '—' && !detail) return '—'; + if (regionText === '—') return detail; + if (!detail) return regionText; + return regionText + ' ' + detail; +} + function h2MatchRegionFilter(recordRegion, filterRegion) { if (!filterRegion || !filterRegion.length) return true; var rr = recordRegion || []; @@ -568,6 +1178,31 @@ function h2EmptyListFilters() { return { name: '', signed: undefined, region: undefined, businessStatus: undefined }; } +/** 签约站点是否已上传合同附件 */ +function h2HasUploadedContract(record) { + var files = record && record.contractFiles; + if (!files || !files.length) return false; + var i; + for (i = 0; i < files.length; i++) { + var f = files[i]; + if (!f) continue; + if ((f.name || '').trim()) return true; + if (f.url || (f.response && (f.response.url || f.response.fileUrl))) return true; + } + return false; +} + +/** 列表签约站点标签文案与样式 */ +function h2RenderStationSignedTag(record) { + var Tag = window.antd && window.antd.Tag; + if (!Tag || !record || !record.isSigned) return null; + var uploaded = h2HasUploadedContract(record); + return React.createElement(Tag, { + color: uploaded ? 'success' : 'warning', + className: 'lc-station-signed-tag' + }, uploaded ? '签约站点' : '签约站点-未上传合同'); +} + /** 距签约结束日天数:正=剩余,负=已过期 */ function h2DaysUntilContractEnd(dateStr) { if (!dateStr) return null; @@ -629,12 +1264,21 @@ function h2ParseAddressText(rawText) { var matchedProvince = null; var provinceStart = -1; + var matchedProvinceLen = 0; for (var i = 0; i < provinces.length; i++) { var pName = provinces[i].value || provinces[i].label; - var idx = compact.indexOf(pName); - if (idx !== -1 && (provinceStart === -1 || idx < provinceStart)) { - provinceStart = idx; - matchedProvince = provinces[i]; + var aliases = [pName]; + if (pName.slice(-1) === '省') aliases.push(pName.slice(0, -1)); + if (pName.slice(-1) === '市') aliases.push(pName.replace(/市$/, '')); + var ai; + for (ai = 0; ai < aliases.length; ai++) { + var alias = aliases[ai]; + var idx = compact.indexOf(alias); + if (idx !== -1 && (provinceStart === -1 || idx < provinceStart || (idx === provinceStart && alias.length > matchedProvinceLen))) { + provinceStart = idx; + matchedProvince = provinces[i]; + matchedProvinceLen = alias.length; + } } } @@ -643,7 +1287,7 @@ function h2ParseAddressText(rawText) { } var provinceName = matchedProvince.value || matchedProvince.label; - var rest = compact.slice(provinceStart + provinceName.length).replace(/^[\s\-—,,、·]+/, ''); + var rest = compact.slice(provinceStart + matchedProvinceLen).replace(/^[\s\-—,,、·]+/, ''); var cities = matchedProvince.children || []; var cityMatch = h2FindCityInAddressRest(rest, cities); @@ -660,12 +1304,23 @@ function h2ParseAddressText(rawText) { } var cityName = cityMatch.city.value || cityMatch.city.label; - var detail = (cityMatch.prefix || '') + rest.slice(cityMatch.end); - detail = detail.replace(/^[\s\-—,,、·]+/, ''); + var detail = rest.slice(cityMatch.end); + detail = detail.replace(/^[\s\-—,,、·省市区县]+/, ''); + if (!detail && cityMatch.prefix) detail = cityMatch.prefix.replace(/^[\s\-—,,、·省市区县]+/, ''); return { region: [provinceName, cityName], detail: detail, matched: true }; } +function h2StripRegionPrefixFromDetail(region, detail) { + var text = String(detail || '').trim(); + if (!text || !region || region.length < 2) return text; + var cityName = region[1] || ''; + var cityPlain = cityName.replace(/市$/, ''); + if (cityName && text.indexOf(cityName) === 0) text = text.slice(cityName.length); + else if (cityPlain && text.indexOf(cityPlain) === 0) text = text.slice(cityPlain.length); + return text.replace(/^[\s\-—,,、·省市区县]+/, ''); +} + function h2FindCityInAddressRest(rest, cities) { if (!rest || !cities || !cities.length) return null; var sorted = cities.slice().sort(function (a, b) { @@ -684,11 +1339,10 @@ function h2FindCityInAddressRest(rest, cities) { return null; } -/** 快速粘贴地址:识别后回填省/市与详细地址 */ +/** 地址解析:失焦后自动识别并回填省/市与详细地址 */ function AddressPasteInput(props) { var antd = window.antd; var Input = antd.Input; - var Button = antd.Button; var message = antd.message; var _paste = React.useState(''); var pasteText = _paste[0]; @@ -696,12 +1350,9 @@ function AddressPasteInput(props) { var disabled = props.disabled; var inputClassName = props.inputClassName || ''; - var handleParse = function () { - var raw = (pasteText || '').trim(); - if (!raw) { - message.warning('请先粘贴完整地址'); - return; - } + var runParse = function (raw) { + raw = (raw || '').trim(); + if (!raw) return; var result = h2ParseAddressText(raw); if (!result.matched) { message.warning('未能识别省/市,请将内容填入详细地址'); @@ -709,12 +1360,11 @@ function AddressPasteInput(props) { return; } if (result.partial || !result.region || result.region.length < 2) { - message.warning('已识别省份,请手动补选城市'); - } else { - message.success('地址识别成功'); + message.warning('已识别省份,请补全城市信息'); } - if (props.onParsed) props.onParsed({ region: result.region || [], detail: result.detail || '' }); - setPasteText(''); + var region = result.region || []; + var detail = h2StripRegionPrefixFromDetail(region, result.detail || ''); + if (props.onParsed) props.onParsed({ region: region, detail: detail }); }; return React.createElement('div', { className: 'h2-address-paste' }, @@ -722,34 +1372,184 @@ function AddressPasteInput(props) { className: inputClassName, value: pasteText, disabled: disabled, - placeholder: props.placeholder || '粘贴完整地址,如:浙江省嘉兴市南湖区科技大道88号', + placeholder: props.placeholder || '填写完整地址自动解析省市和详细地址', autoSize: { minRows: 2, maxRows: 4 }, - onChange: function (e) { setPasteText(e.target.value); } - }), - React.createElement('div', { className: 'h2-address-paste-actions' }, - React.createElement('span', { className: 'h2-address-paste-hint' }, '自动拆分省/市与详细地址'), - React.createElement(Button, { - type: 'link', - size: 'small', - disabled: disabled || !pasteText.trim(), - onClick: handleParse - }, '识别地址') - ) + onChange: function (e) { setPasteText(e.target.value); }, + onBlur: function () { + if (disabled) return; + runParse(pasteText); + } + }) ); } +var H2_BUSINESS_LICENSE_OCR_MOCKS = [ + { + test: function (name) { return /嘉兴|南湖|氢能供应/i.test(name || ''); }, + name: '嘉兴氢能供应有限公司', + taxId: '91330400MA2ABCDEF1', + communicationAddress: '浙江省嘉兴市南湖区科技大道66号' + }, + { + test: function (name) { return /临平|杭州/i.test(name || ''); }, + name: '杭州临平氢能运营有限公司', + taxId: '91330110MA3LINPING1', + communicationAddress: '浙江省杭州市临平区东湖街道能源路16号' + }, + { + test: function (name) { return /羚牛|宝山|上海/i.test(name || ''); }, + name: '上海羚牛氢能科技有限公司', + taxId: '91310100MA1XYZ7890', + communicationAddress: '上海市宝山区富联路200号' + }, + { + test: function (name) { return /苏州|星湖/i.test(name || ''); }, + name: '苏州工业园区氢源科技有限公司', + taxId: '91320500MA1ABCDEF2', + communicationAddress: '江苏省苏州市工业园区星湖街328号' + }, + { + test: function () { return true; }, + name: '浙江羚牛氢能科技有限公司', + taxId: '91330481MA2JXXXXXX', + communicationAddress: '浙江省嘉兴市平湖市乍浦镇杭州湾大道1号' + } +]; + +function h2MatchBusinessLicenseOcrMock(file) { + var fileName = (file && file.name) || ''; + var i; + for (i = 0; i < H2_BUSINESS_LICENSE_OCR_MOCKS.length; i++) { + if (H2_BUSINESS_LICENSE_OCR_MOCKS[i].test(fileName)) { + return H2_BUSINESS_LICENSE_OCR_MOCKS[i]; + } + } + return H2_BUSINESS_LICENSE_OCR_MOCKS[H2_BUSINESS_LICENSE_OCR_MOCKS.length - 1]; +} + +function h2ApplyBusinessLicenseOcr(ctx, ocrData) { + if (!ocrData || !ctx || !ctx.updateSupplier) return; + var commAddr = String(ocrData.communicationAddress || '').trim(); + var parsed = h2ParseAddressText(commAddr); + var region = parsed.region || []; + var detail = commAddr + ? h2StripRegionPrefixFromDetail(region, parsed.detail || commAddr) + : ''; + var supplierPatch = { + name: (ocrData.name || '').trim(), + taxId: (ocrData.taxId || '').trim(), + address: detail, + mailingAddress: commAddr || detail, + businessAddress: commAddr || detail + }; + if (region.length >= 2) { + supplierPatch.city = region.slice(); + supplierPatch.region = h2GetRegionByProvince(region[0]); + } + ctx.updateSupplier(supplierPatch); + if (ctx.updateStation && ctx.supplierMode === 'new' && region.length >= 2) { + ctx.updateStation({ + address: { region: region.slice(), detail: detail } + }); + } +} + +function h2TriggerBusinessLicenseOcr(file, ctx) { + var antd = window.antd; + var message = antd && antd.message; + var hide = message && message.loading('营业执照识别中…', 0); + window.setTimeout(function () { + if (hide) hide(); + var ocrData = h2MatchBusinessLicenseOcrMock(file); + h2ApplyBusinessLicenseOcr(ctx, ocrData); + if (message) { + message.success('识别完成:已填写供应商名称、纳税人识别号与通讯地址(未写入注册地址)'); + } + }, 720); +} + +function h2KpiCardIcon(key) { + var size = 22; + if (key === 'all') { + return React.createElement('svg', { + width: size, + height: size, + viewBox: '0 0 24 24', + fill: 'none', + stroke: 'currentColor', + strokeWidth: 1.75, + strokeLinecap: 'round', + strokeLinejoin: 'round', + 'aria-hidden': true + }, + React.createElement('path', { d: 'M4 20h16' }), + React.createElement('path', { d: 'M7 20v-9h4v9' }), + React.createElement('path', { d: 'M14 20V8l5-3v15' }), + React.createElement('text', { + x: 8.5, + y: 13.5, + fill: 'currentColor', + stroke: 'none', + fontSize: 5.5, + fontWeight: 700, + fontFamily: 'system-ui, -apple-system, sans-serif' + }, 'H₂') + ); + } + if (key === 'yes') { + return h2SvgIcon([ + { d: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z' }, + { d: 'M14 2v6h6' }, + { tag: 'circle', cx: 17.5, cy: 17.5, r: 3.5 }, + { d: 'M16 17.5l1 1 2-2' } + ], size); + } + if (key === 'no') { + return h2SvgIcon([ + { d: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z' }, + { d: 'M14 2v6h6' }, + { tag: 'circle', cx: 17.5, cy: 17.5, r: 3.5 }, + { tag: 'line', x1: 16, y1: 16, x2: 19, y2: 19 }, + { tag: 'line', x1: 19, y1: 16, x2: 16, y2: 19 } + ], size); + } + if (key === 'balanceAlert') { + return h2SvgIcon([ + { d: 'M12 2v4' }, + { d: 'M12 18v4' }, + { d: 'M4.93 4.93l2.83 2.83' }, + { d: 'M16.24 16.24l2.83 2.83' }, + { d: 'M2 12h4' }, + { d: 'M18 12h4' }, + { d: 'M4.93 19.07l2.83-2.83' }, + { d: 'M16.24 7.76l2.83-2.83' }, + { tag: 'circle', cx: 12, cy: 12, r: 4 } + ], size); + } + if (key === 'arrears') { + return h2SvgIcon([ + { d: 'M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z' }, + { tag: 'line', x1: 12, y1: 9, x2: 12, y2: 13 }, + { tag: 'line', x1: 12, y1: 17, x2: 12.01, y2: 17 } + ], size); + } + if (key === 'none') { + return h2SvgIcon([ + { d: 'M6 20h12' }, + { d: 'M9 20v-6h2c1.2 0 2.2 1 2.2 2.2S12.2 18.4 11 18.4H9' }, + { tag: 'circle', cx: 12, cy: 11, r: 7.5 }, + { tag: 'line', x1: 6.8, y1: 6.8, x2: 17.2, y2: 17.2 } + ], size); + } + return h2KpiCardIcon('all'); +} + function h2KpiIcon(key) { - if (key === 'high') return H2_ICONS.high; - if (key === 'low') return H2_ICONS.low; - if (key === 'none') return H2_ICONS.none; - return H2_ICONS.station; + return h2KpiCardIcon(key); } function h2SignedFilterIcon(key) { - if (key === 'yes') { - return h2SvgIcon([{ d: 'M22 11.08V12a10 10 0 1 1-5.93-9.14' }, { d: 'M22 4 12 14.01l-3-3' }], 16); - } - return h2SvgIcon([{ tag: 'circle', cx: 12, cy: 12, r: 10 }, { tag: 'line', x1: 15, y1: 9, x2: 9, y2: 15 }, { tag: 'line', x1: 9, y1: 9, x2: 15, y2: 15 }], 16); + return h2KpiCardIcon(key === 'yes' ? 'yes' : 'no'); } /** 地址组件:省/市级联 + 详细地址(无子标题,挂在「地址」表单项下) */ @@ -849,6 +1649,56 @@ function h2DownloadUploadFile(file) { document.body.removeChild(a); } +function h2IsPdfFileName(name) { + return /\.pdf$/i.test(String(name || '')); +} + +function h2EscapeSvgText(text) { + return String(text || '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); +} + +function h2BuildPrototypeDocPreviewSvg(title, variant) { + var accent = variant === 'license' ? '#10b981' : '#3b82f6'; + var heading = variant === 'license' ? '证照预览' : '附件预览'; + var label = h2EscapeSvgText(String(title || '附件').slice(0, 28)); + var svg = '' + + '' + + '' + + '' + + '' + + '' + heading + '' + + '' + label + '' + + ''; + return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg); +} + +function h2ResolveLicenseDisplayUrl(file) { + var url = h2GetUploadFileUrl(file); + if (url) return url; + return h2BuildPrototypeDocPreviewSvg(file && file.name, 'license'); +} + +function h2ResolveUploadPreview(file, variant) { + var name = (file && file.name) || '附件'; + var url = h2GetUploadFileUrl(file); + var isPdf = h2IsPdfFileName(name); + if (url && isPdf) { + return { url: url, name: name, type: 'pdf' }; + } + if (url) { + return { url: url, name: name, type: 'image' }; + } + return { + url: h2BuildPrototypeDocPreviewSvg(name, variant || 'doc'), + name: name, + type: 'image' + }; +} + /** 合同附件:按钮上传 + 预览/下载 */ function ContractFilesUpload(props) { var antd = window.antd; @@ -856,11 +1706,14 @@ function ContractFilesUpload(props) { var Button = antd.Button; var fileList = props.fileList || []; var disabled = props.disabled; + var showHint = props.showHint !== false; + var wrapClass = 'h2-contract-upload' + (props.wrapClassName ? ' ' + props.wrapClassName : ''); + var uploadClass = props.uploadClassName || 'h2-create-upload-btn'; - return React.createElement('div', { className: 'h2-contract-upload' }, + return React.createElement('div', { className: wrapClass }, React.createElement('div', { className: 'h2-contract-upload-actions' }, React.createElement(Upload, { - className: 'h2-create-upload-btn', + className: uploadClass, multiple: true, disabled: disabled, fileList: fileList, @@ -869,12 +1722,14 @@ function ContractFilesUpload(props) { showUploadList: false }, React.createElement(Button, { - type: 'default', + type: props.buttonType || 'default', icon: H2_ICONS.upload, - disabled: disabled - }, '上传附件') + disabled: disabled, + className: props.buttonClassName, + style: props.buttonStyle + }, props.uploadLabel || '上传附件') ), - React.createElement('span', { style: { fontSize: 12, color: '#94a3b8' } }, '点击上传,可多选') + showHint ? React.createElement('span', { style: { fontSize: 12, color: '#94a3b8' } }, '点击上传,可多选') : null ), fileList.length ? React.createElement('div', { className: 'h2-contract-file-list' }, @@ -922,24 +1777,136 @@ function h2StationContactPhone(station) { return (station.mobilePhone || '').trim() || (station.landlinePhone || '').trim(); } -function h2ApplyStationContactPhone(station, text) { +function h2FilterContactPhoneInput(raw) { + var v = String(raw || '').replace(/[^\d-]/g, ''); + var digits = v.replace(/-/g, ''); + if (!digits) return ''; + if (digits.charAt(0) === '1') return digits.slice(0, 11); + var parts = v.split('-'); + if (parts.length > 2) v = parts[0] + '-' + parts.slice(1).join(''); + return v.slice(0, 16); +} + +function h2IsMobilePhone(text) { + var digits = String(text || '').replace(/[\s-]/g, ''); + return /^1[3-9]\d{9}$/.test(digits); +} + +function h2IsLandlinePhone(text) { + var raw = String(text || '').trim(); + if (!raw) return false; + var compact = raw.replace(/[\s-]/g, ''); + if (/^0\d{2,3}\d{7,8}$/.test(compact)) return true; + if (/^0\d{2,3}-\d{7,8}$/.test(raw)) return true; + if (/^\d{7,8}$/.test(compact)) return true; + return false; +} + +function h2IsValidContactPhone(text) { var v = String(text || '').trim(); - if (/^1\d{10}$/.test(v.replace(/\s/g, ''))) { - return Object.assign({}, station, { mobilePhone: v.replace(/\s/g, ''), landlinePhone: '' }); + if (!v) return false; + return h2IsMobilePhone(v) || h2IsLandlinePhone(v); +} + +function h2GetContactPhoneInputTip(text) { + var v = String(text || '').trim(); + if (!v) return ''; + if (h2IsValidContactPhone(v)) return ''; + var digits = v.replace(/[^\d]/g, ''); + if (digits.charAt(0) === '1') { + if (digits.length < 11) return '手机号码应为11位'; + return '手机号码格式不正确'; + } + if (digits.charAt(0) === '0') { + return '固定电话格式不正确,示例:0573-12345678'; + } + return '固定电话应为7-8位本地号码,或带区号如 0573-12345678'; +} + +function h2FilterMobilePhoneInput(raw) { + return String(raw || '').replace(/\D/g, '').slice(0, 11); +} + +function h2FilterLandlinePhoneInput(raw) { + var v = String(raw || '').replace(/[^\d-]/g, ''); + var parts = v.split('-'); + if (parts.length > 2) v = parts[0] + '-' + parts.slice(1).join(''); + return v.slice(0, 16); +} + +function h2GetMobilePhoneInputTip(text) { + var v = String(text || '').trim(); + if (!v) return ''; + if (h2IsMobilePhone(v)) return ''; + var digits = v.replace(/\D/g, ''); + if (digits.length < 11) return '手机号码应为11位'; + return '手机号码格式不正确'; +} + +function h2GetLandlinePhoneInputTip(text) { + var v = String(text || '').trim(); + if (!v) return ''; + if (h2IsLandlinePhone(v)) return ''; + return '固定电话格式不正确,示例:0573-12345678'; +} + +function h2ValidateStationPhones(station) { + var mobile = (station.mobilePhone || '').trim(); + var landline = (station.landlinePhone || '').trim(); + if (!mobile && !landline) return '请至少填写手机号或固定电话'; + if (mobile && !h2IsMobilePhone(mobile)) return h2GetMobilePhoneInputTip(mobile) || '手机号码格式不正确'; + if (landline && !h2IsLandlinePhone(landline)) return h2GetLandlinePhoneInputTip(landline) || '固定电话格式不正确'; + return ''; +} + +function h2ApplyStationContactPhone(station, text) { + var v = h2FilterContactPhoneInput(text); + var digits = v.replace(/-/g, ''); + if (/^1/.test(digits)) { + return Object.assign({}, station, { mobilePhone: v, landlinePhone: '' }); } return Object.assign({}, station, { mobilePhone: '', landlinePhone: v }); } var H2_BUSINESS_HOURS_MODE_OPTIONS = [ - { label: '全天营业', value: 'allDay' }, - { label: '非全天营业', value: 'custom' } + { label: '全天营业', value: 'allDay', tone: 'allday' }, + { label: '非全天营业', value: 'custom', tone: 'custom' } ]; +var H2_BUSINESS_STATUS_BTN_OPTIONS = [ + { label: '营业中', value: '营业中', tone: 'open' }, + { label: '暂停营业', value: '暂停营业', tone: 'pause' }, + { label: '停止营业', value: '停止营业', tone: 'stop' } +]; + +function h2RenderOptionButtonGroup(options, value, onChange, opts) { + opts = opts || {}; + var groupClass = 'h2-option-btn-group' + (opts.className ? ' ' + opts.className : ''); + return React.createElement('div', { + className: groupClass, + role: 'group', + 'aria-label': opts.ariaLabel || '' + }, + (options || []).map(function (opt) { + var active = value === opt.value; + var btnClass = 'h2-option-btn' + (active ? ' h2-option-btn--active' : '') + (opt.tone ? ' h2-option-btn--' + opt.tone : ''); + return React.createElement('button', { + type: 'button', + key: String(opt.value), + className: btnClass, + 'aria-pressed': active, + disabled: !!opts.disabled, + onClick: function () { if (onChange) onChange(opt.value); } + }, opt.label); + }) + ); +} + function h2IsAllDayBusinessHours(str) { var s = String(str || '').trim(); if (!s || s === '—') return false; if (s === '全天营业' || s === '24小时' || s === '24 小时') return true; - return /^00:00\s*[-–]\s*24:00$/.test(s); + return /^0?0:00\s*[-–]\s*24:00$/.test(s); } function h2ParseBusinessHours(str) { @@ -972,7 +1939,7 @@ function h2FormatBusinessHours(val) { function h2DisplayBusinessHours(str) { var s = String(str || '').trim(); if (!s || s === '—') return '—'; - if (h2IsAllDayBusinessHours(s) || s === '全天营业') return '全天营业'; + if (h2IsAllDayBusinessHours(s) || s === '全天营业') return '00:00-24:00'; var parsed = h2ParseBusinessHours(s); if (parsed.mode === 'custom' && parsed.start && parsed.end) return parsed.start + '-' + parsed.end; return s; @@ -1004,10 +1971,9 @@ function h2DayjsToTimeText(d) { return dayjs(d).format('HH:mm'); } -/** 营业时间:全天 / 非全天 + 起止时间(时:分,可输入) */ +/** 营业时间:全天 / 非全天按钮组 + 起止时间(时:分,可输入) */ function BusinessHoursInput(props) { var antd = window.antd; - var Select = antd.Select; var TimePicker = antd.TimePicker; var Input = antd.Input; var parsed = h2ParseBusinessHours(props.value); @@ -1061,27 +2027,21 @@ function BusinessHoursInput(props) { }; return React.createElement('div', { className: rootClass }, - React.createElement(Select, { - className: inputClassName, - value: parsed.mode, - options: H2_BUSINESS_HOURS_MODE_OPTIONS, - style: fieldStyle, - disabled: disabled, - placeholder: '请选择营业时间', - onChange: function (mode) { - if (mode === 'allDay') emit({ mode: 'allDay', start: parsed.start, end: parsed.end }); - else emit({ - mode: 'custom', - start: parsed.start || '08:00', - end: parsed.end || '22:00' - }); - }, - dropdownStyle: { borderRadius: 8 } - }), + h2RenderOptionButtonGroup(H2_BUSINESS_HOURS_MODE_OPTIONS, parsed.mode, function (mode) { + if (mode === 'allDay') emit({ mode: 'allDay', start: parsed.start, end: parsed.end }); + else emit({ + mode: 'custom', + start: parsed.start || '08:00', + end: parsed.end || '22:00' + }); + }, { ariaLabel: '营业时间类型', disabled: disabled }), parsed.mode === 'custom' - ? React.createElement('div', { className: 'h2-business-hours-range' }, - renderTimeField('开始时间', 'start', '请选择开始时间'), - renderTimeField('结束时间', 'end', '请选择结束时间') + ? React.createElement('div', { className: 'h2-business-hours-custom-panel' }, + React.createElement('div', { className: 'h2-business-hours-custom-panel__title' }, '营业时段'), + React.createElement('div', { className: 'h2-business-hours-range' }, + renderTimeField('开始时间', 'start', '请选择开始时间'), + renderTimeField('结束时间', 'end', '请选择结束时间') + ) ) : null ); @@ -1160,10 +2120,27 @@ var H2_MOCK_STATIONS = [ businessHours: '08:00-22:00', costUnitPrice: 42.5, customerUnitPrice: 45, + costPriceLogs: [ + { id: 'cpl-1-1', operator: '王静', operateTime: '2026-01-10 09:00', beforeCostPrice: null, afterCostPrice: 41.0, effectiveTime: '2026-01-15 00:00' }, + { id: 'cpl-1-2', operator: '系统管理员', operateTime: '2026-05-01 14:20', beforeCostPrice: 41.0, afterCostPrice: 42.5, effectiveTime: '2026-05-05 00:00' } + ], prepaidBalance: 85620.0, + balanceAlertThreshold: 50000, contact: '张三', phone: '13800138001', businessStatusLogs: [], + supplierMode: 'link', + linkedSupplierId: 'sup-h2-1', + bindAccountMode: 'bind', + boundAccountId: 'acc-h2-1', + boundAccount: { + id: 'acc-h2-1', + username: 'jx_h2_admin', + displayName: '嘉兴站管理员', + role: '加氢站管理员', + mobile: '13800138001', + status: '启用' + }, updateTime: '2026-05-20 14:30' }, { @@ -1178,9 +2155,38 @@ var H2_MOCK_STATIONS = [ contractFiles: [{ uid: 'c2', name: '临平加氢站合作协议.pdf' }, { uid: 'c3', name: '临平加氢站补充协议.pdf' }], businessStatus: '营业中', businessHours: '00:00-24:00', + costUnitPrice: 43.0, + customerUnitPrice: 46, + costPriceLogs: [ + { id: 'cpl-2-1', operator: '李四', operateTime: '2026-05-20 11:00', beforeCostPrice: 42.0, afterCostPrice: 43.0, effectiveTime: '2026-05-25 00:00' } + ], prepaidBalance: 125000.5, contact: '李四', phone: '13900139002', + supplierMode: 'new', + supplier: { + signingCompany: '浙江羚牛氢能科技有限公司', + name: '杭州临平氢能运营有限公司', + type: '加氢站', + city: ['浙江省', '杭州市'], + address: '临平区东湖街道能源路16号', + region: '华东', + contactName: '李四', + contactMobile: '13900139002', + contactTitle: '站长', + taxId: '91330110MA3LINPING1', + invoiceAddress: '浙江省杭州市临平区东湖街道能源路16号', + invoicePhone: '0571-88882222', + bankName: '中国农业银行杭州临平支行', + bankAccount: '6228480402631234567', + mailingAddress: '浙江省杭州市临平区东湖街道能源路16号', + businessAddress: '浙江省杭州市临平区东湖街道能源路16号', + businessLicenseFiles: [{ uid: 'bl-hz', name: '营业执照_临平氢能.pdf' }], + fillingLicenseFiles: [ + { uid: 'fl-hz-1', name: '加氢站经营许可证.pdf' }, + { uid: 'fl-hz-2', name: '压力容器检验报告.pdf' } + ] + }, updateTime: '2026-06-01 09:15' }, { @@ -1195,13 +2201,31 @@ var H2_MOCK_STATIONS = [ contractFiles: [{ uid: 'c4', name: '宝山加氢站合同(即将到期).pdf' }], businessStatus: '暂停营业', businessHours: '09:00-18:00', + costUnitPrice: 44.0, + customerUnitPrice: 47, + costPriceLogs: [ + { id: 'cpl-3-1', operator: '王五', operateTime: '2026-03-01 10:30', beforeCostPrice: 43.5, afterCostPrice: 44.0, effectiveTime: '2026-03-05 00:00' } + ], prepaidBalance: -3250.0, + balanceAlertThreshold: 10000, businessStatusLogs: [ { id: 'bsl-3-2', operateTime: '2026-04-01 10:00', operator: '王五', beforeStatus: '营业中', afterStatus: '暂停营业' }, { id: 'bsl-3-1', operateTime: '2026-03-15 09:30', operator: '系统管理员', beforeStatus: '暂停营业', afterStatus: '营业中' } ], contact: '王五', phone: '13700137003', + supplierMode: 'link', + linkedSupplierId: 'sup-h2-2', + bindAccountMode: 'bind', + boundAccountId: 'acc-h2-3', + boundAccount: { + id: 'acc-h2-3', + username: 'sh_bs_h2', + displayName: '宝山站管理员', + role: '加氢站管理员', + mobile: '13700137003', + status: '启用' + }, updateTime: '2026-04-12 16:40' }, { @@ -1234,24 +2258,81 @@ var H2_MOCK_STATIONS = [ businessStatus: '停止营业', businessHours: '10:00-17:00', prepaidBalance: 15800.0, + balanceAlertThreshold: 20000, contact: '钱七', phone: '13500135005', + supplierMode: 'link', + linkedSupplierId: 'sup-h2-3', updateTime: '2026-02-18 08:50' + }, + { + id: 6, + name: '桐乡合作加氢站', + region: ['浙江省', '嘉兴市'], + addressDetail: '桐乡市梧桐街道庆丰路66号', + fullAddress: '浙江省-嘉兴市桐乡市梧桐街道庆丰路66号', + isSigned: true, + contractStart: '2025-06-01', + contractEnd: '2027-05-31', + contractFiles: [], + businessStatus: '营业中', + businessHours: '08:00-20:00', + costUnitPrice: 41.8, + customerUnitPrice: 44.5, + costPriceLogs: [], + prepaidBalance: 42000.0, + contact: '周八', + phone: '13800138006', + updateTime: '2026-05-10 15:20' } ]; +/** 解析站点关联的供应商(内嵌 supplier 或 linkedSupplierId) */ +function h2ResolveStationSupplier(record) { + if (!record) return null; + if (record.supplier && ((record.supplier.name || '').trim() || (record.supplier.taxId || '').trim())) { + return Object.assign({ type: '加氢站' }, record.supplier); + } + if (record.linkedSupplierId) { + var i; + for (i = 0; i < H2_MOCK_EXISTING_SUPPLIERS.length; i++) { + if (H2_MOCK_EXISTING_SUPPLIERS[i].id === record.linkedSupplierId) { + return H2_MOCK_EXISTING_SUPPLIERS[i]; + } + } + } + return null; +} + +var H2_SETTLEMENT_STATUS_OPTIONS = [ + { value: 'customer', label: '客户承担' }, + { value: 'internal', label: '我司承担' }, + { value: 'customer_self', label: '客户自行结算' } +]; + +var H2_SETTLEMENT_STATUS_MAP = { + customer: { label: '客户承担', color: 'blue' }, + internal: { label: '我司承担', color: 'green' }, + customer_self: { label: '客户自行结算', color: 'orange' } +}; + +function h2SettlementStatusLabel(value) { + var info = H2_SETTLEMENT_STATUS_MAP[value]; + return info ? info.label : '—'; +} + /** 站点加氢记录 Mock(字段对齐车辆氢费明细) */ var H2_MOCK_REFUEL_RECORDS = [ - { id: 'rf-1', stationName: '嘉兴加氢站(一期)', hydrogenTime: '2026-05-28 10:21:08', plateNo: '浙A12345F', customerName: '嘉兴市鑫峤供应链科技有限公司', hydrogenKg: 12.5, costUnitPrice: 42.5, costAmount: 531.25, customerUnitPrice: 45, customerAmount: 562.5 }, - { id: 'rf-2', stationName: '嘉兴加氢站(一期)', hydrogenTime: '2026-05-26 14:08:33', plateNo: '浙A67890F', customerName: '浙江绿运物流有限公司', hydrogenKg: 10.0, costUnitPrice: 42.5, costAmount: 425.0, customerUnitPrice: 45, customerAmount: 450.0 }, - { id: 'rf-3', stationName: '嘉兴加氢站(一期)', hydrogenTime: '2026-05-22 09:15:00', plateNo: '浙A88888F', customerName: '嘉兴市鑫峤供应链科技有限公司', hydrogenKg: 18.3, costUnitPrice: 42.5, costAmount: 777.75, customerUnitPrice: 45, customerAmount: 823.5 }, - { id: 'rf-4', stationName: '嘉兴加氢站(一期)', hydrogenTime: '2026-05-18 16:42:11', plateNo: '浙A03561F', customerName: '嘉兴港务氢能运输队', hydrogenKg: 15.6, costUnitPrice: 42.5, costAmount: 663.0, customerUnitPrice: 45, customerAmount: 702.0 }, - { id: 'rf-5', stationName: '杭州临平加氢站', hydrogenTime: '2026-05-30 09:30:22', plateNo: '浙B23456F', customerName: '杭州临平城配中心', hydrogenKg: 15.3, costUnitPrice: 43.0, costAmount: 657.9, customerUnitPrice: 46, customerAmount: 703.8 }, - { id: 'rf-6', stationName: '杭州临平加氢站', hydrogenTime: '2026-05-27 18:10:05', plateNo: '浙B99999F', customerName: '浙江氢运科技', hydrogenKg: 18.2, costUnitPrice: 43.0, costAmount: 782.6, customerUnitPrice: 46, customerAmount: 837.2 }, - { id: 'rf-7', stationName: '杭州临平加氢站', hydrogenTime: '2026-05-24 11:05:40', plateNo: '浙B58888F', customerName: '杭州临平城配中心', hydrogenKg: 11.8, costUnitPrice: 43.0, costAmount: 507.4, customerUnitPrice: 46, customerAmount: 542.8 }, - { id: 'rf-8', stationName: '上海宝山加氢站', hydrogenTime: '2026-04-20 16:45:18', plateNo: '沪A88888F', customerName: '上海羚牛氢运', hydrogenKg: 8.0, costUnitPrice: 44.0, costAmount: 352.0, customerUnitPrice: 47, customerAmount: 376.0 }, - { id: 'rf-9', stationName: '上海宝山加氢站', hydrogenTime: '2026-04-08 09:12:55', plateNo: '沪BDB9161F', customerName: '宝山园区试运车队', hydrogenKg: 9.5, costUnitPrice: 44.0, costAmount: 418.0, customerUnitPrice: 47, customerAmount: 446.5 }, - { id: 'rf-10', stationName: '苏州工业园区备用站', hydrogenTime: '2026-03-15 10:00:00', plateNo: '苏E33333F', customerName: '苏州试运客户', hydrogenKg: 6.2, costUnitPrice: 41.0, costAmount: 254.2, customerUnitPrice: 44, customerAmount: 272.8 } + { id: 'rf-1', stationName: '嘉兴加氢站(一期)', hydrogenTime: '2026-05-28 10:21:08', plateNo: '浙A12345F', customerName: '嘉兴市鑫峤供应链科技有限公司', hydrogenKg: 12.5, costUnitPrice: 42.5, costAmount: 531.25, customerUnitPrice: 45, customerAmount: 562.5, settlementStatus: 'customer' }, + { id: 'rf-2', stationName: '嘉兴加氢站(一期)', hydrogenTime: '2026-05-26 14:08:33', plateNo: '浙A67890F', customerName: '浙江绿运物流有限公司', hydrogenKg: 10.0, costUnitPrice: 42.5, costAmount: 425.0, customerUnitPrice: 45, customerAmount: 450.0, settlementStatus: 'internal' }, + { id: 'rf-3', stationName: '嘉兴加氢站(一期)', hydrogenTime: '2026-05-22 09:15:00', plateNo: '浙A88888F', customerName: '嘉兴市鑫峤供应链科技有限公司', hydrogenKg: 18.3, costUnitPrice: 42.5, costAmount: 777.75, customerUnitPrice: 45, customerAmount: 823.5, settlementStatus: 'customer_self' }, + { id: 'rf-4', stationName: '嘉兴加氢站(一期)', hydrogenTime: '2026-05-18 16:42:11', plateNo: '浙A03561F', customerName: '嘉兴港务氢能运输队', hydrogenKg: 15.6, costUnitPrice: 42.5, costAmount: 663.0, customerUnitPrice: 45, customerAmount: 702.0, settlementStatus: 'customer' }, + { id: 'rf-5', stationName: '杭州临平加氢站', hydrogenTime: '2026-05-30 09:30:22', plateNo: '浙B23456F', customerName: '杭州临平城配中心', hydrogenKg: 15.3, costUnitPrice: 43.0, costAmount: 657.9, customerUnitPrice: 46, customerAmount: 703.8, settlementStatus: 'internal' }, + { id: 'rf-6', stationName: '杭州临平加氢站', hydrogenTime: '2026-05-27 18:10:05', plateNo: '浙B99999F', customerName: '浙江氢运科技', hydrogenKg: 18.2, costUnitPrice: 43.0, costAmount: 782.6, customerUnitPrice: 46, customerAmount: 837.2, settlementStatus: 'customer_self' }, + { id: 'rf-7', stationName: '杭州临平加氢站', hydrogenTime: '2026-05-24 11:05:40', plateNo: '浙B58888F', customerName: '杭州临平城配中心', hydrogenKg: 11.8, costUnitPrice: 43.0, costAmount: 507.4, customerUnitPrice: 46, customerAmount: 542.8, settlementStatus: 'customer' }, + { id: 'rf-8', stationName: '上海宝山加氢站', hydrogenTime: '2026-04-20 16:45:18', plateNo: '沪A88888F', customerName: '上海羚牛氢运', hydrogenKg: 8.0, costUnitPrice: 44.0, costAmount: 352.0, customerUnitPrice: 47, customerAmount: 376.0, settlementStatus: 'internal' }, + { id: 'rf-9', stationName: '上海宝山加氢站', hydrogenTime: '2026-04-08 09:12:55', plateNo: '沪BDB9161F', customerName: '宝山园区试运车队', hydrogenKg: 9.5, costUnitPrice: 44.0, costAmount: 418.0, customerUnitPrice: 47, customerAmount: 446.5, settlementStatus: 'customer_self' }, + { id: 'rf-10', stationName: '苏州工业园区备用站', hydrogenTime: '2026-03-15 10:00:00', plateNo: '苏E33333F', customerName: '苏州试运客户', hydrogenKg: 6.2, costUnitPrice: 41.0, costAmount: 254.2, customerUnitPrice: 44, customerAmount: 272.8, settlementStatus: 'customer' } ]; function h2FormatKgNum(v) { @@ -1264,6 +2345,64 @@ function h2FormatYuanNum(v) { return isNaN(n) ? '0.00' : n.toFixed(2); } +function h2FormatYuanSymbol(v, options) { + var opts = options || {}; + if (v == null || v === '') return '—'; + if (!opts.keepZero && Number(v) === 0) return '—'; + var n = typeof v === 'number' ? v : parseFloat(v); + if (isNaN(n)) return '—'; + return '¥' + n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); +} + +var H2_BALANCE_BIZ_TYPE_MAP = { + recharge: { key: 'recharge', label: '充值', color: 'success' }, + refuel: { key: 'refuel', label: '车辆加氢', color: 'warning' }, + ending: { key: 'ending', label: '期末', color: 'default' } +}; + +function h2Pad2(n) { + return String(n).padStart(2, '0'); +} + +function h2BuildBalanceChangeTime(seqIndex) { + var day = Math.max(1, 28 - seqIndex * 3); + var hour = 8 + ((seqIndex * 3) % 12); + var minute = (seqIndex * 17) % 60; + return '2026-05-' + h2Pad2(day) + ' ' + h2Pad2(hour) + ':' + h2Pad2(minute) + ':00'; +} + +function h2ResolveBalanceBizType(row) { + if (row && row.bizType && H2_BALANCE_BIZ_TYPE_MAP[row.bizType]) { + return H2_BALANCE_BIZ_TYPE_MAP[row.bizType]; + } + if (row && row.incomeAmount != null && row.incomeAmount !== '') return H2_BALANCE_BIZ_TYPE_MAP.recharge; + if (row && row.expenseAmount != null && row.expenseAmount !== '') return H2_BALANCE_BIZ_TYPE_MAP.refuel; + return H2_BALANCE_BIZ_TYPE_MAP.ending; +} + +function h2FormatBalanceExportAmount(v) { + if (v == null || v === '') return ''; + return h2FormatYuanNum(v); +} + +function h2ExportBalanceDrillCsv(stationName, rows) { + var headers = ['序号', '时间', '类型', '收入(元)', '支出(元)', '余额(元)', '订单编号']; + var csvRows = (rows || []).map(function (r, index) { + var typeInfo = h2ResolveBalanceBizType(r); + return [ + index + 1, + r.changeTime || '', + typeInfo.label, + h2FormatBalanceExportAmount(r.incomeAmount), + h2FormatBalanceExportAmount(r.expenseAmount), + h2FormatBalanceExportAmount(r.balance), + r.orderNo || '' + ]; + }); + var safeName = String(stationName || '站点').replace(/[\\/:*?"<>|]/g, '_'); + h2DownloadCsv('余额变更明细_' + safeName + '_' + new Date().getTime() + '.csv', headers, csvRows); +} + function h2NumOrZero(v) { var n = typeof v === 'number' ? v : parseFloat(v); return isNaN(n) ? 0 : n; @@ -1282,16 +2421,19 @@ function h2BuildMockPrepaidBalanceRows(record) { var i; for (i = 0; i < code.length; i++) seed += code.charCodeAt(i); for (i = 0; i < nameKey.length; i++) seed += nameKey.charCodeAt(i); + seed = seed % 100; var stationName = (record && record.name) || '—'; var finalBalance = h2NumOrZero(record && record.prepaidBalance); - var lineCount = Math.max(5, 6 + (seed % 5)); + var lineCount = Math.max(5, 6 + (seed % 4)); var rows = []; - var openingBalance = Math.round((finalBalance + 38000 + seed * 620) * 100) / 100; - if (openingBalance < 0) openingBalance = Math.abs(openingBalance) + 50000; + var openingBalance = Math.round((42000 + seed * 520) * 100) / 100; + if (openingBalance < 0) openingBalance = Math.abs(openingBalance) + 30000; rows.push({ key: code + '-bal-0', stationName: stationName, + changeTime: '2026-01-01 00:00:00', + bizType: 'recharge', incomeAmount: openingBalance, expenseAmount: null, balance: openingBalance, @@ -1304,15 +2446,17 @@ function h2BuildMockPrepaidBalanceRows(record) { var incomeAmount = null; var expenseAmount = null; if (isIncome) { - incomeAmount = Math.round((6000 + (seed + i) * 380) * 100) / 100; + incomeAmount = Math.round((5000 + (seed + i) * 42) * 100) / 100; balance = Math.round((balance + incomeAmount) * 100) / 100; } else { - expenseAmount = Math.round((2800 + (seed + i) * 210) * 100) / 100; + expenseAmount = Math.round((2200 + (seed + i) * 26) * 100) / 100; balance = Math.round((balance - expenseAmount) * 100) / 100; } rows.push({ key: code + '-bal-' + i, stationName: stationName, + changeTime: h2BuildBalanceChangeTime(i), + bizType: isIncome ? 'recharge' : 'refuel', incomeAmount: incomeAmount, expenseAmount: expenseAmount, balance: balance, @@ -1325,6 +2469,8 @@ function h2BuildMockPrepaidBalanceRows(record) { rows.push({ key: code + '-bal-last', stationName: stationName, + changeTime: '2026-06-01 23:59:59', + bizType: 'ending', incomeAmount: null, expenseAmount: null, balance: finalBalance, @@ -1334,6 +2480,8 @@ function h2BuildMockPrepaidBalanceRows(record) { rows.push({ key: code + '-bal-last', stationName: stationName, + changeTime: '2026-06-01 18:30:00', + bizType: 'recharge', incomeAmount: diff, expenseAmount: null, balance: finalBalance, @@ -1343,6 +2491,8 @@ function h2BuildMockPrepaidBalanceRows(record) { rows.push({ key: code + '-bal-last', stationName: stationName, + changeTime: '2026-06-01 18:30:00', + bizType: 'refuel', incomeAmount: null, expenseAmount: Math.round(Math.abs(diff) * 100) / 100, balance: finalBalance, @@ -1353,13 +2503,490 @@ function h2BuildMockPrepaidBalanceRows(record) { return rows; } +function h2FormatBalanceTrendLabel(changeTime) { + if (!changeTime) return '—'; + var text = String(changeTime).trim(); + if (text.length >= 10) return text.slice(5, 10); + return text; +} + +function h2ParseChangeTimeMs(changeTime) { + if (!changeTime) return 0; + var text = String(changeTime).trim(); + var ms = Date.parse(text.indexOf('T') === -1 ? text.replace(' ', 'T') : text); + return isNaN(ms) ? 0 : ms; +} + +function h2ExtractBalanceDateKey(changeTime) { + if (!changeTime) return ''; + var text = String(changeTime).trim(); + return text.length >= 10 ? text.slice(0, 10) : text; +} + +function h2FormatDateOnlyFromDate(date) { + return date.getFullYear() + '-' + String(date.getMonth() + 1).padStart(2, '0') + '-' + String(date.getDate()).padStart(2, '0'); +} + +function h2GetYesterdayDateOnly() { + var d = new Date(); + d.setHours(0, 0, 0, 0); + d.setDate(d.getDate() - 1); + return h2FormatDateOnlyFromDate(d); +} + +function h2AddMonths(dateStr, months) { + var ms = h2ParseDateOnlyMs(dateStr); + if (isNaN(ms)) return dateStr; + var d = new Date(ms); + d.setMonth(d.getMonth() + (months || 0)); + return h2FormatDateOnlyFromDate(d); +} + +/** 趋势图固定展示近三个月:结束日为昨日,起始日为昨日往前推 3 个月 */ +function h2ResolveBalanceTrendRange() { + var endDate = h2GetYesterdayDateOnly(); + var startDate = h2AddMonths(endDate, -3); + return { startDate: startDate, endDate: endDate }; +} + +/** 流水按日聚合为趋势点:每日取末笔余额,无流水日沿用前一日余额 */ +function h2BuildBalanceTrendSeries(rows) { + var range = h2ResolveBalanceTrendRange(); + var startDate = range.startDate; + var endDate = range.endDate; + var startMs = h2ParseDateOnlyMs(startDate); + var endMs = h2ParseDateOnlyMs(endDate); + if (!rows || !rows.length || isNaN(startMs) || isNaN(endMs)) { + return { + points: [], + min: 0, + max: 0, + hasNegative: false, + granularity: 'day', + startDate: startDate, + endDate: endDate + }; + } + var sorted = rows.slice().sort(function (a, b) { + return h2ParseChangeTimeMs(a.changeTime) - h2ParseChangeTimeMs(b.changeTime); + }); + var dayBuckets = {}; + var i; + for (i = 0; i < sorted.length; i++) { + var row = sorted[i]; + var dateKey = h2ExtractBalanceDateKey(row.changeTime); + if (!dateKey) continue; + if (!dayBuckets[dateKey]) { + dayBuckets[dateKey] = row; + } else if (h2ParseChangeTimeMs(row.changeTime) >= h2ParseChangeTimeMs(dayBuckets[dateKey].changeTime)) { + dayBuckets[dateKey] = row; + } + } + var seedBalance = null; + for (i = sorted.length - 1; i >= 0; i--) { + var seedKey = h2ExtractBalanceDateKey(sorted[i].changeTime); + if (seedKey && h2ParseDateOnlyMs(seedKey) < startMs) { + seedBalance = h2NumOrZero(sorted[i].balance); + break; + } + } + var points = []; + var lastBalance = seedBalance; + var cursor = startDate; + var guard = 0; + while (guard < 4000) { + guard += 1; + var cursorMs = h2ParseDateOnlyMs(cursor); + if (isNaN(cursorMs) || cursorMs > endMs) break; + if (dayBuckets[cursor]) { + lastBalance = h2NumOrZero(dayBuckets[cursor].balance); + } + if (lastBalance != null) { + points.push({ + key: 'day-' + cursor, + label: h2FormatBalanceTrendLabel(cursor + ' 00:00:00'), + fullTime: cursor, + balance: lastBalance + }); + } + if (cursor === endDate) break; + cursor = h2AddDays(cursor, 1); + } + if (!points.length) { + return { + points: [], + min: 0, + max: 0, + hasNegative: false, + granularity: 'day', + startDate: startDate, + endDate: endDate + }; + } + var min = points[0].balance; + var max = points[0].balance; + var hasNegative = false; + for (i = 0; i < points.length; i++) { + if (points[i].balance < min) min = points[i].balance; + if (points[i].balance > max) max = points[i].balance; + if (points[i].balance < 0) hasNegative = true; + } + var pad = Math.max((max - min) * 0.12, max === min ? Math.abs(max) * 0.1 || 500 : 0); + if (hasNegative && min > 0) min = 0; + return { + points: points, + min: min - pad, + max: max + pad, + hasNegative: hasNegative, + granularity: 'day', + startDate: startDate, + endDate: endDate + }; +} + +function H2BalanceTrendChart(props) { + var series = props.series || {}; + var points = series.points || []; + var hoverState = React.useState(null); + var hoverIndex = hoverState[0]; + var setHoverIndex = hoverState[1]; + if (!points.length) { + return React.createElement('div', { className: 'h2-balance-trend-empty' }, '暂无趋势数据'); + } + var W = 820; + var H = 220; + var PL = 56; + var PR = 20; + var PT = 18; + var PB = 36; + var chartW = W - PL - PR; + var chartH = H - PT - PB; + var min = series.min; + var max = series.max; + var range = max - min || 1; + var coords = points.map(function (p, idx) { + var x = PL + (points.length === 1 ? chartW / 2 : (idx / (points.length - 1)) * chartW); + var y = PT + chartH - ((p.balance - min) / range) * chartH; + return { x: x, y: y, label: p.label, balance: p.balance, fullTime: p.fullTime, key: p.key }; + }); + var linePath = coords.map(function (c, i) { + return (i === 0 ? 'M' : 'L') + c.x.toFixed(1) + ' ' + c.y.toFixed(1); + }).join(' '); + var areaPath = linePath + + ' L' + coords[coords.length - 1].x.toFixed(1) + ' ' + (PT + chartH) + + ' L' + coords[0].x.toFixed(1) + ' ' + (PT + chartH) + + ' Z'; + var zeroY = null; + if (min < 0 && max > 0) { + zeroY = PT + chartH - ((0 - min) / range) * chartH; + } + var yTicks = [ + { value: max, y: PT }, + { value: min + range / 2, y: PT + chartH / 2 }, + { value: min, y: PT + chartH } + ]; + var xLabels = [ + { x: coords[0].x, label: coords[0].label }, + { x: coords[coords.length - 1].x, label: coords[coords.length - 1].label } + ]; + var pickNearestIndex = function (mouseX) { + var nearest = 0; + var minDist = Infinity; + var j; + for (j = 0; j < coords.length; j++) { + var dist = Math.abs(coords[j].x - mouseX); + if (dist < minDist) { + minDist = dist; + nearest = j; + } + } + return nearest; + }; + var handleChartMouseMove = function (e) { + var rect = e.currentTarget.getBoundingClientRect(); + if (!rect.width) return; + var mouseX = ((e.clientX - rect.left) / rect.width) * W; + if (mouseX < PL - 8 || mouseX > W - PR + 8) { + setHoverIndex(null); + return; + } + setHoverIndex(pickNearestIndex(mouseX)); + }; + var active = hoverIndex != null && coords[hoverIndex] ? coords[hoverIndex] : null; + var tooltipNode = active + ? React.createElement('div', { + className: 'h2-balance-trend-tooltip', + style: { + left: (active.x / W * 100) + '%', + top: (active.y / H * 100) + '%' + }, + role: 'tooltip' + }, + React.createElement('div', { className: 'h2-balance-trend-tooltip__date' }, active.fullTime || '—'), + React.createElement('div', { + className: 'h2-balance-trend-tooltip__balance' + (active.balance < 0 ? ' h2-balance-trend-tooltip__balance--negative' : '') + }, h2FormatYuanSymbol(active.balance, { keepZero: true })) + ) + : null; + + return React.createElement('div', { + className: 'h2-balance-trend-chart-wrap', + onMouseLeave: function () { setHoverIndex(null); } + }, + tooltipNode, + React.createElement('svg', { + className: 'h2-balance-trend-svg', + viewBox: '0 0 ' + W + ' ' + H, + role: 'img', + 'aria-label': '预付余额趋势图', + onMouseMove: handleChartMouseMove, + onMouseLeave: function () { setHoverIndex(null); } + }, + React.createElement('defs', null, + React.createElement('linearGradient', { id: 'h2BalanceTrendArea', x1: '0', y1: '0', x2: '0', y2: '1' }, + React.createElement('stop', { offset: '0%', stopColor: '#10b981', stopOpacity: '0.28' }), + React.createElement('stop', { offset: '100%', stopColor: '#10b981', stopOpacity: '0.02' }) + ) + ), + yTicks.map(function (tick, idx) { + return React.createElement('g', { key: 'yt-' + idx }, + React.createElement('line', { + x1: PL, y1: tick.y, x2: W - PR, y2: tick.y, + stroke: '#f1f5f9', strokeWidth: 1 + }), + React.createElement('text', { + x: PL - 8, y: tick.y + 4, + textAnchor: 'end', fontSize: 10, fill: '#94a3b8', fontFamily: 'ui-sans-serif, system-ui, sans-serif' + }, h2FormatYuanNum(tick.value)) + ); + }), + zeroY != null + ? React.createElement('line', { + x1: PL, y1: zeroY, x2: W - PR, y2: zeroY, + stroke: '#fca5a5', strokeWidth: 1, strokeDasharray: '4 4' + }) + : null, + active + ? React.createElement('line', { + x1: active.x, y1: PT, x2: active.x, y2: PT + chartH, + stroke: '#a7f3d0', strokeWidth: 1, strokeDasharray: '3 3' + }) + : null, + React.createElement('path', { d: areaPath, fill: 'url(#h2BalanceTrendArea)' }), + React.createElement('path', { + d: linePath, fill: 'none', stroke: '#10b981', strokeWidth: 2.5, strokeLinejoin: 'round', strokeLinecap: 'round' + }), + coords.map(function (c, idx) { + var negative = c.balance < 0; + var isActive = hoverIndex === idx; + return React.createElement('g', { key: c.key }, + React.createElement('circle', { + className: 'h2-balance-trend-hit', + cx: c.x, cy: c.y, r: 10, + onMouseEnter: function () { setHoverIndex(idx); } + }), + React.createElement('circle', { + className: isActive ? 'h2-balance-trend-point--active' : '', + cx: c.x, cy: c.y, r: isActive ? 5.5 : 4, + fill: '#fff', + stroke: negative ? '#dc2626' : '#10b981', + strokeWidth: isActive ? 2.5 : 2, + pointerEvents: 'none' + }) + ); + }), + xLabels.map(function (item, idx) { + return React.createElement('text', { + key: 'xl-' + idx, + x: item.x, + y: H - 10, + textAnchor: idx === 0 ? 'start' : 'end', + fontSize: 10, + fill: '#64748b', + fontFamily: 'ui-sans-serif, system-ui, sans-serif' + }, item.label); + }) + ) + ); +} + +function h2BuildRefuelOrderNo(record, index) { + if (record && record.orderNo) return record.orderNo; + var idPart = String((record && record.id) || '').replace(/\D/g, '') || String(index + 1); + return 'JQ' + idPart.padStart(4, '0') + String(100 + index); +} + +/** 对账状态(对齐车辆氢费明细) */ +var H2_RECONCILE_RECONCILED = 'reconciled'; +var H2_RECONCILE_PENDING = 'pending'; +var H2_PAYMENT_STATUS_PAID = 'paid'; +var H2_PAYMENT_STATUS_UNPAID = 'unpaid'; + +function h2ParseDateOnlyMs(dateStr) { + if (!dateStr) return NaN; + var parts = String(dateStr).trim().split('-'); + if (parts.length < 3) return NaN; + var y = parseInt(parts[0], 10); + var m = parseInt(parts[1], 10); + var d = parseInt(parts[2], 10); + if (isNaN(y) || isNaN(m) || isNaN(d)) return NaN; + return new Date(y, m - 1, d, 0, 0, 0, 0).getTime(); +} + +function h2AddDays(dateStr, days) { + var ms = h2ParseDateOnlyMs(dateStr); + if (isNaN(ms)) return dateStr; + var d = new Date(ms + (days || 0) * 86400000); + return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0'); +} + +function h2FormatOperateDateOnly() { + return h2OperateTimestamp().slice(0, 10); +} + +/** 台账加氢记录 Mock(字段对齐车辆氢费明细,costTotal 对应成本总价) */ +function h2BuildLedgerReconciledRecords() { + return H2_MOCK_REFUEL_RECORDS.map(function (r, index) { + var isReconciled = index < 9; + return { + id: r.id, + stationName: r.stationName, + hydrogenTime: r.hydrogenTime, + plateNo: r.plateNo, + customerName: r.customerName, + hydrogenKg: r.hydrogenKg, + costUnitPrice: r.costUnitPrice, + costTotal: r.costAmount, + settlementStatus: r.settlementStatus, + orderNo: h2BuildRefuelOrderNo(r, index), + reconcileStatus: isReconciled ? H2_RECONCILE_RECONCILED : H2_RECONCILE_PENDING, + statementRecordId: null, + reconcileDate: null, + receiptDate: null, + paymentStatus: isReconciled ? H2_PAYMENT_STATUS_UNPAID : null + }; + }); +} + +function h2InitLedgerRecordsStore() { + return h2BuildLedgerReconciledRecords(); +} + +function h2InitStatementRecordsStore() { + return []; +} + +function h2GetLastStatementEndDate(statementRecords, stationName) { + var name = String(stationName || '').trim(); + var list = (statementRecords || []).filter(function (s) { return (s.stationName || '') === name; }); + if (!list.length) return null; + var max = list[0].endDate; + var i; + for (i = 1; i < list.length; i++) { + if (String(list[i].endDate || '') > String(max || '')) max = list[i].endDate; + } + return max || null; +} + +function h2GetAvailableStatementLedgerRows(store, stationName, startDate, endDate) { + var name = String(stationName || '').trim(); + var startMs = h2ParseDateOnlyMs(startDate); + var endMs = h2ParseDateOnlyMs(endDate); + if (!name || isNaN(startMs) || isNaN(endMs)) return []; + var endInclusive = endMs + 86400000 - 1; + return (store || []).filter(function (r) { + if (r.reconcileStatus !== H2_RECONCILE_RECONCILED) return false; + if (r.statementRecordId) return false; + if ((r.stationName || '') !== name) return false; + var t = h2ParseDateTimeMs(r.hydrogenTime); + if (isNaN(t)) return false; + return t >= startMs && t <= endInclusive; + }).slice().sort(function (a, b) { + return String(b.hydrogenTime || '').localeCompare(String(a.hydrogenTime || '')); + }); +} + +function h2GetStatementRecordsByStation(statementRecords, stationName) { + var name = String(stationName || '').trim(); + return (statementRecords || []).filter(function (s) { return (s.stationName || '') === name; }).slice().sort(function (a, b) { + return String(b.reconcileDate || '').localeCompare(String(a.reconcileDate || '')); + }); +} + +function h2GetLedgerRowsByIds(store, ids) { + var idSet = {}; + var i; + for (i = 0; i < (ids || []).length; i++) idSet[ids[i]] = true; + return (store || []).filter(function (r) { return idSet[r.id]; }).slice().sort(function (a, b) { + return String(b.hydrogenTime || '').localeCompare(String(a.hydrogenTime || '')); + }); +} + +function h2PushStatementLedgerPatches(patches) { + if (typeof window === 'undefined') return; + window.H2_STATION_STATEMENT_LEDGER_UPDATES = (window.H2_STATION_STATEMENT_LEDGER_UPDATES || []).concat(patches || []); +} + +function h2CalcStatementSummary(records) { + var rows = records || []; + var totalKg = 0; + var totalCost = 0; + var i; + for (i = 0; i < rows.length; i++) { + totalKg += rows[i].hydrogenKg || 0; + totalCost += rows[i].costTotal || 0; + } + return { count: rows.length, totalKg: totalKg, totalCost: totalCost }; +} + +function h2ExportStatementCsv(stationName, startDate, endDate, records) { + var headers = ['序号', '加氢时间', '车牌号', '加氢量(kg)', '成本单价(元/kg)', '成本总价(元)', '订单编号']; + var csvRows = (records || []).map(function (r, index) { + return [ + index + 1, + r.hydrogenTime || '', + r.plateNo || '', + h2FormatKgNum(r.hydrogenKg), + h2FormatBalanceExportAmount(r.costUnitPrice), + h2FormatBalanceExportAmount(r.costTotal), + r.orderNo || '' + ]; + }); + var safeName = String(stationName || '站点').replace(/[\\/:*?"<>|]/g, '_'); + var rangePart = String(startDate || '').replace(/-/g, '') + '-' + String(endDate || '').replace(/-/g, ''); + h2DownloadCsv('氢费对账单_' + safeName + '_' + rangePart + '_' + new Date().getTime() + '.csv', headers, csvRows); +} + function h2GetRefuelRecordsByStation(stationName) { var name = String(stationName || '').trim(); return H2_MOCK_REFUEL_RECORDS.filter(function (r) { return r.stationName === name; }).slice().sort(function (a, b) { return String(b.hydrogenTime || '').localeCompare(String(a.hydrogenTime || '')); + }).map(function (r, index) { + return Object.assign({}, r, { orderNo: h2BuildRefuelOrderNo(r, index) }); }); } +function h2ExportRefuelDrillCsv(stationName, records) { + var headers = ['序号', '加氢时间', '车牌号', '客户名称', '加氢量(kg)', '成本单价(元/kg)', '成本总价(元)', '加氢单价(元/kg)', '加氢总价(元)', '承担方式', '订单编号']; + var csvRows = (records || []).map(function (r, index) { + return [ + index + 1, + r.hydrogenTime || '', + r.plateNo || '', + r.customerName || '', + h2FormatKgNum(r.hydrogenKg), + h2FormatBalanceExportAmount(r.costUnitPrice), + h2FormatBalanceExportAmount(r.costAmount), + h2FormatBalanceExportAmount(r.customerUnitPrice), + h2FormatBalanceExportAmount(r.customerAmount), + h2SettlementStatusLabel(r.settlementStatus), + r.orderNo || '' + ]; + }); + var safeName = String(stationName || '站点').replace(/[\\/:*?"<>|]/g, '_'); + h2DownloadCsv('加氢明细_' + safeName + '_' + new Date().getTime() + '.csv', headers, csvRows); +} + function h2CalcRefuelStats(stationName) { var records = h2GetRefuelRecordsByStation(stationName); var totalKg = 0; @@ -1368,6 +2995,26 @@ function h2CalcRefuelStats(stationName) { return { count: records.length, totalKg: totalKg }; } +var H2_LINGNIU_SIGNING_COMPANY_NAMES = [ + '浙江羚牛氢能科技有限公司', + '羚牛氢能科技(上海)有限公司', + '羚牛氢能科技(广东)有限公司', + '羚牛新能源运营有限公司' +]; + +function h2GetLingniuSigningCompanyOptions() { + var names = H2_LINGNIU_SIGNING_COMPANY_NAMES.slice(); + if (typeof window !== 'undefined' && window.ONEOS_LESSOR_COMPANIES && window.ONEOS_LESSOR_COMPANIES.length) { + window.ONEOS_LESSOR_COMPANIES.forEach(function (c) { + var n = (c.legalName || '').trim(); + if (n && names.indexOf(n) < 0) names.push(n); + }); + } + return names.map(function (name) { + return { value: name, label: name }; + }); +} + var H2_MOCK_EXISTING_SUPPLIERS = [ { id: 'sup-h2-1', @@ -1390,7 +3037,11 @@ var H2_MOCK_EXISTING_SUPPLIERS = [ mailingAddress: '浙江省嘉兴市南湖区科技大道66号', businessAddress: '浙江省嘉兴市南湖区科技大道66号', businessLicenseFiles: [{ uid: 'bl-1', name: '营业执照_嘉兴氢能.pdf', status: 'done' }], - fillingLicenseFiles: [{ uid: 'fl-1', name: '充装许可证_嘉兴氢能.pdf', status: 'done' }] + fillingLicenseFiles: [ + { uid: 'fl-1a', name: '危化品经营许可证.pdf', status: 'done' }, + { uid: 'fl-1b', name: '特种设备使用登记证.pdf', status: 'done' }, + { uid: 'fl-1c', name: '消防验收合格证.pdf', status: 'done' } + ] }, { id: 'sup-h2-2', @@ -1413,7 +3064,7 @@ var H2_MOCK_EXISTING_SUPPLIERS = [ mailingAddress: '上海市宝山区富联路200号', businessAddress: '上海市宝山区富联路200号', businessLicenseFiles: [{ uid: 'bl-2', name: '营业执照_羚牛氢能.pdf', status: 'done' }], - fillingLicenseFiles: [{ uid: 'fl-2', name: '充装许可证_羚牛氢能.pdf', status: 'done' }] + fillingLicenseFiles: [{ uid: 'fl-2', name: '加氢站备案证明.pdf', status: 'done' }] }, { id: 'sup-h2-3', @@ -1463,6 +3114,100 @@ var H2_MOCK_EXISTING_SUPPLIERS = [ } ]; +/** 可加氢站绑定的系统账号 Mock */ +var H2_MOCK_SYSTEM_ACCOUNTS = [ + { id: 'acc-h2-1', username: 'jx_h2_admin', displayName: '嘉兴站管理员', role: '加氢站管理员', mobile: '13800138001', status: '启用' }, + { id: 'acc-h2-2', username: 'hz_lp_station', displayName: '临平站操作员', role: '加氢站管理员', mobile: '13900139002', status: '启用' }, + { id: 'acc-h2-3', username: 'sh_bs_h2', displayName: '宝山站管理员', role: '加氢站管理员', mobile: '13700137003', status: '启用' }, + { id: 'acc-h2-4', username: 'sz_h2_ops', displayName: '苏州备用站账号', role: '加氢站管理员', mobile: '13500135005', status: '启用' }, + { id: 'acc-h2-5', username: 'h2_station_new', displayName: '待分配站点账号', role: '加氢站管理员', mobile: '13600136099', status: '启用' } +]; + +function h2FindSystemAccountById(accountId) { + if (!accountId) return null; + var i; + for (i = 0; i < H2_MOCK_SYSTEM_ACCOUNTS.length; i++) { + if (H2_MOCK_SYSTEM_ACCOUNTS[i].id === accountId) return H2_MOCK_SYSTEM_ACCOUNTS[i]; + } + return null; +} + +function h2NormalizeBindAccountIds(source) { + if (!source) return []; + if (source.bindAccountIds && source.bindAccountIds.length) return source.bindAccountIds.slice(); + if (source.boundAccountIds && source.boundAccountIds.length) return source.boundAccountIds.slice(); + var single = source.bindAccountId || source.boundAccountId; + return single ? [single] : []; +} + +function h2BuildBoundAccountsFromIds(ids) { + return (ids || []).map(function (id) { + var acc = h2FindSystemAccountById(id); + if (!acc) return null; + return { + id: acc.id, + username: acc.username, + displayName: acc.displayName, + role: acc.role, + mobile: acc.mobile, + status: acc.status + }; + }).filter(Boolean); +} + +function h2CollectUsedBindAccountMap(stations, excludeStationId) { + var map = {}; + (stations || []).forEach(function (s) { + if (excludeStationId != null && s.id === excludeStationId) return; + h2NormalizeBindAccountIds(s).forEach(function (aid) { + map[aid] = s.name || '—'; + }); + }); + return map; +} + +function h2ResolveStationSystemAccounts(record) { + if (!record) return []; + if (record.boundAccounts && record.boundAccounts.length) return record.boundAccounts.slice(); + if (record.boundAccount && (record.boundAccount.username || record.boundAccount.displayName)) { + return [record.boundAccount]; + } + return h2BuildBoundAccountsFromIds(h2NormalizeBindAccountIds(record)); +} + +function h2ResolveStationSystemAccount(record) { + var list = h2ResolveStationSystemAccounts(record); + return list.length ? list[0] : null; +} + +function h2RecordToEditStationForm(record) { + if (!record) return h2CreateEmptyStationForm(); + var ids = h2NormalizeBindAccountIds(record); + return { + name: record.name || '', + address: { + region: (record.region || []).slice(), + detail: record.addressDetail || '' + }, + isSigned: record.isSigned !== false, + contractStart: record.contractStart || '', + contractEnd: record.contractEnd || '', + contractFiles: (record.contractFiles || []).map(function (f, i) { + return Object.assign({}, f, { uid: f.uid || ('f-' + i), status: 'done' }); + }), + businessStatus: record.businessStatus || '营业中', + businessHours: record.businessHours === '—' ? '' : (record.businessHours || ''), + contact: record.contact || '', + mobilePhone: record.mobilePhone || record.phone || '', + landlinePhone: record.landlinePhone || '', + costUnitPrice: record.costUnitPrice != null ? String(record.costUnitPrice) : '', + customerUnitPrice: record.customerUnitPrice != null ? String(record.customerUnitPrice) : '', + prepaidBalance: record.prepaidBalance || 0, + bindAccountMode: record.bindAccountMode || (ids.length ? 'bind' : 'none'), + bindAccountIds: ids + }; +} + function h2CreateEmptyStationForm() { return { name: '', @@ -1478,12 +3223,15 @@ function h2CreateEmptyStationForm() { landlinePhone: '', costUnitPrice: '', customerUnitPrice: '', - prepaidBalance: 0 + prepaidBalance: 0, + bindAccountMode: 'none', + bindAccountIds: [] }; } function h2CreateEmptySupplierForm() { return { + signingCompany: undefined, name: '', city: undefined, address: '', @@ -1522,6 +3270,7 @@ function h2GetRegionByProvince(province) { function h2SupplierToForm(s) { if (!s) return h2CreateEmptySupplierForm(); return { + signingCompany: s.signingCompany || undefined, name: s.name || '', city: s.city ? s.city.slice() : undefined, address: s.address || '', @@ -1548,40 +3297,984 @@ function h2SupplierToForm(s) { }; } -function getH2StationRequirementDoc() { - return [ - '加氢站管理 - 站点信息', - '', - '一、页面定位', - ' 管理全部加氢站点基础信息,支持 KPI 分类筛选、条件检索、新建、编辑、查看与删除。', - '', - '二、站点分类 KPI', - ' · 全部加氢站:展示全部记录', - ' · 高频 / 低频 / 无加氢站点:按加氢次数自动划分(≥3 高频,1~2 低频,0 无加氢)', - '', - '三、列表字段', - ' 加氢站名称(含签约标签)、地址、签约起止时间、营业状态、营业时间、加氢次数、加氢量(含高频/低频标签)、预付余额、联系方式、操作', - ' · 加氢次数/加氢量可点击,弹窗展示该站点加氢记录(对齐车辆氢费明细字段)', - ' · 预付余额:可点击查看余额变更明细;负值红色展示(已欠费);更多菜单支持生成对账单', - '', - '四、表单字段', - ' · 新建:同页内嵌视图,含加氢站基本信息、供应商相关信息两张卡片;供应商类型固定「加氢站」', - ' · 编辑/查看:抽屉;基本信息含名称、地址、营业时间、签约信息、联系方式;营业状态仅在列表「更多-营业状态」维护', - '', - '五、交互', - ' · 筛选:加氢站名称、是否签约、地区(省-市)、营业状态', - ' · KPI 统计:全部加氢站右侧为已签约/未签约站点快捷筛选,其后为高频/低频/无加氢分类', - ' · 未签约时签约时间与附件可选填;已签约建议填写完整', - ' · 查看模式只读;编辑/新建可保存', - ' · 列表支持分页,默认每页 10 条,可切换 5/10/20/50;筛选或 KPI 切换后回到第 1 页', - ' · 操作列:查看 + 更多(编辑、删除、营业状态、价格配置、生成对账单)', - ' · 营业状态弹窗:修改营业状态/时间,并展示状态变更记录(操作时间、操作人、修改前/后状态)', - '', - '六、批量导入', - ' · 下载 CSV 模板,填写后上传;支持 .csv(推荐)、.xlsx、.xls', - ' · 模板字段:加氢站名称、省、市、详细地址、是否签约、签约起止时间、营业状态、营业时间、联系人、联系电话', - ' · 站点名称不可与现有台账重复;解析后确认导入' - ].join('\n'); +var H2_STATION_USER_MANUAL_DOC = `# 加氢站管理 · 站点信息 — 用户使用说明书 + +| 项目 | 内容 | +|------|------| +| 文档版本 | v1.0 | +| 产品模块 | 加氢站管理 → 站点信息 | +| 文档类型 | **用户使用说明书** | +| 适用读者 | 加氢站运营、财务结算、平台管理员、培训与汇报人员 | +| 关联模块 | 台账数据 → 车辆氢费明细 | +| 配图目录 | 与本文件同级的 docs/ 文件夹 | + +--- + +## 使用本说明书 + +本说明书用于**教会一线用户操作**站点管理模块,也可作为**项目汇报、培训宣讲**的配套材料。建议按以下顺序阅读: + +1. **第二章** — 认识页面布局(5 分钟) +2. **第四章** — 新建 / 编辑 / 导入站点(15 分钟) +3. **第七章** — 生成对账单与结算(重点,20 分钟) +4. **第九章** — 汇报话术与常见问题 + +页面右上角可点击 **「查看使用说明书」** 随时打开本文档。 + +--- + +## 一、模块能做什么 + +### 1.1 一句话说明 + +站点信息模块是加氢站的**主数据与运营台账中心**:维护站点档案、营业与成本价,查看加氢量与预付余额,并按周期与加氢站完成**氢费对账、收票登记与付款闭环**。 + +### 1.2 功能地图 + +\`\`\`mermaid +mindmap + root((站点信息)) + 查询统计 + 筛选条件 + KPI 分类卡 + 列表排序分页 + 主数据 + 新建站点 + 编辑站点 + 查看详情 + 批量导入 + 删除站点 + 运营配置 + 营业状态 + 成本价格 + 数据分析 + 加氢量下钻 + 预付余额下钻 + 财务结算 + 生成对账单 + 查看对账记录 +\`\`\` + +### 1.3 谁来做什么事 + +| 角色 | 常用功能 | 注意事项 | +|------|----------|----------| +| **平台管理员(admin)** | 新建、编辑、批量导入、删除、绑定系统账号 | 系统账号支持多选绑定 | +| **加氢站运营账号** | 编辑本站资料、营业状态、价格配置 | 编辑时**不能改**供应商与系统账号 | +| **财务 / 结算人员** | 生成对账单、登记收票、查看对账记录 | 需先在台账完成「已对账」 | + +--- + +## 二、进入模块与页面总览 + +### 2.1 菜单路径 + +左侧导航:**加氢站管理 → 站点信息** + +### 2.2 页面结构(自上而下) + +\`\`\` +┌─────────────────────────────────────────────────────────────┐ +│ 面包屑:加氢站管理 > 站点信息 [查看需求说明][使用说明书] │ +├─────────────────────────────────────────────────────────────┤ +│ 【筛选条件】名称 | 是否签约 | 地区 | 营业状态 [重置][查询] │ +├─────────────────────────────────────────────────────────────┤ +│ KPI:全部 | 已签约 | 未签约 | 高频 | 低频 | 无加氢 │ +├─────────────────────────────────────────────────────────────┤ +│ 工具栏:[批量导入] [新建站点] │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ 站点列表(名称/签约/营业/价格/加氢量/余额/操作) │ │ +│ └─────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +\`\`\` + +![图 2-1 站点列表页总览示意](docs/h2-station-manual-list-page.png) + +### 2.3 列表字段速查 + +| 列名 | 怎么用 | +|------|--------| +| 加氢站名称 | 含「已签约」标签;下方显示完整地址 | +| 合作时间 | 未签约显示为空 | +| 营业状态 / 营业时间 | 只读;在「更多 → 营业状态」修改 | +| 当前成本价格 | 元/kg;由价格配置生效时间驱动 | +| 加氢次数 / 加氢量 | **点击加氢量**可下钻明细 | +| 预付余额 | **点击余额**可下钻流水;负数标红「已欠费」 | +| 操作 | **查看** + **更多**(⋮) | + +--- + +## 三、日常查询与 KPI 筛选 + +### 3.1 操作流程 + +\`\`\`mermaid +flowchart LR + A[输入筛选条件] --> B[点击查询] + B --> C[列表刷新] + D[点击 KPI 卡] --> C + E[点击重置] --> F[清空条件] + F --> B +\`\`\` + +### 3.2 操作步骤 + +1. 在「筛选条件」卡片填写一个或多个条件。 +2. 点击 **查询**;列表按条件过滤,并回到第 1 页。 +3. 点击 **重置** 可清空所有筛选。 +4. 也可直接点击 KPI 卡片快速筛选: + - **全部站点** — 显示所有 + - **已签约 / 未签约** — 按签约状态 + - **高频站点** — 加氢次数 ≥ 3 + - **低频站点** — 加氢次数 1~2 + - **无加氢站点** — 加氢次数 = 0 + +### 3.3 列表排序与分页 + +- 点击 **加氢次数**、**加氢量** 列表头可升序 / 降序排序。 +- 底部分页默认每页 10 条,可切换 5 / 10 / 20 / 50。 + +--- + +## 四、站点主数据维护 + +### 4.1 整体流程 + +\`\`\`mermaid +flowchart TB + subgraph 入口 + N[新建站点] --> CP[整页新建表单] + I[批量导入] --> IM[上传模板文件] + E[更多-编辑] --> EP[整页编辑表单] + V[查看] --> VM[详情弹窗] + D[更多-删除] --> DM[确认删除] + end + CP --> S[提交创建] + EP --> U[保存修改] + IM --> CI[确认导入] +\`\`\` + +![图 4-1 新建/编辑整页表单示意](docs/h2-station-manual-create-page.png) + +### 4.2 新建站点 + +**入口**:列表工具栏 → **新建站点** + +整页表单分三块卡片: + +#### (1)加氢站基本信息 + +| 字段 | 必填 | 说明 | +|------|------|------| +| 加氢站名称 | 是 | 不可与已有站点重名 | +| 省 / 市、详细地址 | 是 | 可用「地址解析」自动解析 | +| 联系人、联系电话 | 是 | 手机或固话 | +| 是否签约 | 否 | 选「签约站点」后须填合作时间与合同附件 | + +#### (2)系统账号绑定 + +1. 选择 **绑定系统账号** 或 **暂不绑定**。 +2. 若绑定:在「选择系统账号」中**多选**账号(已绑定其他站点的不可选)。 +3. 下方可预览已选账号的登录名、姓名、角色、手机号。 + +> 绑定后,所选账号可登录系统并管理该站点。 + +#### (3)供应商相关信息 + +- **关联已有供应商** — 搜索档案绑定 +- **新增供应商** — 填写主体、开票、银行账户、营业执照与其他证件 + +**底部操作**: + +- **返回 / 取消** — 有未保存内容时会二次确认 +- **提交创建** — 底部进度条达 100% 后可提交(新建页**无「重置」按钮**) + +### 4.3 编辑站点 + +**入口**:列表操作 → **更多(⋮)→ 编辑** + +编辑页与新建页**同布局整页**,但有以下差异: + +| 项目 | 编辑页规则 | +|------|------------| +| 供应商信息 | **不展示**,不可修改 | +| 系统账号 | **仅 admin 可改**;加氢站账号登录时为只读展示 | +| 营业状态 | 不在此页维护,请用「更多 → 营业状态」 | +| 底部按钮 | **保存修改**(无重置) | + +\`\`\`mermaid +flowchart TD + A[打开编辑页] --> B{当前登录角色?} + B -->|admin| C[可改基本信息 + 系统账号多选] + B -->|加氢站账号| D[仅可改基本信息] + D --> E[系统账号只读展示] + C --> F[保存修改] + D --> F +\`\`\` + +### 4.4 查看站点 + +**入口**:列表操作 → **查看** + +弹窗分四块只读信息: + +1. 加氢站信息(含签约、营业时间等) +2. 系统账号(支持展示多个绑定账号) +3. 供应商信息 +4. 付款信息 + +### 4.5 删除站点 + +**入口**:列表操作 → **更多 → 删除** + +弹出确认框,确认后从列表移除,**不可撤销**。 + +### 4.6 批量导入 + +**入口**:列表工具栏 → **批量导入** + +\`\`\`mermaid +flowchart LR + A[下载 CSV 模板] --> B[按模板填写] + B --> C[上传 csv/xlsx/xls] + C --> D[系统校验预览] + D --> E{可导入条数>0?} + E -->|是| F[确认导入] + E -->|否| G[修正文件重传] +\`\`\` + +**注意**: + +- 站点名称不能与现有台账重复 +- 模板字段与新建表单一致 +- 上传后查看「可导入 / 不可导入」条数与错误原因 + +--- + +## 五、运营配置 + +### 5.1 营业状态 + +**入口**:更多 → **营业状态** + +\`\`\` +┌──────────────────────────────────────┐ +│ 站点信息卡(名称、地址、当前状态) │ +├──────────────────────────────────────┤ +│ 营业状态:[营业中][暂停营业][停止营业] │ +│ 营业时间:[全天营业][自定义时段] │ +├──────────────────────────────────────┤ +│ 营业状态变更记录(历史列表) │ +│ [取消] [保存] │ +└──────────────────────────────────────┘ +\`\`\` + +**操作要点**: + +1. 营业状态、营业时间均用**按钮组**点选(非下拉)。 +2. 非「全天营业」时,必须填写开始与结束时间(HH:mm)。 +3. 保存后列表「营业状态 / 营业时间」同步更新;若状态变更会写入记录表。 + +### 5.2 价格配置 + +**入口**:更多 → **价格配置** + +1. 填写 **成本价格(元/kg)** 与 **生效时间**。 +2. 保存后: + - 若已到生效时间 → 列表「当前成本价格」**立即更新** + - 若未到生效时间 → 到点后自动更新 +3. 下方展示价格调整记录(操作人、调整前后价格、生效时间)。 + +--- + +## 六、数据下钻分析 + +### 6.1 加氢量下钻 + +**入口**:列表点击 **加氢量** 数字 + +弹窗内容: + +- 统计卡:加氢次数、加氢总量(kg)、成本总价、加氢总价 +- 明细表:字段对齐「车辆氢费明细」 +- **导出 CSV** 按钮 + +### 6.2 预付余额下钻 + +**入口**:列表点击 **预付余额** 数字 + +弹窗内容: + +- 当前余额(负数显示「已欠费」) +- 充值 / 车辆加氢流水明细 +- **导出 CSV** 按钮 + +--- + +## 七、对账与结算(核心流程) + +本流程与 **车辆氢费明细** 联动,完成站点侧付款闭环。 + +### 7.1 前置条件(必知) + +生成对账单前,台账中须已有满足以下**全部**条件的记录: + +1. 对账状态 = **已对账**(业务员已在台账点击「完成对账」) +2. **尚未**被本站点历史对账单结算过 +3. 加氢时间在所选账单日期区间内 +4. 加氢站名称与当前站点一致 + +> 对账单只展示**成本侧**字段(加氢量、成本单价、成本总价),不展示客户加氢价。 + +### 7.2 生成对账单 — 两阶段操作 + +**入口**:更多 → **生成对账单** + +![图 7-1 生成对账单两阶段流程示意](docs/h2-station-manual-statement-flow.png) + +\`\`\`mermaid +flowchart TD + A[打开生成对账单] --> B[选择账单开始/结束日期] + B --> C[查看上次对账单结束时间] + C --> D[点击生成对账单] + D -->|无明细| E[提示调整日期] + D -->|有明细| F[进入结算阶段] + F --> G[查看统计卡与成本明细表] + G --> H[填写收票日期/金额/发票附件] + H --> I[点击提交对账单] + I --> J[更新余额 + 写对账历史 + 回写台账] +\`\`\` + +#### 阶段一:选期生成 + +| 步骤 | 操作 | +|------|------| +| 1 | 选择 **账单开始日期**、**账单结束日期** | +| 2 | 参考「上次对账单结束时间」(无历史显示「暂无」) | +| 3 | 点击 **生成对账单** | +| 4 | 成功后进入阶段二,**日期锁定不可改** | + +#### 阶段二:结算提交 + +**统计卡**(明细表上方): + +| 指标 | 含义 | +|------|------| +| 加氢次数 | 本账单包含的记录笔数 | +| 加氢总量 | kg 合计 | +| 成本总金额 | 成本总价合计 | + +**结算表单**(必填): + +| 字段 | 规则 | +|------|------| +| 结算后加氢站预付款余额 | **只读**,= 当前余额 − 成本总金额 | +| 收票日期 | 必填,与收票金额同一行 | +| 收票金额 | 必填,带 ¥ 前缀;默认等于成本总金额 | +| 发票附件 | 必填,支持上传 | + +点击 **提交对账单** 后系统将: + +1. 写入对账历史(供后续查询) +2. 扣减站点预付余额 +3. 标记台账记录已结算(防重复入账) +4. 回写车辆氢费明细:对账日期、收票日期、加氢站付款状态 → **已付款** + +\`\`\`mermaid +sequenceDiagram + participant 业务 as 台账业务员 + participant 财务 as 财务操作人 + participant 站点 as 站点信息 + participant 台账 as 车辆氢费明细 + + 业务->>台账: 完成对账 → 已对账 + 财务->>站点: 更多 → 生成对账单 + 站点->>台账: 拉取已对账且未结算记录 + 财务->>站点: 填写收票信息并提交 + 站点->>站点: 更新预付余额 + 站点->>台账: 回写已付款 +\`\`\` + +### 7.3 查看对账记录 + +**入口**:更多 → **查看对账记录** + +历史列表字段:对账日期、对账人、账单区间、加氢次数、加氢金额、对账后余额、操作。 + +点击 **查看明细** 可看到: + +- 账单区间与统计三卡 +- 收票时间、收票金额 +- 发票附件下载 +- 成本明细表 + +--- + +## 八、操作列「更多」菜单速查 + +| 菜单项 | 作用 | +|--------|------| +| 编辑 | 整页编辑站点主数据 | +| 营业状态 | 维护营业状态与营业时间 | +| 价格配置 | 设置成本价与生效时间 | +| 生成对账单 | 两阶段对账结算 | +| 查看对账记录 | 历史对账单与明细 | +| 删除 | 删除站点(不可恢复) | + +--- + +## 九、汇报与培训话术要点 + +向领导或客户汇报时,可围绕 **「一个中心、三条主线」** 组织: + +**一个中心**:站点全生命周期台账(档案 → 运营 → 结算) + +**三条主线**: + +1. **主数据线** — 新建 / 导入 / 编辑,保证站点档案完整准确 +2. **运营线** — 营业状态、成本价格、加氢量与余额下钻,支撑日常经营分析 +3. **结算线** — 台账已对账 → 站点生成对账单 → 收票登记 → 余额扣减 → 台账已付款,形成闭环 + +**可量化成果表述示例**: + +- 支持按 KPI(高频/低频/无加氢)快速识别运营薄弱站点 +- 对账单两阶段设计,避免选错账期;提交后自动更新余额并防重复结算 +- 多账号绑定 + 角色权限,满足平台管理与站点自治并存 + +--- + +## 十、常见问题 + +| 问题 | 解答 | +|------|------| +| 生成对账单提示无记录? | 检查台账是否「已对账」、日期区间是否正确、是否已被历史对账单结算 | +| 编辑时看不到供应商? | 编辑页 intentionally 不展示供应商,需在供应商模块或新建时维护 | +| 加氢站账号改不了系统账号? | 仅 admin 可修改绑定;加氢站账号只能查看 | +| 预付余额为负还能提交吗? | 可以;系统标「已欠费」,提醒后续充值 | +| 批量导入失败? | 检查名称重复、营业状态枚举、日期格式;建议用 CSV UTF-8 | + +--- + +## 十一、快速检查清单 + +培训验收时可按此清单实操一遍: + +- [ ] 用筛选 + KPI 找到目标站点 +- [ ] 新建一个站点(含供应商 + 多账号绑定) +- [ ] 用加氢站角色编辑,确认账号只读 +- [ ] 用 admin 编辑,调整系统账号绑定 +- [ ] 配置营业状态与成本价格 +- [ ] 下钻加氢量与预付余额并导出 CSV +- [ ] 完成一次「生成对账单 → 提交」全流程 +- [ ] 在「查看对账记录」中核对明细与发票信息 + +--- + +**文档结束** +`; + +var H2_STATION_REQUIREMENT_DOC = `# 加氢站管理 · 站点信息 — 产品需求说明(PRD) + +| 项目 | 内容 | +|------|------| +| 文档版本 | v1.0 | +| 产品模块 | 加氢站管理 → 站点信息 | +| 文档类型 | 产品需求说明 | +| 适用读者 | 研发、测试、产品、项目 | +| 关联模块 | 台账数据 → 车辆氢费明细 | + +--- + +## 一、为什么做这件事 + +### 1.1 业务背景 + +加氢站是氢费成本结算的关键节点。运营侧需要维护站点主数据(签约、营业、成本价、预付余额),并在财务周期内按站点与加氢站完成对账、收票与付款闭环。 + +### 1.2 本期目标 + +建设 Web 端「站点信息」页面,支撑: + +- 站点台账的查询、新建、编辑、查看、删除与批量导入 +- 营业状态、成本价格、预付余额等运营配置 +- **按站点生成氢费对账单 → 填写收票信息 → 提交结算**,并与「车辆氢费明细」联动回写 + +### 1.3 用户角色 + +| 角色 | 典型诉求 | +|------|----------| +| **加氢站运营** | 维护站点资料、营业与价格;查看加氢量与余额 | +| **财务/结算** | 按周期生成对账单、登记收票、完成站点付款闭环 | +| **管理员** | 批量导入站点、删除无效站点 | + +--- + +## 二、页面整体结构 + +用户进入「加氢站管理 → 站点信息」后,页面自上而下为: + +1. **筛选区** — 名称、签约、地区、营业状态 +2. **KPI 分类** — 全部 / 高频 / 低频 / 无加氢;已签约快捷筛选 +3. **站点列表** — 主数据 + 运营指标 + 操作列 +4. **弹窗/抽屉** — 查看、编辑、营业状态、价格配置、对账单、对账记录等 + +\`\`\`mermaid +flowchart TB + subgraph 列表页 + A[筛选 / KPI] --> B[站点列表] + B --> C[查看站点] + B --> D[编辑 / 删除] + B --> E[更多菜单] + end + E --> F[营业状态] + E --> G[价格配置] + E --> H[生成对账单] + E --> I[查看对账记录] + B --> J[加氢量下钻] + B --> K[预付余额下钻] +\`\`\` + +--- + +## 三、列表与 KPI 规则 + +### 3.1 列表字段 + +| 字段 | 说明 | +|------|------| +| 加氢站名称 | 含签约标签;名称下一行展示省市区+详细地址 | +| 合作时间 | 未签约可空 | +| 营业状态 / 营业时间 | 列表只读;在「更多-营业状态」维护 | +| 当前成本价格 | 元/kg;受价格配置生效时间驱动 | +| 加氢次数 / 加氢量 | 加氢量可点击下钻;列表头支持排序 | +| 预付余额 | 可点击下钻流水;负值标红并显示「已欠费」 | +| 联系方式 | 联系人 + 电话 | +| 操作 | 查看 + 更多 | + +### 3.2 KPI 划分逻辑 + +| 分类 | 规则 | +|------|------| +| 高频站点 | 加氢次数 ≥ 3 | +| 低频站点 | 加氢次数 1~2 | +| 无加氢站点 | 加氢次数 = 0 | + +筛选或 KPI 切换后列表回到第 1 页;默认分页 10 条,可选 5/10/20/50。 + +--- + +## 四、站点主数据(新建 / 编辑 / 查看) + +### 4.1 新建站点 + +同页内嵌视图,包含: + +- **基本信息**:名称、地址、营业时间、签约、联系方式 +- **系统账号绑定**:可选绑定,绑定后该账号可登录管理对应站点 +- **供应商信息**:新建或关联已有供应商、付款信息 + +### 4.2 编辑 / 查看 + +| 模式 | 交互 | +|------|------| +| 编辑 | 整页(与新建同结构);不含供应商、不含营业状态;系统账号仅 admin 可改,支持多选 | +| 查看 | 弹窗分块:加氢站信息、系统账号、供应商信息、付款信息 | + +### 4.3 批量导入 + +- 下载 CSV 模板 → 上传 .csv / .xlsx / .xls +- 站点名称不可与现有台账重复;解析预览后确认导入 + +--- + +## 五、运营配置 + +### 5.1 营业状态 + +- 卡片式弹窗:营业状态、营业时间均为**按钮组** +- 非「全天营业」须配置开始/结束时段 +- 保存时校验时段完整性;下方展示状态变更记录 + +### 5.2 价格配置 + +- 配置**成本价格(元/kg)**与**生效时间** +- 到达生效时间后自动刷新列表「当前成本价格」 +- 下方展示价格调整记录(操作人、调整前后价格、生效时间) + +### 5.3 加氢量 / 预付余额下钻 + +**加氢量下钻**:统计卡(次数、加氢量、成本总价、加氢总价)+ 明细表 + 导出 CSV,字段对齐车辆氢费明细。 + +**预付余额下钻**:充值/车辆加氢流水 + 导出 CSV;余额为负显示已欠费。 + +--- + +## 六、核心流程:生成对账单(重点) + +本模块与「车辆氢费明细」配合完成**站点侧氢费结算闭环**。研发需重点理解以下逻辑。 + +### 6.1 数据来源 + +对账单明细取自 **车辆氢费明细** 中同时满足以下条件的记录: + +1. 对账状态 = **已对账**(业务侧已在台账完成「完成对账」) +2. **尚未**被本站点历史对账单结算过(未绑定 statementRecordId) +3. 加氢时间落在用户选择的 **[账单开始日期, 账单结束日期]** 内 +4. 加氢站名称与当前操作站点一致 + +> 对账单明细**仅展示成本字段**(加氢量、成本单价、成本总价等),不展示加氢单价、加氢总价。 + +### 6.2 两阶段交互 + +\`\`\`mermaid +flowchart TD + A[打开「生成对账单」] --> B[选择账单开始/结束日期] + B --> C[展示上次对账单结束时间] + C --> D{点击「生成对账单」} + D -->|无符合条件记录| E[提示调整日期] + D -->|有记录| F[生成对账记录草稿] + F --> G[展示统计:加氢次数/总量/成本总金额] + G --> H[展示成本明细表] + H --> I[填写结算信息] + I --> J{点击「提交对账单」} + J -->|校验失败| K[提示必填项] + J -->|校验通过| L[完成闭环] +\`\`\` + +**阶段一 · 选期生成** + +- 日期区下方展示:**上次对账单结束时间 YYYY-MM-DD**(无历史则「暂无」) +- 默认开始日期建议 = 上次结束日 + 1 天 +- 点击「生成对账单」后进入阶段二,日期锁定不可改 + +**阶段二 · 结算提交** + +统计卡(明细表上方): + +| 指标 | 说明 | +|------|------| +| 加氢次数 | 本账单包含的已对账记录笔数 | +| 加氢总量 | kg 合计 | +| 成本总金额 | 成本总价合计 | + +明细表下方**必填**结算项: + +| 字段 | 规则 | +|------|------| +| 结算后加氢站预付款余额 | **只读**,= 当前预付余额 − 成本总金额;不足为负值并标「已欠费」 | +| 收票日期 | 必填,与收票金额同一行展示 | +| 收票金额 | 必填,输入框带 ¥ 前缀;默认填入成本总金额 | +| 发票附件 | 必填,支持上传 | + +### 6.3 提交后的系统行为(关键) + +提交成功后需**原子性**完成: + +1. **写入对账历史**(供「查看对账记录」查询) +2. **扣减站点预付余额** = 结算后余额 +3. **标记台账记录已结算**(绑定 statementRecordId,避免重复入账) +4. **回写车辆氢费明细**(原型通过 window 补丁 / API 同步): + +| 车辆氢费明细字段 | 回写值 | +|------------------|--------| +| 对账日期 | 操作完成时间,格式 YYYY-MM-DD HH:MM | +| 收票日期 | 操作完成日期,格式 YYYY-MM-DD | +| 加氢站付款状态 | 已付款 | + +\`\`\`mermaid +sequenceDiagram + participant 台账 as 车辆氢费明细 + participant 站点 as 站点信息 + participant 财务 as 结算操作人 + + 台账->>台账: 业务员「完成对账」→ 已对账 + 财务->>站点: 更多 → 生成对账单 + 站点->>台账: 拉取已对账且未结算记录 + 财务->>站点: 生成对账记录 + 填收票信息 + 财务->>站点: 提交对账单 + 站点->>站点: 更新预付余额 / 写对账历史 + 站点->>台账: 回写对账日期、收票日期、已付款 +\`\`\` + +--- + +## 七、查看对账记录 + +入口:列表操作列 → 更多 → **查看对账记录** + +### 7.1 历史列表字段 + +| 列 | 说明 | +|----|------| +| 对账日期 | 提交对账单的操作时间 | +| 对账人 | 当前操作人 | +| 账单开始/结束日期 | 本次账单覆盖区间 | +| 加氢次数 | 本单笔数 | +| 加氢金额 | 成本总金额 | +| 对账后加氢站预付款余额 | 结算后余额;负值标「已欠费」 | +| 操作 | 查看明细 | + +### 7.2 查看明细 + +弹窗展示: + +- 账单区间、加氢统计三卡 +- **收票时间、收票金额**(同一信息区) +- **发票附件下载** +- 成本明细表(同生成对账单字段范围) + +--- + +## 八、校验与异常 + +| 场景 | 处理 | +|------|------| +| 日期未填 / 开始晚于结束 | 阻止生成,提示用户 | +| 区间内无已对账未结算记录 | 阻止生成,提示调整日期 | +| 收票日期/金额/附件未填 | 阻止提交 | +| 收票金额 ≤ 0 | 阻止提交 | +| 预付余额结算后为负 | 允许提交,列表与明细标「已欠费」 | + +--- + +## 九、研发实现要点(给开发) + +1. **对账单幂等**:同一笔已对账记录只能进入一次成功提交的对账单(statementRecordId 约束)。 +2. **上次结束时间**:取该站点已成功提交对账单的最大 endDate 字段。 +3. **余额计算**:balanceAfter = prepaidBalance - sum(costTotal),提交后写回站点主数据。 +4. **跨模块同步**:生产环境应对接统一台账服务;原型使用 H2_STATION_STATEMENT_LEDGER_UPDATES / H2_VEHICLE_LEDGER_API 模拟回写。 +5. **明细字段范围**:对账相关弹窗统一不展示客户侧加氢单价/总价,避免与成本结算混淆。 +6. **权限**(后续迭代):生成/提交对账单建议限制财务或站点管理员角色。 + +--- + +## 十、本期不做 + +- 对账单审批流、ERP 自动过账 +- 发票 OCR 识别与验真 +- 多币种 / 多税率 +- 按组织架构的复杂数据权限(本期按功能原型描述) + +--- + +**文档结束** +`; + +function h2StationParsePrdInlineText(text) { + var parts = String(text || '').split(/(\*\*[^*]+\*\*)/g); + var nodes = []; + var i; + for (i = 0; i < parts.length; i++) { + var p = parts[i]; + if (!p) continue; + if (p.indexOf('**') === 0 && p.lastIndexOf('**') === p.length - 2) { + nodes.push(React.createElement('strong', { key: i }, p.slice(2, -2))); + } else { + nodes.push(p); + } + } + return nodes.length === 1 ? nodes[0] : nodes; +} + +function h2StationIsPrdTableRow(line) { + return /^\|.+\|$/.test(String(line || '').trim()); +} + +function h2StationIsPrdTableSep(line) { + return /^\|[\s\-:|]+\|$/.test(String(line || '').trim()); +} + +function h2StationRenderPrdTableRow(line, rowKey, isHeader) { + var cells = String(line).trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map(function (c) { return c.trim(); }); + return React.createElement('tr', { key: rowKey }, + cells.map(function (cell, ci) { + var Tag = isHeader ? 'th' : 'td'; + return React.createElement(Tag, { + key: ci, + style: { + border: '1px solid #e5e7eb', + padding: '8px 10px', + textAlign: 'left', + verticalAlign: 'top', + fontWeight: isHeader ? 600 : 400, + background: isHeader ? '#f8fafc' : '#fff', + fontSize: 13, + lineHeight: 1.5 + } + }, h2StationParsePrdInlineText(cell)); + }) + ); +} + +function h2StationRenderPrdMarkdown(markdown) { + var lines = String(markdown || '').split(/\r?\n/); + var nodes = []; + var i = 0; + var inCode = false; + var codeBuf = []; + + while (i < lines.length) { + var line = lines[i]; + var trimmed = String(line || '').trim(); + + if (trimmed.indexOf('```') === 0) { + if (inCode) { + nodes.push(React.createElement('pre', { + key: 'code-' + i, + style: { + margin: '12px 0', + padding: '12px 14px', + background: '#f6f8fa', + border: '1px solid #e5e7eb', + borderRadius: 8, + fontSize: 12, + lineHeight: 1.6, + overflow: 'auto', + color: '#334155', + whiteSpace: 'pre-wrap' + } + }, codeBuf.join('\n'))); + codeBuf = []; + inCode = false; + } else { + inCode = true; + } + i += 1; + continue; + } + + if (inCode) { + codeBuf.push(line); + i += 1; + continue; + } + + if (trimmed === '---') { + nodes.push(React.createElement('hr', { key: 'hr-' + i, style: { border: 'none', borderTop: '1px solid #e8ecf0', margin: '20px 0' } })); + i += 1; + continue; + } + + if (h2StationIsPrdTableRow(trimmed)) { + var tableLines = []; + while (i < lines.length && h2StationIsPrdTableRow(String(lines[i]).trim())) { + tableLines.push(String(lines[i]).trim()); + i += 1; + } + var bodyRows = []; + var ti; + for (ti = 0; ti < tableLines.length; ti++) { + if (h2StationIsPrdTableSep(tableLines[ti])) continue; + bodyRows.push(h2StationRenderPrdTableRow(tableLines[ti], 'tr-' + i + '-' + ti, ti === 0)); + } + if (bodyRows.length) { + nodes.push(React.createElement('div', { key: 'tbl-' + i, style: { overflowX: 'auto', margin: '12px 0 16px' } }, + React.createElement('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: 13 } }, + React.createElement('tbody', null, bodyRows) + ) + )); + } + continue; + } + + if (!trimmed) { + i += 1; + continue; + } + + if (trimmed.indexOf('# ') === 0) { + nodes.push(React.createElement('h1', { key: 'h1-' + i, style: { fontSize: 20, fontWeight: 700, color: '#0f172a', margin: '0 0 16px', lineHeight: 1.35 } }, h2StationParsePrdInlineText(trimmed.slice(2).trim()))); + i += 1; + continue; + } + + if (trimmed.indexOf('## ') === 0) { + nodes.push(React.createElement('h2', { key: 'h2-' + i, style: { fontSize: 16, fontWeight: 700, color: '#1e293b', margin: '24px 0 12px', paddingBottom: 6, borderBottom: '2px solid #e0f2fe', lineHeight: 1.4 } }, h2StationParsePrdInlineText(trimmed.slice(3).trim()))); + i += 1; + continue; + } + + if (trimmed.indexOf('### ') === 0) { + nodes.push(React.createElement('h3', { key: 'h3-' + i, style: { fontSize: 14, fontWeight: 600, color: '#334155', margin: '16px 0 8px', lineHeight: 1.45 } }, h2StationParsePrdInlineText(trimmed.slice(4).trim()))); + i += 1; + continue; + } + + if (/^!\[([^\]]*)\]\(([^)]+)\)$/.test(trimmed)) { + var imgMatch = trimmed.match(/^!\[([^\]]*)\]\(([^)]+)\)$/); + var imgAlt = imgMatch[1] || '配图'; + var imgSrc = imgMatch[2] || ''; + nodes.push(React.createElement('figure', { + key: 'img-' + i, + style: { margin: '16px 0 20px', textAlign: 'center' } + }, + React.createElement('img', { + src: imgSrc, + alt: imgAlt, + style: { + maxWidth: '100%', + borderRadius: 10, + border: '1px solid #e5e7eb', + boxShadow: '0 4px 12px rgba(15,23,42,0.06)' + }, + onError: function (e) { + if (e && e.target) { + e.target.style.display = 'none'; + } + } + }), + React.createElement('figcaption', { + style: { fontSize: 12, color: '#94a3b8', marginTop: 8, lineHeight: 1.5 } + }, imgAlt) + )); + i += 1; + continue; + } + + if (trimmed === '**文档结束**') { + nodes.push(React.createElement('div', { key: 'end-' + i, style: { marginTop: 24, paddingTop: 16, borderTop: '1px dashed #e2e8f0', color: '#94a3b8', fontSize: 13, textAlign: 'center' } }, '— 文档结束 —')); + i += 1; + continue; + } + + if (/^\d+\.\s/.test(trimmed)) { + nodes.push(React.createElement('div', { key: 'ol-' + i, style: { fontSize: 13, color: '#475569', lineHeight: 1.75, margin: '6px 0 6px 4px', paddingLeft: 4 } }, h2StationParsePrdInlineText(trimmed))); + i += 1; + continue; + } + + if (trimmed.indexOf('- ') === 0) { + nodes.push(React.createElement('div', { key: 'ul-' + i, style: { display: 'flex', gap: 8, fontSize: 13, color: '#475569', lineHeight: 1.75, margin: '4px 0 4px 2px' } }, + React.createElement('span', { style: { color: '#1677ff', flexShrink: 0 } }, '•'), + React.createElement('span', { style: { flex: 1 } }, h2StationParsePrdInlineText(trimmed.slice(2).trim())) + )); + i += 1; + continue; + } + + if (trimmed.indexOf('> ') === 0) { + nodes.push(React.createElement('div', { key: 'quote-' + i, style: { margin: '8px 0', padding: '8px 12px', borderLeft: '3px solid #c4b5fd', background: '#faf5ff', borderRadius: '0 8px 8px 0', fontSize: 13, color: '#5b21b6', lineHeight: 1.65 } }, h2StationParsePrdInlineText(trimmed.slice(2).trim()))); + i += 1; + continue; + } + + nodes.push(React.createElement('p', { key: 'p-' + i, style: { fontSize: 13, color: '#475569', lineHeight: 1.75, margin: '6px 0' } }, h2StationParsePrdInlineText(trimmed))); + i += 1; + } + + return nodes; +} + +function renderH2StationRequirementDocPanel() { + return React.createElement('div', { className: 'h2-req-doc-panel', style: { padding: '4px 4px 16px' } }, + h2StationRenderPrdMarkdown(H2_STATION_REQUIREMENT_DOC) + ); +} + +function renderH2StationUserManualDocPanel() { + return React.createElement('div', { className: 'h2-req-doc-panel h2-user-manual-panel', style: { padding: '4px 4px 16px' } }, + h2StationRenderPrdMarkdown(H2_STATION_USER_MANUAL_DOC) + ); } var H2_PRIMARY_BTN_STYLE = { @@ -1601,33 +4294,96 @@ var H2_REQ_BTN_STYLE = { color: '#475569' }; +function h2ResolveRecordSupplierMode(record) { + if (!record) return 'link'; + if (record.supplierMode === 'none') return 'none'; + if (record.supplierMode === 'link' || record.linkedSupplierId) return 'link'; + if (record.supplierMode === 'new') return 'new'; + var supplier = h2ResolveStationSupplier(record); + if (supplier && (supplier.name || '').trim()) return 'new'; + return 'none'; +} + +function h2SupplierModeLabel(mode) { + if (mode === 'link') return '关联已有供应商'; + if (mode === 'new') return '新增供应商'; + if (mode === 'none') return '不关联供应商'; + return '—'; +} + function h2CreateFormDirty(station, supplier, supplierMode, linkedSupplierId) { if ((station.name || '').trim()) return true; if ((station.contact || '').trim()) return true; if ((station.mobilePhone || '').trim() || (station.landlinePhone || '').trim()) return true; if (station.address && ((station.address.region && station.address.region.length) || (station.address.detail || '').trim())) return true; - if ((supplier.name || '').trim()) return true; - if (supplierMode === 'link' && linkedSupplierId) return true; - if ((supplier.businessLicenseFiles || []).length || (supplier.fillingLicenseFiles || []).length) return true; + if (supplierMode !== 'none') { + if ((supplier.signingCompany || '').trim()) return true; + if ((supplier.name || '').trim()) return true; + if (supplierMode === 'link' && linkedSupplierId) return true; + if ((supplier.businessLicenseFiles || []).length || (supplier.fillingLicenseFiles || []).length) return true; + } if ((station.contractFiles || []).length) return true; + if (supplierMode !== 'none' && station.bindAccountMode === 'bind' && station.bindAccountIds && station.bindAccountIds.length) return true; return false; } +function h2EditFormDirty(station, originForm) { + if (!originForm) return h2CreateFormDirty(station, h2CreateEmptySupplierForm(), 'new', undefined); + var keys = ['name', 'contact', 'mobilePhone', 'landlinePhone', 'businessHours', 'isSigned', 'contractStart', 'contractEnd', 'bindAccountMode']; + var i; + for (i = 0; i < keys.length; i++) { + if (String(station[keys[i]] || '') !== String(originForm[keys[i]] || '')) return true; + } + if (JSON.stringify(station.address || {}) !== JSON.stringify(originForm.address || {})) return true; + if (JSON.stringify(station.bindAccountIds || []) !== JSON.stringify(originForm.bindAccountIds || [])) return true; + if ((station.contractFiles || []).length !== (originForm.contractFiles || []).length) return true; + return false; +} + +function h2EditFormProgress(station) { + var checks = [ + !!(station.name || '').trim(), + station.address && station.address.region && station.address.region.length >= 2 && !!(station.address.detail || '').trim(), + !!(station.contact || '').trim(), + !!((station.mobilePhone || '').trim() || (station.landlinePhone || '').trim()) + ]; + var done = checks.filter(Boolean).length; + return Math.round((done / checks.length) * 100); +} + function h2CreateFormProgress(station, supplier, supplierMode, linkedSupplierId) { var checks = [ !!(station.name || '').trim(), station.address && station.address.region && station.address.region.length >= 2 && !!(station.address.detail || '').trim(), !!(station.contact || '').trim(), !!((station.mobilePhone || '').trim() || (station.landlinePhone || '').trim()), - supplierMode === 'link' ? !!linkedSupplierId : !!(supplier.name || '').trim(), - supplierMode === 'new' - ? (supplier.businessLicenseFiles || []).length > 0 && (supplier.fillingLicenseFiles || []).length > 0 - : !!linkedSupplierId + supplierMode === 'none' ? true : (supplierMode === 'link' ? !!linkedSupplierId : !!(supplier.name || '').trim()) ]; var done = checks.filter(Boolean).length; return Math.round((done / checks.length) * 100); } +function h2CardTitleWithStep(step, text) { + return React.createElement('span', { className: 'h2-card-title-bar h2-card-title-bar--step' }, + React.createElement('span', { className: 'h2-card-step-badge', 'aria-hidden': true }, step), + React.createElement('span', { className: 'h2-card-title-text' }, text) + ); +} + +function h2MaskMobile(mobile) { + if (!mobile) return '—'; + var s = String(mobile); + if (s.length >= 11) return s.slice(0, 3) + '****' + s.slice(-4); + return s; +} + +function h2AccountRoleTagColor(role) { + var text = String(role || ''); + if (text.indexOf('管理') >= 0) return 'success'; + if (text.indexOf('运营') >= 0) return 'processing'; + return 'default'; +} + function h2BuildStationCreateView(ctx) { var antd = window.antd; var Card = antd.Card; @@ -1639,16 +4395,20 @@ function h2BuildStationCreateView(ctx) { var DatePicker = antd.DatePicker; var Upload = antd.Upload; var Form = antd.Form; - var Progress = antd.Progress; var Row = antd.Row; var Col = antd.Col; + var Tag = antd.Tag; var Divider = antd.Divider; - var Breadcrumb = antd.Breadcrumb; var dayjs = window.dayjs; var inputCls = 'h2-create-input'; - var progressPct = h2CreateFormProgress(ctx.station, ctx.supplier, ctx.supplierMode, ctx.linkedSupplierId); + var pageMode = ctx.pageMode || 'create'; + var isEdit = pageMode === 'edit'; + var showSupplier = ctx.showSupplier !== false; + var accountEditable = ctx.accountEditable !== false; var stationAddr = ctx.station.address || { region: [], detail: '' }; + var bindAccountIds = ctx.station.bindAccountIds || []; + var showAccountBind = ctx.supplierMode !== 'none'; var formItem = function (label, required, node, extra) { return React.createElement(Form.Item, { @@ -1688,40 +4448,81 @@ function h2BuildStationCreateView(ctx) { return formRow(col24(React.createElement('div', { className: 'h2-create-subsection-title' }, text))); }; - var cardTitle = function (text, icon) { - return React.createElement('span', { className: 'h2-card-title-bar' }, - icon ? React.createElement('span', { className: 'h2-card-title-icon', 'aria-hidden': true }, icon) : null, - text - ); - }; - - var supplierModeCard = function (mode, icon, title, desc) { - var active = ctx.supplierMode === mode; - return React.createElement('button', { - type: 'button', - className: 'h2-supplier-mode-card' + (active ? ' h2-supplier-mode-card--active' : ''), - 'aria-pressed': active, - 'aria-label': title + ':' + desc, - onClick: function () { ctx.handleSupplierModeChange({ target: { value: mode } }); } - }, - React.createElement('span', { className: 'h2-supplier-mode-card-icon', 'aria-hidden': true }, icon), - React.createElement('span', { className: 'h2-supplier-mode-card-body' }, - React.createElement('div', { className: 'h2-supplier-mode-card-title' }, title), - React.createElement('div', { className: 'h2-supplier-mode-card-desc' }, desc) + var renderAccountBindLayout = function () { + var removeAccount = function (accountId) { + var nextIds = bindAccountIds.filter(function (id) { return id !== accountId; }); + ctx.updateStation({ + bindAccountMode: nextIds.length ? 'bind' : 'none', + bindAccountIds: nextIds + }); + }; + return React.createElement('div', { className: 'h2-account-bind-layout' }, + React.createElement('div', { className: 'h2-account-bind-side' }, + formItem('绑定系统账号', false, React.createElement(Select, { + className: inputCls, + mode: 'multiple', + showSearch: true, + placeholder: '请选择要绑定的系统账号', + value: bindAccountIds, + options: systemAccountOptions, + optionFilterProp: 'label', + maxTagCount: 'responsive', + onChange: function (v) { + var ids = v || []; + ctx.updateStation({ bindAccountMode: ids.length ? 'bind' : 'none', bindAccountIds: ids }); + }, + dropdownStyle: { borderRadius: 8 } + })), + React.createElement('p', { className: 'h2-account-bind-side__hint' }, '可选择多个账号,绑定后可使用对应账号登录系统'), + React.createElement('div', { className: 'h2-account-bind-selected-row' }, + React.createElement('span', { className: 'h2-account-bind-selected-label' }, '已选账号'), + React.createElement(Tag, { color: 'success', className: 'h2-account-bind-selected-count' }, selectedSystemAccounts.length + ' 个') + ) + ), + React.createElement('div', { className: 'h2-account-bind-preview-area' }, + React.createElement('div', { className: 'h2-account-bind-preview-title' }, '账号预览'), + selectedSystemAccounts.length + ? React.createElement('div', { className: 'h2-account-bind-preview-cards' }, + selectedSystemAccounts.map(function (acc) { + return React.createElement('div', { key: acc.id, className: 'h2-account-bind-user-card' }, + React.createElement('button', { + type: 'button', + className: 'h2-account-bind-user-card__remove', + 'aria-label': '移除账号 ' + (acc.displayName || acc.username || ''), + onClick: function () { removeAccount(acc.id); } + }, '×'), + React.createElement('span', { className: 'h2-account-bind-user-card__avatar', 'aria-hidden': true }, H2_ICONS.user), + React.createElement('div', { className: 'h2-account-bind-user-card__body' }, + React.createElement('div', { className: 'h2-account-bind-user-card__name-row' }, + React.createElement('span', { className: 'h2-account-bind-user-card__name' }, acc.displayName || acc.username || '—'), + acc.role + ? React.createElement(Tag, { color: h2AccountRoleTagColor(acc.role), className: 'h2-account-bind-user-card__role' }, acc.role) + : null + ), + React.createElement('div', { className: 'h2-account-bind-user-card__meta' }, '账号:' + (acc.username || '—')), + React.createElement('div', { className: 'h2-account-bind-user-card__meta' }, '手机号:' + h2MaskMobile(acc.mobile)) + ) + ); + }) + ) + : React.createElement('div', { className: 'h2-account-bind-preview-empty' }, '请在左侧选择系统账号,预览将显示在此处') ) ); }; - var uploadDragger = function (fileList, onChange, btnText, hint, disabled) { - return React.createElement(Upload.Dragger, { - className: 'h2-create-upload', - multiple: true, + var uploadDragger = function (fileList, onChange, btnText, hint, disabled, options) { + options = options || {}; + var uploadProps = { + className: 'h2-create-upload' + (disabled ? ' h2-create-upload--readonly' : ''), + multiple: options.multiple !== false, disabled: disabled, fileList: fileList, beforeUpload: function () { return false; }, onChange: onChange, showUploadList: true - }, + }; + if (options.maxCount != null) uploadProps.maxCount = options.maxCount; + return React.createElement(Upload.Dragger, uploadProps, React.createElement('p', { className: 'ant-upload-drag-icon', style: { marginBottom: 8 } }, H2_ICONS.upload), React.createElement('p', { style: { margin: 0, fontWeight: 600, color: '#334155' } }, btnText), React.createElement('p', { className: 'h2-create-upload-hint' }, hint) @@ -1734,6 +4535,229 @@ function h2BuildStationCreateView(ctx) { label: s.name + (s.type ? '(' + s.type + ')' : '') + (s.city && s.city.length ? ' · ' + s.city.join('-') : '') }; }); + var lingniuSigningCompanyOptions = h2GetLingniuSigningCompanyOptions(); + + var supplierReadonlyInput = function (value, placeholder) { + return React.createElement(Input, { + className: inputCls + ' h2-create-readonly-input', + value: value != null && value !== '' ? String(value) : '', + placeholder: placeholder || '—', + disabled: true + }); + }; + + var renderSigningCompanySelect = function (supplier) { + return formItem('签约公司', true, React.createElement(Select, { + className: inputCls, + showSearch: true, + placeholder: '请选择羚牛签约主体', + value: supplier.signingCompany, + options: lingniuSigningCompanyOptions, + optionFilterProp: 'label', + onChange: function (v) { ctx.updateSupplier({ signingCompany: v }); }, + dropdownStyle: { borderRadius: 8 } + })); + }; + + var renderSupplierDetailFields = function (opts) { + opts = opts || {}; + var supplier = opts.supplier || ctx.supplier; + var readonly = !!opts.readonly; + var licensesEditable = !!opts.licensesEditable; + var licensesReadonly = readonly && !licensesEditable; + var rows = [ + gridSubsection('主体信息') + ]; + if (!readonly) { + rows.push(formRow(col16(renderSigningCompanySelect(supplier)))); + } + rows.push( + formRow( + col8(formItem('供应商名称', true, readonly + ? supplierReadonlyInput(supplier.name) + : React.createElement(Input, { + className: inputCls, + value: supplier.name, + placeholder: '请输入供应商名称', + onChange: function (e) { ctx.updateSupplier({ name: e.target.value }); } + }))), + col8(formItem('省市', false, supplierReadonlyInput( + supplier.city && supplier.city.length ? h2FormatRegion(supplier.city) : '', + '自动从地址解析' + ))), + col8(formItem('详细地址', false, supplierReadonlyInput(supplier.address, '自动从地址解析'))) + ) + ); + if (!readonly) { + rows.push(formRow( + col24(formItem('地址解析', false, React.createElement(AddressPasteInput, { + inputClassName: inputCls, + onParsed: function (parsed) { + ctx.updateSupplier({ + city: parsed.region, + address: parsed.detail + }); + } + }))) + )); + } + rows.push( + gridSubsection('开票信息'), + formRow( + col8(formItem('纳税人识别号', false, readonly + ? supplierReadonlyInput(supplier.taxId) + : React.createElement(Input, { + className: inputCls, + value: supplier.taxId, + placeholder: '请输入纳税人识别号', + onChange: function (e) { ctx.updateSupplier({ taxId: e.target.value }); } + }))), + col8(formItem('注册电话', false, readonly + ? supplierReadonlyInput(supplier.invoicePhone) + : React.createElement(Input, { + className: inputCls, + value: supplier.invoicePhone, + placeholder: '手机号或固定电话', + type: 'tel', + onChange: function (e) { ctx.updateSupplier({ invoicePhone: e.target.value }); } + }))), + col8(formItem('注册地址', false, readonly + ? supplierReadonlyInput(supplier.invoiceAddress) + : React.createElement(Input, { + className: inputCls, + value: supplier.invoiceAddress, + placeholder: '请输入注册地址', + onChange: function (e) { ctx.updateSupplier({ invoiceAddress: e.target.value }); } + }))) + ), + gridSubsection('银行账户'), + formRow( + col8(formItem('开户行', false, readonly + ? supplierReadonlyInput(supplier.bankName) + : React.createElement(Input, { + className: inputCls, + value: supplier.bankName, + placeholder: '请输入开户行', + onChange: function (e) { ctx.updateSupplier({ bankName: e.target.value }); } + }))), + col16(formItem('银行账号', false, readonly + ? supplierReadonlyInput(supplier.bankAccount) + : React.createElement(Input, { + className: inputCls, + value: supplier.bankAccount, + placeholder: '请输入银行账号', + onChange: function (e) { ctx.updateSupplier({ bankAccount: e.target.value }); } + }))) + ), + gridSubsection('资质证照'), + formRow( + col12(formItem('营业执照', false, uploadDragger( + supplier.businessLicenseFiles, + function (info) { + ctx.updateSupplier({ businessLicenseFiles: info.fileList }); + if (licensesReadonly) return; + if (info.file && info.file.status !== 'removed') { + h2TriggerBusinessLicenseOcr(info.file, ctx); + } + }, + licensesReadonly ? '已关联证照' : '点击或拖拽上传', + licensesReadonly ? '仅展示,不可修改' : '支持 OCR 识别,上传后自动反写名称、税号、通讯地址', + licensesReadonly, + { multiple: false } + ))), + col12(formItem('其他证件', false, uploadDragger( + supplier.fillingLicenseFiles, + licensesReadonly ? function () {} : function (info) { ctx.updateSupplier({ fillingLicenseFiles: info.fileList }); }, + licensesReadonly ? '已关联证照' : '点击或拖拽上传', + licensesReadonly ? '仅展示,不可修改' : '支持多文件上传,不限数量,PDF、图片', + licensesReadonly, + { multiple: true } + ))) + ) + ); + return gridBlock.apply(null, rows); + }; + + var renderSupplierLinkForm = function () { + return React.createElement(React.Fragment, null, + gridBlock( + formRow( + col16(formItem('选择供应商', true, React.createElement(Select, { + className: inputCls, + showSearch: true, + placeholder: '请搜索并选择供应商', + value: ctx.linkedSupplierId, + options: supplierOptions, + optionFilterProp: 'label', + onChange: ctx.handleLinkSupplierChange, + dropdownStyle: { borderRadius: 8 } + }))), + col8(renderSigningCompanySelect(ctx.supplier)) + ) + ), + ctx.linkedSupplierId + ? React.createElement('div', { className: 'h2-supplier-linked-preview' }, + renderSupplierDetailFields({ readonly: true, licensesEditable: true, supplier: ctx.supplier })) + : null + ); + }; + + var renderSupplierNewForm = function () { + return renderSupplierDetailFields({ readonly: false }); + }; + + var renderSupplierNonePanel = function () { + return gridBlock( + formRow( + col24(React.createElement('p', { className: 'h2-supplier-none-hint' }, '当前不关联供应商,无需填写或关联任何供应商信息。')) + ) + ); + }; + + var renderSupplierTabs = function () { + var tabs = [ + { key: 'link', label: '关联已有' }, + { key: 'new', label: '新建供应商' }, + { key: 'none', label: '不关联供应商' } + ]; + return React.createElement('div', { className: 'h2-supplier-tabs' }, + React.createElement('div', { className: 'h2-supplier-tabs__nav', role: 'tablist', 'aria-label': '供应商关联方式' }, + tabs.map(function (tab) { + var active = ctx.supplierMode === tab.key; + return React.createElement('button', { + type: 'button', + key: tab.key, + role: 'tab', + className: 'h2-supplier-tabs__item' + (active ? ' h2-supplier-tabs__item--active' : ''), + 'aria-selected': active, + onClick: function () { + if (ctx.supplierMode !== tab.key) { + ctx.handleSupplierModeChange({ target: { value: tab.key } }); + } + } + }, tab.label); + }) + ), + React.createElement('div', { className: 'h2-supplier-tabs__panel', role: 'tabpanel' }, + ctx.supplierMode === 'link' + ? renderSupplierLinkForm() + : ctx.supplierMode === 'new' + ? renderSupplierNewForm() + : renderSupplierNonePanel() + ) + ); + }; + + var usedBindAccountMap = ctx.usedBindAccountMap || {}; + var systemAccountOptions = H2_MOCK_SYSTEM_ACCOUNTS.filter(function (a) { + return !usedBindAccountMap[a.id]; + }).map(function (a) { + return { + value: a.id, + label: '「' + (a.username || '—') + '」「' + (a.role || '—') + '」' + }; + }); + var selectedSystemAccounts = h2BuildBoundAccountsFromIds(bindAccountIds); var contractDateRangeValue = null; if (dayjs && ctx.station.contractStart && ctx.station.contractEnd) { @@ -1742,6 +4766,15 @@ function h2BuildStationCreateView(ctx) { if (ds.isValid() && de.isValid()) contractDateRangeValue = [ds, de]; } + var mobilePhoneVal = (ctx.station.mobilePhone || '').trim(); + var landlinePhoneVal = (ctx.station.landlinePhone || '').trim(); + var mobilePhoneError = mobilePhoneVal && !h2IsMobilePhone(mobilePhoneVal) + ? h2GetMobilePhoneInputTip(mobilePhoneVal) + : ''; + var landlinePhoneError = landlinePhoneVal && !h2IsLandlinePhone(landlinePhoneVal) + ? h2GetLandlinePhoneInputTip(landlinePhoneVal) + : ''; + var renderContractDates = function () { if (dayjs && DatePicker && DatePicker.RangePicker) { return React.createElement(DatePicker.RangePicker, { @@ -1783,131 +4816,115 @@ function h2BuildStationCreateView(ctx) { return React.createElement(React.Fragment, null, React.createElement('div', { className: 'h2-create-shell' }, - React.createElement('div', { style: { marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 12 } }, - React.createElement(Breadcrumb, { items: [{ title: '加氢站管理' }, { title: '站点信息' }, { title: '新建加氢站点' }] }), + React.createElement('div', { className: 'h2-create-back-only' }, React.createElement(Button, { + className: 'h2-create-back-btn', type: 'default', - icon: ctx.reqIcon, - style: ctx.reqBtnStyle, - onClick: ctx.onOpenRequirement, - 'aria-label': '查看需求说明' - }, '查看需求说明') - ), - React.createElement('div', { className: 'h2-create-pagehead' }, - React.createElement('div', { className: 'h2-create-pagehead-left' }, - React.createElement(Button, { - className: 'h2-create-back-btn', - type: 'default', - icon: H2_ICONS.back, - onClick: ctx.onCancel, - disabled: ctx.submitting, - style: { borderRadius: 8 } - }, '返回'), - React.createElement('div', null, - React.createElement('h1', { className: 'h2-create-pagehead-title' }, '新建加氢站点'), - React.createElement('p', { className: 'h2-create-pagehead-desc' }, '填写站点基础信息并关联供应商') - ) - ) + icon: H2_ICONS.back, + onClick: ctx.onCancel, + disabled: ctx.submitting + }, '返回') ), React.createElement(Form, { layout: 'vertical', requiredMark: false, className: 'h2-create-form h2-create-form--list h2-create-form--grid' }, React.createElement(Card, { className: 'h2-create-card', id: 'h2-create-station-card', - title: cardTitle('加氢站基本信息', H2_ICONS.building), + title: h2CardTitleWithStep(1, '加氢站基本信息'), bordered: false }, gridBlock( formRow( - col24(formItem('加氢站名称', true, React.createElement(Input, { + col8(formItem('加氢站名称', true, React.createElement(Input, { className: inputCls, value: ctx.station.name, placeholder: '请输入加氢站名称', maxLength: 80, onChange: function (e) { ctx.updateStation({ name: e.target.value }); }, onBlur: ctx.syncSupplierFromStation - }))) - ), - formRow( - col8(formItem('省 / 市', true, React.createElement(Cascader, { - className: inputCls, - options: H2_REGION_CASCADER_OPTIONS, - value: stationAddr.region && stationAddr.region.length ? stationAddr.region : undefined, - onChange: function (v) { - ctx.updateStation({ - address: { region: v || [], detail: stationAddr.detail || '' } - }); - }, - placeholder: '请选择省 / 市', - allowClear: true }))), - col16(formItem('详细地址', true, React.createElement(Input, { - className: inputCls, + col8(formItem('省市', true, React.createElement(Input, { + className: inputCls + ' h2-create-readonly-input', + value: stationAddr.region && stationAddr.region.length ? h2FormatRegion(stationAddr.region) : '', + placeholder: '自动从地址解析', + disabled: true + }))), + col8(formItem('通讯地址', true, React.createElement(Input, { + className: inputCls + ' h2-create-readonly-input', value: stationAddr.detail || '', - placeholder: '街道、门牌号等', - maxLength: 200, - onChange: function (e) { - ctx.updateStation({ - address: { region: stationAddr.region || [], detail: e.target.value } - }); - }, - onBlur: ctx.syncSupplierFromStation + placeholder: '自动从地址解析', + disabled: true }))) ), formRow( - col24(formItem('快速粘贴地址', false, React.createElement(AddressPasteInput, { + col24(formItem('地址解析', false, React.createElement(AddressPasteInput, { inputClassName: inputCls, onParsed: function (parsed) { ctx.updateStation({ address: { region: parsed.region, detail: parsed.detail } }); if (ctx.supplierMode === 'new' && parsed.region && parsed.region.length >= 2) { ctx.updateSupplier({ city: parsed.region.slice(), - address: parsed.detail || '', - region: h2GetRegionByProvince(parsed.region[0]) + address: parsed.detail || '' }); } } }))) ), formRow( - col12(formItem('联系人', true, React.createElement(Input, { + col8(formItem('联系人', true, React.createElement(Input, { className: inputCls, value: ctx.station.contact, placeholder: '请输入联系人', onChange: function (e) { ctx.updateStation({ contact: e.target.value }); }, onBlur: ctx.syncSupplierFromStation }))), - col12(formItem('联系电话', true, React.createElement(Input, { + col8(formItem('手机号', false, React.createElement(Input, { className: inputCls, - value: h2StationContactPhone(ctx.station), - placeholder: '手机号或固定电话', + value: ctx.station.mobilePhone || '', + placeholder: '请输入手机号码', type: 'tel', autoComplete: 'tel', - onChange: function (e) { ctx.updateStation(h2ApplyStationContactPhone(ctx.station, e.target.value)); }, + maxLength: 11, + status: mobilePhoneError ? 'error' : undefined, + onChange: function (e) { + ctx.updateStation({ mobilePhone: h2FilterMobilePhoneInput(e.target.value) }); + }, onBlur: ctx.syncSupplierFromStation - }))) + }), mobilePhoneError)), + col8(formItem('固定电话', false, React.createElement(Input, { + className: inputCls, + value: ctx.station.landlinePhone || '', + placeholder: '请输入固定电话', + type: 'tel', + autoComplete: 'tel', + status: landlinePhoneError ? 'error' : undefined, + onChange: function (e) { + ctx.updateStation({ landlinePhone: h2FilterLandlinePhoneInput(e.target.value) }); + }, + onBlur: ctx.syncSupplierFromStation + }), landlinePhoneError)) ), formRow( - col24(formItem('是否签约站点', false, React.createElement(Radio.Group, { - className: 'h2-create-radio-group', - value: ctx.station.isSigned ? 'signed' : 'unsigned', - onChange: function (e) { - var signed = e.target.value === 'signed'; + col8(formItem('站点类型', false, React.createElement('div', { className: 'h2-station-type-field' }, + h2RenderOptionButtonGroup([ + { label: '签约站点', value: 'signed', tone: 'allday' }, + { label: '普通站点', value: 'unsigned', tone: 'custom' } + ], ctx.station.isSigned ? 'signed' : 'unsigned', function (v) { + var signed = v === 'signed'; ctx.updateStation({ isSigned: signed, contractStart: signed ? ctx.station.contractStart : '', contractEnd: signed ? ctx.station.contractEnd : '' }); - } - }, - React.createElement(Radio, { value: 'signed' }, '签约站点'), - React.createElement(Radio, { value: 'unsigned' }, '非签约站点') - ))) + }, { ariaLabel: '站点类型' }) + ))), + ctx.station.isSigned + ? col16(formItem('合作时间', true, renderContractDates())) + : null ), ctx.station.isSigned ? formRow( - col12(formItem('签约时间', true, renderContractDates())), - col12(formItem('合同附件', false, React.createElement('div', { className: 'h2-contract-panel' }, + col24(formItem('合同附件', false, React.createElement('div', { className: 'h2-contract-panel' }, React.createElement(ContractFilesUpload, { fileList: ctx.station.contractFiles, onChange: function (info) { ctx.updateStation({ contractFiles: info.fileList }); } @@ -1918,162 +4935,51 @@ function h2BuildStationCreateView(ctx) { ) ), - React.createElement(Card, { + showSupplier ? React.createElement(Card, { className: 'h2-create-card', id: 'h2-create-supplier-card', - title: cardTitle('供应商相关信息', H2_ICONS.truck), + title: h2CardTitleWithStep(2, '供应商相关信息'), + bordered: false + }, + renderSupplierTabs() + ) : null, + + showSupplier && showAccountBind ? React.createElement(Card, { + className: 'h2-create-card', + id: 'h2-create-account-card', + title: h2CardTitleWithStep(3, '系统账号绑定'), bordered: false }, gridBlock( - formRow( - col24(formItem('关联方式', false, React.createElement('div', { className: 'h2-supplier-mode-picker-row' }, - supplierModeCard('link', H2_ICONS.link, '关联已有供应商', '从档案中搜索并绑定'), - supplierModeCard('new', H2_ICONS.plus, '新增供应商', '填写证照与开票信息') - ))) - ), - ctx.supplierMode === 'link' + !accountEditable && isEdit ? formRow( - col16(formItem('选择供应商', true, React.createElement(Select, { - className: inputCls, - showSearch: true, - placeholder: '请搜索并选择供应商', - value: ctx.linkedSupplierId, - options: supplierOptions, - optionFilterProp: 'label', - onChange: ctx.handleLinkSupplierChange, - dropdownStyle: { borderRadius: 8 } - }))) + col24(React.createElement('div', { className: 'h2-account-bind-readonly-wrap' }, + React.createElement('p', { className: 'h2-account-bind-hint', style: { marginBottom: 10 } }, + '当前为加氢站账号登录,系统账号绑定仅管理员(admin)可修改。' + ), + selectedSystemAccounts.length + ? selectedSystemAccounts.map(function (acc) { + return React.createElement('div', { key: acc.id, className: 'h2-account-bind-readonly-item' }, + React.createElement('div', { className: 'h2-account-bind-readonly-item__name' }, acc.displayName || acc.username || '—'), + React.createElement('div', { className: 'h2-account-bind-readonly-item__meta' }, + (acc.username || '—') + ' · ' + (acc.role || '—') + ' · ' + (acc.mobile || '—') + ) + ); + }) + : React.createElement('p', { className: 'h2-account-bind-hint' }, '暂未绑定系统账号') + )) ) : null, - ctx.supplierMode === 'new' - ? React.createElement(React.Fragment, null, - gridSubsection('主体信息'), - formRow( - col24(formItem('供应商名称', true, React.createElement(Input, { - className: inputCls, - value: ctx.supplier.name, - placeholder: '请输入供应商名称', - onChange: function (e) { ctx.updateSupplier({ name: e.target.value }); } - }))) - ), - formRow( - col12(formItem('省 / 市', false, React.createElement(Cascader, { - className: inputCls, - options: H2_REGION_CASCADER_OPTIONS, - value: ctx.supplier.city && ctx.supplier.city.length ? ctx.supplier.city : undefined, - onChange: ctx.handleSupplierCityChange, - placeholder: '请选择省 / 市', - allowClear: true - }))), - col12(formItem('所属区域', false, React.createElement(Input, { - className: inputCls, - value: ctx.supplier.region, - disabled: true, - placeholder: '根据省市自动关联' - }))) - ), - formRow( - col24(formItem('详细地址', false, React.createElement(Input, { - className: inputCls, - value: ctx.supplier.address, - placeholder: '街道、门牌号等', - maxLength: 200, - onChange: function (e) { ctx.updateSupplier({ address: e.target.value }); } - }))) - ), - formRow( - col24(formItem('快速粘贴地址', false, React.createElement(AddressPasteInput, { - inputClassName: inputCls, - onParsed: function (parsed) { - ctx.updateSupplier({ - city: parsed.region, - address: parsed.detail, - region: parsed.region && parsed.region[0] ? h2GetRegionByProvince(parsed.region[0]) : '' - }); - } - }))) - ), - gridSubsection('开票信息'), - formRow( - col8(formItem('纳税人识别号', false, React.createElement(Input, { - className: inputCls, - value: ctx.supplier.taxId, - placeholder: '请输入纳税人识别号', - onChange: function (e) { ctx.updateSupplier({ taxId: e.target.value }); } - }))), - col8(formItem('注册电话', false, React.createElement(Input, { - className: inputCls, - value: ctx.supplier.invoicePhone, - placeholder: '手机号或固定电话', - type: 'tel', - onChange: function (e) { ctx.updateSupplier({ invoicePhone: e.target.value }); } - }))), - col8(formItem('注册地址', false, React.createElement(Input, { - className: inputCls, - value: ctx.supplier.invoiceAddress, - placeholder: '请输入注册地址', - onChange: function (e) { ctx.updateSupplier({ invoiceAddress: e.target.value }); } - }))) - ), - gridSubsection('银行账户'), - formRow( - col8(formItem('开户行', false, React.createElement(Input, { - className: inputCls, - value: ctx.supplier.bankName, - placeholder: '请输入开户行', - onChange: function (e) { ctx.updateSupplier({ bankName: e.target.value }); } - }))), - col8(formItem('银行账号', false, React.createElement(Input, { - className: inputCls, - value: ctx.supplier.bankAccount, - placeholder: '请输入银行账号', - onChange: function (e) { ctx.updateSupplier({ bankAccount: e.target.value }); } - }))), - col8(formItem('营业地址', false, React.createElement(Input, { - className: inputCls, - value: ctx.supplier.businessAddress, - placeholder: '请输入营业地址', - onChange: function (e) { ctx.updateSupplier({ businessAddress: e.target.value }); } - }))) - ), - gridSubsection('资质证照'), - formRow( - col12(formItem('营业执照', true, uploadDragger( - ctx.supplier.businessLicenseFiles, - function (info) { ctx.updateSupplier({ businessLicenseFiles: info.fileList }); }, - '点击或拖拽上传', - '支持 PDF、图片', - false - ))), - col12(formItem('充装许可证', true, uploadDragger( - ctx.supplier.fillingLicenseFiles, - function (info) { ctx.updateSupplier({ fillingLicenseFiles: info.fileList }); }, - '点击或拖拽上传', - '支持 PDF、图片', - false - ))) - ) - ) + accountEditable + ? formRow(col24(renderAccountBindLayout())) : null ) - ) + ) : null ) ), React.createElement('footer', { className: 'h2-create-footer' }, React.createElement('div', { className: 'h2-create-footer-inner' }, - React.createElement('div', { className: 'h2-create-footer-hint' }, - React.createElement(Progress, { - className: 'h2-create-footer-progress', - type: 'line', - percent: progressPct, - strokeColor: { from: '#34d399', to: '#059669' }, - trailColor: '#e2e8f0', - size: 'small', - 'aria-label': '表单完成度 ' + progressPct + '%' - }), - React.createElement('span', null, progressPct >= 100 ? '必填项已就绪,可提交' : '请完善必填项后提交(' + progressPct + '%)') - ), React.createElement('div', { className: 'h2-create-footer-actions' }, React.createElement(Button, { onClick: ctx.onCancel, @@ -2081,19 +4987,19 @@ function h2BuildStationCreateView(ctx) { style: { borderRadius: 8 }, 'aria-label': '取消并返回列表' }, '取消'), - React.createElement(Button, { + ctx.showReset ? React.createElement(Button, { onClick: ctx.onReset, disabled: ctx.submitting, style: { borderRadius: 8 }, 'aria-label': '重置表单' - }, '重置'), + }, '重置') : null, React.createElement(Button, { type: 'primary', style: H2_PRIMARY_BTN_STYLE, onClick: ctx.onSubmit, loading: ctx.submitting, - 'aria-label': '提交新建加氢站点' - }, '提交创建') + 'aria-label': isEdit ? '保存编辑加氢站点' : '提交新建加氢站点' + }, ctx.submitLabel || (isEdit ? '保存修改' : '提交创建')) ) ) ) @@ -2113,8 +5019,8 @@ const Component = function () { var Select = antd.Select; var Input = antd.Input; var Space = antd.Space; + var Image = antd.Image; var Modal = antd.Modal; - var Drawer = antd.Drawer; var Form = antd.Form; var Switch = antd.Switch; var Upload = antd.Upload; @@ -2123,11 +5029,16 @@ const Component = function () { var Divider = antd.Divider; var Alert = antd.Alert; var Dropdown = antd.Dropdown; + var Descriptions = antd.Descriptions; var Cascader = antd.Cascader; var Radio = antd.Radio; + var DatePicker = antd.DatePicker; var message = antd.message; + var dayjs = window.dayjs; - var listState = useState(H2_MOCK_STATIONS.slice()); + var listState = useState(function () { + return h2ApplyDueCostPricesToList(H2_MOCK_STATIONS.slice()).records; + }); var listData = listState[0]; var setListData = listState[1]; @@ -2143,22 +5054,26 @@ const Component = function () { var appliedFilters = appliedFiltersState[0]; var setAppliedFilters = appliedFiltersState[1]; - var drawerState = useState({ open: false, mode: 'create', record: null }); - var drawer = drawerState[0]; - var setDrawer = drawerState[1]; - - var formState = useState(h2CreateEmptyForm()); - var form = formState[0]; - var setForm = formState[1]; - var deleteModalState = useState({ open: false, record: null }); var deleteModal = deleteModalState[0]; var setDeleteModal = deleteModalState[1]; + var viewModalState = useState({ open: false, record: null }); + var viewModal = viewModalState[0]; + var setViewModal = viewModalState[1]; + + var filePreviewModalState = useState({ open: false, url: '', name: '', type: 'image' }); + var filePreviewModal = filePreviewModalState[0]; + var setFilePreviewModal = filePreviewModalState[1]; + var requirementModalState = useState(false); var requirementModalOpen = requirementModalState[0]; var setRequirementModalOpen = requirementModalState[1]; + var userManualModalState = useState(false); + var userManualModalOpen = userManualModalState[0]; + var setUserManualModalOpen = userManualModalState[1]; + var importModalState = useState(false); var importModalOpen = importModalState[0]; var setImportModalOpen = importModalState[1]; @@ -2179,22 +5094,65 @@ const Component = function () { var pageSize = pageSizeState[0]; var setPageSize = pageSizeState[1]; + var refuelSortState = useState({ key: null, order: null }); + var refuelSort = refuelSortState[0]; + var setRefuelSort = refuelSortState[1]; + var businessModalState = useState({ open: false, record: null, businessStatus: '营业中', businessHours: '', originBusinessStatus: '营业中', statusLogs: [] }); var businessModal = businessModalState[0]; var setBusinessModal = businessModalState[1]; - var priceModalState = useState({ open: false, record: null, costUnitPrice: '', customerUnitPrice: '' }); + var priceModalState = useState({ open: false, record: null, costUnitPrice: '', effectiveTime: '', priceLogs: [] }); var priceModal = priceModalState[0]; var setPriceModal = priceModalState[1]; + var balanceAlertModalState = useState({ open: false, record: null, threshold: '' }); + var balanceAlertModal = balanceAlertModalState[0]; + var setBalanceAlertModal = balanceAlertModalState[1]; + + var rechargeModalState = useState({ open: false, lines: [] }); + var rechargeModal = rechargeModalState[0]; + var setRechargeModal = rechargeModalState[1]; + + var alertStationModalState = useState({ open: false, type: '', title: '', stations: [] }); + var alertStationModal = alertStationModalState[0]; + var setAlertStationModal = alertStationModalState[1]; + var refuelModalState = useState({ open: false, station: null, records: [] }); var refuelModal = refuelModalState[0]; var setRefuelModal = refuelModalState[1]; - var statementModalState = useState({ open: false, record: null, period: '2026-05' }); + var ledgerStoreState = useState(function () { return h2InitLedgerRecordsStore(); }); + var ledgerStore = ledgerStoreState[0]; + var setLedgerStore = ledgerStoreState[1]; + + var statementRecordsState = useState(function () { return h2InitStatementRecordsStore(); }); + var statementRecords = statementRecordsState[0]; + var setStatementRecords = statementRecordsState[1]; + + var statementModalState = useState({ + open: false, + record: null, + startDate: '', + endDate: '', + phase: 'select', + draftRows: [], + balanceAfterSettlement: 0, + receiptDate: '', + receiptAmount: '', + invoiceFiles: [] + }); var statementModal = statementModalState[0]; var setStatementModal = statementModalState[1]; + var statementHistoryModalState = useState({ open: false, record: null }); + var statementHistoryModal = statementHistoryModalState[0]; + var setStatementHistoryModal = statementHistoryModalState[1]; + + var statementDetailModalState = useState({ open: false, statementRecord: null, detailRows: [] }); + var statementDetailModal = statementDetailModalState[0]; + var setStatementDetailModal = statementDetailModalState[1]; + var prepaidBalanceDrillState = useState({ open: false, stationName: '', endingBalance: 0, rows: [] }); var prepaidBalanceDrill = prepaidBalanceDrillState[0]; var setPrepaidBalanceDrill = prepaidBalanceDrillState[1]; @@ -2207,7 +5165,7 @@ const Component = function () { var createStation = createStationState[0]; var setCreateStation = createStationState[1]; - var createSupplierModeState = useState('new'); + var createSupplierModeState = useState('link'); var createSupplierMode = createSupplierModeState[0]; var setCreateSupplierMode = createSupplierModeState[1]; @@ -2223,9 +5181,37 @@ const Component = function () { var createSubmitting = createSubmittingState[0]; var setCreateSubmitting = createSubmittingState[1]; - var createSupplierReadonly = createSupplierMode === 'link' && !!createLinkedSupplierId; + var editStationState = useState(h2CreateEmptyStationForm()); + var editStation = editStationState[0]; + var setEditStation = editStationState[1]; - var readOnly = drawer.mode === 'view'; + var editRecordState = useState(null); + var editRecord = editRecordState[0]; + var setEditRecord = editRecordState[1]; + + var editOriginFormState = useState(null); + var editOriginForm = editOriginFormState[0]; + var setEditOriginForm = editOriginFormState[1]; + + var editSubmittingState = useState(false); + var editSubmitting = editSubmittingState[0]; + var setEditSubmitting = editSubmittingState[1]; + + var editSupplierModeState = useState('link'); + var editSupplierMode = editSupplierModeState[0]; + var setEditSupplierMode = editSupplierModeState[1]; + + var editLinkedSupplierIdState = useState(undefined); + var editLinkedSupplierId = editLinkedSupplierIdState[0]; + var setEditLinkedSupplierId = editLinkedSupplierIdState[1]; + + var editSupplierState = useState(h2CreateEmptySupplierForm()); + var editSupplier = editSupplierState[0]; + var setEditSupplier = editSupplierState[1]; + + var isAdminUser = h2IsAdminUser(); + + var createSupplierReadonly = createSupplierMode === 'link' && !!createLinkedSupplierId; var existingNameMap = useMemo(function () { var map = {}; @@ -2236,11 +5222,12 @@ const Component = function () { }, [listData]); var categoryCounts = useMemo(function () { - var counts = { all: listData.length, high: 0, low: 0, none: 0 }; + var counts = { all: listData.length, balanceAlert: 0, arrears: 0, none: 0 }; listData.forEach(function (r) { + if (h2IsArrearsStation(r)) counts.arrears += 1; + else if (h2IsBalanceAlertStation(r)) counts.balanceAlert += 1; var stats = h2CalcRefuelStats(r.name); - var k = h2DeriveFrequencyByRefuelCount(stats.count); - if (counts[k] != null) counts[k] += 1; + if (h2DeriveFrequencyByRefuelCount(stats.count) === 'none') counts.none += 1; }); return counts; }, [listData]); @@ -2255,8 +5242,13 @@ const Component = function () { var list = listData.slice(); if (categoryTab !== 'all') { list = list.filter(function (r) { - var stats = h2CalcRefuelStats(r.name); - return h2DeriveFrequencyByRefuelCount(stats.count) === categoryTab; + if (categoryTab === 'balanceAlert') return h2IsBalanceAlertStation(r); + if (categoryTab === 'arrears') return h2IsArrearsStation(r); + if (categoryTab === 'none') { + var stats = h2CalcRefuelStats(r.name); + return h2DeriveFrequencyByRefuelCount(stats.count) === 'none'; + } + return true; }); } var kw = (appliedFilters.name || '').trim().toLowerCase(); @@ -2279,12 +5271,25 @@ const Component = function () { }); }, [listData, categoryTab, appliedFilters]); - var totalCount = filteredList.length; + var sortedList = useMemo(function () { + var list = filteredList.slice(); + if (!refuelSort.key || !refuelSort.order) return list; + var dir = refuelSort.order === 'ascend' ? 1 : -1; + list.sort(function (a, b) { + var av = refuelSort.key === 'refuelCount' ? (a.refuelCount || 0) : (a.refuelTotalKg || 0); + var bv = refuelSort.key === 'refuelCount' ? (b.refuelCount || 0) : (b.refuelTotalKg || 0); + if (av === bv) return 0; + return av > bv ? dir : -dir; + }); + return list; + }, [filteredList, refuelSort]); + + var totalCount = sortedList.length; var displayList = useMemo(function () { var start = (page - 1) * pageSize; - return filteredList.slice(start, start + pageSize); - }, [filteredList, page, pageSize]); + return sortedList.slice(start, start + pageSize); + }, [sortedList, page, pageSize]); var tablePagination = useMemo(function () { return { @@ -2303,11 +5308,16 @@ const Component = function () { var handleKpiCardClick = useCallback(function (key) { setCategoryTab(key); + if (key === 'all' || key === 'none') { + setListFilters(function (p) { return Object.assign({}, p, { signed: undefined }); }); + setAppliedFilters(function (p) { return Object.assign({}, p, { signed: undefined }); }); + } setPage(1); }, []); var handleSignedFilterCardClick = useCallback(function (key) { var nextSigned = appliedFilters.signed === key ? undefined : key; + if (nextSigned) setCategoryTab('all'); setListFilters(function (p) { return Object.assign({}, p, { signed: nextSigned }); }); setAppliedFilters(function (p) { return Object.assign({}, p, { signed: nextSigned }); }); setPage(1); @@ -2320,6 +5330,7 @@ const Component = function () { region: listFilters.region, businessStatus: listFilters.businessStatus }); + if (listFilters.signed) setCategoryTab('all'); setPage(1); }, [listFilters]); @@ -2327,6 +5338,21 @@ const Component = function () { var empty = h2EmptyListFilters(); setListFilters(empty); setAppliedFilters(empty); + setCategoryTab('all'); + setPage(1); + }, []); + + var handleListTableChange = useCallback(function (_pagination, _filters, sorter) { + var nextSorter = Array.isArray(sorter) ? sorter[0] : sorter; + if (!nextSorter || !nextSorter.columnKey) { + setRefuelSort({ key: null, order: null }); + return; + } + if (nextSorter.columnKey !== 'refuelCount' && nextSorter.columnKey !== 'refuelTotalKg') return; + setRefuelSort({ + key: nextSorter.columnKey, + order: nextSorter.order || 'descend' + }); setPage(1); }, []); @@ -2370,17 +5396,17 @@ const Component = function () { ) ) ), - React.createElement('div', { className: 'lc-alert-card-icon' }, icon), + React.createElement('div', { className: 'lc-alert-card-icon', 'aria-hidden': true }, icon), React.createElement('div', { className: 'lc-alert-card-main' }, - React.createElement('div', { className: 'lc-alert-card-val' }, count), - React.createElement('div', { className: 'lc-alert-card-title' }, card.title) + React.createElement('div', { className: 'lc-alert-card-title' }, card.title), + React.createElement('div', { className: 'lc-alert-card-val' }, count) ) ); }, []); var resetCreateForm = useCallback(function () { setCreateStation(h2CreateEmptyStationForm()); - setCreateSupplierMode('new'); + setCreateSupplierMode('link'); setCreateLinkedSupplierId(undefined); setCreateSupplier(h2CreateEmptySupplierForm()); }, []); @@ -2421,9 +5447,15 @@ const Component = function () { }, []); var handleCreateSupplierModeChange = useCallback(function (e) { - setCreateSupplierMode(e.target.value); + var nextMode = e.target.value; + setCreateSupplierMode(nextMode); setCreateLinkedSupplierId(undefined); setCreateSupplier(h2CreateEmptySupplierForm()); + if (nextMode === 'none') { + setCreateStation(function (prev) { + return Object.assign({}, prev, { bindAccountMode: 'none', bindAccountIds: [] }); + }); + } }, []); var handleCreateLinkSupplierChange = useCallback(function (id) { @@ -2461,24 +5493,25 @@ const Component = function () { return false; } if (!createStation.address.region || createStation.address.region.length < 2) { - message.warning('请选择加氢站地址的省 / 市'); + message.warning('请通过地址解析填写省 / 市'); return false; } if (!(createStation.address.detail || '').trim()) { - message.warning('请填写加氢站详细地址'); + message.warning('请通过地址解析填写通讯地址'); return false; } if (!(createStation.contact || '').trim()) { message.warning('请填写联系人'); return false; } - if (!h2StationContactPhone(createStation)) { - message.warning('请填写联系电话'); + var createPhoneErr = h2ValidateStationPhones(createStation); + if (createPhoneErr) { + message.warning(createPhoneErr); return false; } if (createStation.isSigned) { if (!createStation.contractStart || !createStation.contractEnd) { - message.warning('签约站点请填写签约开始与结束时间'); + message.warning('签约站点请填写合作时间'); return false; } } @@ -2497,22 +5530,27 @@ const Component = function () { message.warning('请选择要关联的已有供应商'); return false; } - if (createSupplierMode === 'new') { - if (!(createSupplier.name || '').trim()) { - message.warning('请填写供应商名称'); - return false; - } - if (!createSupplier.businessLicenseFiles || !createSupplier.businessLicenseFiles.length) { - message.warning('请上传营业执照'); - return false; - } - if (!createSupplier.fillingLicenseFiles || !createSupplier.fillingLicenseFiles.length) { - message.warning('请上传充装许可证'); - return false; + if (createSupplierMode === 'new' && !(createSupplier.name || '').trim()) { + message.warning('请填写供应商名称'); + return false; + } + if (createSupplierMode !== 'none' && !(createSupplier.signingCompany || '').trim()) { + message.warning('请选择签约公司'); + return false; + } + if (createSupplierMode !== 'none' && createStation.bindAccountIds && createStation.bindAccountIds.length) { + var usedMapCreate = h2CollectUsedBindAccountMap(listData); + var ci; + for (ci = 0; ci < createStation.bindAccountIds.length; ci++) { + var boundName = usedMapCreate[createStation.bindAccountIds[ci]]; + if (boundName) { + message.warning('系统账号已绑定站点「' + boundName + '」,请重新选择'); + return false; + } } } return true; - }, [createStation, createSupplierMode, createLinkedSupplierId, createSupplier]); + }, [createStation, createSupplierMode, createLinkedSupplierId, createSupplier, listData]); var handleCreatePageSubmit = useCallback(function (payload) { var station = payload && payload.station; @@ -2522,8 +5560,16 @@ const Component = function () { setListData(function (prev) { return [created].concat(prev); }); setPage(1); setSubView('list'); - var supplierHint = payload.supplierMode === 'link' ? '已关联已有供应商' : '已同步创建加氢站类型供应商'; - message.success('站点已创建;' + supplierHint + '(原型)'); + var supplierHint = payload.supplierMode === 'link' + ? '已关联已有供应商' + : payload.supplierMode === 'new' + ? '已同步创建加氢站类型供应商' + : '未关联供应商'; + var accountHint = ''; + if (station.bindAccountMode === 'bind' && station.boundAccounts && station.boundAccounts.length) { + accountHint = ';已绑定 ' + station.boundAccounts.length + ' 个系统账号'; + } + message.success('站点已创建;' + supplierHint + accountHint + '(原型)'); }, [listData]); var handleCreateSubmit = useCallback(function () { @@ -2534,6 +5580,9 @@ const Component = function () { var now = h2OperateTimestamp(); var phoneDisplay = createStation.mobilePhone || createStation.landlinePhone || ''; if (createStation.mobilePhone && createStation.landlinePhone) phoneDisplay = createStation.mobilePhone; + var createBindIds = createSupplierMode === 'none' ? [] : (createStation.bindAccountIds || []).slice(); + var boundAccounts = h2BuildBoundAccountsFromIds(createBindIds); + var boundAccount = boundAccounts.length ? boundAccounts[0] : null; var stationRecord = { name: (createStation.name || '').trim(), region: createStation.address.region || [], @@ -2555,10 +5604,17 @@ const Component = function () { customerUnitPrice: createStation.customerUnitPrice ? parseFloat(createStation.customerUnitPrice) : undefined, prepaidBalance: 0, businessStatusLogs: [], + costPriceLogs: [], updateTime: now, supplierMode: createSupplierMode, linkedSupplierId: createSupplierMode === 'link' ? createLinkedSupplierId : undefined, - supplier: Object.assign({ type: '加氢站' }, createSupplier) + supplier: createSupplierMode === 'none' ? undefined : Object.assign({ type: '加氢站' }, createSupplier), + bindAccountMode: createBindIds.length ? 'bind' : 'none', + bindAccountIds: createBindIds, + boundAccountIds: createBindIds, + boundAccountId: boundAccount ? boundAccount.id : undefined, + boundAccount: boundAccount, + boundAccounts: boundAccounts }; window.setTimeout(function () { handleCreatePageSubmit({ station: stationRecord, supplier: createSupplier, supplierMode: createSupplierMode }); @@ -2566,10 +5622,223 @@ const Component = function () { }, 380); }, [createSubmitting, validateCreateForm, syncCreateSupplierFromStation, createStation, createSupplier, createSupplierMode, createLinkedSupplierId, handleCreatePageSubmit]); - var handleCreateReset = useCallback(function () { - resetCreateForm(); - message.info('表单已重置'); - }, [resetCreateForm]); + var handleEditPageBack = useCallback(function () { + setEditSubmitting(false); + setEditRecord(null); + setEditOriginForm(null); + setSubView('list'); + }, []); + + var handleEditCancel = useCallback(function () { + if (editSubmitting) return; + if (h2EditFormDirty(editStation, editOriginForm)) { + Modal.confirm({ + title: '放弃未保存的更改?', + content: '当前修改尚未保存,返回列表后将丢失。', + okText: '放弃并返回', + cancelText: '继续编辑', + okButtonProps: { danger: true }, + centered: true, + onOk: handleEditPageBack + }); + return; + } + handleEditPageBack(); + }, [editSubmitting, editStation, editOriginForm, handleEditPageBack]); + + var updateEditStation = useCallback(function (patch) { + setEditStation(function (prev) { return Object.assign({}, prev, patch); }); + }, []); + + var updateEditSupplier = useCallback(function (patch) { + setEditSupplier(function (prev) { return Object.assign({}, prev, patch); }); + }, []); + + var confirmEditSupplierChange = useCallback(function (applyChange) { + Modal.confirm({ + title: '确认修改供应商', + content: '修改供应商会影响对账时的供应商信息,请确认是否进行修改', + okText: '确认修改', + cancelText: '取消', + centered: true, + onOk: function () { + applyChange(); + } + }); + }, []); + + var handleEditSupplierModeChange = useCallback(function (e) { + var nextMode = e.target.value; + if (nextMode === editSupplierMode) return; + confirmEditSupplierChange(function () { + setEditSupplierMode(nextMode); + setEditLinkedSupplierId(undefined); + setEditSupplier(h2CreateEmptySupplierForm()); + if (nextMode === 'none') { + setEditStation(function (prev) { + return Object.assign({}, prev, { bindAccountMode: 'none', bindAccountIds: [] }); + }); + } + }); + }, [editSupplierMode, confirmEditSupplierChange]); + + var handleEditLinkSupplierChange = useCallback(function (id) { + if (id === editLinkedSupplierId) return; + confirmEditSupplierChange(function () { + setEditLinkedSupplierId(id); + var found = H2_MOCK_EXISTING_SUPPLIERS.filter(function (s) { return s.id === id; })[0]; + setEditSupplier(h2SupplierToForm(found)); + }); + }, [editLinkedSupplierId, confirmEditSupplierChange]); + + var handleEditSupplierCityChange = useCallback(function (v) { + updateEditSupplier({ + city: v, + region: v && v[0] ? h2GetRegionByProvince(v[0]) : '' + }); + }, [updateEditSupplier]); + + var syncEditSupplierFromStation = useCallback(function () { + if (editSupplierMode !== 'new') return; + setEditSupplier(function (supplier) { + var patch = {}; + if (editStation.name && !supplier.name) patch.name = editStation.name; + if (editStation.address && editStation.address.region && editStation.address.region.length >= 2) { + patch.city = editStation.address.region.slice(); + patch.region = h2GetRegionByProvince(editStation.address.region[0]); + } + if (editStation.address && editStation.address.detail && !supplier.address) patch.address = editStation.address.detail; + if (editStation.contact && !supplier.contactName) patch.contactName = editStation.contact; + if (editStation.mobilePhone && !supplier.contactMobile) patch.contactMobile = editStation.mobilePhone; + return Object.keys(patch).length ? Object.assign({}, supplier, patch) : supplier; + }); + }, [editSupplierMode, editStation]); + + var validateEditForm = useCallback(function () { + if (!(editStation.name || '').trim()) { + message.warning('请填写加氢站名称'); + return false; + } + if (!editStation.address.region || editStation.address.region.length < 2) { + message.warning('请通过地址解析填写省 / 市'); + return false; + } + if (!(editStation.address.detail || '').trim()) { + message.warning('请通过地址解析填写通讯地址'); + return false; + } + if (!(editStation.contact || '').trim()) { + message.warning('请填写联系人'); + return false; + } + var editPhoneErr = h2ValidateStationPhones(editStation); + if (editPhoneErr) { + message.warning(editPhoneErr); + return false; + } + if (editStation.isSigned) { + if (!editStation.contractStart || !editStation.contractEnd) { + message.warning('签约站点请填写合作时间'); + return false; + } + } + var hoursParsedEdit = h2ParseBusinessHours(editStation.businessHours); + if (hoursParsedEdit.mode === 'custom') { + if (!(hoursParsedEdit.start || '').trim() || !(hoursParsedEdit.end || '').trim()) { + message.warning('非全天营业请选择开始与结束时间'); + return false; + } + } + if (editSupplierMode === 'link' && !editLinkedSupplierId) { + message.warning('请选择要关联的已有供应商'); + return false; + } + if (editSupplierMode === 'new' && !(editSupplier.name || '').trim()) { + message.warning('请填写供应商名称'); + return false; + } + if (editSupplierMode !== 'none' && !(editSupplier.signingCompany || '').trim()) { + message.warning('请选择签约公司'); + return false; + } + if (isAdminUser && editSupplierMode !== 'none' && editStation.bindAccountIds && editStation.bindAccountIds.length) { + var usedMapEdit = h2CollectUsedBindAccountMap(listData, editRecord && editRecord.id); + var ei; + for (ei = 0; ei < editStation.bindAccountIds.length; ei++) { + var boundEditName = usedMapEdit[editStation.bindAccountIds[ei]]; + if (boundEditName) { + message.warning('系统账号已绑定站点「' + boundEditName + '」,请重新选择'); + return false; + } + } + } + return true; + }, [editStation, editRecord, editSupplierMode, editLinkedSupplierId, editSupplier, listData, isAdminUser]); + + var handleEditSubmit = useCallback(function () { + if (editSubmitting || !editRecord) return; + if (!validateEditForm()) return; + setEditSubmitting(true); + syncEditSupplierFromStation(); + var now = h2OperateTimestamp(); + var phoneDisplay = editStation.mobilePhone || editStation.landlinePhone || ''; + if (editStation.mobilePhone && editStation.landlinePhone) phoneDisplay = editStation.mobilePhone; + var nextBoundAccounts = []; + var nextBindMode = editRecord.bindAccountMode || 'none'; + var nextBindIds = h2NormalizeBindAccountIds(editRecord); + if (editSupplierMode === 'none') { + nextBindMode = 'none'; + nextBindIds = []; + nextBoundAccounts = []; + } else if (isAdminUser) { + nextBindIds = (editStation.bindAccountIds || []).slice(); + nextBindMode = nextBindIds.length ? 'bind' : 'none'; + nextBoundAccounts = h2BuildBoundAccountsFromIds(nextBindIds); + } else { + nextBoundAccounts = h2ResolveStationSystemAccounts(editRecord); + nextBindIds = h2NormalizeBindAccountIds(editRecord); + nextBindMode = editRecord.bindAccountMode || (nextBindIds.length ? 'bind' : 'none'); + } + var nextBoundAccount = nextBoundAccounts.length ? nextBoundAccounts[0] : null; + var recordId = editRecord.id; + setListData(function (prev) { + return prev.map(function (r) { + if (r.id !== recordId) return r; + return Object.assign({}, r, { + name: (editStation.name || '').trim(), + region: editStation.address.region || [], + addressDetail: (editStation.address.detail || '').trim(), + fullAddress: h2BuildFullAddress(editStation.address.region, editStation.address.detail), + isSigned: !!editStation.isSigned, + contractStart: editStation.isSigned ? editStation.contractStart : '', + contractEnd: editStation.isSigned ? editStation.contractEnd : '', + contractFiles: (editStation.contractFiles || []).map(function (f) { + return { uid: f.uid, name: f.name || '合同附件.pdf', url: f.url || '' }; + }), + businessHours: (editStation.businessHours || '').trim() || '—', + contact: (editStation.contact || '').trim(), + phone: phoneDisplay, + mobilePhone: (editStation.mobilePhone || '').trim(), + landlinePhone: (editStation.landlinePhone || '').trim(), + bindAccountMode: nextBindMode, + bindAccountIds: nextBindIds, + boundAccountIds: nextBindIds, + boundAccountId: nextBoundAccount ? nextBoundAccount.id : undefined, + boundAccount: nextBoundAccount, + boundAccounts: nextBoundAccounts, + supplierMode: editSupplierMode, + linkedSupplierId: editSupplierMode === 'link' ? editLinkedSupplierId : undefined, + supplier: editSupplierMode === 'none' ? undefined : Object.assign({ type: '加氢站' }, editSupplier), + updateTime: now + }); + }); + }); + window.setTimeout(function () { + message.success('站点已保存(原型)'); + setEditSubmitting(false); + handleEditPageBack(); + }, 320); + }, [editSubmitting, editRecord, editStation, editSupplierMode, editLinkedSupplierId, editSupplier, validateEditForm, syncEditSupplierFromStation, isAdminUser, handleEditPageBack]); var openRefuelModal = useCallback(function (record) { setRefuelModal({ @@ -2583,6 +5852,17 @@ const Component = function () { setRefuelModal({ open: false, station: null, records: [] }); }, []); + var handleExportRefuelDrill = useCallback(function () { + var records = refuelModal.records || []; + if (!records.length) { + message.warning('暂无加氢明细可导出'); + return; + } + var stationName = (refuelModal.station && refuelModal.station.name) || ''; + h2ExportRefuelDrillCsv(stationName, records); + message.success('已导出 ' + records.length + ' 条加氢记录'); + }, [refuelModal]); + var openPrepaidBalanceDrill = useCallback(function (record) { setPrepaidBalanceDrill({ open: true, @@ -2596,33 +5876,241 @@ const Component = function () { setPrepaidBalanceDrill({ open: false, stationName: '', endingBalance: 0, rows: [] }); }, []); + var handleExportBalanceDrill = useCallback(function () { + var rows = prepaidBalanceDrill.rows || []; + if (!rows.length) { + message.warning('暂无流水明细可导出'); + return; + } + h2ExportBalanceDrillCsv(prepaidBalanceDrill.stationName, rows); + message.success('已导出 ' + rows.length + ' 条流水记录'); + }, [prepaidBalanceDrill]); + var openStatementModal = useCallback(function (record) { - setStatementModal({ open: true, record: record, period: '2026-05' }); - }, []); + var lastEnd = h2GetLastStatementEndDate(statementRecords, record.name); + var defaultStart = lastEnd ? h2AddDays(lastEnd, 1) : '2026-05-01'; + setStatementModal({ + open: true, + record: record, + startDate: defaultStart, + endDate: '2026-05-31', + phase: 'select', + draftRows: [], + balanceAfterSettlement: h2NumOrZero(record.prepaidBalance), + receiptDate: '', + receiptAmount: '', + invoiceFiles: [] + }); + }, [statementRecords]); var closeStatementModal = useCallback(function () { - setStatementModal({ open: false, record: null, period: '2026-05' }); + setStatementModal({ + open: false, + record: null, + startDate: '', + endDate: '', + phase: 'select', + draftRows: [], + balanceAfterSettlement: 0, + receiptDate: '', + receiptAmount: '', + invoiceFiles: [] + }); }, []); + var statementDraftSummary = useMemo(function () { + return h2CalcStatementSummary(statementModal.draftRows || []); + }, [statementModal.draftRows]); + + var handleExportStatement = useCallback(function () { + var rows = statementModal.draftRows || []; + if (!statementModal.record || !rows.length) { + message.warning('暂无可导出的对账单明细'); + return; + } + h2ExportStatementCsv(statementModal.record.name, statementModal.startDate, statementModal.endDate, rows); + message.success('已导出 ' + rows.length + ' 条对账明细'); + }, [statementModal]); + var handleGenerateStatement = useCallback(function () { if (!statementModal.record) return; - if (!statementModal.period) { - message.warning('请选择对账周期'); + if (!statementModal.startDate || !statementModal.endDate) { + message.warning('请选择账单开始日期和结束日期'); return; } - var periodLabel = statementModal.period.replace('-', '年') + '月'; - message.success('已生成「' + statementModal.record.name + '」' + periodLabel + '氢费对账单(原型)'); + var startMs = h2ParseDateOnlyMs(statementModal.startDate); + var endMs = h2ParseDateOnlyMs(statementModal.endDate); + if (isNaN(startMs) || isNaN(endMs)) { + message.warning('日期格式不正确'); + return; + } + if (startMs > endMs) { + message.warning('开始日期不能晚于结束日期'); + return; + } + var rows = h2GetAvailableStatementLedgerRows(ledgerStore, statementModal.record.name, statementModal.startDate, statementModal.endDate); + if (!rows.length) { + message.warning('所选日期范围内暂无「已对账」且未结算的加氢记录,请调整日期后重试'); + return; + } + var summary = h2CalcStatementSummary(rows); + var balanceAfter = Math.round((h2NumOrZero(statementModal.record.prepaidBalance) - summary.totalCost) * 100) / 100; + setStatementModal(function (m) { + return Object.assign({}, m, { + phase: 'draft', + draftRows: rows, + balanceAfterSettlement: balanceAfter, + receiptAmount: summary.totalCost ? h2FormatYuanNum(summary.totalCost) : '', + receiptDate: '', + invoiceFiles: [] + }); + }); + message.success('已生成对账记录,共 ' + summary.count + ' 笔,请填写结算信息并提交'); + }, [statementModal, ledgerStore]); + + var handleSubmitStatement = useCallback(function () { + if (!statementModal.record || statementModal.phase !== 'draft') return; + var rows = statementModal.draftRows || []; + if (!rows.length) { + message.warning('暂无对账明细可提交'); + return; + } + if (!(statementModal.receiptDate || '').trim()) { + message.warning('请填写收票日期'); + return; + } + var receiptAmountNum = parseFloat(statementModal.receiptAmount); + if (isNaN(receiptAmountNum) || receiptAmountNum <= 0) { + message.warning('请填写有效的收票金额'); + return; + } + if (!statementModal.invoiceFiles || !statementModal.invoiceFiles.length) { + message.warning('请上传发票附件'); + return; + } + var summary = h2CalcStatementSummary(rows); + var operateTime = h2OperateTimestamp(); + var operateDate = h2FormatOperateDateOnly(); + var statementId = 'stmt-' + Date.now(); + var recordIds = rows.map(function (r) { return r.id; }); + var stationRecord = statementModal.record; + var balanceAfter = h2NumOrZero(statementModal.balanceAfterSettlement); + var newStatement = { + id: statementId, + stationId: stationRecord.id, + stationName: stationRecord.name, + reconcileDate: operateTime, + reconciler: H2_CURRENT_OPERATOR, + startDate: statementModal.startDate, + endDate: statementModal.endDate, + refuelCount: summary.count, + totalKg: summary.totalKg, + totalCost: summary.totalCost, + balanceAfterSettlement: balanceAfter, + receiptDate: statementModal.receiptDate, + receiptAmount: receiptAmountNum, + invoiceFiles: (statementModal.invoiceFiles || []).slice(), + recordIds: recordIds + }; + var patches = []; + setLedgerStore(function (prev) { + return prev.map(function (r) { + if (recordIds.indexOf(r.id) < 0) return r; + patches.push({ + ledgerId: r.id, + orderNo: r.orderNo, + stationName: r.stationName, + plateNo: r.plateNo, + hydrogenTime: r.hydrogenTime, + reconcileDate: operateTime, + receiptDate: operateDate, + paymentStatus: H2_PAYMENT_STATUS_PAID + }); + return Object.assign({}, r, { + statementRecordId: statementId, + reconcileDate: operateTime, + receiptDate: operateDate, + paymentStatus: H2_PAYMENT_STATUS_PAID + }); + }); + }); + setStatementRecords(function (prev) { return [newStatement].concat(prev); }); + setListData(function (prev) { + return prev.map(function (item) { + if (item.id !== stationRecord.id) return item; + return Object.assign({}, item, { + prepaidBalance: balanceAfter, + updateTime: operateTime + }); + }); + }); + h2PushStatementLedgerPatches(patches); + if (typeof window !== 'undefined' && window.H2_VEHICLE_LEDGER_API && typeof window.H2_VEHICLE_LEDGER_API.applyPatches === 'function') { + window.H2_VEHICLE_LEDGER_API.applyPatches(patches); + } + message.success('对账单已提交,共结算 ' + summary.count + ' 笔加氢记录'); closeStatementModal(); }, [statementModal, closeStatementModal]); + var openStatementHistoryModal = useCallback(function (record) { + setStatementHistoryModal({ open: true, record: record }); + }, []); + + var closeStatementHistoryModal = useCallback(function () { + setStatementHistoryModal({ open: false, record: null }); + }, []); + + var openStatementDetailModal = useCallback(function (statementRecord) { + var detailRows = h2GetLedgerRowsByIds(ledgerStore, statementRecord.recordIds); + setStatementDetailModal({ open: true, statementRecord: statementRecord, detailRows: detailRows }); + }, [ledgerStore]); + + var closeStatementDetailModal = useCallback(function () { + setStatementDetailModal({ open: false, statementRecord: null, detailRows: [] }); + }, []); + var openEdit = useCallback(function (record) { - setForm(h2RecordToForm(record)); - setDrawer({ open: true, mode: 'edit', record: record }); + var formSnapshot = h2RecordToEditStationForm(record); + var supplierMode = h2ResolveRecordSupplierMode(record); + var linkedSupplier = record.linkedSupplierId; + var supplierForm = supplierMode === 'none' + ? h2CreateEmptySupplierForm() + : h2SupplierToForm(h2ResolveStationSupplier(record)); + setEditStation(formSnapshot); + setEditOriginForm(JSON.parse(JSON.stringify(formSnapshot))); + setEditSupplierMode(supplierMode); + setEditLinkedSupplierId(linkedSupplier); + setEditSupplier(supplierForm); + setEditRecord(record); + setSubView('edit'); }, []); var openView = useCallback(function (record) { - setForm(h2RecordToForm(record)); - setDrawer({ open: true, mode: 'view', record: record }); + setViewModal({ open: true, record: record }); + }, []); + + var closeViewModal = useCallback(function () { + setViewModal({ open: false, record: null }); + setFilePreviewModal({ open: false, url: '', name: '', type: 'image' }); + }, []); + + var openFilePreview = useCallback(function (file, variant) { + if (!file) return; + var preview = h2ResolveUploadPreview(file, variant || 'doc'); + if (preview.type === 'image' && Image && typeof Image.preview === 'function') { + Image.preview({ src: preview.url }); + return; + } + setFilePreviewModal({ + open: true, + url: preview.url, + name: preview.name, + type: preview.type + }); + }, [Image]); + + var closeFilePreviewModal = useCallback(function () { + setFilePreviewModal({ open: false, url: '', name: '', type: 'image' }); }, []); var openBusinessSetting = useCallback(function (record) { @@ -2642,6 +6130,17 @@ const Component = function () { var handleSaveBusinessSetting = useCallback(function () { if (!businessModal.record) return; + var hoursParsed = h2ParseBusinessHours(businessModal.businessHours); + if (hoursParsed.mode === 'custom') { + if (!(hoursParsed.start || '').trim() || !(hoursParsed.end || '').trim()) { + message.warning('非全天营业请选择开始与结束时间'); + return; + } + if (!/^(\d{1,2}):(\d{2})$/.test(h2NormalizeTimeText(hoursParsed.start)) || !/^(\d{1,2}):(\d{2})$/.test(h2NormalizeTimeText(hoursParsed.end))) { + message.warning('营业时间格式须为 HH:mm'); + return; + } + } var nextStatus = businessModal.businessStatus || '营业中'; var statusChanged = nextStatus !== businessModal.originBusinessStatus; var newLog = statusChanged @@ -2668,105 +6167,229 @@ const Component = function () { setPriceModal({ open: true, record: record, - costUnitPrice: record.costUnitPrice != null ? String(record.costUnitPrice) : '', - customerUnitPrice: record.customerUnitPrice != null ? String(record.customerUnitPrice) : '' + costUnitPrice: '', + effectiveTime: dayjs ? dayjs().format('YYYY-MM-DD HH:mm') : h2OperateTimestamp(), + priceLogs: (record.costPriceLogs || []).slice() }); }, []); var closePriceModal = useCallback(function () { - setPriceModal({ open: false, record: null, costUnitPrice: '', customerUnitPrice: '' }); + setPriceModal({ open: false, record: null, costUnitPrice: '', effectiveTime: '', priceLogs: [] }); }, []); var handleSavePriceConfig = useCallback(function () { if (!priceModal.record) return; - var cost = parseFloat(priceModal.costUnitPrice); - var customer = parseFloat(priceModal.customerUnitPrice); - if (priceModal.costUnitPrice !== '' && isNaN(cost)) { - message.warning('请输入有效的成本单价'); + var costText = (priceModal.costUnitPrice || '').trim(); + var effectiveText = (priceModal.effectiveTime || '').trim(); + if (!costText) { + message.warning('请填写成本价格'); return; } - if (priceModal.customerUnitPrice !== '' && isNaN(customer)) { - message.warning('请输入有效的对客单价'); + var cost = parseFloat(costText); + if (isNaN(cost) || cost < 0) { + message.warning('请输入有效的成本价格(元/kg)'); return; } + if (!effectiveText) { + message.warning('请选择生效时间'); + return; + } + var beforePrice = h2ResolveCurrentCostPrice(priceModal.record); + var newLog = h2CreateCostPriceLog(beforePrice, cost, effectiveText, H2_CURRENT_OPERATOR); + var recordId = priceModal.record.id; setListData(function (prev) { return prev.map(function (r) { - if (r.id !== priceModal.record.id) return r; + if (r.id !== recordId) return r; + var logs = [newLog].concat(r.costPriceLogs || []); + var updated = Object.assign({}, r, { + costPriceLogs: logs, + updateTime: h2OperateTimestamp() + }); + return h2ApplyDueCostPriceToRecord(updated); + }); + }); + var appliedNow = h2ParseDateTimeMs(effectiveText) <= Date.now(); + message.success(appliedNow ? '价格配置已保存,当前成本价格已更新(原型)' : '价格配置已保存,将于生效时间自动更新(原型)'); + closePriceModal(); + }, [priceModal, closePriceModal]); + + var openBalanceAlertSetting = useCallback(function (record) { + var threshold = h2ResolveBalanceAlertThreshold(record); + setBalanceAlertModal({ + open: true, + record: record, + threshold: threshold != null ? h2FormatYuanNum(threshold) : '' + }); + }, []); + + var closeBalanceAlertModal = useCallback(function () { + setBalanceAlertModal({ open: false, record: null, threshold: '' }); + }, []); + + var handleSaveBalanceAlert = useCallback(function () { + if (!balanceAlertModal.record) return; + var thresholdText = h2FormatReceiptAmountInput(balanceAlertModal.threshold); + if (!thresholdText) { + message.warning('请填写有效的提醒金额'); + return; + } + var thresholdNum = parseFloat(thresholdText); + if (isNaN(thresholdNum) || thresholdNum <= 0) { + message.warning('提醒金额须大于 0'); + return; + } + var recordId = balanceAlertModal.record.id; + setListData(function (prev) { + return prev.map(function (r) { + if (r.id !== recordId) return r; return Object.assign({}, r, { - costUnitPrice: priceModal.costUnitPrice === '' ? null : cost, - customerUnitPrice: priceModal.customerUnitPrice === '' ? null : customer, - updateTime: new Date().toISOString().slice(0, 16).replace('T', ' ') + balanceAlertThreshold: thresholdNum, + updateTime: h2OperateTimestamp() }); }); }); - message.success('价格配置已保存(原型)'); - closePriceModal(); - }, [priceModal, closePriceModal]); + message.success('余额提醒设置已保存(原型)'); + closeBalanceAlertModal(); + }, [balanceAlertModal, closeBalanceAlertModal]); + + var openRechargeModal = useCallback(function (stations) { + var lines = []; + if (stations && stations.length) { + lines = stations.map(function (r) { return h2BuildRechargeLineFromStation(r); }); + } else { + lines = [h2CreateEmptyRechargeLine()]; + } + setRechargeModal({ open: true, lines: lines }); + }, []); + + var closeRechargeModal = useCallback(function () { + setRechargeModal({ open: false, lines: [] }); + }, []); + + var updateRechargeLineField = useCallback(function (lineId, field, value) { + setRechargeModal(function (m) { + return Object.assign({}, m, { + lines: m.lines.map(function (line) { + if (line.id !== lineId) return line; + return Object.assign({}, line, { [field]: value }); + }) + }); + }); + }, []); + + var handleRechargeStationSelect = useCallback(function (lineId, stationId) { + if (!stationId) { + setRechargeModal(function (m) { + return Object.assign({}, m, { + lines: m.lines.map(function (line) { + if (line.id !== lineId) return line; + var empty = h2CreateEmptyRechargeLine(); + empty.id = lineId; + empty.payAmount = line.payAmount; + return empty; + }) + }); + }); + return; + } + var record = listData.filter(function (r) { return r.id === stationId; })[0]; + if (!record) return; + setRechargeModal(function (m) { + return Object.assign({}, m, { + lines: m.lines.map(function (line) { + if (line.id !== lineId) return line; + var built = h2BuildRechargeLineFromStation(record, line.payAmount); + built.id = lineId; + return built; + }) + }); + }); + }, [listData]); + + var addRechargeLine = useCallback(function () { + setRechargeModal(function (m) { + return Object.assign({}, m, { lines: m.lines.concat([h2CreateEmptyRechargeLine()]) }); + }); + }, []); + + var removeRechargeLine = useCallback(function (lineId) { + setRechargeModal(function (m) { + if (m.lines.length <= 1) return m; + return Object.assign({}, m, { lines: m.lines.filter(function (l) { return l.id !== lineId; }) }); + }); + }, []); + + var handleSubmitRecharge = useCallback(function () { + var lines = rechargeModal.lines || []; + var valid = []; + var i; + for (i = 0; i < lines.length; i++) { + var line = lines[i]; + if (!line.stationId) continue; + var payText = h2FormatReceiptAmountInput(line.payAmount); + var payNum = parseFloat(payText); + if (!payText || isNaN(payNum) || payNum <= 0) continue; + valid.push(line); + } + if (!valid.length) { + message.warning('请至少添加一条有效的付款信息(已选站点且付款金额大于 0)'); + return; + } + var missingSupplier = valid.filter(function (l) { + return !(l.companyName || '').trim() || !(l.bankAccount || '').trim() || !(l.bankName || '').trim(); + }); + if (missingSupplier.length) { + message.warning('部分站点未关联供应商或缺少收款信息,请补全后再发起'); + return; + } + message.success('已发起审批流程'); + closeRechargeModal(); + }, [rechargeModal, closeRechargeModal]); + + var openAlertStationModal = useCallback(function (type) { + var title = type === 'balanceAlert' ? '预付余额预警站点' : '已欠费站点'; + var stations = listData.filter(function (r) { + if (type === 'balanceAlert') return h2IsBalanceAlertStation(r); + if (type === 'arrears') return h2IsArrearsStation(r); + return false; + }); + setAlertStationModal({ + open: true, + type: type, + title: title, + stations: h2EnrichStationListRows(stations) + }); + }, [listData]); + + var closeAlertStationModal = useCallback(function () { + setAlertStationModal({ open: false, type: '', title: '', stations: [] }); + }, []); + + var handleBatchRechargeFromAlert = useCallback(function () { + var stations = alertStationModal.stations || []; + if (!stations.length) { + message.warning('暂无站点可发起充值'); + return; + } + var type = alertStationModal.type; + closeAlertStationModal(); + openRechargeModal(stations); + if (type === 'balanceAlert') message.info('已载入 ' + stations.length + ' 个预警站点,请填写付款金额后发起'); + else if (type === 'arrears') message.info('已载入 ' + stations.length + ' 个欠费站点,请填写付款金额后发起'); + }, [alertStationModal, closeAlertStationModal, openRechargeModal]); var getRowMoreMenuItems = useCallback(function (record) { return [ { key: 'edit', label: '编辑', onClick: function () { openEdit(record); } }, { key: 'business', label: '营业状态', onClick: function () { openBusinessSetting(record); } }, { key: 'price', label: '价格配置', onClick: function () { openPriceConfig(record); } }, + { key: 'balanceAlert', label: '余额提醒设置', onClick: function () { openBalanceAlertSetting(record); } }, { key: 'statement', label: '生成对账单', onClick: function () { openStatementModal(record); } }, + { key: 'statementHistory', label: '查看对账记录', onClick: function () { openStatementHistoryModal(record); } }, { type: 'divider' }, { key: 'delete', label: '删除', danger: true, onClick: function () { setDeleteModal({ open: true, record: record }); } } ]; - }, [openEdit, openBusinessSetting, openPriceConfig, openStatementModal]); - - var closeDrawer = useCallback(function () { - setDrawer({ open: false, mode: 'create', record: null }); - }, []); - - var validateForm = useCallback(function () { - if (!(form.name || '').trim()) { - message.warning('请填写加氢站名称'); - return false; - } - if (!form.address.region || form.address.region.length < 2) { - message.warning('请选择省 / 市'); - return false; - } - if (!(form.address.detail || '').trim()) { - message.warning('请填写详细地址'); - return false; - } - if (!(form.contact || '').trim()) { - message.warning('请填写联系人'); - return false; - } - if (!(form.phone || '').trim()) { - message.warning('请填写联系电话'); - return false; - } - if (form.isSigned) { - if (!form.contractStart || !form.contractEnd) { - message.warning('已签约站点请填写签约开始与结束时间'); - return false; - } - } - return true; - }, [form]); - - var handleSave = useCallback(function () { - if (!validateForm()) return; - if (!drawer.record) return; - var updated = h2FormToRecord(form, drawer.record.id); - updated.businessStatus = drawer.record.businessStatus || '营业中'; - updated.costUnitPrice = drawer.record.costUnitPrice; - updated.customerUnitPrice = drawer.record.customerUnitPrice; - updated.businessStatusLogs = drawer.record.businessStatusLogs || []; - updated.prepaidBalance = drawer.record.prepaidBalance; - updated.mobilePhone = drawer.record.mobilePhone; - updated.landlinePhone = drawer.record.landlinePhone; - updated.supplier = drawer.record.supplier; - updated.linkedSupplierId = drawer.record.linkedSupplierId; - setListData(function (prev) { - return prev.map(function (r) { return r.id === updated.id ? updated : r; }); - }); - message.success('站点已保存(原型)'); - closeDrawer(); - }, [validateForm, drawer, form, closeDrawer]); + }, [openEdit, openBusinessSetting, openPriceConfig, openBalanceAlertSetting, openStatementModal, openStatementHistoryModal]); var closeImportModal = useCallback(function () { setImportModalOpen(false); @@ -2856,6 +6479,18 @@ const Component = function () { if (page > maxPage) setPage(maxPage); }, [totalCount, pageSize, page]); + React.useEffect(function () { + var applyDuePrices = function () { + setListData(function (prev) { + var result = h2ApplyDueCostPricesToList(prev); + return result.changed ? result.records : prev; + }); + }; + applyDuePrices(); + var timer = window.setInterval(applyDuePrices, 60000); + return function () { window.clearInterval(timer); }; + }, []); + var renderRefuelFreqShortTag = function (freqKey) { if (freqKey === 'high') { return React.createElement(Tag, { @@ -2928,68 +6563,445 @@ const Component = function () { ); }, [openPrepaidBalanceDrill]); + var renderBalanceDrillMoney = function (text, tone, rawValue) { + if (text === '—') { + return React.createElement('span', { className: 'h2-balance-money h2-balance-money--muted' }, '—'); + } + var className = 'h2-balance-money h2-balance-money--' + tone; + if (tone === 'balance' && rawValue != null && h2NumOrZero(rawValue) < 0) className += ' h2-balance-money--negative'; + return React.createElement('span', { className: className }, text); + }; + var prepaidBalanceDrillColumns = [ - { title: '加氢站名称', dataIndex: 'stationName', key: 'stationName', width: 220, ellipsis: true }, - { title: '收入金额(元)', dataIndex: 'incomeAmount', key: 'incomeAmount', width: 120, align: 'right', render: function (v) { return h2FormatLedgerMoney(v); } }, - { title: '支出金额(元)', dataIndex: 'expenseAmount', key: 'expenseAmount', width: 120, align: 'right', render: function (v) { return h2FormatLedgerMoney(v); } }, + { + title: '序号', + key: 'seq', + width: 52, + align: 'center', + render: function (_, __, index) { + return React.createElement('span', { className: 'h2-balance-seq' }, index + 1); + } + }, + { + title: '时间', + dataIndex: 'changeTime', + key: 'changeTime', + width: 158, + render: function (v) { + return React.createElement('span', { + style: { color: '#334155', fontVariantNumeric: 'tabular-nums', fontSize: 12, whiteSpace: 'nowrap' } + }, v || '—'); + } + }, + { + title: '类型', + key: 'bizType', + width: 92, + align: 'center', + render: function (_, row) { + var typeInfo = h2ResolveBalanceBizType(row); + return React.createElement(Tag, { + color: typeInfo.color, + style: { margin: 0, borderRadius: 6, fontWeight: 600, fontSize: 12 } + }, typeInfo.label); + } + }, + { + title: '收入(元)', + dataIndex: 'incomeAmount', + key: 'incomeAmount', + width: 120, + align: 'right', + render: function (v) { return renderBalanceDrillMoney(h2FormatYuanSymbol(v), 'income'); } + }, + { + title: '支出(元)', + dataIndex: 'expenseAmount', + key: 'expenseAmount', + width: 120, + align: 'right', + render: function (v) { return renderBalanceDrillMoney(h2FormatYuanSymbol(v), 'expense'); } + }, { title: '余额(元)', dataIndex: 'balance', key: 'balance', - width: 120, + width: 128, align: 'right', render: function (v) { - var bn = h2NumOrZero(v); - var style = bn < 0 ? { color: '#dc2626', fontWeight: 600, fontVariantNumeric: 'tabular-nums' } : { fontVariantNumeric: 'tabular-nums' }; - return React.createElement('span', { style: style }, h2FormatYuanNum(v)); + return renderBalanceDrillMoney(h2FormatYuanSymbol(v, { keepZero: true }), 'balance', v); } }, { title: '订单编号', dataIndex: 'orderNo', key: 'orderNo', - width: 168, - align: 'center', + width: 148, + ellipsis: true, render: function (v) { - return React.createElement('span', { style: { fontFamily: 'monospace', fontSize: 12 } }, v || '—'); + if (!v) return React.createElement('span', { className: 'h2-balance-money--muted' }, '—'); + return React.createElement('span', { className: 'h2-balance-order-no', title: v }, v); } } ]; var prepaidBalanceDrillSummary = useMemo(function () { if (!prepaidBalanceDrill.rows || !prepaidBalanceDrill.rows.length) { - return { incomeTotal: 0, expenseTotal: 0, endingBalance: 0 }; + return { incomeTotal: 0, expenseTotal: 0, currentBalance: 0 }; } var last = prepaidBalanceDrill.rows[prepaidBalanceDrill.rows.length - 1]; return prepaidBalanceDrill.rows.reduce(function (acc, r) { acc.incomeTotal += h2NumOrZero(r.incomeAmount); acc.expenseTotal += h2NumOrZero(r.expenseAmount); return acc; - }, { incomeTotal: 0, expenseTotal: 0, endingBalance: h2NumOrZero(last.balance) }); + }, { incomeTotal: 0, expenseTotal: 0, currentBalance: h2NumOrZero(last.balance) }); }, [prepaidBalanceDrill.rows]); - var renderPrepaidBalanceDrillSummary = useCallback(function () { - return React.createElement(Table.Summary, null, - React.createElement(Table.Summary.Row, null, - React.createElement(Table.Summary.Cell, { index: 0, align: 'center' }, '合计'), - React.createElement(Table.Summary.Cell, { index: 1, align: 'right' }, h2FormatYuanNum(prepaidBalanceDrillSummary.incomeTotal)), - React.createElement(Table.Summary.Cell, { index: 2, align: 'right' }, h2FormatYuanNum(prepaidBalanceDrillSummary.expenseTotal)), - React.createElement(Table.Summary.Cell, { index: 3, align: 'right' }, h2FormatYuanNum(prepaidBalanceDrillSummary.endingBalance)), - React.createElement(Table.Summary.Cell, { index: 4 }) + var prepaidBalanceTrendSeries = useMemo(function () { + return h2BuildBalanceTrendSeries(prepaidBalanceDrill.rows || []); + }, [prepaidBalanceDrill.rows]); + + var renderPrepaidBalanceDrillPanel = useCallback(function () { + var rows = prepaidBalanceDrill.rows || []; + var summary = prepaidBalanceDrillSummary; + var trendSeries = prepaidBalanceTrendSeries; + var balanceNegative = h2NumOrZero(summary.currentBalance) < 0; + var balanceText = h2FormatYuanSymbol(summary.currentBalance, { keepZero: true }); + var stats = [ + { key: 'balance', label: '当前预付余额', value: balanceText, mod: 'balance', valueMod: balanceNegative ? 'negative' : 'income' }, + { key: 'income', label: '收入合计', value: h2FormatYuanSymbol(summary.incomeTotal, { keepZero: true }), mod: 'income', valueMod: 'income' }, + { key: 'expense', label: '支出合计', value: h2FormatYuanSymbol(summary.expenseTotal, { keepZero: true }), mod: 'expense', valueMod: 'expense' } + ]; + return React.createElement('div', { className: 'h2-balance-drill-panel' }, + React.createElement('div', { className: 'h2-balance-drill-stats', role: 'group', 'aria-label': '余额统计' }, + stats.map(function (item) { + return React.createElement('div', { + key: item.key, + className: 'h2-balance-drill-stat h2-balance-drill-stat--' + item.mod, + role: 'article', + 'aria-label': item.label + ' ' + item.value + }, + React.createElement('div', { className: 'h2-balance-drill-stat__label' }, item.label), + React.createElement('div', { + className: 'h2-balance-drill-stat__value' + (item.valueMod ? ' h2-balance-drill-stat__value--' + item.valueMod : '') + }, item.value) + ); + }) + ), + React.createElement('div', { className: 'h2-balance-trend-wrap' }, + React.createElement('div', { className: 'h2-balance-trend-head' }, + React.createElement('span', { className: 'h2-balance-trend-head__title' }, '余额趋势'), + React.createElement('span', { className: 'h2-balance-trend-head__meta' }, + (trendSeries.startDate || '—') + ' 至 ' + (trendSeries.endDate || '—') + ' · 按日 · 悬浮查看明细' + ) + ), + React.createElement('div', { className: 'h2-balance-trend-body' }, + React.createElement(H2BalanceTrendChart, { series: trendSeries }) + ), + rows.length + ? React.createElement('div', { className: 'h2-balance-trend-legend' }, + React.createElement('span', { className: 'h2-balance-trend-legend__item' }, + React.createElement('span', { className: 'h2-balance-trend-legend__dot h2-balance-trend-legend__dot--line' }), + '余额走势' + ), + React.createElement('span', { className: 'h2-balance-trend-legend__item' }, + React.createElement('span', { className: 'h2-balance-trend-legend__dot h2-balance-trend-legend__dot--area' }), + '区间填充' + ), + trendSeries.hasNegative + ? React.createElement('span', { className: 'h2-balance-trend-legend__item', style: { color: '#dc2626' } }, '红点表示欠费时点') + : null + ) + : null + ), + React.createElement('div', { className: 'h2-balance-drill-table-wrap' }, + React.createElement('div', { className: 'h2-balance-drill-table-head' }, + React.createElement('div', { className: 'h2-balance-drill-table-head__left' }, + React.createElement('span', { className: 'h2-balance-drill-table-head__title' }, '流水明细'), + React.createElement('span', { className: 'h2-balance-drill-table-head__count' }, '共 ' + rows.length + ' 条') + ), + React.createElement(Button, { + size: 'small', + icon: H2_ICONS.download, + onClick: handleExportBalanceDrill, + style: { borderRadius: 8, fontWeight: 600, borderColor: '#10b981', color: '#059669' }, + 'aria-label': '导出流水明细' + }, '导出') + ), + React.createElement(Table, { + className: 'h2-balance-record-table', + size: 'small', + bordered: false, + rowKey: 'key', + columns: prepaidBalanceDrillColumns, + dataSource: rows, + pagination: rows.length > 8 ? { pageSize: 8, showSizeChanger: false, size: 'small', showTotal: function (t) { return '共 ' + t + ' 条'; } } : false, + locale: { emptyText: '暂无余额变更记录' }, + scroll: rows.length > 8 ? { x: 'max-content', y: 320 } : { x: 'max-content' } + }) ) ); - }, [prepaidBalanceDrillSummary]); + }, [prepaidBalanceDrill, prepaidBalanceDrillSummary, prepaidBalanceTrendSeries, prepaidBalanceDrillColumns, handleExportBalanceDrill]); + + var renderStationViewPanel = useCallback(function () { + var record = viewModal.record; + if (!record) return null; + var supplier = h2ResolveStationSupplier(record); + var descColumn = { xs: 1, sm: 1, md: 2, lg: 2, xl: 2, xxl: 2 }; + var price = h2ResolveCurrentCostPrice(record); + var balanceNegative = h2NumOrZero(record.prepaidBalance) < 0; + var balanceText = h2FormatYuanSymbol(record.prepaidBalance, { keepZero: true }); + + var renderText = function (v) { + return v != null && v !== '' ? v : '—'; + }; + + var renderViewContractFiles = function (files) { + if (!files || !files.length) return '—'; + return React.createElement('div', { className: 'h2-station-view-files' }, + files.map(function (f, idx) { + return React.createElement('button', { + key: f.uid || f.name || idx, + type: 'button', + className: 'h2-station-view-file-link', + title: '点击预览 ' + (f.name || '附件'), + onClick: function () { openFilePreview(f, 'doc'); } + }, f.name || '附件'); + }) + ); + }; + + var renderViewLicenseImages = function (files, opts) { + opts = opts || {}; + if (!files || !files.length) { + return React.createElement('span', { className: 'h2-station-view-license-empty' }, '未上传'); + } + return React.createElement('div', { className: 'h2-station-view-license-block' }, + opts.showCount + ? React.createElement('div', { className: 'h2-station-view-license-count' }, '共 ' + files.length + ' 张') + : null, + React.createElement('div', { className: 'h2-station-view-license-images' }, + files.map(function (f, idx) { + var imgUrl = h2ResolveLicenseDisplayUrl(f); + return React.createElement('button', { + key: f.uid || f.name || idx, + type: 'button', + className: 'h2-station-view-license-thumb', + title: '点击预览 ' + (f.name || '证照'), + onClick: function () { openFilePreview(f, 'license'); } + }, + React.createElement('img', { + src: imgUrl, + alt: f.name || '证照图片' + }) + ); + }) + ) + ); + }; + + var renderViewSection = function (title, items) { + return React.createElement('div', { className: 'h2-station-view-section' }, + React.createElement('div', { className: 'h2-station-view-section__title' }, title), + React.createElement(Descriptions, { + bordered: true, + size: 'small', + column: descColumn + }, + items.map(function (item) { + return React.createElement(Descriptions.Item, { + key: item.key, + label: item.label, + span: item.span || 1 + }, item.value); + }) + ) + ); + }; + + var renderEmptySection = function (title, hint) { + return React.createElement('div', { className: 'h2-station-view-section' }, + React.createElement('div', { className: 'h2-station-view-section__title' }, title), + React.createElement('div', { className: 'h2-station-view-empty' }, hint) + ); + }; + + var contractValue = '—'; + if (record.isSigned) { + contractValue = React.createElement('div', null, + React.createElement('span', null, (record.contractStart || '—') + ' 至 ' + (record.contractEnd || '—')), + record.contractEnd ? React.createElement('div', { style: { marginTop: 4 } }, h2RenderContractRemainTag(record.contractEnd)) : null + ); + } + + var stationItems = [ + { key: 'name', label: '加氢站名称', value: renderText(record.name) }, + { key: 'region', label: '省 / 市', value: renderText(h2FormatRegion(record.region)) }, + { key: 'detail', label: '详细地址', value: renderText(record.addressDetail), span: 2 }, + { key: 'status', label: '营业状态', value: renderBusinessStatusTag(record.businessStatus) }, + { key: 'hours', label: '营业时间', value: renderText(h2DisplayBusinessHours(record.businessHours)) }, + { key: 'signed', label: '是否签约', value: record.isSigned ? '已签约' : '未签约' } + ]; + if (record.isSigned) { + stationItems.push( + { key: 'contract', label: '合作时间', value: contractValue, span: 2 }, + { key: 'files', label: '签约附件', value: renderViewContractFiles(record.contractFiles), span: 2 } + ); + } + stationItems.push( + { key: 'contact', label: '联系人', value: renderText(record.contact) }, + { key: 'phone', label: '联系电话', value: renderText(record.phone) }, + { + key: 'balance', + label: '预付余额', + value: React.createElement('span', { + style: { fontWeight: 700, color: balanceNegative ? '#dc2626' : '#059669', fontVariantNumeric: 'tabular-nums' } + }, balanceText) + }, + { + key: 'price', + label: '当前成本价格', + value: price != null ? h2FormatYuanSymbol(price, { keepZero: true }) + ' / kg' : '—' + } + ); + + var supplierItems = supplier ? [ + { key: 'name', label: '供应商名称', value: renderText(supplier.name), span: 2 }, + { key: 'signingCompany', label: '签约公司', value: renderText(supplier.signingCompany), span: 2 }, + { key: 'type', label: '供应商类型', value: renderText(supplier.type || '加氢站') }, + { key: 'city', label: '省 / 市', value: renderText(h2FormatRegion(supplier.city)) }, + { key: 'address', label: '详细地址', value: renderText(supplier.address), span: 2 }, + { key: 'region', label: '所属区域', value: renderText(supplier.region) }, + { key: 'dept', label: '归属部门', value: renderText(supplier.dept) }, + { key: 'manager', label: '负责人', value: renderText(supplier.manager) }, + { key: 'contactName', label: '联系人', value: renderText(supplier.contactName) }, + { key: 'contactMobile', label: '联系电话', value: renderText(supplier.contactMobile) }, + { key: 'bl', label: '营业执照', value: renderViewLicenseImages(supplier.businessLicenseFiles), span: 2 }, + { key: 'fl', label: '其他证照', value: renderViewLicenseImages(supplier.fillingLicenseFiles, { showCount: true }), span: 2 } + ] : []; + + var paymentItems = supplier ? [ + { key: 'taxId', label: '纳税人识别号', value: renderText(supplier.taxId), span: 2 }, + { key: 'invoicePhone', label: '注册电话', value: renderText(supplier.invoicePhone) }, + { key: 'invoiceAddress', label: '注册地址', value: renderText(supplier.invoiceAddress), span: 2 }, + { key: 'bankName', label: '开户行', value: renderText(supplier.bankName), span: 2 }, + { key: 'bankAccount', label: '银行账号', value: renderText(supplier.bankAccount), span: 2 }, + { key: 'businessAddress', label: '营业地址', value: renderText(supplier.businessAddress), span: 2 } + ] : []; + + return React.createElement('div', { className: 'h2-station-view-panel' }, + renderViewSection('加氢站信息', stationItems), + supplier ? renderViewSection('供应商信息', supplierItems) : renderEmptySection('供应商信息', '暂未关联供应商'), + supplier ? renderViewSection('付款信息', paymentItems) : renderEmptySection('付款信息', '暂无付款信息') + ); + }, [viewModal.record, openFilePreview]); + + var renderRefuelDrillMoney = function (text, tone) { + return React.createElement('span', { className: 'h2-refuel-drill-money h2-refuel-drill-money--' + tone }, text); + }; var refuelRecordColumns = [ - { title: '序号', key: 'seq', width: 52, align: 'center', render: function (_, __, index) { return index + 1; } }, - { title: '加氢时间', dataIndex: 'hydrogenTime', key: 'hydrogenTime', width: 168 }, - { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 110 }, - { title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 180, ellipsis: true }, - { title: '加氢量(kg)', dataIndex: 'hydrogenKg', key: 'hydrogenKg', width: 100, align: 'right', render: function (v) { return h2FormatKgNum(v); } }, - { title: '成本单价', dataIndex: 'costUnitPrice', key: 'costUnitPrice', width: 92, align: 'right', render: function (v) { return h2FormatYuanNum(v); } }, - { title: '成本总价', dataIndex: 'costAmount', key: 'costAmount', width: 92, align: 'right', render: function (v) { return h2FormatYuanNum(v); } }, - { title: '加氢单价', dataIndex: 'customerUnitPrice', key: 'customerUnitPrice', width: 92, align: 'right', render: function (v) { return h2FormatYuanNum(v); } }, - { title: '加氢总价', dataIndex: 'customerAmount', key: 'customerAmount', width: 92, align: 'right', render: function (v) { return h2FormatYuanNum(v); } } + { + title: '序号', + key: 'seq', + width: 52, + align: 'center', + render: function (_, __, index) { + return React.createElement('span', { className: 'h2-refuel-drill-seq' }, index + 1); + } + }, + { + title: '加氢时间', + dataIndex: 'hydrogenTime', + key: 'hydrogenTime', + width: 158, + render: function (v) { + return React.createElement('span', { className: 'h2-refuel-drill-time' }, v || '—'); + } + }, + { + title: '车牌号', + dataIndex: 'plateNo', + key: 'plateNo', + width: 108, + render: function (v) { + return React.createElement('span', { className: 'h2-refuel-drill-plate' }, v || '—'); + } + }, + { title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 168, ellipsis: true }, + { + title: '加氢量(kg)', + dataIndex: 'hydrogenKg', + key: 'hydrogenKg', + width: 96, + align: 'right', + render: function (v) { + return React.createElement('span', { className: 'h2-refuel-drill-kg' }, h2FormatKgNum(v)); + } + }, + { + title: '成本单价(元/kg)', + dataIndex: 'costUnitPrice', + key: 'costUnitPrice', + width: 118, + align: 'right', + render: function (v) { + return renderRefuelDrillMoney(h2FormatYuanSymbol(v, { keepZero: true }), 'cost'); + } + }, + { + title: '成本总价(元)', + dataIndex: 'costAmount', + key: 'costAmount', + width: 118, + align: 'right', + render: function (v) { + return renderRefuelDrillMoney(h2FormatYuanSymbol(v, { keepZero: true }), 'cost'); + } + }, + { + title: '加氢单价(元/kg)', + dataIndex: 'customerUnitPrice', + key: 'customerUnitPrice', + width: 118, + align: 'right', + render: function (v) { + return renderRefuelDrillMoney(h2FormatYuanSymbol(v, { keepZero: true }), 'customer'); + } + }, + { + title: '加氢总价(元)', + dataIndex: 'customerAmount', + key: 'customerAmount', + width: 118, + align: 'right', + render: function (v) { + return renderRefuelDrillMoney(h2FormatYuanSymbol(v, { keepZero: true }), 'customer'); + } + }, + { + title: '承担方式', + dataIndex: 'settlementStatus', + key: 'settlementStatus', + width: 120, + render: function (v) { + var info = H2_SETTLEMENT_STATUS_MAP[v]; + if (!info) return '—'; + return React.createElement(Tag, { color: info.color, style: { margin: 0, borderRadius: 6, fontWeight: 600 } }, info.label); + } + }, + { + title: '订单编号', + dataIndex: 'orderNo', + key: 'orderNo', + width: 148, + align: 'center', + ellipsis: true, + render: function (v) { + if (!v) return React.createElement('span', { style: { color: '#cbd5e1' } }, '—'); + return React.createElement('span', { className: 'h2-refuel-drill-order-no', title: v }, v); + } + } ]; var refuelModalSummary = useMemo(function () { @@ -3006,25 +7018,473 @@ const Component = function () { return { count: records.length, totalKg: totalKg, totalCost: totalCost, totalCustomer: totalCustomer }; }, [refuelModal.records]); - var renderRefuelTotalsBar = useCallback(function (summary) { - var items = [ - { key: 'count', label: '加氢次数(次)', value: String(summary.count || 0) }, - { key: 'hydrogenKg', label: '加氢量(kg)', value: h2FormatKgNum(summary.totalKg) }, - { key: 'costTotal', label: '成本总价(元)', value: h2FormatYuanNum(summary.totalCost) }, - { key: 'customerAmount', label: '加氢总价(元)', value: h2FormatYuanNum(summary.totalCustomer) } + var renderRefuelDrillPanel = useCallback(function () { + var records = refuelModal.records || []; + var summary = refuelModalSummary; + var stationName = (refuelModal.station && refuelModal.station.name) || '—'; + var stats = [ + { key: 'count', label: '加氢次数', value: String(summary.count || 0), mod: 'count', valueMod: 'count' }, + { key: 'kg', label: '加氢量合计(kg)', value: h2FormatKgNum(summary.totalKg), mod: 'kg', valueMod: 'kg' }, + { key: 'cost', label: '成本总价', value: h2FormatYuanSymbol(summary.totalCost, { keepZero: true }), mod: 'cost', valueMod: 'cost' }, + { key: 'customer', label: '加氢总价', value: h2FormatYuanSymbol(summary.totalCustomer, { keepZero: true }), mod: 'customer', valueMod: 'customer' } ]; - return React.createElement('div', { className: 'h2-ledger-totals-bar' }, - React.createElement('div', { className: 'h2-ledger-totals-bar__title' }, '合计'), - React.createElement('div', { className: 'h2-ledger-totals-bar__items' }, - items.map(function (item) { - return React.createElement('div', { key: item.key, className: 'h2-ledger-totals-bar__item' }, - React.createElement('div', { className: 'h2-ledger-totals-bar__label' }, item.label), - React.createElement('div', { className: 'h2-ledger-totals-bar__value' }, item.value) + return React.createElement('div', { className: 'h2-refuel-drill-panel' }, + React.createElement('div', { className: 'h2-refuel-drill-station-card' }, + React.createElement('div', { className: 'h2-refuel-drill-station-card__name' }, stationName), + React.createElement('div', { className: 'h2-refuel-drill-station-card__meta' }, '车辆加氢记录 · 共 ' + records.length + ' 笔') + ), + React.createElement('div', { className: 'h2-refuel-drill-stats', role: 'group', 'aria-label': '加氢统计' }, + stats.map(function (item) { + return React.createElement('div', { + key: item.key, + className: 'h2-refuel-drill-stat h2-refuel-drill-stat--' + item.mod, + role: 'article', + 'aria-label': item.label + ' ' + item.value + }, + React.createElement('div', { className: 'h2-refuel-drill-stat__label' }, item.label), + React.createElement('div', { + className: 'h2-refuel-drill-stat__value h2-refuel-drill-stat__value--' + item.valueMod + }, item.value) ); }) + ), + React.createElement('div', { className: 'h2-refuel-drill-table-wrap' }, + React.createElement('div', { className: 'h2-refuel-drill-table-head' }, + React.createElement('div', { className: 'h2-refuel-drill-table-head__left' }, + React.createElement('span', { className: 'h2-refuel-drill-table-head__title' }, '加氢明细'), + React.createElement('span', { className: 'h2-refuel-drill-table-head__count' }, '共 ' + records.length + ' 条') + ), + React.createElement(Button, { + size: 'small', + icon: H2_ICONS.download, + onClick: handleExportRefuelDrill, + style: { borderRadius: 8, fontWeight: 600, borderColor: '#10b981', color: '#059669' }, + 'aria-label': '导出加氢明细' + }, '导出') + ), + React.createElement(Table, { + className: 'h2-refuel-record-table', + size: 'small', + bordered: false, + rowKey: 'id', + columns: refuelRecordColumns, + dataSource: records, + pagination: records.length > 8 ? { pageSize: 8, showSizeChanger: false, size: 'small', showTotal: function (t) { return '共 ' + t + ' 条'; } } : false, + locale: { emptyText: '暂无加氢记录' }, + scroll: records.length > 8 ? { x: 'max-content', y: 320 } : { x: 'max-content' } + }) ) ); - }, []); + }, [refuelModal, refuelModalSummary, refuelRecordColumns, handleExportRefuelDrill]); + + var statementRecordColumns = [ + { + title: '序号', + key: 'seq', + width: 52, + align: 'center', + render: function (_, __, index) { + return React.createElement('span', { className: 'h2-refuel-drill-seq' }, index + 1); + } + }, + { + title: '加氢时间', + dataIndex: 'hydrogenTime', + key: 'hydrogenTime', + width: 158, + render: function (v) { + return React.createElement('span', { className: 'h2-refuel-drill-time' }, v || '—'); + } + }, + { + title: '车牌号', + dataIndex: 'plateNo', + key: 'plateNo', + width: 108, + render: function (v) { + return React.createElement('span', { className: 'h2-refuel-drill-plate' }, v || '—'); + } + }, + { + title: '加氢量(kg)', + dataIndex: 'hydrogenKg', + key: 'hydrogenKg', + width: 96, + align: 'right', + render: function (v) { + return React.createElement('span', { className: 'h2-refuel-drill-kg' }, h2FormatKgNum(v)); + } + }, + { + title: '成本单价(元/kg)', + dataIndex: 'costUnitPrice', + key: 'costUnitPrice', + width: 118, + align: 'right', + render: function (v) { + return renderRefuelDrillMoney(h2FormatYuanSymbol(v, { keepZero: true }), 'cost'); + } + }, + { + title: '成本总价(元)', + dataIndex: 'costTotal', + key: 'costTotal', + width: 118, + align: 'right', + render: function (v) { + return renderRefuelDrillMoney(h2FormatYuanSymbol(v, { keepZero: true }), 'cost'); + } + }, + { + title: '订单编号', + dataIndex: 'orderNo', + key: 'orderNo', + width: 148, + align: 'center', + ellipsis: true, + render: function (v) { + if (!v) return React.createElement('span', { style: { color: '#cbd5e1' } }, '—'); + return React.createElement('span', { className: 'h2-refuel-drill-order-no', title: v }, v); + } + } + ]; + + var renderStatementBalanceReadonly = function (balance) { + var n = h2NumOrZero(balance); + var isArrears = n < 0; + return React.createElement('div', { className: 'h2-statement-balance-readonly' }, + React.createElement('span', { + className: 'h2-statement-balance-readonly__amount ' + (isArrears ? 'h2-statement-balance-readonly__amount--negative' : 'h2-statement-balance-readonly__amount--positive') + }, h2FormatYuanNum(n)), + isArrears ? React.createElement(Tag, { color: 'error', className: 'lc-station-signed-tag' }, '已欠费') : null + ); + }; + + var renderStatementPanel = useCallback(function () { + var record = statementModal.record; + var stationName = (record && record.name) || '—'; + var isDraft = statementModal.phase === 'draft'; + var rows = isDraft ? (statementModal.draftRows || []) : []; + var summary = isDraft ? statementDraftSummary : { count: 0, totalKg: 0, totalCost: 0 }; + var lastEndDate = h2GetLastStatementEndDate(statementRecords, stationName); + var stats = [ + { key: 'count', label: '加氢次数', value: String(summary.count || 0), mod: 'count', valueMod: 'count' }, + { key: 'kg', label: '加氢总量(kg)', value: h2FormatKgNum(summary.totalKg), mod: 'kg', valueMod: 'kg' }, + { key: 'cost', label: '成本总金额', value: h2FormatYuanSymbol(summary.totalCost, { keepZero: true }), mod: 'cost', valueMod: 'cost' } + ]; + return React.createElement('div', { className: 'h2-statement-panel' }, + React.createElement('div', { className: 'h2-statement-station-card' }, + React.createElement('div', { className: 'h2-statement-station-card__name' }, stationName), + React.createElement('div', { className: 'h2-statement-station-card__meta' }, + isDraft ? '对账记录 · 待提交' : '选择账单日期 · 生成对账记录' + ) + ), + React.createElement('div', { className: 'h2-statement-form-wrap' }, + React.createElement('div', { className: 'h2-statement-date-grid' }, + formItem('账单开始日期', true, React.createElement(Input, { + type: 'date', + style: { width: '100%', borderRadius: 8 }, + disabled: isDraft, + value: statementModal.startDate || '', + onChange: function (e) { + setStatementModal(function (m) { return Object.assign({}, m, { startDate: e.target.value, phase: 'select', draftRows: [] }); }); + } + })), + formItem('账单结束日期', true, React.createElement(Input, { + type: 'date', + style: { width: '100%', borderRadius: 8 }, + disabled: isDraft, + value: statementModal.endDate || '', + onChange: function (e) { + setStatementModal(function (m) { return Object.assign({}, m, { endDate: e.target.value, phase: 'select', draftRows: [] }); }); + } + })) + ), + React.createElement('div', { className: 'h2-statement-last-end' }, + '上次对账单结束时间:', + React.createElement('strong', null, lastEndDate || '暂无') + ), + !isDraft ? React.createElement('div', { className: 'h2-statement-form-hint' }, + '数据来源:', + React.createElement('strong', null, '车辆氢费明细'), + ' 中标记为「已对账」且尚未结算的加氢记录;明细仅展示成本价与成本总价。' + ) : null + ), + isDraft ? React.createElement('div', { className: 'h2-statement-stats', role: 'group', 'aria-label': '对账统计' }, + stats.map(function (item) { + return React.createElement('div', { + key: item.key, + className: 'h2-statement-stat h2-statement-stat--' + item.mod, + role: 'article', + 'aria-label': item.label + ' ' + item.value + }, + React.createElement('div', { className: 'h2-statement-stat__label' }, item.label), + React.createElement('div', { + className: 'h2-statement-stat__value h2-statement-stat__value--' + item.valueMod + }, item.value) + ); + }) + ) : null, + isDraft ? React.createElement('div', { className: 'h2-statement-table-wrap' }, + React.createElement('div', { className: 'h2-statement-table-head' }, + React.createElement('div', { className: 'h2-statement-table-head__left' }, + React.createElement('span', { className: 'h2-statement-table-head__title' }, '对账明细(成本字段)'), + React.createElement('span', { className: 'h2-statement-table-head__count' }, '共 ' + rows.length + ' 条') + ), + React.createElement(Button, { + size: 'small', + icon: H2_ICONS.download, + disabled: !rows.length, + onClick: handleExportStatement, + style: { borderRadius: 8, fontWeight: 600, borderColor: '#7c3aed', color: '#6d28d9' }, + 'aria-label': '导出对账单明细' + }, '导出') + ), + React.createElement(Table, { + className: 'h2-statement-record-table', + size: 'small', + bordered: false, + rowKey: 'id', + columns: statementRecordColumns, + dataSource: rows, + pagination: rows.length > 8 ? { pageSize: 8, showSizeChanger: false, size: 'small', showTotal: function (t) { return '共 ' + t + ' 条'; } } : false, + locale: { emptyText: '暂无对账明细' }, + scroll: rows.length > 8 ? { x: 'max-content', y: 320 } : { x: 'max-content' } + }) + ) : null, + isDraft ? React.createElement('div', { className: 'h2-statement-settlement-wrap' }, + formItem('结算后加氢站预付款余额', true, renderStatementBalanceReadonly(statementModal.balanceAfterSettlement)), + React.createElement('div', { className: 'h2-statement-receipt-grid' }, + formItem('收票日期', true, dayjs && DatePicker + ? React.createElement(DatePicker, { + style: { width: '100%', borderRadius: 8 }, + format: 'YYYY-MM-DD', + placeholder: '请选择收票日期', + allowClear: true, + value: h2ToDateDayjs(statementModal.receiptDate), + onChange: function (d, dateStr) { + setStatementModal(function (m) { + return Object.assign({}, m, { receiptDate: dateStr || '' }); + }); + } + }) + : React.createElement(Input, { + type: 'date', + style: { width: '100%', borderRadius: 8 }, + placeholder: '请选择收票日期', + value: statementModal.receiptDate || '', + onChange: function (e) { + setStatementModal(function (m) { return Object.assign({}, m, { receiptDate: e.target.value }); }); + } + })), + formItem('收票金额', true, React.createElement(Input, { + className: 'h2-statement-receipt-amount-input', + inputMode: 'decimal', + prefix: '¥', + style: { width: '100%', borderRadius: 8 }, + placeholder: '请输入收票金额', + value: statementModal.receiptAmount || '', + onChange: function (e) { + var next = h2SanitizeReceiptAmountInput(e.target.value); + setStatementModal(function (m) { return Object.assign({}, m, { receiptAmount: next }); }); + }, + onBlur: function (e) { + var formatted = h2FormatReceiptAmountInput(e.target.value); + setStatementModal(function (m) { return Object.assign({}, m, { receiptAmount: formatted }); }); + } + })) + ), + formItem('发票附件', true, React.createElement(ContractFilesUpload, { + fileList: statementModal.invoiceFiles || [], + showHint: false, + wrapClassName: 'h2-statement-invoice-upload', + uploadClassName: 'h2-statement-upload-btn-wrap', + buttonClassName: 'h2-statement-upload-btn', + onChange: function (info) { + setStatementModal(function (m) { return Object.assign({}, m, { invoiceFiles: info.fileList || [] }); }); + } + })) + ) : null + ); + }, [statementModal, statementRecords, statementDraftSummary, statementRecordColumns, handleExportStatement]); + + var statementHistoryColumns = [ + { title: '对账日期', dataIndex: 'reconcileDate', key: 'reconcileDate', width: 150 }, + { title: '对账人', dataIndex: 'reconciler', key: 'reconciler', width: 100, ellipsis: true }, + { title: '账单开始日期', dataIndex: 'startDate', key: 'startDate', width: 118 }, + { title: '账单结束日期', dataIndex: 'endDate', key: 'endDate', width: 118 }, + { title: '加氢次数', dataIndex: 'refuelCount', key: 'refuelCount', width: 88, align: 'right' }, + { + title: '加氢金额', + dataIndex: 'totalCost', + key: 'totalCost', + width: 110, + align: 'right', + render: function (v) { return h2FormatYuanSymbol(v, { keepZero: true }); } + }, + { + title: '对账后加氢站预付款余额', + dataIndex: 'balanceAfterSettlement', + key: 'balanceAfterSettlement', + width: 168, + align: 'right', + render: function (v) { + var n = h2NumOrZero(v); + var isArrears = n < 0; + return React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 6, flexWrap: 'wrap' } }, + React.createElement('span', { + className: 'h2-statement-history-balance ' + (isArrears ? 'h2-statement-history-balance--negative' : 'h2-statement-history-balance--positive') + }, h2FormatYuanNum(n)), + isArrears ? React.createElement(Tag, { color: 'error', className: 'lc-station-signed-tag' }, '已欠费') : null + ); + } + }, + { + title: '操作', + key: 'action', + width: 96, + fixed: 'right', + render: function (_, row) { + return React.createElement(Button, { + type: 'link', + size: 'small', + onClick: function () { openStatementDetailModal(row); } + }, '查看明细'); + } + } + ]; + + var renderStatementHistoryPanel = useCallback(function () { + var record = statementHistoryModal.record; + var stationName = (record && record.name) || '—'; + var rows = h2GetStatementRecordsByStation(statementRecords, stationName); + return React.createElement('div', { className: 'h2-statement-history-panel' }, + React.createElement('div', { className: 'h2-statement-history-station-card' }, + React.createElement('div', { className: 'h2-statement-history-station-card__name' }, stationName), + React.createElement('div', { className: 'h2-statement-history-station-card__meta' }, '历史对账记录 · 共 ' + rows.length + ' 条') + ), + React.createElement('div', { className: 'h2-statement-history-table-wrap' }, + React.createElement(Table, { + size: 'small', + bordered: false, + rowKey: 'id', + columns: statementHistoryColumns, + dataSource: rows, + pagination: rows.length > 8 ? { pageSize: 8, showSizeChanger: false, size: 'small', showTotal: function (t) { return '共 ' + t + ' 条'; } } : false, + locale: { emptyText: '暂无对账记录' }, + scroll: { x: 'max-content' } + }) + ) + ); + }, [statementHistoryModal, statementRecords, statementHistoryColumns, openStatementDetailModal]); + + var renderStatementDetailPanel = useCallback(function () { + var stmt = statementDetailModal.statementRecord; + var rows = statementDetailModal.detailRows || []; + if (!stmt) return null; + var summary = h2CalcStatementSummary(rows); + var invoiceFiles = stmt.invoiceFiles || []; + return React.createElement('div', { className: 'h2-statement-panel' }, + React.createElement('div', { className: 'h2-statement-station-card' }, + React.createElement('div', { className: 'h2-statement-station-card__name' }, stmt.stationName || '—'), + React.createElement('div', { className: 'h2-statement-station-card__meta' }, + (stmt.startDate || '—') + ' 至 ' + (stmt.endDate || '—') + ) + ), + React.createElement('div', { className: 'h2-statement-stats', role: 'group', 'aria-label': '对账统计' }, + [ + { key: 'count', label: '加氢次数', value: String(summary.count || stmt.refuelCount || 0), mod: 'count', valueMod: 'count' }, + { key: 'kg', label: '加氢总量(kg)', value: h2FormatKgNum(summary.totalKg || stmt.totalKg), mod: 'kg', valueMod: 'kg' }, + { key: 'cost', label: '成本总金额', value: h2FormatYuanSymbol(summary.totalCost || stmt.totalCost, { keepZero: true }), mod: 'cost', valueMod: 'cost' } + ].map(function (item) { + return React.createElement('div', { + key: item.key, + className: 'h2-statement-stat h2-statement-stat--' + item.mod + }, + React.createElement('div', { className: 'h2-statement-stat__label' }, item.label), + React.createElement('div', { className: 'h2-statement-stat__value h2-statement-stat__value--' + item.valueMod }, item.value) + ); + }) + ), + React.createElement('div', { className: 'h2-statement-detail-receipt-wrap' }, + React.createElement('div', { className: 'h2-statement-detail-receipt-grid' }, + React.createElement('div', { className: 'h2-statement-detail-receipt-item' }, + React.createElement('div', { className: 'h2-statement-detail-receipt-item__label' }, '收票时间'), + React.createElement('div', { className: 'h2-statement-detail-receipt-item__value' }, stmt.receiptDate || '—') + ), + React.createElement('div', { className: 'h2-statement-detail-receipt-item' }, + React.createElement('div', { className: 'h2-statement-detail-receipt-item__label' }, '收票金额'), + React.createElement('div', { className: 'h2-statement-detail-receipt-item__value h2-statement-detail-receipt-item__value--amount' }, + stmt.receiptAmount != null ? h2FormatYuanSymbol(stmt.receiptAmount, { keepZero: true }) : '—' + ) + ) + ), + React.createElement('div', { className: 'h2-statement-detail-receipt-files' }, + React.createElement('div', { className: 'h2-statement-detail-receipt-item__label' }, '发票附件'), + invoiceFiles.length + ? React.createElement('div', { className: 'h2-statement-detail-receipt-file-list' }, + invoiceFiles.map(function (file) { + return React.createElement('div', { key: file.uid || file.name, className: 'h2-statement-detail-receipt-file-item' }, + React.createElement('span', { className: 'h2-statement-detail-receipt-file-name', title: file.name }, file.name || '未命名附件'), + React.createElement(Button, { + type: 'link', + size: 'small', + icon: H2_ICONS.download, + onClick: function () { h2DownloadUploadFile(file); } + }, '下载') + ); + }) + ) + : React.createElement('div', { style: { fontSize: 13, color: '#94a3b8', marginTop: 8 } }, '暂无发票附件') + ) + ), + React.createElement('div', { className: 'h2-statement-table-wrap' }, + React.createElement('div', { className: 'h2-statement-table-head' }, + React.createElement('div', { className: 'h2-statement-table-head__left' }, + React.createElement('span', { className: 'h2-statement-table-head__title' }, '对账明细'), + React.createElement('span', { className: 'h2-statement-table-head__count' }, '共 ' + rows.length + ' 条') + ) + ), + React.createElement(Table, { + className: 'h2-statement-record-table', + size: 'small', + bordered: false, + rowKey: 'id', + columns: statementRecordColumns, + dataSource: rows, + pagination: rows.length > 8 ? { pageSize: 8, showSizeChanger: false, size: 'small' } : false, + locale: { emptyText: '暂无明细' }, + scroll: rows.length > 8 ? { x: 'max-content', y: 320 } : { x: 'max-content' } + }) + ) + ); + }, [statementDetailModal, statementRecordColumns]); + + var renderPriceConfigMoney = function (text, tone) { + return React.createElement('span', { className: 'h2-price-config-money h2-price-config-money--' + tone }, text); + }; + + var costPriceLogColumns = [ + { title: '操作人', dataIndex: 'operator', key: 'operator', width: 100, ellipsis: true }, + { title: '操作时间', dataIndex: 'operateTime', key: 'operateTime', width: 150 }, + { + title: '调整前成本价格', + dataIndex: 'beforeCostPrice', + key: 'beforeCostPrice', + width: 130, + align: 'right', + render: function (v) { return renderPriceConfigMoney(h2FormatCostPriceDisplay(v), 'before'); } + }, + { + title: '调整后成本价格', + dataIndex: 'afterCostPrice', + key: 'afterCostPrice', + width: 130, + align: 'right', + render: function (v) { return renderPriceConfigMoney(h2FormatCostPriceDisplay(v), 'after'); } + }, + { title: '生效时间', dataIndex: 'effectiveTime', key: 'effectiveTime', width: 150 } + ]; var businessStatusLogColumns = [ { title: '操作时间', dataIndex: 'operateTime', key: 'operateTime', width: 150 }, @@ -3041,7 +7501,7 @@ const Component = function () { dataIndex: 'afterStatus', key: 'afterStatus', width: 110, - render: function (v) { return renderBusinessStatusText(v); } + render: function (v) { return renderBusinessStatusTag(v); } } ]; @@ -3050,33 +7510,22 @@ const Component = function () { title: '加氢站名称', dataIndex: 'name', key: 'name', - width: 248, + width: 300, fixed: 'left', - render: function (v, r) { - return React.createElement('div', { className: 'lc-station-name-row' }, - React.createElement('div', { className: 'lc-station-name' }, v || '—'), - React.createElement(Tag, { - color: r.isSigned ? 'success' : 'default', - className: 'lc-station-signed-tag', - style: r.isSigned ? undefined : { color: '#64748b', background: '#f1f5f9', border: '1px solid #e2e8f0' } - }, r.isSigned ? '已签约' : '未签约') - ); - } - }, - { - title: '地址', - key: 'address', - width: 280, ellipsis: true, - render: function (_, r) { - return React.createElement('div', null, - React.createElement('div', { className: 'lc-station-region' }, h2FormatRegion(r.region)), - React.createElement('div', { style: { color: '#334155' } }, r.addressDetail || '—') + render: function (v, r) { + var addressLine = h2FormatStationAddressLine(r); + return React.createElement('div', { className: 'lc-station-name-cell' }, + React.createElement('div', { className: 'lc-station-name-row' }, + React.createElement('div', { className: 'lc-station-name' }, v || '—'), + h2RenderStationSignedTag(r) + ), + React.createElement('div', { className: 'lc-station-address-line', title: addressLine }, addressLine) ); } }, { - title: '签约时间', + title: '合作时间', key: 'contractRange', width: 220, render: function (_, r) { @@ -3097,22 +7546,47 @@ const Component = function () { render: renderBusinessStatusTag }, { title: '营业时间', dataIndex: 'businessHours', key: 'businessHours', width: 140, render: function (v) { return h2DisplayBusinessHours(v); } }, + { + title: '当前成本价格(元/kg)', + key: 'currentCostPrice', + width: 128, + align: 'right', + render: function (_, r) { + var price = h2ResolveCurrentCostPrice(r); + if (price == null) { + return React.createElement('span', { style: { color: '#94a3b8' } }, '—'); + } + return React.createElement('span', { + style: { fontVariantNumeric: 'tabular-nums', fontWeight: 600, color: '#0f172a' } + }, '¥' + h2FormatYuanNum(price)); + } + }, { title: '加氢次数', dataIndex: 'refuelCount', key: 'refuelCount', - width: 96, + width: 110, align: 'right', - render: function (v, record) { - return renderRefuelDrillLink(String(v || 0), record, '查看加氢记录,共 ' + (v || 0) + ' 次'); + sortOrder: refuelSort.key === 'refuelCount' ? refuelSort.order : null, + sorter: function () { return 0; }, + sortDirections: ['descend', 'ascend'], + showSorterTooltip: { title: '点击切换倒序 / 正序' }, + render: function (v) { + return React.createElement('span', { + style: { fontVariantNumeric: 'tabular-nums', fontWeight: 600, color: '#0f172a' } + }, String(v || 0)); } }, { title: '加氢量(kg)', dataIndex: 'refuelTotalKg', key: 'refuelTotalKg', - width: 148, + width: 160, align: 'right', + sortOrder: refuelSort.key === 'refuelTotalKg' ? refuelSort.order : null, + sorter: function () { return 0; }, + sortDirections: ['descend', 'ascend'], + showSorterTooltip: { title: '点击切换倒序 / 正序' }, render: function (v, record) { return React.createElement('div', { className: 'lc-refuel-kg-row' }, renderRefuelFreqShortTag(record.refuelFreqKey), @@ -3187,18 +7661,386 @@ const Component = function () { }, node); }; - var drawerSection = function (children) { - return React.createElement('div', { className: 'h2-drawer-section' }, children); - }; + var renderBusinessStatusPanel = useCallback(function () { + var record = businessModal.record; + var logs = businessModal.statusLogs || []; + var stationName = (record && record.name) || '—'; + var originStatus = businessModal.originBusinessStatus || (record && record.businessStatus) || '营业中'; + var originHours = (record && record.businessHours) || '—'; + var displayHours = businessModal.businessHours || originHours; + var pendingChanged = (businessModal.businessStatus || '营业中') !== originStatus; + var stats = [ + { + key: 'status', + label: '当前营业状态', + value: renderBusinessStatusTag(originStatus), + mod: 'status', + valueMod: '' + }, + { + key: 'hours', + label: '当前营业时间', + value: h2DisplayBusinessHours(displayHours), + mod: 'hours', + valueMod: 'hours' + } + ]; - var drawerTitle = drawer.mode === 'edit' ? '编辑加氢站点' : '查看加氢站点'; + return React.createElement('div', { className: 'h2-business-status-panel' }, + React.createElement('div', { className: 'h2-business-status-station-card' }, + React.createElement('div', { className: 'h2-business-status-station-card__name' }, stationName), + React.createElement('div', { className: 'h2-business-status-station-card__meta' }, '营业状态维护 · 共 ' + logs.length + ' 条记录') + ), + React.createElement('div', { className: 'h2-business-status-stats', role: 'group', 'aria-label': '营业状态统计' }, + stats.map(function (item) { + return React.createElement('div', { + key: item.key, + className: 'h2-business-status-stat h2-business-status-stat--' + item.mod, + role: 'article', + 'aria-label': item.label + }, + React.createElement('div', { className: 'h2-business-status-stat__label' }, item.label), + React.createElement('div', { + className: 'h2-business-status-stat__value' + (item.valueMod ? ' h2-business-status-stat__value--' + item.valueMod : '') + }, item.value) + ); + }) + ), + React.createElement('div', { className: 'h2-business-status-form-wrap' }, + React.createElement(Form, { layout: 'vertical', requiredMark: false }, + formItem('营业状态', true, h2RenderOptionButtonGroup( + H2_BUSINESS_STATUS_BTN_OPTIONS, + businessModal.businessStatus || '营业中', + function (v) { setBusinessModal(function (m) { return Object.assign({}, m, { businessStatus: v }); }); }, + { ariaLabel: '营业状态' } + )), + formItem('营业时间', false, React.createElement(BusinessHoursInput, { + value: businessModal.businessHours, + fieldStyle: { width: '100%', borderRadius: 8 }, + onChange: function (v) { setBusinessModal(function (m) { return Object.assign({}, m, { businessHours: v }); }); } + })) + ), + React.createElement('div', { className: 'h2-business-status-form-hint', role: 'note' }, + pendingChanged + ? '保存后将记录营业状态变更,并更新列表展示。' + : '选择「非全天营业」后需设置开始与结束时间;暂停或停止营业时建议配置具体时段。' + ) + ), + React.createElement('div', { className: 'h2-business-status-table-wrap' }, + React.createElement('div', { className: 'h2-business-status-table-head' }, + React.createElement('span', { className: 'h2-business-status-table-head__title' }, '营业状态变更记录'), + React.createElement('span', { className: 'h2-business-status-table-head__count' }, '共 ' + logs.length + ' 条') + ), + React.createElement(Table, { + className: 'h2-business-status-record-table', + size: 'small', + bordered: false, + rowKey: 'id', + columns: businessStatusLogColumns, + dataSource: logs, + pagination: logs.length > 5 ? { pageSize: 5, showSizeChanger: false, size: 'small', showTotal: function (t) { return '共 ' + t + ' 条'; } } : false, + locale: { emptyText: '暂无状态变更记录' }, + scroll: logs.length > 5 ? { x: 520, y: 240 } : { x: 520 } + }) + ) + ); + }, [businessModal, businessStatusLogColumns]); + + var renderPriceConfigPanel = useCallback(function () { + var record = priceModal.record; + var logs = priceModal.priceLogs || []; + var stationName = (record && record.name) || '—'; + var currentPrice = record ? h2ResolveCurrentCostPrice(record) : null; + var pendingCount = h2CountPendingCostPriceLogs(logs); + var currentPriceText = currentPrice != null ? h2FormatYuanSymbol(currentPrice, { keepZero: true }) + '/kg' : '—'; + var stats = [ + { key: 'current', label: '当前成本价格', value: currentPriceText, mod: 'current', valueMod: 'current' }, + { key: 'history', label: '历史调整次数', value: String(logs.length), mod: 'history', valueMod: 'history' }, + { key: 'pending', label: '待生效配置', value: String(pendingCount), mod: 'pending', valueMod: 'pending' } + ]; + var effectiveTimeValue = h2ToDateTimeDayjs(priceModal.effectiveTime); + var effectivePicker = dayjs && DatePicker + ? React.createElement(DatePicker, { + showTime: true, + needConfirm: false, + style: { width: '100%', borderRadius: 8 }, + format: 'YYYY-MM-DD HH:mm', + placeholder: '请选择生效时间', + allowClear: true, + value: effectiveTimeValue, + onChange: function (d, dateStr) { + setPriceModal(function (m) { + var next = dateStr || (d && d.isValid && d.isValid() ? d.format('YYYY-MM-DD HH:mm') : ''); + return Object.assign({}, m, { effectiveTime: next }); + }); + } + }) + : React.createElement(Input, { + type: 'datetime-local', + style: { borderRadius: 8 }, + value: priceModal.effectiveTime ? priceModal.effectiveTime.replace(' ', 'T') : '', + onChange: function (e) { + var raw = e.target.value || ''; + setPriceModal(function (m) { + return Object.assign({}, m, { effectiveTime: raw ? raw.replace('T', ' ') : '' }); + }); + } + }); + + return React.createElement('div', { className: 'h2-price-config-panel' }, + React.createElement('div', { className: 'h2-price-config-station-card' }, + React.createElement('div', { className: 'h2-price-config-station-card__name' }, stationName), + React.createElement('div', { className: 'h2-price-config-station-card__meta' }, '成本价格配置 · 共 ' + logs.length + ' 条记录') + ), + React.createElement('div', { className: 'h2-price-config-stats', role: 'group', 'aria-label': '价格统计' }, + stats.map(function (item) { + return React.createElement('div', { + key: item.key, + className: 'h2-price-config-stat h2-price-config-stat--' + item.mod, + role: 'article', + 'aria-label': item.label + ' ' + item.value + }, + React.createElement('div', { className: 'h2-price-config-stat__label' }, item.label), + React.createElement('div', { + className: 'h2-price-config-stat__value h2-price-config-stat__value--' + item.valueMod + }, item.value) + ); + }) + ), + React.createElement('div', { className: 'h2-price-config-form-wrap' }, + React.createElement(Form, { layout: 'vertical', requiredMark: false }, + formItem('成本价格(元/kg)', true, React.createElement(Input, { + value: priceModal.costUnitPrice, + placeholder: '请输入成本价格', + style: { borderRadius: 8 }, + onChange: function (e) { setPriceModal(function (m) { return Object.assign({}, m, { costUnitPrice: e.target.value }); }); } + })), + formItem('生效时间', true, effectivePicker) + ), + React.createElement('div', { className: 'h2-price-config-form-hint', role: 'note' }, + '到达生效时间后,列表「当前成本价格」将自动更新为调整后价格' + ) + ), + React.createElement('div', { className: 'h2-price-config-table-wrap' }, + React.createElement('div', { className: 'h2-price-config-table-head' }, + React.createElement('span', { className: 'h2-price-config-table-head__title' }, '价格调整记录'), + React.createElement('span', { className: 'h2-price-config-table-head__count' }, '共 ' + logs.length + ' 条') + ), + React.createElement(Table, { + className: 'h2-price-config-record-table', + size: 'small', + bordered: false, + rowKey: 'id', + columns: costPriceLogColumns, + dataSource: logs, + pagination: logs.length > 5 ? { pageSize: 5, showSizeChanger: false, size: 'small', showTotal: function (t) { return '共 ' + t + ' 条'; } } : false, + locale: { emptyText: '暂无价格调整记录' }, + scroll: logs.length > 5 ? { x: 680, y: 240 } : { x: 680 } + }) + ) + ); + }, [priceModal, costPriceLogColumns]); + + var getRechargeStationOptions = useCallback(function (currentLineId) { + var usedIds = {}; + (rechargeModal.lines || []).forEach(function (line) { + if (line.id !== currentLineId && line.stationId != null) usedIds[line.stationId] = true; + }); + return listData + .filter(function (r) { return !usedIds[r.id]; }) + .map(function (r) { return { value: r.id, label: r.name || '—' }; }); + }, [listData, rechargeModal.lines]); + + var rechargeModalColumns = useMemo(function () { + return [ + { + title: '站点', + key: 'station', + width: 180, + fixed: 'left', + render: function (_, line) { + return React.createElement(Select, { + showSearch: true, + allowClear: true, + placeholder: '请选择站点', + style: { width: '100%' }, + value: line.stationId, + options: getRechargeStationOptions(line.id), + optionFilterProp: 'label', + onChange: function (v) { handleRechargeStationSelect(line.id, v); } + }); + } + }, + { + title: '当前余额', + key: 'currentBalance', + width: 108, + align: 'right', + render: function (_, line) { + if (!line.stationId) { + return React.createElement('span', { className: 'h2-recharge-readonly h2-recharge-readonly--muted' }, '—'); + } + var num = h2NumOrZero(line.currentBalance); + return React.createElement('span', { + className: 'h2-recharge-readonly', + style: { color: num < 0 ? '#dc2626' : '#059669', fontWeight: 600, fontVariantNumeric: 'tabular-nums' } + }, '¥' + (line.currentBalance || '0.00')); + } + }, + { + title: '付款金额', + key: 'payAmount', + width: 130, + render: function (_, line) { + return React.createElement(Input, { + className: 'h2-statement-receipt-amount-input', + inputMode: 'decimal', + prefix: '¥', + placeholder: '请输入金额', + style: { width: '100%', borderRadius: 8 }, + value: line.payAmount || '', + onChange: function (e) { + updateRechargeLineField(line.id, 'payAmount', h2SanitizeReceiptAmountInput(e.target.value)); + }, + onBlur: function (e) { + updateRechargeLineField(line.id, 'payAmount', h2FormatReceiptAmountInput(e.target.value)); + } + }); + } + }, + { + title: '企业全称', + key: 'companyName', + width: 160, + render: function (_, line) { + return h2RenderRechargeAutoField(line.companyName, !!line.stationId, '未关联供应商'); + } + }, + { + title: '收款银行账号', + key: 'bankAccount', + width: 150, + render: function (_, line) { + return h2RenderRechargeAutoField(line.bankAccount, !!line.stationId, '未配置账号'); + } + }, + { + title: '收款开户行全称', + key: 'bankName', + width: 160, + render: function (_, line) { + return h2RenderRechargeAutoField(line.bankName, !!line.stationId, '未配置开户行'); + } + }, + { + title: '转账用途', + key: 'transferPurpose', + width: 200, + render: function (_, line) { + return h2RenderRechargeAutoField(line.transferPurpose, !!line.stationId, '—'); + } + }, + { + title: '操作', + key: 'action', + width: 72, + fixed: 'right', + render: function (_, line) { + return React.createElement(Button, { + type: 'link', + size: 'small', + danger: true, + disabled: (rechargeModal.lines || []).length <= 1, + onClick: function () { removeRechargeLine(line.id); } + }, '删除'); + } + } + ]; + }, [rechargeModal.lines, getRechargeStationOptions, handleRechargeStationSelect, updateRechargeLineField, removeRechargeLine]); + + var listColumnsWithoutAction = useMemo(function () { + return columns.filter(function (c) { return c.key !== 'action'; }); + }, [columns]); + + var renderRechargeModalPanel = useCallback(function () { + var lines = rechargeModal.lines || []; + return React.createElement('div', { className: 'h2-recharge-panel' }, + React.createElement('p', { className: 'h2-recharge-hint' }, + '选择站点后将根据供应商信息自动带出企业全称、收款银行账号、开户行及转账用途(均为只读);仅需填写付款金额。支持添加多行,一次发起多个站点的充值付款。' + ), + React.createElement('div', { className: 'h2-recharge-toolbar' }, + React.createElement('span', { className: 'h2-recharge-toolbar__count' }, '共 ' + lines.length + ' 条付款信息'), + React.createElement(Button, { + type: 'dashed', + icon: H2_ICONS.plus, + style: { borderRadius: 8, fontWeight: 600, borderColor: '#10b981', color: '#059669' }, + onClick: addRechargeLine + }, '添加站点') + ), + React.createElement('div', { className: 'h2-recharge-table-wrap' }, + React.createElement(Table, { + className: 'h2-recharge-record-table', + size: 'small', + bordered: false, + rowKey: 'id', + columns: rechargeModalColumns, + dataSource: lines, + pagination: false, + locale: { emptyText: '请添加付款站点' }, + scroll: { x: 1180 } + }) + ) + ); + }, [rechargeModal.lines, rechargeModalColumns, addRechargeLine]); + + var renderAlertStationPanel = useCallback(function () { + var stations = alertStationModal.stations || []; + return React.createElement('div', { className: 'h2-alert-station-panel' }, + React.createElement('div', { className: 'h2-alert-station-head' }, + React.createElement('div', null, + React.createElement('div', { className: 'h2-alert-station-head__title' }, alertStationModal.title || '站点列表'), + React.createElement('div', { className: 'h2-alert-station-head__count' }, + '共 ' + stations.length + ' 个站点 · 字段与列表页一致' + ) + ), + React.createElement(Button, { + type: 'primary', + style: H2_PRIMARY_BTN_STYLE, + disabled: !stations.length, + onClick: handleBatchRechargeFromAlert + }, '一键发起充值单') + ), + React.createElement('div', { className: 'h2-alert-station-table-wrap' }, + React.createElement(Table, { + className: 'lc-list-table', + rowKey: 'id', + columns: listColumnsWithoutAction, + dataSource: stations, + pagination: stations.length > 8 ? { pageSize: 8, showSizeChanger: false, size: 'small', showTotal: function (t) { return '共 ' + t + ' 条'; } } : false, + size: 'middle', + scroll: { x: 1182 }, + locale: { emptyText: '暂无站点' } + }) + ) + ); + }, [alertStationModal, listColumnsWithoutAction, handleBatchRechargeFromAlert]); + + var usedBindAccountMap = useMemo(function () { + var excludeId = subView === 'edit' && editRecord ? editRecord.id : null; + return h2CollectUsedBindAccountMap(listData, excludeId); + }, [listData, subView, editRecord]); var createViewNode = subView === 'create' ? h2BuildStationCreateView({ + pageMode: 'create', + showSupplier: true, + accountEditable: true, + showReset: false, station: createStation, supplier: createSupplier, supplierMode: createSupplierMode, linkedSupplierId: createLinkedSupplierId, supplierReadonly: createSupplierReadonly, + usedBindAccountMap: usedBindAccountMap, updateStation: updateCreateStation, updateSupplier: updateCreateSupplier, syncSupplierFromStation: syncCreateSupplierFromStation, @@ -3206,12 +8048,32 @@ const Component = function () { handleLinkSupplierChange: handleCreateLinkSupplierChange, handleSupplierCityChange: handleCreateSupplierCityChange, onCancel: handleCreateCancel, - onReset: handleCreateReset, onSubmit: handleCreateSubmit, submitting: createSubmitting, - onOpenRequirement: function () { setRequirementModalOpen(true); }, - reqIcon: H2_ICONS.doc, - reqBtnStyle: H2_REQ_BTN_STYLE + submitLabel: '提交创建' + }) : null; + + var editViewNode = subView === 'edit' ? h2BuildStationCreateView({ + pageMode: 'edit', + showSupplier: true, + accountEditable: isAdminUser, + showReset: false, + station: editStation, + supplier: editSupplier, + supplierMode: editSupplierMode, + linkedSupplierId: editLinkedSupplierId, + supplierReadonly: editSupplierMode === 'link' && !!editLinkedSupplierId, + usedBindAccountMap: usedBindAccountMap, + updateStation: updateEditStation, + updateSupplier: updateEditSupplier, + syncSupplierFromStation: syncEditSupplierFromStation, + handleSupplierModeChange: handleEditSupplierModeChange, + handleLinkSupplierChange: handleEditLinkSupplierChange, + handleSupplierCityChange: handleEditSupplierCityChange, + onCancel: handleEditCancel, + onSubmit: handleEditSubmit, + submitting: editSubmitting, + submitLabel: '保存修改' }) : null; var emptyNode = React.createElement('div', { style: { padding: '40px 0', textAlign: 'center' } }, @@ -3220,20 +8082,31 @@ const Component = function () { ); return React.createElement('div', { - className: 'h2-station-page lc-edit-page' + (subView === 'create' ? ' h2-station-page--create' : '') + className: 'h2-station-page lc-edit-page' + + (subView === 'create' || subView === 'edit' ? ' h2-station-page--create' : '') }, React.createElement('style', null, H2_PAGE_STYLE), createViewNode, + editViewNode, subView === 'list' ? React.createElement('div', { style: { display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' } }, React.createElement('div', { style: { marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 12 } }, React.createElement(Breadcrumb, { items: [{ title: '加氢站管理' }, { title: '站点信息' }] }), - React.createElement(Button, { - type: 'default', - icon: H2_ICONS.doc, - style: H2_REQ_BTN_STYLE, - onClick: function () { setRequirementModalOpen(true); }, - 'aria-label': '查看需求说明' - }, '查看需求说明') + React.createElement(Space, { size: 8 }, + React.createElement(Button, { + type: 'default', + icon: H2_ICONS.doc, + style: H2_REQ_BTN_STYLE, + onClick: function () { setUserManualModalOpen(true); }, + 'aria-label': '查看使用说明书' + }, '查看使用说明书'), + React.createElement(Button, { + type: 'default', + icon: H2_ICONS.doc, + style: H2_REQ_BTN_STYLE, + onClick: function () { setRequirementModalOpen(true); }, + 'aria-label': '查看需求说明' + }, '查看需求说明') + ) ), React.createElement(Card, { className: 'lc-filter-card', title: '筛选条件', bordered: false }, @@ -3244,14 +8117,14 @@ const Component = function () { onChange: function (e) { setListFilters(function (p) { return Object.assign({}, p, { name: e.target.value }); }); }, onPressEnter: handleListFilterQuery, allowClear: true, - style: { borderRadius: 8 } + style: { width: '100%', height: 32, borderRadius: 8 } })), renderFilterField('是否签约', React.createElement(Select, { placeholder: '全部', allowClear: true, value: listFilters.signed, onChange: function (v) { setListFilters(function (p) { return Object.assign({}, p, { signed: v }); }); }, - options: [{ value: 'yes', label: '已签约' }, { value: 'no', label: '未签约' }], + options: [{ value: 'yes', label: '签约站点' }, { value: 'no', label: '普通站点' }], style: { width: '100%' }, dropdownStyle: { borderRadius: 8 } })), @@ -3282,10 +8155,10 @@ const Component = function () { React.createElement('div', { className: 'lc-alert-stats-row' }, renderAlertStatCard(H2_STATION_KPI_CARDS[0], { - active: categoryTab === 'all', + active: categoryTab === 'all' && !appliedFilters.signed, count: categoryCounts.all || 0, onClick: function () { handleKpiCardClick('all'); }, - icon: h2KpiIcon('all') + icon: h2KpiCardIcon('all') }), H2_SIGNED_FILTER_CARDS.map(function (card) { return renderAlertStatCard(card, { @@ -3296,10 +8169,16 @@ const Component = function () { }); }), H2_STATION_KPI_CARDS.slice(1).map(function (card) { + var isAlertKpi = card.key === 'balanceAlert' || card.key === 'arrears'; return renderAlertStatCard(card, { - active: categoryTab === card.key, + active: isAlertKpi + ? (alertStationModal.open && alertStationModal.type === card.key) + : (categoryTab === card.key && !appliedFilters.signed), count: categoryCounts[card.key] || 0, - onClick: function () { handleKpiCardClick(card.key); }, + onClick: function () { + if (isAlertKpi) openAlertStationModal(card.key); + else handleKpiCardClick(card.key); + }, icon: h2KpiIcon(card.key) }); }) @@ -3315,6 +8194,13 @@ const Component = function () { onClick: function () { setImportModalOpen(true); }, 'aria-label': '批量导入加氢站点' }, '批量导入'), + React.createElement(Button, { + type: 'default', + icon: H2_ICONS.doc, + style: { borderRadius: 8, fontWeight: 600, borderColor: '#10b981', color: '#059669' }, + onClick: function () { openRechargeModal(); }, + 'aria-label': '发起充值单' + }, '发起充值单'), React.createElement(Button, { type: 'primary', style: H2_PRIMARY_BTN_STYLE, @@ -3331,128 +8217,14 @@ const Component = function () { dataSource: displayList, pagination: tablePagination, size: 'middle', - scroll: { x: 1570 }, - locale: { emptyText: emptyNode } + scroll: { x: 1290 }, + locale: { emptyText: emptyNode }, + onChange: handleListTableChange }) ) ) ) : null, - React.createElement(Drawer, { - title: drawerTitle, - width: 580, - open: drawer.open, - onClose: closeDrawer, - destroyOnClose: true, - styles: { body: { paddingTop: 8 } }, - footer: readOnly ? React.createElement(Button, { onClick: closeDrawer, style: { borderRadius: 8 } }, '关闭') : React.createElement(Space, null, - React.createElement(Button, { onClick: closeDrawer, style: { borderRadius: 8 } }, '取消'), - React.createElement(Button, { type: 'primary', onClick: handleSave, style: H2_PRIMARY_BTN_STYLE }, '保存') - ) - }, - React.createElement(Form, { layout: 'vertical', requiredMark: false }, - drawerSection( - React.createElement(React.Fragment, null, - formItem('加氢站名称', true, React.createElement(Input, { - value: form.name, - disabled: readOnly, - placeholder: '请输入加氢站名称', - maxLength: 80, - style: { borderRadius: 8 }, - onChange: function (e) { setForm(function (f) { return Object.assign({}, f, { name: e.target.value }); }); } - })), - formItem('省 / 市', true, React.createElement(Cascader, { - options: H2_REGION_CASCADER_OPTIONS, - value: form.address && form.address.region && form.address.region.length ? form.address.region : undefined, - disabled: readOnly, - placeholder: '请选择省 / 市', - style: { width: '100%', borderRadius: 8 }, - allowClear: !readOnly, - onChange: function (v) { - setForm(function (f) { - return Object.assign({}, f, { - address: { region: v || [], detail: (f.address && f.address.detail) || '' } - }); - }); - } - })), - formItem('详细地址', true, React.createElement(Input, { - value: (form.address && form.address.detail) || '', - disabled: readOnly, - placeholder: '街道、门牌号等', - maxLength: 200, - style: { borderRadius: 8 }, - onChange: function (e) { - setForm(function (f) { - return Object.assign({}, f, { - address: { region: (f.address && f.address.region) || [], detail: e.target.value } - }); - }); - } - })), - readOnly ? null : formItem('快速粘贴', false, React.createElement(AddressPasteInput, { - onParsed: function (parsed) { - setForm(function (f) { - return Object.assign({}, f, { address: { region: parsed.region, detail: parsed.detail } }); - }); - } - })), - formItem('营业时间', false, React.createElement(BusinessHoursInput, { - value: form.businessHours, - disabled: readOnly, - fieldStyle: { width: '100%', borderRadius: 8 }, - onChange: function (v) { setForm(function (f) { return Object.assign({}, f, { businessHours: v }); }); } - })) - ) - ), - React.createElement(Divider, null), - drawerSection( - React.createElement(React.Fragment, null, - formItem('是否签约', false, React.createElement(Switch, { - checked: form.isSigned, - disabled: readOnly, - checkedChildren: '已签约', - unCheckedChildren: '未签约', - onChange: function (v) { setForm(function (f) { return Object.assign({}, f, { isSigned: v }); }); } - })), - formItem('签约开始时间', form.isSigned, React.createElement(Input, { - type: 'date', - style: { width: '100%', borderRadius: 8 }, - disabled: readOnly || !form.isSigned, - value: form.contractStart || '', - onChange: function (e) { setForm(function (f) { return Object.assign({}, f, { contractStart: e.target.value }); }); } - })), - formItem('签约结束时间', form.isSigned, React.createElement(Input, { - type: 'date', - style: { width: '100%', borderRadius: 8 }, - disabled: readOnly || !form.isSigned, - value: form.contractEnd || '', - onChange: function (e) { setForm(function (f) { return Object.assign({}, f, { contractEnd: e.target.value }); }); } - })) - ) - ), - React.createElement(Divider, null), - drawerSection( - React.createElement(React.Fragment, null, - formItem('联系人', true, React.createElement(Input, { - value: form.contact, - disabled: readOnly, - placeholder: '请输入联系人', - style: { borderRadius: 8 }, - onChange: function (e) { setForm(function (f) { return Object.assign({}, f, { contact: e.target.value }); }); } - })), - formItem('联系电话', true, React.createElement(Input, { - value: form.phone, - disabled: readOnly, - placeholder: '请输入联系电话', - style: { borderRadius: 8 }, - onChange: function (e) { setForm(function (f) { return Object.assign({}, f, { phone: e.target.value }); }); } - })) - ) - ) - ) - ), - React.createElement(Modal, { title: '确认删除', open: deleteModal.open, @@ -3470,15 +8242,29 @@ const Component = function () { cancelText: '取消' }, '确定删除站点「' + ((deleteModal.record && deleteModal.record.name) || '') + '」吗?此操作不可撤销。'), + React.createElement(Modal, { + className: 'h2-prd-modal h2-user-manual-modal', + title: React.createElement('span', { style: { fontWeight: 700 } }, '使用说明书 · 站点信息'), + open: userManualModalOpen, + onCancel: function () { setUserManualModalOpen(false); }, + width: 900, + centered: true, + destroyOnClose: true, + styles: { body: { maxHeight: '72vh', overflow: 'auto', paddingTop: 8, paddingBottom: 16 } }, + footer: React.createElement(Button, { onClick: function () { setUserManualModalOpen(false); }, style: { borderRadius: 8 } }, '关闭') + }, renderH2StationUserManualDocPanel()), + React.createElement(Modal, { className: 'h2-prd-modal', title: React.createElement('span', { style: { fontWeight: 700 } }, '需求说明 · 站点信息'), open: requirementModalOpen, onCancel: function () { setRequirementModalOpen(false); }, - width: 640, + width: 840, centered: true, + destroyOnClose: true, + styles: { body: { maxHeight: '72vh', overflow: 'auto', paddingTop: 8, paddingBottom: 16 } }, footer: React.createElement(Button, { onClick: function () { setRequirementModalOpen(false); }, style: { borderRadius: 8 } }, '关闭') - }, React.createElement('div', { className: 'h2-prd-content' }, getH2StationRequirementDoc())), + }, renderH2StationRequirementDocPanel()), React.createElement(Modal, { title: '批量导入加氢站点', @@ -3553,159 +8339,254 @@ const Component = function () { ), React.createElement(Modal, { + className: 'h2-business-status-modal', title: '营业状态', open: businessModal.open, onCancel: closeBusinessModal, - onOk: handleSaveBusinessSetting, - okText: '保存', - cancelText: '取消', - centered: true, - width: 680, - destroyOnClose: true, - okButtonProps: { style: H2_PRIMARY_BTN_STYLE } - }, - React.createElement('div', { style: { marginBottom: 12, fontSize: 13, color: '#64748b' } }, - '站点:', React.createElement('strong', { style: { color: '#0f172a' } }, (businessModal.record && businessModal.record.name) || '—') + footer: React.createElement(Space, null, + React.createElement(Button, { onClick: closeBusinessModal, style: { borderRadius: 8 } }, '取消'), + React.createElement(Button, { type: 'primary', onClick: handleSaveBusinessSetting, style: H2_PRIMARY_BTN_STYLE }, '保存') ), - formItem('营业状态', true, React.createElement(Select, { - style: { width: '100%' }, - value: businessModal.businessStatus, - options: H2_BUSINESS_STATUS_OPTIONS, - onChange: function (v) { setBusinessModal(function (m) { return Object.assign({}, m, { businessStatus: v }); }); }, - dropdownStyle: { borderRadius: 8 } - })), - formItem('营业时间', false, React.createElement(BusinessHoursInput, { - value: businessModal.businessHours, - fieldStyle: { width: '100%', borderRadius: 8 }, - onChange: function (v) { setBusinessModal(function (m) { return Object.assign({}, m, { businessHours: v }); }); } - }), '暂停或停止营业时可将类型改为非全天并清空时间'), - React.createElement('div', { className: 'h2-status-log-section' }, - React.createElement('div', { className: 'h2-status-log-title' }, '营业状态变更记录'), - React.createElement(Table, { - className: 'h2-status-log-table', - size: 'small', - rowKey: 'id', - columns: businessStatusLogColumns, - dataSource: businessModal.statusLogs || [], - pagination: (businessModal.statusLogs || []).length > 5 ? { pageSize: 5, size: 'small' } : false, - locale: { emptyText: '暂无状态变更记录' }, - scroll: { x: 520 } - }) + centered: true, + width: 760, + destroyOnClose: true, + styles: { body: { maxHeight: '78vh', overflow: 'auto' } } + }, + renderBusinessStatusPanel() + ), + + React.createElement(Modal, { + className: 'h2-refuel-drill-modal', + title: '加氢记录', + open: refuelModal.open, + onCancel: closeRefuelModal, + footer: React.createElement(Button, { type: 'primary', onClick: closeRefuelModal, style: H2_PRIMARY_BTN_STYLE }, '知道了'), + width: 980, + centered: true, + destroyOnClose: true, + styles: { body: { maxHeight: '78vh', overflow: 'auto' } } + }, + renderRefuelDrillPanel() + ), + + React.createElement(Modal, { + className: 'h2-station-view-modal', + title: '查看加氢站点', + open: viewModal.open, + onCancel: closeViewModal, + footer: React.createElement(Button, { type: 'primary', onClick: closeViewModal, style: H2_PRIMARY_BTN_STYLE }, '知道了'), + width: 920, + centered: true, + destroyOnClose: true, + styles: { body: { maxHeight: '78vh', overflow: 'auto' } } + }, + renderStationViewPanel() + ), + + React.createElement(Modal, { + className: 'h2-file-preview-modal', + title: filePreviewModal.name || '附件预览', + open: filePreviewModal.open, + onCancel: closeFilePreviewModal, + footer: React.createElement(Button, { type: 'primary', onClick: closeFilePreviewModal, style: H2_PRIMARY_BTN_STYLE }, '关闭'), + width: filePreviewModal.type === 'pdf' ? 920 : 720, + centered: true, + destroyOnClose: true + }, + React.createElement('div', { className: 'h2-file-preview-body' }, + filePreviewModal.type === 'pdf' && filePreviewModal.url + ? React.createElement('iframe', { title: filePreviewModal.name || '附件预览', src: filePreviewModal.url }) + : React.createElement('img', { + src: filePreviewModal.url, + alt: filePreviewModal.name || '附件预览' + }) ) ), React.createElement(Modal, { - title: '加氢记录' + (refuelModal.station && refuelModal.station.name ? ' · ' + refuelModal.station.name : ''), - open: refuelModal.open, - onCancel: closeRefuelModal, - footer: React.createElement(Button, { onClick: closeRefuelModal, style: { borderRadius: 8 } }, '关闭'), - width: 960, - centered: true, - destroyOnClose: true, - styles: { body: { maxHeight: '72vh', overflow: 'auto', paddingTop: 8 } } - }, - renderRefuelTotalsBar(refuelModalSummary), - React.createElement(Table, { - className: 'h2-refuel-record-table', - size: 'small', - bordered: true, - rowKey: 'id', - columns: refuelRecordColumns, - dataSource: refuelModal.records || [], - pagination: (refuelModal.records || []).length > 8 ? { pageSize: 8, showSizeChanger: false, size: 'small' } : false, - locale: { emptyText: '暂无加氢记录' }, - scroll: { x: 'max-content', y: 360 } - }) - ), - - React.createElement(Modal, { - title: '余额变更明细' + (prepaidBalanceDrill.stationName ? ' · ' + prepaidBalanceDrill.stationName : ''), + className: 'h2-balance-drill-modal', + title: '余额变更明细', open: prepaidBalanceDrill.open, onCancel: closePrepaidBalanceDrill, - footer: React.createElement(Button, { onClick: closePrepaidBalanceDrill, style: { borderRadius: 8 } }, '关闭'), - width: 880, + footer: React.createElement(Button, { type: 'primary', onClick: closePrepaidBalanceDrill, style: H2_PRIMARY_BTN_STYLE }, '知道了'), + width: 920, centered: true, destroyOnClose: true, - styles: { body: { maxHeight: '72vh', overflow: 'auto', paddingTop: 8 } } + styles: { body: { maxHeight: '78vh', overflow: 'auto' } } }, - React.createElement(Table, { - className: 'h2-balance-record-table', - size: 'small', - bordered: true, - rowKey: 'key', - columns: prepaidBalanceDrillColumns, - dataSource: prepaidBalanceDrill.rows || [], - pagination: (prepaidBalanceDrill.rows || []).length > 10 ? { pageSize: 10, showSizeChanger: false, size: 'small' } : false, - summary: renderPrepaidBalanceDrillSummary, - locale: { emptyText: '暂无余额变更记录' }, - scroll: { x: 'max-content' } - }) + renderPrepaidBalanceDrillPanel() ), React.createElement(Modal, { + className: 'h2-statement-modal', title: '生成对账单', open: statementModal.open, onCancel: closeStatementModal, - onOk: handleGenerateStatement, - okText: '生成对账单', - cancelText: '取消', - centered: true, - width: 520, - destroyOnClose: true, - okButtonProps: { style: H2_PRIMARY_BTN_STYLE } - }, - React.createElement('div', { style: { marginBottom: 12, fontSize: 13, color: '#64748b' } }, - '站点:', React.createElement('strong', { style: { color: '#0f172a' } }, (statementModal.record && statementModal.record.name) || '—') + footer: React.createElement(Space, null, + React.createElement(Button, { onClick: closeStatementModal, style: { borderRadius: 8 } }, '取消'), + statementModal.phase === 'draft' + ? React.createElement(Button, { + type: 'primary', + onClick: handleSubmitStatement, + style: H2_PRIMARY_BTN_STYLE + }, '提交对账单') + : React.createElement(Button, { + type: 'primary', + onClick: handleGenerateStatement, + style: H2_PRIMARY_BTN_STYLE + }, '生成对账单') ), - React.createElement(Alert, { - type: 'info', - showIcon: true, - style: { marginBottom: 14, borderRadius: 10 }, - message: '按周期汇总站点加氢明细,生成氢费对账单(对站)', - description: '对账单将汇总该周期内加氢量、成本总价等,提交后可进入审批流程(原型演示)。' - }), - formItem('对账周期', true, React.createElement(Input, { - type: 'month', - style: { width: '100%', borderRadius: 8 }, - value: statementModal.period || '', - onChange: function (e) { setStatementModal(function (m) { return Object.assign({}, m, { period: e.target.value }); }); } - })), - statementModal.record ? React.createElement('div', { style: { fontSize: 12, color: '#64748b', lineHeight: 1.6 } }, - '当前预付余额:', - renderPrepaidBalance(statementModal.record.prepaidBalance), - ';本站点累计加氢 ', - React.createElement('strong', { style: { color: '#0f172a' } }, String(statementModal.record.refuelCount != null ? statementModal.record.refuelCount : h2CalcRefuelStats(statementModal.record.name).count)), - ' 次' - ) : null + centered: true, + width: 820, + destroyOnClose: true, + styles: { body: { maxHeight: '78vh', overflow: 'auto' } } + }, + renderStatementPanel() ), React.createElement(Modal, { + className: 'h2-statement-history-modal', + title: '查看对账记录', + open: statementHistoryModal.open, + onCancel: closeStatementHistoryModal, + footer: React.createElement(Button, { type: 'primary', onClick: closeStatementHistoryModal, style: H2_PRIMARY_BTN_STYLE }, '知道了'), + centered: true, + width: 1080, + destroyOnClose: true, + styles: { body: { maxHeight: '78vh', overflow: 'auto' } } + }, + renderStatementHistoryPanel() + ), + + React.createElement(Modal, { + className: 'h2-statement-modal', + title: '对账明细', + open: statementDetailModal.open, + onCancel: closeStatementDetailModal, + footer: React.createElement(Button, { type: 'primary', onClick: closeStatementDetailModal, style: H2_PRIMARY_BTN_STYLE }, '知道了'), + centered: true, + width: 820, + destroyOnClose: true, + styles: { body: { maxHeight: '78vh', overflow: 'auto' } } + }, + renderStatementDetailPanel() + ), + + React.createElement(Modal, { + className: 'h2-price-config-modal', title: '价格配置', open: priceModal.open, onCancel: closePriceModal, - onOk: handleSavePriceConfig, - okText: '保存', - cancelText: '取消', + footer: React.createElement(Space, null, + React.createElement(Button, { onClick: closePriceModal, style: { borderRadius: 8 } }, '取消'), + React.createElement(Button, { type: 'primary', onClick: handleSavePriceConfig, style: H2_PRIMARY_BTN_STYLE }, '保存') + ), + centered: true, + width: 820, + destroyOnClose: true, + styles: { body: { maxHeight: '78vh', overflow: 'auto' } } + }, + renderPriceConfigPanel() + ), + + React.createElement(Modal, { + title: '余额提醒设置', + open: balanceAlertModal.open, + onCancel: closeBalanceAlertModal, + footer: React.createElement(Space, null, + React.createElement(Button, { onClick: closeBalanceAlertModal, style: { borderRadius: 8 } }, '取消'), + React.createElement(Button, { type: 'primary', onClick: handleSaveBalanceAlert, style: H2_PRIMARY_BTN_STYLE }, '保存') + ), centered: true, width: 480, - destroyOnClose: true, - okButtonProps: { style: H2_PRIMARY_BTN_STYLE } + destroyOnClose: true }, - React.createElement('div', { style: { marginBottom: 12, fontSize: 13, color: '#64748b' } }, - '站点:', React.createElement('strong', { style: { color: '#0f172a' } }, (priceModal.record && priceModal.record.name) || '—') + balanceAlertModal.record + ? React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 14 } }, + React.createElement('div', { + style: { + padding: '12px 14px', + borderRadius: 10, + background: '#f8fafc', + border: '1px solid #e2e8f0', + fontSize: 13, + color: '#475569', + lineHeight: 1.55 + } + }, + React.createElement('div', { style: { fontWeight: 700, color: '#0f172a', marginBottom: 4 } }, balanceAlertModal.record.name || '—'), + '当预付余额低于所设金额时,站点将列入「预付余额预警站点」。当前预付余额:', + React.createElement('strong', { + style: { + color: h2NumOrZero(balanceAlertModal.record.prepaidBalance) < 0 ? '#dc2626' : '#059669', + fontVariantNumeric: 'tabular-nums' + } + }, h2FormatYuanNum(balanceAlertModal.record.prepaidBalance)), + ' 元' + ), + React.createElement(Form, { layout: 'vertical', requiredMark: false }, + React.createElement(Form.Item, { + label: React.createElement('span', null, React.createElement('span', { style: { color: '#ef4444', marginRight: 4 } }, '*'), '提醒金额(元)') + }, + React.createElement(Input, { + className: 'h2-statement-receipt-amount-input', + inputMode: 'decimal', + prefix: '¥', + style: { width: '100%', borderRadius: 8 }, + placeholder: '请输入提醒金额', + value: balanceAlertModal.threshold || '', + onChange: function (e) { + var next = h2SanitizeReceiptAmountInput(e.target.value); + setBalanceAlertModal(function (m) { return Object.assign({}, m, { threshold: next }); }); + }, + onBlur: function (e) { + var formatted = h2FormatReceiptAmountInput(e.target.value); + setBalanceAlertModal(function (m) { return Object.assign({}, m, { threshold: formatted }); }); + } + }) + ) + ) + ) + : null + ), + + React.createElement(Modal, { + className: 'h2-recharge-modal', + title: '发起充值单', + open: rechargeModal.open, + onCancel: closeRechargeModal, + footer: React.createElement(Space, null, + React.createElement(Button, { onClick: closeRechargeModal, style: { borderRadius: 8 } }, '取消'), + React.createElement(Button, { type: 'primary', onClick: handleSubmitRecharge, style: H2_PRIMARY_BTN_STYLE }, '发起付款流程') ), - formItem('成本单价(元/kg)', false, React.createElement(Input, { - value: priceModal.costUnitPrice, - placeholder: '请输入成本单价', - style: { borderRadius: 8 }, - onChange: function (e) { setPriceModal(function (m) { return Object.assign({}, m, { costUnitPrice: e.target.value }); }); } - })), - formItem('对客单价(元/kg)', false, React.createElement(Input, { - value: priceModal.customerUnitPrice, - placeholder: '请输入对客单价', - style: { borderRadius: 8 }, - onChange: function (e) { setPriceModal(function (m) { return Object.assign({}, m, { customerUnitPrice: e.target.value }); }); } - }), '用于加氢订单结算参考,联调后对接价格体系') + centered: true, + width: 1120, + destroyOnClose: true, + styles: { body: { maxHeight: '78vh', overflow: 'auto' } } + }, + renderRechargeModalPanel() + ), + + React.createElement(Modal, { + className: 'h2-alert-station-modal', + title: alertStationModal.title || '站点列表', + open: alertStationModal.open, + onCancel: closeAlertStationModal, + footer: React.createElement(Space, null, + React.createElement(Button, { onClick: closeAlertStationModal, style: { borderRadius: 8 } }, '关闭'), + React.createElement(Button, { + type: 'primary', + disabled: !(alertStationModal.stations || []).length, + onClick: handleBatchRechargeFromAlert, + style: H2_PRIMARY_BTN_STYLE + }, '一键发起充值单') + ), + centered: true, + width: 1200, + destroyOnClose: true, + styles: { body: { maxHeight: '78vh', overflow: 'auto' } } + }, + renderAlertStationPanel() ) ); };