迭代 ONE-OS 多原型:租赁台账与租赁合同增强,加氢站站点独立原型与氢费数据桥接

- 租赁业务台账:空值留白、排序/列宽拖拽、操作列与交互优化
- 租赁合同:列表展开列合并、在租明细弹窗、编辑回填与样式修复
- 加氢站:新增站点信息独立原型,订单/记录页与共享样式调整
- 车辆氢费明细对接 h2VehicleLedgerBridge;移除 vehicle-management-clay
- 同步 sidebar-tree 与标注提取 output

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
王冕
2026-07-02 03:33:18 +08:00
parent 10b35d3da9
commit f5e31268dc
34 changed files with 3617 additions and 3163 deletions

View File

@@ -0,0 +1,285 @@
/**
* 车辆氢费明细 ↔ 加氢站模块 共享数据桥
* 加氢站侧氢费/对账/加氢记录均读取本 Store车辆氢费明细页运行时同步全量台账行。
*/
var H2_RECONCILE_RECONCILED = 'reconciled';
var H2_RECONCILE_PENDING = 'pending';
var H2_STATION_CODE_MAP = {
'嘉兴加氢站(一期)': 'JX-H2-001',
'杭州临平加氢站': 'HZ-H2-002',
'上海宝山加氢站': 'SH-H2-003',
'苏州工业园区备用站': 'SZ-H2-004'
};
/** 与车辆氢费明细、加氢站模块对齐的 canonical seed10 条) */
var H2_CANONICAL_LEDGER_SEED = [
{ key: 'rf-1', id: 'rf-1', stationId: 'JX-H2-001', stationName: '嘉兴加氢站(一期)', hydrogenTime: '2026-05-28 10:21:08', plateNo: '浙A12345F', customerName: '嘉兴市鑫峤供应链科技有限公司', hydrogenKg: 12.5, costUnitPrice: 42.5, costTotal: 531.25, customerUnitPrice: 45, customerAmount: 562.5, settlementStatus: 'customer', mileageKm: 128560, creatorName: '张三', reconcileStatus: H2_RECONCILE_RECONCILED },
{ key: 'rf-2', id: 'rf-2', stationId: 'JX-H2-001', stationName: '嘉兴加氢站(一期)', hydrogenTime: '2026-05-26 14:08:33', plateNo: '浙A67890F', customerName: '浙江绿运物流有限公司', hydrogenKg: 10.0, costUnitPrice: 42.5, costTotal: 425.0, customerUnitPrice: 45, customerAmount: 450.0, settlementStatus: 'internal', mileageKm: 95230, creatorName: '李四', reconcileStatus: H2_RECONCILE_RECONCILED },
{ key: 'rf-3', id: 'rf-3', stationId: 'JX-H2-001', stationName: '嘉兴加氢站(一期)', hydrogenTime: '2026-05-22 09:15:00', plateNo: '浙A88888F', customerName: '嘉兴市鑫峤供应链科技有限公司', hydrogenKg: 18.3, costUnitPrice: 42.5, costTotal: 777.75, customerUnitPrice: 45, customerAmount: 823.5, settlementStatus: 'customer_self', mileageKm: 201880, creatorName: '王静', reconcileStatus: H2_RECONCILE_RECONCILED },
{ key: 'rf-4', id: 'rf-4', stationId: 'JX-H2-001', stationName: '嘉兴加氢站(一期)', hydrogenTime: '2026-05-18 16:42:11', plateNo: '浙A03561F', customerName: '嘉兴港务氢能运输队', hydrogenKg: 15.6, costUnitPrice: 42.5, costTotal: 663.0, customerUnitPrice: 45, customerAmount: 702.0, settlementStatus: 'customer', mileageKm: 167420, creatorName: '张三', reconcileStatus: H2_RECONCILE_RECONCILED },
{ key: 'rf-5', id: 'rf-5', stationId: 'HZ-H2-002', stationName: '杭州临平加氢站', hydrogenTime: '2026-05-30 09:30:22', plateNo: '浙B23456F', customerName: '杭州临平城配中心', hydrogenKg: 15.3, costUnitPrice: 43.0, costTotal: 657.9, customerUnitPrice: 46, customerAmount: 703.8, settlementStatus: 'internal', mileageKm: 143200, creatorName: '赵敏', reconcileStatus: H2_RECONCILE_RECONCILED },
{ key: 'rf-6', id: 'rf-6', stationId: 'HZ-H2-002', stationName: '杭州临平加氢站', hydrogenTime: '2026-05-27 18:10:05', plateNo: '浙B99999F', customerName: '浙江氢运科技', hydrogenKg: 18.2, costUnitPrice: 43.0, costTotal: 782.6, customerUnitPrice: 46, customerAmount: 837.2, settlementStatus: 'customer_self', mileageKm: 189650, creatorName: '陈浩', reconcileStatus: H2_RECONCILE_RECONCILED },
{ key: 'rf-7', id: 'rf-7', stationId: 'HZ-H2-002', stationName: '杭州临平加氢站', hydrogenTime: '2026-05-24 11:05:40', plateNo: '浙B58888F', customerName: '杭州临平城配中心', hydrogenKg: 11.8, costUnitPrice: 43.0, costTotal: 507.4, customerUnitPrice: 46, customerAmount: 542.8, settlementStatus: 'customer', mileageKm: 110340, creatorName: '李四', reconcileStatus: H2_RECONCILE_RECONCILED },
{ key: 'rf-8', id: 'rf-8', stationId: 'SH-H2-003', stationName: '上海宝山加氢站', hydrogenTime: '2026-04-20 16:45:18', plateNo: '沪A88888F', customerName: '上海羚牛氢运', hydrogenKg: 8.0, costUnitPrice: 44.0, costTotal: 352.0, customerUnitPrice: 47, customerAmount: 376.0, settlementStatus: 'internal', mileageKm: 88420, creatorName: '王静', reconcileStatus: H2_RECONCILE_RECONCILED },
{ key: 'rf-9', id: 'rf-9', stationId: 'SH-H2-003', stationName: '上海宝山加氢站', hydrogenTime: '2026-04-08 09:12:55', plateNo: '沪BDB9161F', customerName: '宝山园区试运车队', hydrogenKg: 9.5, costUnitPrice: 44.0, costTotal: 418.0, customerUnitPrice: 47, customerAmount: 446.5, settlementStatus: 'customer_self', mileageKm: 76500, creatorName: '张三', reconcileStatus: H2_RECONCILE_RECONCILED },
{ key: 'rf-10', id: 'rf-10', stationId: 'SZ-H2-004', stationName: '苏州工业园区备用站', hydrogenTime: '2026-03-15 10:00:00', plateNo: '苏E33333F', customerName: '苏州试运客户', hydrogenKg: 6.2, costUnitPrice: 41.0, costTotal: 254.2, customerUnitPrice: 44, customerAmount: 272.8, settlementStatus: 'customer', mileageKm: 45210, creatorName: '赵敏', reconcileStatus: H2_RECONCILE_PENDING }
];
function h2BridgeCloneRows(rows) {
return (rows || []).map(function (r) { return Object.assign({}, r); });
}
function h2BridgeFormatDateTime(value) {
if (!value) return '';
if (typeof value === 'string') return value;
if (value.format) return value.format('YYYY-MM-DD HH:mm:ss');
return String(value);
}
function h2BridgeMatchStatementPatch(row, patch) {
if (patch.ledgerId && row.ledgerSyncId && patch.ledgerId === row.ledgerSyncId) return true;
var rowTime = h2BridgeFormatDateTime(row.hydrogenTime);
var patchTime = String(patch.hydrogenTime || '');
return (row.stationName || '') === (patch.stationName || '')
&& (row.plateNo || '') === (patch.plateNo || '')
&& (rowTime === patchTime || String(row.hydrogenTime || '') === patchTime);
}
function h2BridgeApplyStatementPatches(rows, patches) {
if (!patches || !patches.length) return h2BridgeCloneRows(rows);
return (rows || []).map(function (r) {
var patch = null;
var i;
for (i = 0; i < patches.length; i++) {
if (h2BridgeMatchStatementPatch(r, patches[i])) {
patch = patches[i];
break;
}
}
if (!patch) return r;
return Object.assign({}, r, {
statementPatched: true,
reconcileDate: patch.reconcileDate,
receiptDate: patch.receiptDate,
stationPaymentStatus: patch.paymentStatus || 'paid',
paymentStatus: patch.paymentStatus || 'paid',
reconciledAt: patch.reconcileDate
});
});
}
function h2BridgeBuildInitialRows() {
var patches = (typeof window !== 'undefined' && window.H2_STATION_STATEMENT_LEDGER_UPDATES) || [];
return h2BridgeApplyStatementPatches(h2BridgeCloneRows(H2_CANONICAL_LEDGER_SEED), patches);
}
function h2BridgeCreateStore() {
var listeners = [];
var state = { rows: h2BridgeBuildInitialRows() };
function notify() {
listeners.forEach(function (fn) {
try { fn(state.rows); } catch (_e) { /* noop */ }
});
}
return {
getRows: function () { return h2BridgeCloneRows(state.rows); },
setRows: function (rows) {
state.rows = h2BridgeCloneRows(rows);
notify();
},
subscribe: function (fn) {
listeners.push(fn);
return function () {
listeners = listeners.filter(function (item) { return item !== fn; });
};
},
applyPatches: function (patches) {
if (!patches || !patches.length) return;
state.rows = h2BridgeApplyStatementPatches(state.rows, patches);
notify();
}
};
}
function h2BridgeMapToRefuelRecord(row) {
var costTotal = row.costTotal != null ? row.costTotal : row.costAmount;
return {
id: row.id || row.key,
stationName: row.stationName,
hydrogenTime: h2BridgeFormatDateTime(row.hydrogenTime),
plateNo: row.plateNo,
customerName: row.customerName,
hydrogenKg: row.hydrogenKg,
costUnitPrice: row.costUnitPrice,
costAmount: costTotal,
customerUnitPrice: row.customerUnitPrice,
customerAmount: row.customerAmount,
settlementStatus: row.settlementStatus,
orderNo: row.orderNo,
mileageKm: row.mileageKm
};
}
function h2BridgeBuildRefuelOrderNo(record, index) {
if (record && record.orderNo) return record.orderNo;
var idPart = String((record && record.id) || (record && record.key) || '').replace(/\D/g, '') || String(index + 1);
return 'JQ' + idPart.padStart(4, '0') + String(100 + index);
}
function h2BridgeMapToStationLedgerRow(row, index) {
var costTotal = row.costTotal != null ? row.costTotal : row.costAmount;
var isReconciled = row.reconcileStatus === H2_RECONCILE_RECONCILED;
return {
id: row.id || row.key || ('ledger-' + index),
stationName: row.stationName,
hydrogenTime: h2BridgeFormatDateTime(row.hydrogenTime),
plateNo: row.plateNo,
customerName: row.customerName,
hydrogenKg: row.hydrogenKg,
costUnitPrice: row.costUnitPrice,
costTotal: costTotal,
settlementStatus: row.settlementStatus,
orderNo: h2BridgeBuildRefuelOrderNo(row, index),
reconcileStatus: row.reconcileStatus || (isReconciled ? H2_RECONCILE_RECONCILED : H2_RECONCILE_PENDING),
statementRecordId: row.statementRecordId || null,
reconcileDate: row.reconcileDate || null,
receiptDate: row.receiptDate || null,
paymentStatus: row.paymentStatus || (isReconciled ? 'unpaid' : null)
};
}
function h2BridgeMapToHrRecord(row, index, fillerNames) {
var fillers = fillerNames || ['张三', '李四', '王静', '赵敏', '陈浩'];
return {
id: row.id || row.key || ('hr-' + (index + 1)),
hydrogenTime: h2BridgeFormatDateTime(row.hydrogenTime),
plateNo: row.plateNo,
hydrogenKg: row.hydrogenKg,
customerAmount: row.customerAmount,
mileageKm: row.mileageKm,
fillerName: row.creatorName || fillers[index % fillers.length],
settlementStatus: row.settlementStatus,
stationName: row.stationName,
stationCode: row.stationId || H2_STATION_CODE_MAP[row.stationName] || ''
};
}
function h2BridgeMapToOrderRecord(row, index) {
var time = h2BridgeFormatDateTime(row.hydrogenTime);
return {
id: index + 1,
stationCode: row.stationId || H2_STATION_CODE_MAP[row.stationName] || '',
stationName: row.stationName,
time: time.length >= 16 ? time.slice(0, 16) : time,
plateNo: (row.plateNo || '').replace(/F$/, ''),
amountKg: row.hydrogenKg,
amountYuan: row.customerAmount
};
}
function h2BridgeGetStore() {
if (typeof window === 'undefined') return null;
if (!window.H2_VEHICLE_LEDGER_STORE) {
window.H2_VEHICLE_LEDGER_STORE = h2BridgeCreateStore();
}
return window.H2_VEHICLE_LEDGER_STORE;
}
function h2BridgeEnsureApi() {
if (typeof window === 'undefined') return;
var store = h2BridgeGetStore();
var existing = window.H2_VEHICLE_LEDGER_API || {};
window.H2_VEHICLE_LEDGER_API = Object.assign({}, existing, {
getRows: function () { return store.getRows(); },
syncRows: function (rows) {
if (!rows) return;
store.setRows(rows);
},
applyPatches: function (patches) {
store.applyPatches(patches);
if (typeof existing.applyPatches === 'function') {
existing.applyPatches(patches);
}
}
});
}
function h2BridgeGetAllRefuelRecords() {
var store = h2BridgeGetStore();
return store.getRows().map(h2BridgeMapToRefuelRecord);
}
function h2BridgeGetRefuelRecordsByStation(stationName) {
var name = String(stationName || '').trim();
return h2BridgeGetAllRefuelRecords()
.filter(function (r) { return (r.stationName || '') === name; })
.sort(function (a, b) {
return String(b.hydrogenTime || '').localeCompare(String(a.hydrogenTime || ''));
});
}
function h2BridgeBuildStationLedgerStore() {
var store = h2BridgeGetStore();
return store.getRows().map(h2BridgeMapToStationLedgerRow);
}
function h2BridgeGetHrRecords(fillerNames) {
var store = h2BridgeGetStore();
return store.getRows().map(function (row, index) {
return h2BridgeMapToHrRecord(row, index, fillerNames);
});
}
function h2BridgeGetOrderRecords() {
var store = h2BridgeGetStore();
return store.getRows().map(h2BridgeMapToOrderRecord);
}
function h2BridgeGetStationList() {
var seen = {};
var list = [];
h2BridgeGetStore().getRows().forEach(function (row) {
var code = row.stationId || H2_STATION_CODE_MAP[row.stationName];
if (!code || seen[code]) return;
seen[code] = true;
list.push({ value: code, label: row.stationName });
});
return list;
}
function h2BridgeInit() {
h2BridgeEnsureApi();
var store = h2BridgeGetStore();
if (typeof window !== 'undefined') {
window.H2VehicleLedgerBridge = {
getRows: function () { return store.getRows(); },
getAllRefuelRecords: h2BridgeGetAllRefuelRecords,
getRefuelRecordsByStation: h2BridgeGetRefuelRecordsByStation,
buildStationLedgerStore: h2BridgeBuildStationLedgerStore,
getHrRecords: h2BridgeGetHrRecords,
getOrderRecords: h2BridgeGetOrderRecords,
getStationList: h2BridgeGetStationList,
subscribe: function (fn) { return store.subscribe(fn); }
};
}
return store;
}
h2BridgeInit();
export {
H2_CANONICAL_LEDGER_SEED,
H2_STATION_CODE_MAP,
h2BridgeInit,
h2BridgeGetStore,
h2BridgeGetAllRefuelRecords,
h2BridgeGetRefuelRecordsByStation,
h2BridgeBuildStationLedgerStore,
h2BridgeGetHrRecords,
h2BridgeGetOrderRecords,
h2BridgeGetStationList
};

View File

@@ -0,0 +1,42 @@
import React from 'react';
import { X } from 'lucide-react';
interface DeleteConfirmModalProps {
open: boolean;
plateNo: string;
onCancel: () => void;
onConfirm: () => void;
}
export function DeleteConfirmModal({ open, plateNo, onCancel, onConfirm }: DeleteConfirmModalProps) {
if (!open) return null;
return (
<div className="vm-modal-backdrop" role="presentation" onClick={onCancel}>
<div
className="vm-modal vm-modal-sm"
role="dialog"
aria-modal="true"
aria-labelledby="delete-confirm-title"
onClick={(e) => e.stopPropagation()}
>
<header className="vm-modal-header">
<h2 id="delete-confirm-title" className="vm-modal-title"></h2>
<button type="button" className="vm-modal-close" onClick={onCancel} aria-label="关闭">
<X size={18} aria-hidden />
</button>
</header>
<div className="vm-modal-body">
<p className="ldb-confirm-text">
</p>
<p className="ldb-confirm-meta">{plateNo ? `车牌:${plateNo}` : ''}</p>
</div>
<footer className="vm-modal-footer">
<button type="button" className="vm-btn vm-btn-ghost" onClick={onCancel}></button>
<button type="button" className="vm-btn vm-btn-danger" onClick={onConfirm}></button>
</footer>
</div>
</div>
);
}

View File

@@ -35,37 +35,52 @@ export function FilterPanel({ records, filters, onChange, onReset, onSearch }: F
{ label: '付款日期', control: (
<div className="ldb-date-range">
<input type="date" className="vm-input" value={filters.paymentFrom} aria-label="付款开始日期" onChange={(e) => onChange({ paymentFrom: e.target.value })} />
<span aria-hidden></span>
<span className="ldb-date-range-sep" aria-hidden></span>
<input type="date" className="vm-input" value={filters.paymentTo} aria-label="付款结束日期" onChange={(e) => onChange({ paymentTo: e.target.value })} />
</div>
),
},
];
const visible = expanded ? fields : fields.slice(0, DEFAULT_VISIBLE);
const primaryFields = fields.slice(0, DEFAULT_VISIBLE);
const extraFields = fields.slice(DEFAULT_VISIBLE);
return (
<section className="vm-filter-card" data-annotation-id="lbl-filter">
<section className="vm-filter-card ldb-filter-card" data-annotation-id="lbl-filter">
<header className="vm-filter-header">
<h1 className="ldb-page-title"></h1>
</header>
<div className="vm-filter-grid">
{visible.map((field) => (
{primaryFields.map((field) => (
<label key={field.label} className="vm-filter-field">
<span>{field.label}</span>
{field.control}
</label>
))}
</div>
{extraFields.length > 0 && (
<div className={`ldb-filter-expand ${expanded ? 'is-expanded' : ''}`}>
<div className="ldb-filter-expand-inner">
<div className="vm-filter-grid ldb-filter-expand-grid">
{extraFields.map((field) => (
<label key={field.label} className="vm-filter-field">
<span>{field.label}</span>
{field.control}
</label>
))}
</div>
</div>
</div>
)}
<div className="vm-filter-actions">
{fields.length > DEFAULT_VISIBLE && (
<button type="button" className="vm-btn vm-btn-link" onClick={() => setExpanded(!expanded)}>
{extraFields.length > 0 && (
<button type="button" className="vm-btn vm-btn-link ldb-filter-toggle" onClick={() => setExpanded(!expanded)} aria-expanded={expanded}>
{expanded ? '收起' : '更多筛选'}
<ChevronDown size={14} aria-hidden style={{ transform: expanded ? 'rotate(180deg)' : undefined }} />
<ChevronDown size={14} aria-hidden className={`ldb-filter-toggle-icon ${expanded ? 'is-open' : ''}`} />
</button>
)}
<button type="button" className="vm-btn vm-btn-ghost" onClick={onReset}></button>
<button type="button" className="vm-btn vm-btn-primary" onClick={onSearch}>
<button type="button" className="vm-btn vm-btn-ghost ldb-toolbar-btn" onClick={onReset}></button>
<button type="button" className="vm-btn vm-btn-primary ldb-toolbar-btn" onClick={onSearch}>
<Search size={16} aria-hidden />
</button>

View File

@@ -1,20 +1,24 @@
import React from 'react';
import { AlertTriangle, CircleHelp } from 'lucide-react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { ArrowDown, ArrowUp, ArrowUpDown, AlertTriangle, CircleHelp } from 'lucide-react';
import type { AccidentRecord, LeaseLedgerRow, MaintenanceOrder } from '../types';
import {
calcMonthlyIncome,
calcUnreceived,
canStaffDeleteRow,
canStaffEditRow,
displayMoney,
displayText,
formatMoney,
isBlankDisplayValue,
needsAccidentLinkWarning,
needsMaintenanceLinkWarning,
} from '../utils/ledger';
import { LEASE_TABLE_COLUMNS, STATUS_CLASS } from './tableColumns';
import { LEASE_TABLE_COLUMNS, STATUS_CLASS, type LeaseTableColumn } from './tableColumns';
interface LeaseLedgerTableProps {
records: LeaseLedgerRow[];
selectedIds: Set<string>;
loading?: boolean;
maintenanceOrders: MaintenanceOrder[];
accidentRecords: AccidentRecord[];
onToggleRow: (id: string, checked: boolean) => void;
@@ -31,6 +35,62 @@ interface LeaseLedgerTableProps {
onDeductionDetailChange: (recordId: string, value: string) => void;
}
type SortDir = 'asc' | 'desc';
const CHECK_COL_WIDTH = 48;
const ACTION_COL_WIDTH = 168;
const DEFAULT_COLUMN_WIDTHS: Record<string, number> = {
_index: 56,
status: 88,
maintainerName: 88,
year: 72,
month: 64,
dept: 120,
paymentDate: 108,
plateNo: 120,
customerName: 160,
valueAddedService: 96,
exemptionPolicy: 96,
pickupDate: 108,
contractStartDate: 116,
contractEndDate: 116,
periodStartDate: 108,
returnDate: 108,
avgDays: 88,
deposit: 88,
contractRent: 112,
receivableTotal: 112,
receivableRent: 112,
monthlyIncome: 112,
monthlyRent: 112,
maintenanceIncome: 112,
insuranceSurcharge: 112,
opsFeeIncome: 100,
otherIncome: 100,
mileageDeduction: 112,
otherDeduction: 112,
receivedAmount: 112,
unreceived: 112,
invoiceDate: 108,
actualPaymentDate: 116,
paymentMethod: 96,
vehicleStandardCost: 112,
vehicleActualCost: 112,
insuranceFee: 96,
hydrogenFee: 96,
opsFeeCost: 100,
brokerageFee: 96,
otherCost: 96,
totalCost: 112,
profitLoss: 112,
gpsMileage: 96,
otherDeductionDetail: 160,
assetOwner: 140,
signingCompany: 160,
remark: 140,
};
function IncomeWarning({ title, onClick }: { title: string; onClick: () => void }) {
return (
<button type="button" className="ldb-income-warn" title={title} aria-label={title} onClick={onClick}>
@@ -39,17 +99,47 @@ function IncomeWarning({ title, onClick }: { title: string; onClick: () => void
);
}
function HeaderWithTip({ title, tip }: { title: string; tip?: string }) {
if (!tip) return <>{title}</>;
return (
<span className="ldb-th-with-tip">
function HeaderWithTip({
title,
tip,
sortable,
sortActive,
sortDir,
onSort,
}: {
title: string;
tip?: string;
sortable?: boolean;
sortActive?: boolean;
sortDir?: SortDir;
onSort?: () => void;
}) {
const label = (
<span className="ldb-th-label">
{title}
<span className="ldb-th-tip" tabIndex={0} aria-label={`${title}说明`}>
<CircleHelp size={14} aria-hidden />
<span className="ldb-kpi-tooltip" role="tooltip">{tip}</span>
</span>
{tip && (
<span className="ldb-th-tip" tabIndex={0} aria-label={`${title}说明`} onClick={(e) => e.stopPropagation()}>
<CircleHelp size={14} aria-hidden />
<span className="ldb-kpi-tooltip" role="tooltip">{tip}</span>
</span>
)}
</span>
);
if (!sortable) {
return tip ? <span className="ldb-th-with-tip">{label}</span> : <>{title}</>;
}
return (
<button type="button" className={`ldb-th-sort ${sortActive ? 'is-active' : ''}`} onClick={onSort}>
{label}
<span className="ldb-th-sort-icon" aria-hidden>
{sortActive
? (sortDir === 'asc' ? <ArrowUp size={14} /> : <ArrowDown size={14} />)
: <ArrowUpDown size={14} />}
</span>
</button>
);
}
function MoneyInput({
@@ -103,9 +193,32 @@ function TextInput({
);
}
function getSortValue(row: LeaseLedgerRow, col: LeaseTableColumn): string | number {
switch (col.render) {
case 'monthly-income':
return calcMonthlyIncome(row);
case 'stack-dept':
return row.dept;
case 'stack-plate':
return row.plateNo;
case 'stack-customer':
return row.customerName;
case 'status':
return row.status;
default:
break;
}
if (col.key === 'unreceived') return calcUnreceived(row);
if (col.key === 'profitLoss') return row.profitLoss;
const raw = row[col.key as keyof LeaseLedgerRow];
if (typeof raw === 'number') return raw;
if (typeof raw === 'string') return raw;
return '';
}
function renderCell(
row: LeaseLedgerRow,
col: (typeof LEASE_TABLE_COLUMNS)[number],
col: LeaseTableColumn,
index: number,
ctx: {
maintenanceOrders: MaintenanceOrder[];
@@ -126,33 +239,33 @@ function renderCell(
case 'stack-dept':
return (
<div className="ldb-cell-stack">
<span className="ldb-cell-primary">{row.dept}</span>
<span className="ldb-cell-secondary">{row.salesman}</span>
<span className="ldb-cell-primary">{displayText(row.dept)}</span>
<span className="ldb-cell-secondary">{displayText(row.salesman)}</span>
</div>
);
case 'stack-plate': {
const vehicleLine = row.vehicleType || [row.vehicleBrand, row.vehicleModel].filter(Boolean).join('');
return (
<div className="ldb-cell-stack">
<span className="ldb-cell-primary tabular-nums">{row.plateNo}</span>
<span className="ldb-cell-secondary" title={vehicleLine}>{vehicleLine}</span>
<span className="ldb-cell-primary tabular-nums">{displayText(row.plateNo)}</span>
<span className="ldb-cell-secondary" title={vehicleLine}>{displayText(vehicleLine)}</span>
</div>
);
}
case 'stack-customer':
return (
<div className="ldb-cell-stack">
<span className="ldb-cell-primary" title={row.customerName}>{row.customerName}</span>
<span className="ldb-cell-secondary" title={row.projectName}>{row.projectName}</span>
<span className="ldb-cell-primary" title={row.customerName}>{displayText(row.customerName)}</span>
<span className="ldb-cell-secondary" title={row.projectName}>{displayText(row.projectName)}</span>
</div>
);
case 'deposit':
if (row.deposit === '/' || row.deposit === '') return '';
return typeof row.deposit === 'number' ? formatMoney(row.deposit) : String(row.deposit);
if (isBlankDisplayValue(row.deposit)) return '';
return typeof row.deposit === 'number' ? formatMoney(row.deposit) : displayText(row.deposit);
case 'asset-owner':
return <span title={row.assetOwner}>{row.assetOwner || '—'}</span>;
return <span title={row.assetOwner}>{displayText(row.assetOwner)}</span>;
case 'signing-company':
return <span className="ldb-cell-readonly" title="租赁合同甲方自动获取">{row.signingCompany || '—'}</span>;
return <span className="ldb-cell-readonly" title="租赁合同甲方自动获取">{displayText(row.signingCompany)}</span>;
case 'monthly-income':
return (
<span className="ldb-computed-val" title="租金 + 服务费 里程减免 其他减免">
@@ -201,7 +314,7 @@ function renderCell(
</span>
);
case 'other-income':
if (!ctx.canEdit) return row.otherIncome ? formatMoney(row.otherIncome) : '—';
if (!ctx.canEdit) return displayMoney(row.otherIncome, true);
return (
<MoneyInput
value={row.otherIncome}
@@ -211,7 +324,7 @@ function renderCell(
/>
);
case 'other-cost':
if (!ctx.canEdit) return row.otherCost ? formatMoney(row.otherCost) : '—';
if (!ctx.canEdit) return displayMoney(row.otherCost, true);
return (
<MoneyInput
value={row.otherCost}
@@ -222,7 +335,7 @@ function renderCell(
/>
);
case 'brokerage-fee':
if (!ctx.canEdit) return row.brokerageFee ? formatMoney(row.brokerageFee) : '—';
if (!ctx.canEdit) return displayMoney(row.brokerageFee, true);
return (
<MoneyInput
value={row.brokerageFee}
@@ -241,7 +354,7 @@ function renderCell(
</span>
);
case 'invoice-date':
return <span className="ldb-cell-readonly tabular-nums">{row.invoiceDate || '—'}</span>;
return <span className="ldb-cell-readonly tabular-nums">{displayText(row.invoiceDate)}</span>;
case 'vehicle-standard-cost':
return (
<span className="ldb-cell-readonly" title="车型明细维护的固定标准成本">
@@ -257,7 +370,7 @@ function renderCell(
case 'insurance-readonly':
return (
<span className="ldb-cell-readonly" title="暂不可编辑与导入,后续可能自动计算">
{row.insuranceFee ? formatMoney(row.insuranceFee) : '—'}
{displayMoney(row.insuranceFee, true)}
</span>
);
case 'hydrogen-fee':
@@ -266,17 +379,17 @@ function renderCell(
className={row.hydrogenBorneBy === '我司承担' ? 'ldb-computed-val' : 'ldb-cell-readonly'}
title={row.hydrogenBorneBy === '我司承担' ? '我司承担氢费账单期汇总' : '客户承担或无氢费'}
>
{row.hydrogenFee ? formatMoney(row.hydrogenFee) : '—'}
{displayMoney(row.hydrogenFee, true)}
</span>
);
case 'gps-mileage':
return (
<span className="ldb-computed-val tabular-nums" title="BI 里程 · 账单时间段">
{row.gpsMileage || '—'}
{displayText(row.gpsMileage)}
</span>
);
case 'deduction-detail':
if (!ctx.canEdit) return row.otherDeductionDetail || '—';
if (!ctx.canEdit) return displayText(row.otherDeductionDetail);
return (
<TextInput
value={row.otherDeductionDetail}
@@ -286,7 +399,7 @@ function renderCell(
/>
);
case 'remark-text':
if (!ctx.canEdit) return row.remark || '—';
if (!ctx.canEdit) return displayText(row.remark);
return (
<TextInput
value={row.remark}
@@ -312,13 +425,24 @@ function renderCell(
const raw = col.key === '_index' ? index + 1 : row[col.key as keyof LeaseLedgerRow];
if (col.money && typeof raw === 'number') return formatMoney(raw);
if (raw === '' || raw === null || raw === undefined) return '';
if (isBlankDisplayValue(raw)) return '';
return String(raw);
}
function TableSkeleton() {
return (
<div className="ldb-table-skeleton" aria-busy="true" aria-label="表格加载中">
{Array.from({ length: 6 }, (_, i) => (
<div key={i} className="ldb-skeleton-row" />
))}
</div>
);
}
export function LeaseLedgerTable({
records,
selectedIds,
loading = false,
maintenanceOrders,
accidentRecords,
onToggleRow,
@@ -334,47 +458,145 @@ export function LeaseLedgerTable({
onRemarkChange,
onDeductionDetailChange,
}: LeaseLedgerTableProps) {
const allSelected = records.length > 0 && records.every((r) => selectedIds.has(r.id));
const [sortKey, setSortKey] = useState<string | null>(null);
const [sortDir, setSortDir] = useState<SortDir>('asc');
const [columnWidths, setColumnWidths] = useState<Record<string, number>>(() => ({ ...DEFAULT_COLUMN_WIDTHS }));
const resizeRef = useRef<{ key: string; startX: number; startWidth: number } | null>(null);
if (!records.length) {
const sortedRecords = useMemo(() => {
if (!sortKey) return records;
const col = LEASE_TABLE_COLUMNS.find((c) => c.key === sortKey);
if (!col) return records;
const dir = sortDir === 'asc' ? 1 : -1;
return [...records].sort((a, b) => {
const av = getSortValue(a, col);
const bv = getSortValue(b, col);
if (typeof av === 'number' && typeof bv === 'number') return (av - bv) * dir;
return String(av).localeCompare(String(bv), 'zh-CN') * dir;
});
}, [records, sortKey, sortDir]);
const allSelected = sortedRecords.length > 0 && sortedRecords.every((r) => selectedIds.has(r.id));
const handleSort = useCallback((key: string) => {
setSortKey((prev) => {
if (prev === key) {
setSortDir((d) => (d === 'asc' ? 'desc' : 'asc'));
return key;
}
setSortDir('asc');
return key;
});
}, []);
const handleResizeStart = useCallback((key: string, event: React.MouseEvent) => {
event.preventDefault();
event.stopPropagation();
resizeRef.current = {
key,
startX: event.clientX,
startWidth: columnWidths[key] ?? DEFAULT_COLUMN_WIDTHS[key] ?? 100,
};
}, [columnWidths]);
useEffect(() => {
const onMove = (event: MouseEvent) => {
if (!resizeRef.current) return;
const delta = event.clientX - resizeRef.current.startX;
const next = Math.max(56, resizeRef.current.startWidth + delta);
setColumnWidths((prev) => ({ ...prev, [resizeRef.current!.key]: next }));
};
const onUp = () => {
resizeRef.current = null;
};
window.addEventListener('mousemove', onMove);
window.addEventListener('mouseup', onUp);
return () => {
window.removeEventListener('mousemove', onMove);
window.removeEventListener('mouseup', onUp);
};
}, []);
if (loading) {
return (
<div className="vm-table-wrap ldb-table-wrap" data-annotation-id="lbl-table">
<TableSkeleton />
</div>
);
}
if (!sortedRecords.length) {
return <div className="vm-empty" data-annotation-id="lbl-table"><p></p></div>;
}
const tableMinWidth = CHECK_COL_WIDTH + ACTION_COL_WIDTH + LEASE_TABLE_COLUMNS.reduce(
(sum, col) => sum + (columnWidths[col.key] ?? DEFAULT_COLUMN_WIDTHS[col.key] ?? 100),
0,
);
return (
<div className="vm-table-wrap ldb-table-wrap" data-annotation-id="lbl-table">
<table className="vm-table ldb-table">
<table className="vm-table ldb-table" style={{ minWidth: tableMinWidth }}>
<colgroup>
<col style={{ width: CHECK_COL_WIDTH }} />
{LEASE_TABLE_COLUMNS.map((col) => (
<col key={col.key} style={{ width: columnWidths[col.key] ?? DEFAULT_COLUMN_WIDTHS[col.key] ?? 100 }} />
))}
<col style={{ width: ACTION_COL_WIDTH }} />
</colgroup>
<thead>
<tr>
<th className="ldb-col-check sticky-col-left">
<input type="checkbox" checked={allSelected} onChange={(e) => onToggleAll(e.target.checked)} aria-label="全选" />
</th>
{LEASE_TABLE_COLUMNS.map((col) => (
<th
key={col.key}
className={[
col.money ? 'ldb-col-money' : '',
col.className,
col.sticky === 'left' ? 'sticky-col-left' : '',
].filter(Boolean).join(' ')}
data-annotation-id={col.annotationId}
title={
col.key === 'opsFeeIncome'
? '收入侧运维费'
: col.key === 'opsFeeCost'
? '成本侧运维费,与收入侧平进平出'
: undefined
}
>
<HeaderWithTip title={col.title} tip={col.headerTip} />
</th>
))}
{LEASE_TABLE_COLUMNS.map((col) => {
const sortable = col.sortable !== false && col.key !== '_index';
return (
<th
key={col.key}
className={[
col.money ? 'ldb-col-money' : '',
col.className,
col.sticky === 'left' ? 'sticky-col-left' : '',
'ldb-col-resizable',
].filter(Boolean).join(' ')}
data-annotation-id={col.annotationId}
title={
col.key === 'opsFeeIncome'
? '收入侧运维费'
: col.key === 'opsFeeCost'
? '成本侧运维费,与收入侧平进平出'
: undefined
}
>
<div className="ldb-th-inner">
<HeaderWithTip
title={col.title}
tip={col.headerTip}
sortable={sortable}
sortActive={sortKey === col.key}
sortDir={sortDir}
onSort={() => handleSort(col.key)}
/>
<span
className="ldb-col-resize-handle"
role="separator"
aria-orientation="vertical"
aria-label={`调整${col.title}列宽`}
onMouseDown={(e) => handleResizeStart(col.key, e)}
/>
</div>
</th>
);
})}
<th className="sticky-col-right ldb-col-actions"></th>
</tr>
</thead>
<tbody>
{records.map((row, index) => {
{sortedRecords.map((row, index) => {
const canEdit = canStaffEditRow(row);
const canDelete = canStaffDeleteRow(row);
const isDraft = row.status === '草稿';
const showReceiptLink = row.status === '已确认' || row.status === '部分收款';
const cellCtx = {
maintenanceOrders,
@@ -412,37 +634,23 @@ export function LeaseLedgerTable({
</td>
))}
<td className="sticky-col-right ldb-col-actions">
{showReceiptLink && (
<>
<button type="button" className="vm-btn vm-btn-link" onClick={() => onLinkReceipt(row)}>
<div className="ldb-row-actions">
{showReceiptLink && (
<button type="button" className="ldb-action-btn ldb-action-btn--link" onClick={() => onLinkReceipt(row)}>
</button>
<span className="ldb-action-sep" aria-hidden>|</span>
</>
)}
{needsMaintenanceLinkWarning(row, maintenanceOrders) && (
<>
<button type="button" className="vm-btn vm-btn-link" onClick={() => onLinkMaintenance(row)}>
)}
{canEdit && (
<button type="button" className="ldb-action-btn ldb-action-btn--edit" onClick={() => onEdit(row)}>
</button>
<span className="ldb-action-sep" aria-hidden>|</span>
</>
)}
{needsAccidentLinkWarning(row, accidentRecords) && (
<>
<button type="button" className="vm-btn vm-btn-link" onClick={() => onLinkAccident(row)}>
)}
{isDraft && canDelete && (
<button type="button" className="ldb-action-btn ldb-action-btn--delete" onClick={() => onDelete(row)}>
</button>
<span className="ldb-action-sep" aria-hidden>|</span>
</>
)}
<button type="button" className="vm-btn vm-btn-link" disabled={!canEdit} onClick={() => onEdit(row)}>
</button>
<span className="ldb-action-sep" aria-hidden>|</span>
<button type="button" className="vm-btn vm-btn-link ldb-link-danger" disabled={!canDelete} onClick={() => onDelete(row)}>
</button>
)}
</div>
</td>
</tr>
);

View File

@@ -10,6 +10,8 @@ export interface LeaseTableColumn {
className?: string;
annotationId?: string;
headerTip?: string;
sortable?: boolean;
defaultWidth?: number;
render?:
| 'stack-dept' | 'stack-plate' | 'stack-customer' | 'status' | 'deposit'
| 'asset-owner' | 'signing-company'

View File

@@ -21,6 +21,7 @@ import { FilterPanel } from './components/FilterPanel';
import { LedgerKpiRow } from './components/LedgerKpiRow';
import { LeaseLedgerTable } from './components/LeaseLedgerTable';
import { ConfirmMarkModal } from './components/ConfirmMarkModal';
import { DeleteConfirmModal } from './components/DeleteConfirmModal';
import { ReceiptLinkModal } from './components/ReceiptLinkModal';
import { MaintenanceLinkModal } from './components/MaintenanceLinkModal';
import { AccidentLinkModal } from './components/AccidentLinkModal';
@@ -77,6 +78,9 @@ export default function LeaseBusinessLedgerApp() {
const [showUnpaidOnly, setShowUnpaidOnly] = useState(false);
const [listViewMode, setListViewMode] = useState<ListViewMode>('all');
const [visibleCount, setVisibleCount] = useState(LOAD_BATCH_SIZE);
const [tableLoading, setTableLoading] = useState(false);
const [deleteTarget, setDeleteTarget] = useState<LeaseLedgerRow | null>(null);
const [submitApprovalOpen, setSubmitApprovalOpen] = useState(false);
const showToast = useCallback((msg: string) => {
setToast(msg);
@@ -109,8 +113,12 @@ export default function LeaseBusinessLedgerApp() {
);
const handleSearch = () => {
setAppliedFilters(pendingFilters);
setSelectedIds(new Set());
setTableLoading(true);
window.setTimeout(() => {
setAppliedFilters(pendingFilters);
setSelectedIds(new Set());
setTableLoading(false);
}, 320);
};
const handleReset = () => {
@@ -274,17 +282,17 @@ export default function LeaseBusinessLedgerApp() {
</label>
<div className="vm-table-actions">
<button type="button" className="vm-btn vm-btn-secondary" onClick={() => showToast('批量导入(原型演示)')}>
<button type="button" className="vm-btn vm-btn-secondary ldb-toolbar-btn" onClick={() => showToast('批量导入(原型演示)')}>
<FileUp size={16} aria-hidden />
</button>
<button type="button" className="vm-btn vm-btn-ghost" onClick={() => showToast(`导出 ${listFiltered.length} 条(原型演示)`)}>
<button type="button" className="vm-btn vm-btn-ghost ldb-toolbar-btn" onClick={() => showToast(`导出 ${listFiltered.length} 条(原型演示)`)}>
<Download size={16} aria-hidden />
</button>
<button
type="button"
className="vm-btn vm-btn-ghost"
className="vm-btn vm-btn-ghost ldb-toolbar-btn"
data-annotation-id="lbl-btn-mark-confirmed"
onClick={handleMarkConfirmedClick}
>
@@ -293,8 +301,14 @@ export default function LeaseBusinessLedgerApp() {
</button>
<button
type="button"
className="vm-btn vm-btn-primary"
onClick={() => showToast(`提交收款审批:已选 ${selectedIds.size} 条(原型演示)`)}
className="vm-btn vm-btn-primary ldb-toolbar-btn"
onClick={() => {
if (selectedIds.size === 0) {
showToast('请先勾选需要提交审批的记录');
return;
}
setSubmitApprovalOpen(true);
}}
>
<Send size={16} aria-hidden />
@@ -305,6 +319,7 @@ export default function LeaseBusinessLedgerApp() {
<div className="vm-table-card">
<LeaseLedgerTable
records={visibleRecords}
loading={tableLoading}
maintenanceOrders={MAINTENANCE_ORDERS}
accidentRecords={ACCIDENT_RECORDS}
selectedIds={selectedIds}
@@ -327,11 +342,8 @@ export default function LeaseBusinessLedgerApp() {
showToast(`行内编辑:${row.plateNo}(原型演示)`);
}}
onDelete={(row) => {
if (!canStaffDeleteRow(row)) {
showToast('已确认记录仅业务服务组主管可删除');
return;
}
showToast(`删除:${row.plateNo}(原型演示)`);
if (!canStaffDeleteRow(row)) return;
setDeleteTarget(row);
}}
onLinkReceipt={(row) => setReceiptRow(row)}
onLinkMaintenance={(row) => setMaintenanceRow(row)}
@@ -379,6 +391,47 @@ export default function LeaseBusinessLedgerApp() {
onCancel={() => setConfirmOpen(false)}
onConfirm={handleConfirmMark}
/>
<DeleteConfirmModal
open={Boolean(deleteTarget)}
plateNo={deleteTarget?.plateNo ?? ''}
onCancel={() => setDeleteTarget(null)}
onConfirm={() => {
if (!deleteTarget) return;
setRecords((prev) => prev.filter((row) => row.id !== deleteTarget.id));
setSelectedIds((prev) => {
const next = new Set(prev);
next.delete(deleteTarget.id);
return next;
});
setDeleteTarget(null);
showToast(`已删除 ${deleteTarget.plateNo}(原型)`);
}}
/>
{submitApprovalOpen && (
<div className="vm-modal-backdrop" role="presentation" onClick={() => setSubmitApprovalOpen(false)}>
<div className="vm-modal vm-modal-sm" role="dialog" aria-modal="true" onClick={(e) => e.stopPropagation()}>
<header className="vm-modal-header">
<h2 className="vm-modal-title"></h2>
</header>
<div className="vm-modal-body">
<p className="ldb-confirm-text"> {selectedIds.size} </p>
</div>
<footer className="vm-modal-footer">
<button type="button" className="vm-btn vm-btn-ghost" onClick={() => setSubmitApprovalOpen(false)}></button>
<button
type="button"
className="vm-btn vm-btn-primary"
onClick={() => {
setSubmitApprovalOpen(false);
showToast(`已提交 ${selectedIds.size} 条收款审批(原型演示)`);
}}
>
</button>
</footer>
</div>
</div>
)}
<ReceiptLinkModal
key={receiptRow?.id ?? 'closed'}
open={Boolean(receiptRow)}

View File

@@ -40,24 +40,25 @@
.ldb-kpi-card {
position: relative;
padding: 14px 16px;
padding: 16px 18px;
border-radius: var(--ln-radius-card);
border: 1px solid var(--ln-hairline);
background: var(--ln-surface-card);
box-shadow: var(--ln-shadow-soft);
min-height: 72px;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04), 0 4px 12px rgba(15, 23, 42, 0.03);
min-height: 76px;
}
.ldb-kpi-label {
display: block;
font-size: 0.75rem;
font-size: 0.6875rem;
font-weight: 500;
color: var(--ln-muted);
margin-bottom: 4px;
color: var(--ln-muted-soft);
letter-spacing: 0.02em;
margin-bottom: 6px;
}
.ldb-kpi-val {
font-size: 1.25rem;
font-size: 1.375rem;
font-weight: 700;
color: var(--ln-ink);
line-height: 1.2;
@@ -159,7 +160,26 @@
}
.ldb-table-wrap { overflow: auto; }
.ldb-table { min-width: 4200px; }
.ldb-table { min-width: 4200px; table-layout: fixed; }
.ldb-table .vm-table thead th,
.ldb-table .vm-table tbody td {
padding: 10px 12px;
transition: background-color 0.18s ease;
}
.ldb-table .vm-table tbody tr {
transition: background-color 0.18s ease;
}
.ldb-table .vm-table tbody tr:hover td {
background: var(--ln-canvas-soft);
}
.ldb-table .vm-table tbody tr:hover td.sticky-col-left,
.ldb-table .vm-table tbody tr:hover td.sticky-col-right {
background: var(--ln-canvas-soft);
}
.ldb-cell-stack {
display: flex;
@@ -187,7 +207,198 @@
.ldb-col-money { text-align: right; white-space: nowrap; }
.ldb-col-name { max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ldb-col-remark { max-width: 160px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ldb-col-actions { min-width: 200px; white-space: nowrap; }
.ldb-col-actions { min-width: 168px; white-space: nowrap; }
.ldb-row-actions {
display: inline-flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
}
.ldb-action-btn {
display: inline-flex;
align-items: center;
min-height: 28px;
padding: 0;
border: none;
background: none;
font-size: 0.8125rem;
font-weight: 600;
line-height: 1.2;
cursor: pointer;
border-radius: var(--ln-radius-xs);
transition: color 0.15s ease, opacity 0.15s ease, background-color 0.15s ease;
}
.ldb-action-btn:hover {
text-decoration: underline;
text-underline-offset: 2px;
}
.ldb-action-btn--edit {
color: var(--ln-primary);
}
.ldb-action-btn--edit:hover {
color: var(--ln-primary-hover);
}
.ldb-action-btn--link {
color: #2563eb;
}
.ldb-action-btn--link:hover {
color: #1d4ed8;
}
.ldb-action-btn--delete {
color: var(--ln-error);
}
.ldb-action-btn--delete:hover {
color: #b91c1c;
}
.ldb-th-inner {
position: relative;
display: flex;
align-items: center;
min-height: 24px;
padding-right: 8px;
}
.ldb-th-sort {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 0;
border: none;
background: none;
font: inherit;
font-weight: 600;
color: inherit;
cursor: pointer;
text-align: left;
}
.ldb-th-sort:hover {
color: var(--ln-primary);
}
.ldb-th-sort.is-active {
color: var(--ln-primary);
}
.ldb-th-sort-icon {
display: inline-flex;
color: var(--ln-muted-soft);
flex-shrink: 0;
}
.ldb-th-sort.is-active .ldb-th-sort-icon {
color: var(--ln-primary);
}
.ldb-th-label {
display: inline-flex;
align-items: center;
gap: 4px;
}
.ldb-col-resizable {
position: relative;
}
.ldb-col-resize-handle {
position: absolute;
top: 0;
right: 0;
width: 8px;
height: 100%;
cursor: col-resize;
user-select: none;
}
.ldb-col-resize-handle:hover {
background: color-mix(in srgb, var(--ln-primary) 18%, transparent);
}
.ldb-toolbar-btn {
transition: transform 0.12s ease, box-shadow 0.15s ease, border-color 0.15s ease, background-color 0.15s ease;
}
.ldb-toolbar-btn:hover {
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06);
}
.ldb-toolbar-btn:active {
transform: translateY(1px);
}
.ldb-filter-expand {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.28s ease, opacity 0.22s ease;
opacity: 0;
}
.ldb-filter-expand.is-expanded {
grid-template-rows: 1fr;
opacity: 1;
}
.ldb-filter-expand-inner {
overflow: hidden;
}
.ldb-filter-expand-grid {
padding-top: 4px;
}
.ldb-filter-toggle-icon {
transition: transform 0.25s ease;
}
.ldb-filter-toggle-icon.is-open {
transform: rotate(180deg);
}
.ldb-date-range-sep {
flex-shrink: 0;
font-size: 0.8125rem;
color: var(--ln-muted);
}
.ldb-table-skeleton {
padding: 12px 0;
}
.ldb-skeleton-row {
height: 44px;
margin: 0 12px 8px;
border-radius: var(--ln-radius-control);
background: linear-gradient(90deg, var(--ln-canvas-soft) 25%, #eef1f4 50%, var(--ln-canvas-soft) 75%);
background-size: 200% 100%;
animation: ldb-skeleton-shimmer 1.2s ease-in-out infinite;
}
@keyframes ldb-skeleton-shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.vm-btn-danger {
background: var(--ln-error);
color: #fff;
border-color: var(--ln-error);
}
.vm-btn-danger:hover {
background: #b91c1c;
border-color: #b91c1c;
color: #fff;
}
.sticky-col-left {
position: sticky;
@@ -205,17 +416,19 @@
.ldb-status-tag {
display: inline-flex;
padding: 0 8px;
align-items: center;
padding: 2px 10px;
border-radius: var(--ln-radius-pill);
font-size: 0.75rem;
font-weight: 500;
line-height: 22px;
font-weight: 600;
line-height: 20px;
border: 1px solid transparent;
}
.ldb-status--draft { background: #ecfdf5; color: #047857; }
.ldb-status--confirmed { background: #eff6ff; color: #1d4ed8; }
.ldb-status--partial { background: #fff7ed; color: #c2410c; }
.ldb-status--paid { background: #f0fdf4; color: #15803d; }
.ldb-status--draft { background: #ecfdf5; color: #047857; border-color: #a7f3d0; }
.ldb-status--confirmed { background: #eff6ff; color: #1d4ed8; border-color: #bfdbfe; }
.ldb-status--partial { background: #fff7ed; color: #c2410c; border-color: #fed7aa; }
.ldb-status--paid { background: #f0fdf4; color: #15803d; border-color: #bbf7d0; }
.ldb-confirm-text {
margin: 0 0 8px;

View File

@@ -94,6 +94,27 @@ export function formatMoney(value: number): string {
return value.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
/** 表格/卡片空值:不展示占位横杠 */
export function isBlankDisplayValue(value: unknown): boolean {
return value === null
|| value === undefined
|| value === ''
|| value === '—'
|| value === '-'
|| value === '/';
}
export function displayText(value: unknown): string {
if (isBlankDisplayValue(value)) return '';
return String(value);
}
export function displayMoney(value: number | null | undefined, treatZeroAsEmpty = false): string {
if (value === null || value === undefined) return '';
if (treatZeroAsEmpty && value === 0) return '';
return formatMoney(value);
}
export function calcUnreceived(row: Pick<LeaseLedgerRow, 'receivableTotal' | 'receivedAmount'>): number {
return Math.round((row.receivableTotal - row.receivedAmount) * 100) / 100;
}

View File

@@ -8,6 +8,7 @@ import {
getContractTemplatePreviewBadge,
getContractTypeLabel,
} from '../contract-template-management/contract-template-catalog.js';
import { buildLeaseContractEditFormState } from './lease-contract-edit-bridge.js';
import { createDefaultLeaseOrderState, createDefaultPowerOfAttorneyState } from './lease-order-vars.js';
import { buildLeaseContractPreviewHtml } from './lease-contract-preview-build.js';
import {
@@ -250,6 +251,40 @@ export default function LeaseContractCreate({ onBack, initialScrollSection, edit
return function () { window.clearTimeout(timer); };
}, [initialScrollSection, editRecord && editRecord.id]);
useEffect(function () {
if (!editRecord || !editRecord.id) return;
var formState = buildLeaseContractEditFormState(editRecord);
if (!formState) return;
setContractTemplateId(formState.contractTemplateId || '');
setLessorId(formState.lessorId || '');
setCustomerId(formState.customerId || '');
setContractCode(formState.contractCode || '');
setBusinessDept(formState.businessDept || '');
setBusinessOwner(formState.businessOwner || '');
setMileage(Object.assign({}, formState.mileage));
setFeeInfo(Object.assign({}, formState.feeInfo));
setLeaseOrder({
insuredVehicleCount: formState.leaseOrder.insuredVehicleCount,
thirdPartyLiabilityMillion: formState.leaseOrder.thirdPartyLiabilityMillion,
deliveryRegion: (formState.leaseOrder.deliveryRegion || []).slice(),
deliveryDate: formState.leaseOrder.deliveryDate,
rows: (formState.leaseOrder.rows || []).map(function (row) {
return Object.assign({}, row, {
brandModels: row.brandModels ? row.brandModels.map(function (pair) { return pair.slice(); }) : [],
plateNos: row.plateNos ? row.plateNos.slice() : undefined,
extraServices: row.extraServices ? row.extraServices.slice() : [],
});
}),
});
setPowerOfAttorney({
delegates: (formState.powerOfAttorney.delegates || []).map(function (row) {
return Object.assign({}, row);
}),
});
setContractRemark(formState.contractRemark || '');
setSealTypes((formState.sealTypes || ['contract']).slice());
}, [editRecord && editRecord.id]);
var createSteps = useMemo(function () {
return [
{

View File

@@ -1,26 +1,13 @@
// 在租车辆概览 — 按品牌型号统计
import { buildOnLeaseFleetByBrandModel, formatDeliveryRegion } from './lease-contract-list-data.js';
function formatPlateLabel(plateNo) {
var plate = plateNo && String(plateNo).trim();
if (!plate || plate === '-') return '待上牌';
return plate;
}
function formatMileageLabel(vehicle) {
var raw = vehicle.currentMileage;
if (raw == null || raw === '' || raw === '-') return '-';
var num = Number(String(raw).replace(/,/g, ''));
if (!Number.isFinite(num)) return String(raw);
return num.toLocaleString('zh-CN') + '\u00a0km';
}
import { buildOnLeaseFleetByBrandModel } from './lease-contract-list-data.js';
function FleetDetailModal(props) {
var Modal = props.Modal;
var item = props.item;
var open = props.open;
var onClose = props.onClose;
var onViewContract = props.onViewContract;
var renderVehicleNativeTableHeader = props.renderVehicleNativeTableHeader;
var renderVehicleNativeTableRow = props.renderVehicleNativeTableRow;
if (!Modal) return null;
var titleNode = item
@@ -31,6 +18,14 @@ function FleetDetailModal(props) {
)
: null;
var canRenderTable = Boolean(
item
&& item.entries
&& item.entries.length
&& renderVehicleNativeTableHeader
&& renderVehicleNativeTableRow,
);
return React.createElement(Modal, {
className: 'lc-fleet-detail-modal',
rootClassName: 'lc-fleet-detail-modal-root',
@@ -46,86 +41,29 @@ function FleetDetailModal(props) {
centered: true,
destroyOnClose: true,
},
item && item.entries && item.entries.length
canRenderTable
? React.createElement('div', { className: 'lc-fleet-detail-modal__body' },
React.createElement('div', { className: 'lc-fleet-detail-table-wrap' },
React.createElement('table', { className: 'lc-fleet-detail-table' },
React.createElement('thead', null,
React.createElement('tr', null,
React.createElement('th', { scope: 'col', className: 'lc-fleet-detail-table__col-plate' }, '车牌号'),
React.createElement('th', { scope: 'col', className: 'lc-fleet-detail-table__col-vin' }, '识别代码'),
React.createElement('th', { scope: 'col', className: 'lc-fleet-detail-table__col-type' }, '车辆类型'),
React.createElement('th', { scope: 'col', className: 'lc-fleet-detail-table__col-announce' }, '公告型号'),
React.createElement('th', { scope: 'col', className: 'lc-fleet-detail-table__col-city' }, '运营城市'),
React.createElement('th', { scope: 'col', className: 'lc-fleet-detail-table__col-mileage' }, '里程数'),
React.createElement('th', { scope: 'col', className: 'lc-fleet-detail-table__col-project' }, '所属项目'),
React.createElement('th', { scope: 'col', className: 'lc-fleet-detail-table__col-action' }, '操作'),
React.createElement('div', { className: 'lc-fleet-detail-modal__table-host vm-page lc-page' },
React.createElement('div', { className: 'lc-contract-vehicle-table-wrap' },
React.createElement('table', { className: 'lc-contract-vehicle-native-table lc-fleet-detail-modal__vehicle-table' },
renderVehicleNativeTableHeader(),
React.createElement('tbody', null,
item.entries.map(function (entry, index) {
var record = entry.record;
var vehicle = entry.vehicle;
var vehicleIndex = (record && record.vehicles)
? record.vehicles.indexOf(vehicle)
: -1;
if (vehicleIndex < 0) vehicleIndex = index;
return renderVehicleNativeTableRow(
record,
vehicle,
vehicleIndex,
(record && record.id ? record.id : 'rec') + '-fleet-' + index,
);
}),
),
),
React.createElement('tbody', null,
item.entries.map(function (entry, index) {
var vehicle = entry.vehicle;
var record = entry.record;
var plate = formatPlateLabel(vehicle.plateNo);
var vin = vehicle.vin && String(vehicle.vin).trim() ? vehicle.vin : '-';
var regionLabel = formatDeliveryRegion(record.deliveryRegion) || '-';
var mileageLabel = formatMileageLabel(vehicle);
return React.createElement('tr', { key: record.id + '-' + index },
React.createElement('td', { className: 'lc-fleet-detail-table__col-plate' },
React.createElement('span', {
className: 'lc-fleet-detail-table__plate tabular-nums',
title: plate,
}, plate),
),
React.createElement('td', { className: 'lc-fleet-detail-table__col-vin' },
React.createElement('span', {
className: 'lc-fleet-detail-table__vin tabular-nums',
title: vin,
}, vin),
),
React.createElement('td', { className: 'lc-fleet-detail-table__col-type' },
React.createElement('span', { className: 'lc-fleet-detail-table__type-pill' }, vehicle.vehicleType || '-'),
),
React.createElement('td', { className: 'lc-fleet-detail-table__col-announce' },
React.createElement('span', {
className: 'lc-fleet-detail-table__announce',
title: vehicle.officialModel || '-',
}, vehicle.officialModel || '-'),
),
React.createElement('td', { className: 'lc-fleet-detail-table__col-city' },
React.createElement('span', { className: 'lc-fleet-detail-table__city' }, regionLabel),
),
React.createElement('td', { className: 'lc-fleet-detail-table__col-mileage' },
React.createElement('span', { className: 'lc-fleet-detail-table__mileage tabular-nums' }, mileageLabel),
),
React.createElement('td', { className: 'lc-fleet-detail-table__col-project' },
React.createElement('div', { className: 'lc-fleet-detail-table__project' },
React.createElement('span', {
className: 'lc-fleet-detail-table__project-name',
title: record.projectName || '-',
}, record.projectName || '-'),
React.createElement('button', {
type: 'button',
className: 'lc-fleet-detail-table__contract-link tabular-nums',
onClick: function () { onViewContract(record); },
title: '查看合同 ' + (record.contractCode || ''),
}, record.contractCode || '-'),
React.createElement('span', {
className: 'lc-fleet-detail-table__customer',
title: record.customerName || '-',
}, record.customerName || '-'),
),
),
React.createElement('td', { className: 'lc-fleet-detail-table__col-action' },
React.createElement('button', {
type: 'button',
className: 'lc-fleet-detail-table__action',
onClick: function () { onViewContract(record); },
}, '查看合同'),
),
);
}),
),
),
),
)
@@ -135,8 +73,9 @@ function FleetDetailModal(props) {
export default function LeaseContractFleetSummary(props) {
var records = props.records || [];
var onViewContract = props.onViewContract || function () {};
var Modal = props.Modal;
var renderVehicleNativeTableHeader = props.renderVehicleNativeTableHeader;
var renderVehicleNativeTableRow = props.renderVehicleNativeTableRow;
var useState = React.useState;
var useMemo = React.useMemo;
@@ -299,7 +238,8 @@ export default function LeaseContractFleetSummary(props) {
item: detailItem,
open: detailOpen,
onClose: closeDetail,
onViewContract: onViewContract,
renderVehicleNativeTableHeader: renderVehicleNativeTableHeader,
renderVehicleNativeTableRow: renderVehicleNativeTableRow,
}),
);
}

View File

@@ -40,12 +40,13 @@ import {
DELIVERY_DATE_TBD_LABEL,
isDeliveryDateTbd,
formatContractDeliveryDateLabel,
formatVehicleDeliveryPlanDateLabel,
resolveVehicleDeliveryDateTbd,
resolveVehiclePlannedDeliveryDate,
resolveContractStatusByVehicleLease,
} from './lease-contract-list-data.js';
import { getLessorCompanies } from '../contract-template-management/contract-template-vars.js';
var LC_LIST_TABLE_EXPAND_WIDTH = 48;
const Component = function(props) {
var useEffect = React.useEffect;
var ONEOS_ANT_TABLE_GLOBAL_FIX = [
@@ -70,6 +71,7 @@ const Component = function(props) {
var Tag = antd.Tag;
var Avatar = antd.Avatar;
var Dropdown = antd.Dropdown;
var Tooltip = antd.Tooltip;
var Modal = antd.Modal;
var Upload = antd.Upload;
var message = antd.message;
@@ -119,6 +121,9 @@ const Component = function(props) {
var _deliveryChangePopoverRecord = useState(null);
var _deliveryChangeDraftDate = useState(null);
var _deliveryChangeDraftTbd = useState(false);
var _vehicleDeliveryChangeTarget = useState(null);
var _vehicleDeliveryChangeDraftDate = useState(null);
var _vehicleDeliveryChangeDraftTbd = useState(false);
var _delegatePopoverRecord = useState(null);
var _authorizedModalVisible = useState(false);
var _authorizedModalRecord = useState(null);
@@ -139,6 +144,7 @@ const Component = function(props) {
var _stampedUploadedOverride = useState({});
// 撤回等列表行状态覆盖(原型)
var _recordOverrides = useState({});
var _expandedRowKeys = useState([]);
useEffect(function() {
if (props && props.onPageViewChange) {
@@ -1000,16 +1006,16 @@ const Component = function(props) {
message.info('查看(跳转车辆租赁合同-查看)' + (code ? '' + code : ''));
}
function renderMileageProgressCell(vehicle) {
var percent = Math.min(100, Math.max(0, Number(vehicle.mileageProgress) || 0));
return React.createElement('div', { className: 'lc-vehicle-mileage-progress' },
function renderMileageProgressBarRow(percent, ariaLabel) {
percent = Math.min(100, Math.max(0, Number(percent) || 0));
return React.createElement('div', { className: 'lc-contract-mileage-completion__bar-row' },
React.createElement('div', {
className: 'lc-vehicle-mileage-progress__track',
role: 'progressbar',
'aria-valuenow': percent,
'aria-valuemin': 0,
'aria-valuemax': 100,
'aria-label': '里程完成 ' + percent + '%',
'aria-label': ariaLabel,
},
React.createElement('div', {
className: 'lc-vehicle-mileage-progress__bar',
@@ -1261,15 +1267,17 @@ const Component = function(props) {
);
}
var rangeData = getMileageCompletionRangeData(vehicle, contractRecord);
var percent = Math.min(100, Math.max(0, Number(vehicle.mileageProgress) || 0));
var rangeRow = rangeData
? React.createElement('div', { className: 'lc-contract-mileage-completion__range-row' },
renderMileageCompletionRangeStart(rangeData),
renderMileageCompletionRangeEnd(rangeData),
)
: null;
return React.createElement('div', { className: 'lc-contract-mileage-completion' },
remainingNode,
React.createElement('div', { className: 'lc-contract-mileage-completion__progress-row' },
React.createElement('div', { className: 'lc-contract-mileage-completion__progress-main' },
renderMileageProgressCell(vehicle),
),
renderMileageCompletionRangeEnd(rangeData),
),
renderMileageCompletionRangeStart(rangeData),
renderMileageProgressBarRow(percent, '里程完成 ' + percent + '%'),
rangeRow,
);
}
@@ -1376,6 +1384,98 @@ const Component = function(props) {
);
}
function renderVehicleDeliveryPlanCell(record, vehicle, vehicleIndex) {
var regionLabel = formatDeliveryRegion(record.deliveryRegion);
var dateLabel = formatVehicleDeliveryPlanDateLabel(vehicle, record);
var delivered = isContractVehicleDelivered(vehicle);
var isTbd = !delivered && resolveVehicleDeliveryDateTbd(vehicle, record);
var target = _vehicleDeliveryChangeTarget[0];
var popoverOpen = Boolean(
target
&& target.recordId === record.id
&& target.vehicleIndex === vehicleIndex,
);
return React.createElement('div', { className: 'lc-delivery-cell lc-delivery-cell--vehicle' },
React.createElement('span', {
className: 'lc-delivery-cell__region',
title: regionLabel,
}, regionLabel),
React.createElement('div', { className: 'lc-delivery-cell__date-row' },
React.createElement('span', {
className: 'lc-delivery-cell__date tabular-nums' + (isTbd ? ' lc-delivery-cell__date--tbd' : ''),
title: delivered ? '实际交车 ' + dateLabel : '计划交车 ' + dateLabel,
}, dateLabel),
delivered
? null
: React.createElement(Popover, {
overlayClassName: 'lc-delivery-change-popover',
placement: 'bottom',
trigger: 'click',
arrow: { pointAtCenter: true },
destroyTooltipOnHide: true,
open: popoverOpen,
title: null,
content: renderVehicleDeliveryChangePanel(record, vehicle, vehicleIndex),
onOpenChange: function (visible) {
if (!visible) {
closeVehicleDeliveryChangePopover();
return;
}
closeDeliveryChangePopover();
_vehiclePopoverRecord[1](null);
_deliveredPopoverRecord[1](null);
closeDelegatePopover();
var vehicleTbd = resolveVehicleDeliveryDateTbd(vehicle, record);
_vehicleDeliveryChangeTarget[1]({
recordId: record.id,
vehicleIndex: vehicleIndex,
});
_vehicleDeliveryChangeDraftTbd[1](!!vehicleTbd);
_vehicleDeliveryChangeDraftDate[1](vehicleTbd
? null
: (resolveVehiclePlannedDeliveryDate(vehicle, record) || null));
},
},
React.createElement('button', {
type: 'button',
className: 'lc-delivery-cell__edit-btn',
'aria-label': '编辑本车交车时间',
}, renderDeliveryEditIcon()),
),
),
);
}
function renderContractVehicleNativeTableHeader() {
return React.createElement('thead', null,
React.createElement('tr', null,
React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__info' }, '车辆信息'),
React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__pickup' }, '提车应收款'),
React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__handover', title: '交车情况' }, '交车'),
React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__handover', title: '还车情况' }, '还车'),
React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__return-settlement' }, '还车应结款'),
React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__delivery-plan', title: '交车安排' }, '交车安排'),
React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__mileage-req', title: '里程要求' }, '里程要求'),
React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__mileage-completion', title: '里程完成情况' }, '里程完成情况'),
React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__action' }, '操作'),
),
);
}
function renderContractVehicleNativeTableRow(record, vehicle, index, rowKey) {
return React.createElement('tr', { key: rowKey || (record.id + '-vehicle-' + index) },
React.createElement('td', { className: 'lc-contract-vehicle-native-table__info' }, renderVehicleInfoCell(vehicle)),
React.createElement('td', { className: 'lc-contract-vehicle-native-table__pickup' }, renderPickupReceivableCell(vehicle)),
React.createElement('td', { className: 'lc-contract-vehicle-native-table__handover' }, renderHandoverSituationCell(vehicle, 'delivery', { compact: true })),
React.createElement('td', { className: 'lc-contract-vehicle-native-table__handover' }, renderHandoverSituationCell(vehicle, 'return', { compact: true })),
React.createElement('td', { className: 'lc-contract-vehicle-native-table__return-settlement' }, renderReturnSettlementCell(vehicle)),
React.createElement('td', { className: 'lc-contract-vehicle-native-table__delivery-plan' }, renderVehicleDeliveryPlanCell(record, vehicle, index)),
React.createElement('td', { className: 'lc-contract-vehicle-native-table__mileage-req' }, renderMileageRequirementCell(record, vehicle)),
React.createElement('td', { className: 'lc-contract-vehicle-native-table__mileage-completion' }, renderMileageCompletionCell(vehicle, record)),
React.createElement('td', { className: 'lc-contract-vehicle-native-table__action' }, renderContractVehicleActionCell(vehicle)),
);
}
function renderExpandedVehicleTable(record) {
var vehicles = record.vehicles || [];
if (!vehicles.length) {
@@ -1383,30 +1483,10 @@ const Component = function(props) {
}
return React.createElement('div', { className: 'lc-contract-vehicle-table-wrap' },
React.createElement('table', { className: 'lc-contract-vehicle-native-table' },
React.createElement('thead', null,
React.createElement('tr', null,
React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__info' }, '车辆信息'),
React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__pickup' }, '提车应收款'),
React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__handover', title: '交车情况' }, '交车'),
React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__handover', title: '还车情况' }, '还车'),
React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__return-settlement' }, '还车应结款'),
React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__mileage-req', title: '里程要求' }, '里程要求'),
React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__mileage-completion', title: '里程完成情况' }, '里程完成情况'),
React.createElement('th', { scope: 'col', className: 'lc-contract-vehicle-native-table__action' }, '操作'),
),
),
renderContractVehicleNativeTableHeader(),
React.createElement('tbody', null,
vehicles.map(function (vehicle, index) {
return React.createElement('tr', { key: record.id + '-vehicle-' + index },
React.createElement('td', { className: 'lc-contract-vehicle-native-table__info' }, renderVehicleInfoCell(vehicle)),
React.createElement('td', { className: 'lc-contract-vehicle-native-table__pickup' }, renderPickupReceivableCell(vehicle)),
React.createElement('td', { className: 'lc-contract-vehicle-native-table__handover' }, renderHandoverSituationCell(vehicle, 'delivery', { compact: true })),
React.createElement('td', { className: 'lc-contract-vehicle-native-table__handover' }, renderHandoverSituationCell(vehicle, 'return', { compact: true })),
React.createElement('td', { className: 'lc-contract-vehicle-native-table__return-settlement' }, renderReturnSettlementCell(vehicle)),
React.createElement('td', { className: 'lc-contract-vehicle-native-table__mileage-req' }, renderMileageRequirementCell(record, vehicle)),
React.createElement('td', { className: 'lc-contract-vehicle-native-table__mileage-completion' }, renderMileageCompletionCell(vehicle, record)),
React.createElement('td', { className: 'lc-contract-vehicle-native-table__action' }, renderContractVehicleActionCell(vehicle)),
);
return renderContractVehicleNativeTableRow(record, vehicle, index);
}),
),
),
@@ -1485,31 +1565,73 @@ const Component = function(props) {
}, fullName);
}
function toggleContractRowExpand(recordId) {
_expandedRowKeys[1](function (prev) {
if (prev.indexOf(recordId) >= 0) {
return prev.filter(function (key) { return key !== recordId; });
}
return prev.concat([recordId]);
});
}
function renderContractRowExpandIcon(record) {
var vehicles = record.vehicles || [];
var expandable = vehicles.length > 0;
var expanded = _expandedRowKeys[0].indexOf(record.id) >= 0;
if (!expandable) {
return React.createElement('span', {
className: 'ant-table-row-expand-icon ant-table-row-spaced',
'aria-hidden': true,
});
}
return React.createElement('span', {
className: 'ant-table-row-expand-icon ' + (expanded ? 'ant-table-row-expanded' : 'ant-table-row-collapsed'),
role: 'button',
tabIndex: 0,
'aria-label': expanded ? '收起车辆明细' : '展开车辆明细',
'aria-expanded': expanded,
onClick: function (e) {
e.stopPropagation();
toggleContractRowExpand(record.id);
},
onKeyDown: function (e) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
e.stopPropagation();
toggleContractRowExpand(record.id);
}
},
});
}
function renderProjectInfoCell(_, record) {
var status = record.contractStatus || '-';
return React.createElement('div', { className: 'lc-project-info-cell' },
React.createElement(
'button',
{
type: 'button',
className: 'lc-project-info-btn',
onClick: function() { openContractView(record); },
title: '查看合同详情'
},
return React.createElement('div', { className: 'lc-project-info-merged-cell' },
renderContractRowExpandIcon(record),
React.createElement('div', { className: 'lc-project-info-cell' },
React.createElement(
'div',
{ className: 'vm-stack' },
React.createElement('span', { className: 'primary' }, record.projectName || '-'),
React.createElement('span', { className: 'code' }, record.contractCode || '-'),
React.createElement('span', { className: 'sub' }, record.customerName || '-')
)
),
React.createElement('div', { className: 'lc-project-info-cell__status' },
React.createElement(StatusTag, {
label: status,
tone: contractStatusTone(status),
title: status,
}),
'button',
{
type: 'button',
className: 'lc-project-info-btn',
onClick: function() { openContractView(record); },
title: '查看合同详情'
},
React.createElement(
'div',
{ className: 'vm-stack' },
React.createElement('span', { className: 'primary' }, record.projectName || '-'),
React.createElement('span', { className: 'code' }, record.contractCode || '-'),
React.createElement('span', { className: 'sub' }, record.customerName || '-')
)
),
React.createElement('div', { className: 'lc-project-info-cell__status' },
React.createElement(StatusTag, {
label: status,
tone: contractStatusTone(status),
title: status,
}),
),
),
);
}
@@ -1576,6 +1698,12 @@ const Component = function(props) {
_deliveryChangeDraftTbd[1](false);
}
function closeVehicleDeliveryChangePopover() {
_vehicleDeliveryChangeTarget[1](null);
_vehicleDeliveryChangeDraftDate[1](null);
_vehicleDeliveryChangeDraftTbd[1](false);
}
function confirmDeliveryDateChange(record) {
var draftTbd = _deliveryChangeDraftTbd[0];
var newDate = draftTbd ? DELIVERY_DATE_TBD_LABEL : _deliveryChangeDraftDate[0];
@@ -1623,6 +1751,48 @@ const Component = function(props) {
closeDeliveryChangePopover();
}
function confirmVehicleDeliveryDateChange(record, vehicleIndex) {
var vehicles = record.vehicles || [];
var vehicle = vehicles[vehicleIndex];
if (!vehicle || isContractVehicleDelivered(vehicle)) {
closeVehicleDeliveryChangePopover();
return;
}
var draftTbd = _vehicleDeliveryChangeDraftTbd[0];
var newDate = draftTbd ? DELIVERY_DATE_TBD_LABEL : _vehicleDeliveryChangeDraftDate[0];
var oldTbd = resolveVehicleDeliveryDateTbd(vehicle, record);
var oldDate = oldTbd
? DELIVERY_DATE_TBD_LABEL
: (resolveVehiclePlannedDeliveryDate(vehicle, record) || null);
if (!draftTbd && !newDate) {
message.warning('请选择交车日期,或标记为暂未确定');
return;
}
if (draftTbd === oldTbd && (draftTbd || newDate === oldDate)) {
closeVehicleDeliveryChangePopover();
return;
}
_recordOverrides[1](function (prev) {
var prevRec = prev[record.id] || {};
var baseVehicles = prevRec.vehicles || record.vehicles || [];
var updatedVehicles = baseVehicles.map(function (item, index) {
if (index !== vehicleIndex || isContractVehicleDelivered(item)) return item;
return Object.assign({}, item, {
plannedDeliveryDate: draftTbd ? null : newDate,
deliveryDateTbd: draftTbd,
deliveryTaskSuspended: draftTbd,
});
});
return Object.assign({}, prev, {
[record.id]: Object.assign({}, prevRec, {
vehicles: updatedVehicles,
}),
});
});
message.success(draftTbd ? '本车交车时间已标记为暂未确定(原型)' : '本车交车安排已更新(原型)');
closeVehicleDeliveryChangePopover();
}
function renderDeliveryChangePanel(record) {
var dayjs = window.dayjs;
var logs = record.deliveryDateChangeLogs || [];
@@ -1726,6 +1896,70 @@ const Component = function(props) {
);
}
function renderVehicleDeliveryChangePanel(record, vehicle, vehicleIndex) {
var dayjs = window.dayjs;
var draftDate = _vehicleDeliveryChangeDraftDate[0];
var draftTbd = _vehicleDeliveryChangeDraftTbd[0];
var draftValue = draftDate && dayjs && !draftTbd ? dayjs(draftDate) : null;
var plate = formatVehiclePlateLabel(vehicle.plateNo) || '待上牌';
return React.createElement('div', {
className: 'lc-delivery-change__panel',
role: 'dialog',
'aria-label': '修改本车交车时间',
},
React.createElement('div', { className: 'lc-delivery-change__header' },
React.createElement('div', { className: 'lc-delivery-change__title' }, '修改本车交车时间'),
React.createElement('div', { className: 'lc-delivery-change__subtitle tabular-nums' }, plate),
),
React.createElement('div', { className: 'lc-delivery-change__form' },
React.createElement('label', { className: 'lc-delivery-change__field' },
React.createElement('span', { className: 'lc-delivery-change__label' }, '计划交车日期'),
React.createElement(DatePicker, {
style: { width: '100%' },
value: draftValue,
disabled: draftTbd,
placeholder: draftTbd ? '暂未确定' : '请选择交车日期',
'aria-label': '计划交车日期',
onChange: function (_date, dateString) {
_vehicleDeliveryChangeDraftDate[1](dateString || null);
},
}),
),
React.createElement('label', { className: 'lc-delivery-change__tbd' },
React.createElement(Checkbox, {
checked: draftTbd,
onChange: function (event) {
var checked = event.target.checked;
_vehicleDeliveryChangeDraftTbd[1](checked);
if (checked) {
_vehicleDeliveryChangeDraftDate[1](null);
} else {
var vehicleTbd = resolveVehicleDeliveryDateTbd(vehicle, record);
_vehicleDeliveryChangeDraftDate[1](vehicleTbd
? null
: (resolveVehiclePlannedDeliveryDate(vehicle, record) || null));
}
},
}, '暂未确定'),
),
React.createElement('p', { className: 'lc-delivery-change__hint' },
'仅修改当前车辆;在项目信息中调整交车安排后,仍会同步至所有未交车车辆。',
),
React.createElement('div', { className: 'lc-delivery-change__actions' },
React.createElement(Button, {
size: 'small',
onClick: closeVehicleDeliveryChangePopover,
}, '取消'),
React.createElement(Button, {
size: 'small',
type: 'primary',
onClick: function () { confirmVehicleDeliveryDateChange(record, vehicleIndex); },
}, '确定'),
),
),
);
}
function renderDeliveryEditIcon() {
return React.createElement('svg', {
xmlns: 'http://www.w3.org/2000/svg',
@@ -1871,6 +2105,7 @@ const Component = function(props) {
closeDeliveryChangePopover();
return;
}
closeVehicleDeliveryChangePopover();
_vehiclePopoverRecord[1](null);
_deliveredPopoverRecord[1](null);
closeDelegatePopover();
@@ -1983,6 +2218,20 @@ const Component = function(props) {
return renderStackCell(record.updater, record.updateTime, 'code');
}
function renderOperationMoreIcon() {
return React.createElement('svg', {
viewBox: '0 0 16 16',
width: 16,
height: 16,
fill: 'currentColor',
'aria-hidden': true,
},
React.createElement('circle', { cx: 8, cy: 3, r: 1.5 }),
React.createElement('circle', { cx: 8, cy: 8, r: 1.5 }),
React.createElement('circle', { cx: 8, cy: 13, r: 1.5 }),
);
}
function getOperationButtons(record) {
var menuItems = getMoreMenuItems(record);
var moreDropdown = menuItems.length > 0
@@ -1990,21 +2239,29 @@ const Component = function(props) {
Dropdown,
{
menu: { items: menuItems },
trigger: ['click']
trigger: ['click'],
placement: 'bottomRight',
},
React.createElement(Button, { type: 'link', size: 'small' }, '更多')
React.createElement(Tooltip, { title: '更多' },
React.createElement('span', {
className: 'lc-action-more-btn',
role: 'button',
tabIndex: 0,
'aria-label': '更多操作',
onClick: function (e) { e.stopPropagation(); },
}, renderOperationMoreIcon()),
),
)
: null;
return React.createElement(
Space,
{ size: 4, wrap: true },
'div',
{ className: 'lc-row-actions' },
React.createElement(Button, { type: 'link', size: 'small', onClick: function() { openContractView(record); } }, '查看'),
moreDropdown
moreDropdown,
);
}
var columns = [
Table.EXPAND_COLUMN,
{
title: '项目信息',
key: 'projectInfo',
@@ -2132,9 +2389,6 @@ const Component = function(props) {
];
var listTableScrollX = columns.reduce(function (sum, col) {
if (col === Table.EXPAND_COLUMN) {
return sum + LC_LIST_TABLE_EXPAND_WIDTH;
}
return sum + (Number(col.width) || 0);
}, 0);
@@ -2358,6 +2612,8 @@ const Component = function(props) {
React.createElement(LeaseContractFleetSummary, {
records: fleetSourceList,
Modal: Modal,
renderVehicleNativeTableHeader: renderContractVehicleNativeTableHeader,
renderVehicleNativeTableRow: renderContractVehicleNativeTableRow,
onViewContract: function (record) {
message.info('查看合同(原型):' + (record.contractCode || record.projectName || ''));
},
@@ -2365,11 +2621,6 @@ const Component = function(props) {
React.createElement('section', { className: 'vm-table-section' },
React.createElement('div', { className: 'vm-table-toolbar', 'data-annotation-id': 'lc-list-toolbar' },
React.createElement('div', { className: 'vm-table-actions' },
React.createElement('button', {
type: 'button',
className: 'vm-btn vm-btn-ghost',
onClick: function() { message.info('租赁费用模板(原型)'); }
}, '租赁费用模板'),
React.createElement('button', {
type: 'button',
className: 'vm-btn vm-btn-ghost',
@@ -2412,8 +2663,10 @@ const Component = function(props) {
size: 'small',
pagination: false,
expandable: {
fixed: 'left',
columnWidth: LC_LIST_TABLE_EXPAND_WIDTH,
showExpandColumn: false,
expandIconColumnIndex: -1,
expandedRowKeys: _expandedRowKeys[0],
onExpandedRowsChange: function (keys) { _expandedRowKeys[1](keys); },
expandedRowRender: renderExpandedVehicleTable,
rowExpandable: function (record) {
return (record.vehicles || []).length > 0;

View File

@@ -0,0 +1,134 @@
/**
* 列表合同记录 → 新增/编辑页表单初始态(原型回填)
*/
import { DEFAULT_MILEAGE_STANDARD, DEFAULT_FEE_INFO } from '../contract-template-management/contract-template-vars.js';
import {
createDefaultLeaseOrderState,
createEmptyLeaseOrderRow,
createEmptyDelegateRow,
PLATE_ACTUAL_DELIVERY,
} from './lease-order-vars.js';
import {
inferContractTemplateId,
isDeliveryDateTbd,
SIGNING_COMPANY_LESSOR_IDS,
} from './lease-contract-list-data.js';
var LEASE_CUSTOMER_IDS_BY_NAME = {
'嘉兴某某物流有限公司': '1',
'上海某某运输公司': '2',
'杭州某某租赁有限公司': '3',
};
function extractContractCodeInput(code) {
if (!code) return '';
return String(code).replace(/^HT-ZL-/, '').replace(/^LNZLHT-?/i, '');
}
function mapVehicleToLeaseOrderRow(vehicle, index) {
var row = createEmptyLeaseOrderRow();
row.id = 'lo-bridge-' + index;
if (vehicle.brand && vehicle.model) {
row.brandModels = [[vehicle.brand, vehicle.model]];
}
var plate = vehicle.plateNo;
row.plateNos = plate && plate !== '-' ? [plate] : [PLATE_ACTUAL_DELIVERY];
row.rent = vehicle.rent != null ? vehicle.rent : 8800;
row.serviceFee = vehicle.serviceFee != null ? vehicle.serviceFee : 1200;
row.deposit = vehicle.deposit != null ? vehicle.deposit : 20000;
row.leasePeriodMonths = vehicle.leasePeriodMonths != null ? vehicle.leasePeriodMonths : 12;
return row;
}
function mapAuthorizedDelegatesToPoaRows(delegates) {
if (!delegates || !delegates.length) {
return [createEmptyDelegateRow()];
}
return delegates.map(function (person, index) {
return {
id: 'del-bridge-' + index,
name: person.name || '',
contact: person.contact || person.phone || '',
idNumber: person.idNumber || person.idCard || '',
};
});
}
function buildLeaseOrderFromRecord(record) {
var defaultOrder = createDefaultLeaseOrderState();
var vehicles = record.vehicles || [];
var rows = vehicles.length
? vehicles.map(mapVehicleToLeaseOrderRow)
: defaultOrder.rows.slice();
var deliveryDate = record.deliveryDate;
if (deliveryDate && isDeliveryDateTbd(deliveryDate)) {
deliveryDate = null;
}
return {
insuredVehicleCount: record.insuredVehicleCount != null ? record.insuredVehicleCount : rows.length || 1,
thirdPartyLiabilityMillion: record.thirdPartyLiabilityMillion != null
? record.thirdPartyLiabilityMillion
: defaultOrder.thirdPartyLiabilityMillion,
deliveryRegion: (record.deliveryRegion || defaultOrder.deliveryRegion).slice(),
deliveryDate: deliveryDate || null,
rows: rows,
};
}
function buildMileageFromRecord(record) {
return Object.assign({}, DEFAULT_MILEAGE_STANDARD, {
hasRequirement: Boolean(record.hasMinimumMileage),
period: record.mileagePeriod || DEFAULT_MILEAGE_STANDARD.period,
targetKm: record.mileageTargetKm != null
? record.mileageTargetKm
: DEFAULT_MILEAGE_STANDARD.targetKm,
reductionYuan: record.mileageReductionYuan != null
? record.mileageReductionYuan
: DEFAULT_MILEAGE_STANDARD.reductionYuan,
validUntil: record.mileageValidUntil || DEFAULT_MILEAGE_STANDARD.validUntil,
});
}
function buildFeeInfoFromRecord(record) {
var isPrepay = record.hydrogenPaymentMethod === 'prepay';
return Object.assign({}, DEFAULT_FEE_INFO, {
paymentPeriod: record.paymentPeriod != null ? record.paymentPeriod : DEFAULT_FEE_INFO.paymentPeriod,
hydrogenPaymentMethod: record.hydrogenPaymentMethod || DEFAULT_FEE_INFO.hydrogenPaymentMethod,
prepayAmount: record.prepayAmount != null ? record.prepayAmount : (isPrepay ? 50000 : null),
payAheadWorkdays: record.payAheadWorkdays != null ? record.payAheadWorkdays : DEFAULT_FEE_INFO.payAheadWorkdays,
returnHydrogenDiffUnitPrice: record.returnHydrogenDiffUnitPrice != null
? record.returnHydrogenDiffUnitPrice
: 8.5,
});
}
/** 列表行 → 编辑页表单初始值;授权委托书未维护时 delegates 为空行 */
export function buildLeaseContractEditFormState(record) {
if (!record) return null;
var lessorId = record.lessorId || SIGNING_COMPANY_LESSOR_IDS[record.signingCompany] || '';
var customerId = record.customerId || LEASE_CUSTOMER_IDS_BY_NAME[record.customerName] || '';
var poaMaintained = record.poaUploaded === true
&& record.authorizedDelegates
&& record.authorizedDelegates.length > 0;
return {
contractTemplateId: record.contractTemplateId || inferContractTemplateId(record) || '',
lessorId: lessorId,
customerId: customerId,
contractCode: extractContractCodeInput(record.contractCode),
businessDept: record.businessDept || '',
businessOwner: record.businessOwner || '',
mileage: buildMileageFromRecord(record),
feeInfo: buildFeeInfoFromRecord(record),
leaseOrder: buildLeaseOrderFromRecord(record),
powerOfAttorney: {
delegates: poaMaintained
? mapAuthorizedDelegatesToPoaRows(record.authorizedDelegates)
: [createEmptyDelegateRow()],
},
contractRemark: record.poaRemark || '',
sealTypes: Array.isArray(record.sealTypes) && record.sealTypes.length
? record.sealTypes.slice()
: ['contract'],
};
}

View File

@@ -375,25 +375,32 @@ var SAMPLE_AUTHORIZED_DELEGATES = [
{ name: '周磊', contact: '13900139005', idNumber: '330102199005055678' },
];
export function buildAuthorizedDelegatesSample(count) {
var total = Math.max(0, Number(count) || 0);
var list = [];
for (var i = 0; i < total; i++) {
list.push(Object.assign({}, SAMPLE_AUTHORIZED_DELEGATES[i % SAMPLE_AUTHORIZED_DELEGATES.length]));
}
return list;
}
export function getAuthorizedDelegates(record) {
if (record.authorizedDelegates && record.authorizedDelegates.length) {
return record.authorizedDelegates;
}
var count = record.delegateCount != null ? record.delegateCount : 0;
if (count <= 0) return [];
var list = [];
for (var i = 0; i < count; i++) {
list.push(SAMPLE_AUTHORIZED_DELEGATES[i % SAMPLE_AUTHORIZED_DELEGATES.length]);
if (record.poaUploaded === true && (record.delegateCount || 0) > 0) {
return buildAuthorizedDelegatesSample(record.delegateCount);
}
return list;
return [];
}
/** 列表「受托人」列是否需提示补充授权委托书与受托人信息 */
export function needsContractDelegateSupplement(record) {
if (!record) return false;
if (record.delegateSupplementRequired === false) return false;
if (record.poaUploaded === true && (record.delegateCount || 0) > 0) return false;
if ((record.delegateCount || 0) > 0 && getAuthorizedDelegates(record).length > 0) return false;
if (record.poaUploaded === true && (record.delegateCount || 0) > 0 && getAuthorizedDelegates(record).length > 0) {
return false;
}
return true;
}
@@ -507,6 +514,7 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
mileagePeriod: 'month',
mileageTargetKm: 6000,
delegateCount: 0,
poaUploaded: false,
contractEndDate: '2026-02-16',
contactName: '张三',
contactPhone: '13800138001',
@@ -579,6 +587,12 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
mileagePeriod: 'quarter',
mileageTargetKm: 18000,
delegateCount: 2,
poaUploaded: true,
authorizedDelegates: [
{ name: '王明', contact: '13900139001', idNumber: '330102199001011234' },
{ name: '李华', contact: '13900139002', idNumber: '330102199002022345' },
],
poaRemark: '授权委托书已上传,两名受托人信息与现场核验一致。',
contractEndDate: '2026-06-30',
contactName: '王五',
contactPhone: '13800138003',
@@ -619,6 +633,11 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
mileagePeriod: 'month',
mileageTargetKm: 8000,
delegateCount: 1,
poaUploaded: true,
authorizedDelegates: [
{ name: '孙丽华', contact: '13900139011', idNumber: '330102198803033210' },
],
poaRemark: '授权委托书已归档,受托人孙丽华为客户指定接车人。',
contractEndDate: '2026-03-01',
contactName: '赵六',
contactPhone: '13900139001',
@@ -703,6 +722,13 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
mileagePeriod: 'year',
mileageTargetKm: 120000,
delegateCount: 3,
poaUploaded: true,
authorizedDelegates: [
{ name: '王明', contact: '13900139001', idNumber: '330102199001011234' },
{ name: '李华', contact: '13900139002', idNumber: '330102199002022345' },
{ name: '赵强', contact: '13900139003', idNumber: '330102199003033456' },
],
poaRemark: '三车批量合同授权委托书已上传,受托人名单与盖章扫描件一致。',
contractEndDate: '2026-12-31',
contactName: '周八',
contactPhone: '13900139003',
@@ -745,6 +771,11 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
mileagePeriod: 'month',
mileageTargetKm: null,
delegateCount: 1,
poaUploaded: true,
authorizedDelegates: [
{ name: '吴九', contact: '13900139004', idNumber: '320211199005051122' },
],
poaRemark: '试用合同授权委托书已维护,受托人吴九负责无锡区域接车。',
contractEndDate: '2025-07-10',
contactName: '吴九',
contactPhone: '13900139004',
@@ -869,6 +900,11 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
mileagePeriod: 'month',
mileageTargetKm: null,
delegateCount: 1,
poaUploaded: true,
authorizedDelegates: [
{ name: '张三', contact: '13800138001', idNumber: '330102198501011234' },
],
poaRemark: '历史合同授权委托书已补录归档。',
contractEndDate: '2025-01-15',
contactName: '张三',
contactPhone: '13800138001',
@@ -944,6 +980,11 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
mileageTargetKm: 8000,
delegateCount: 2,
poaUploaded: true,
authorizedDelegates: [
{ name: '陈静', contact: '13900139004', idNumber: '330102199004044567' },
{ name: '周磊', contact: '13900139005', idNumber: '330102199005055678' },
],
poaRemark: '在租车队合同授权委托书已维护,两名受托人负责七车接还对接。',
contractEndDate: '2027-01-31',
contactName: '张三',
contactPhone: '13800138001',
@@ -1018,6 +1059,36 @@ export function formatContractDeliveryDateLabel(record) {
return record.deliveryDate || '-';
}
export function resolveVehicleDeliveryDateTbd(vehicle, record) {
if (!vehicle || isContractVehicleDelivered(vehicle)) return false;
if (vehicle.deliveryDateTbd != null) return Boolean(vehicle.deliveryDateTbd);
if (!record) return false;
return Boolean(record.deliveryDateTbd || isDeliveryDateTbd(record.deliveryDate));
}
export function resolveVehiclePlannedDeliveryDate(vehicle, record) {
if (!vehicle) return null;
if (isContractVehicleDelivered(vehicle)) {
return vehicle.deliveryTime || vehicle.actualDelivery || null;
}
if (vehicle.plannedDeliveryDate != null && String(vehicle.plannedDeliveryDate).trim()) {
return vehicle.plannedDeliveryDate;
}
return record ? record.deliveryDate || null : null;
}
export function formatVehicleDeliveryPlanDateLabel(vehicle, record) {
if (!vehicle) return '-';
if (isContractVehicleDelivered(vehicle)) {
var deliveredAt = formatContractHandoverDateTimeMinute(vehicle.deliveryTime || vehicle.actualDelivery);
return deliveredAt && deliveredAt !== '-' ? deliveredAt : '-';
}
if (resolveVehicleDeliveryDateTbd(vehicle, record)) return DELIVERY_DATE_TBD_LABEL;
var date = resolveVehiclePlannedDeliveryDate(vehicle, record);
if (!date || isDeliveryDateTbd(date)) return DELIVERY_DATE_TBD_LABEL;
return date;
}
function addMonthsToDate(date, months) {
var next = new Date(date.getTime());
next.setMonth(next.getMonth() + months);
@@ -1271,6 +1342,9 @@ export function enrichLeaseContractVehicle(vehicle, index, recordIndex, record)
plannedDeliveryDate: vehicle.plannedDeliveryDate != null
? vehicle.plannedDeliveryDate
: (delivered ? null : (record && record.deliveryDate) || null),
deliveryDateTbd: vehicle.deliveryDateTbd != null
? vehicle.deliveryDateTbd
: (delivered ? false : resolveVehicleDeliveryDateTbd(vehicle, record)),
dailyAvgMileage7d: dailyAvgMileage7d,
});
if (delivered && hasMinMileage && record) {

View File

@@ -40,6 +40,34 @@
margin-bottom: 0;
}
.vm-page.lc-page .lc-row-actions {
display: inline-flex;
align-items: center;
gap: 4px;
}
.vm-page.lc-page .lc-action-more-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 8px;
color: var(--ln-muted, #64748b);
cursor: pointer;
transition: background-color 0.15s ease, color 0.15s ease;
}
.vm-page.lc-page .lc-action-more-btn:hover {
background: #f1f5f9;
color: var(--ln-body, #334155);
}
.vm-page.lc-page .lc-action-more-btn:focus-visible {
outline: 2px solid var(--ln-primary, #32a06e);
outline-offset: 2px;
}
/* 列表区Ant Table 嵌入 vm-table-card */
.vm-page.lc-page .vm-table-card .vm-list-table.ant-table-wrapper,
.vm-page.lc-page .vm-table-card .vm-list-table .ant-table-wrapper {
@@ -74,7 +102,7 @@
max-width: 100%;
}
/* 左侧冻结:展开列 + 项目信息(背景与层级,不覆盖 antd 的 sticky 偏移 */
/* 左侧冻结:项目信息列(含展开按钮 */
.vm-page.lc-page .lc-list-table--nested .ant-table-cell-fix-start,
.vm-page.lc-page .lc-list-table--nested .ant-table-cell-fix-left {
background: var(--ln-surface-card);
@@ -97,15 +125,6 @@
background: var(--ln-canvas-soft) !important;
}
.vm-page.lc-page .lc-list-table--nested .ant-table-row-expand-icon-cell.ant-table-cell-fix-start {
z-index: 4;
}
.vm-page.lc-page .lc-list-table--nested .ant-table-thead > tr > th.ant-table-row-expand-icon-cell {
z-index: 4;
background: var(--ln-canvas-soft) !important;
}
.vm-page.lc-page .lc-list-table--nested .lc-project-info-col.ant-table-cell-fix-start,
.vm-page.lc-page .lc-list-table--nested .lc-project-info-col.ant-table-cell-fix-left {
z-index: 3;
@@ -121,6 +140,73 @@
box-shadow: inset 10px 0 8px -8px rgba(15, 23, 42, 0.12);
}
/* 右侧冻结:操作列 + 滚动条占位缝(避免横向滚动时右侧漏出其他列) */
.vm-page.lc-page .lc-list-table--nested .ant-table-cell-fix-end:not(.ant-table-cell-scrollbar),
.vm-page.lc-page .lc-list-table--nested .ant-table-cell-fix-right:not(.ant-table-cell-scrollbar) {
background: var(--ln-surface-card) !important;
}
.vm-page.lc-page .lc-list-table--nested .ant-table-thead > tr > th.ant-table-cell-fix-end:not(.ant-table-cell-scrollbar),
.vm-page.lc-page .lc-list-table--nested .ant-table-thead > tr > th.ant-table-cell-fix-right:not(.ant-table-cell-scrollbar) {
z-index: 4;
background: var(--ln-canvas-soft) !important;
}
.vm-page.lc-page .lc-list-table--nested .ant-table-tbody > tr:not(.ant-table-expanded-row) > td.ant-table-cell-fix-end:not(.ant-table-cell-scrollbar),
.vm-page.lc-page .lc-list-table--nested .ant-table-tbody > tr:not(.ant-table-expanded-row) > td.ant-table-cell-fix-right:not(.ant-table-cell-scrollbar) {
z-index: 3;
background: var(--ln-surface-card) !important;
}
.vm-page.lc-page .lc-list-table--nested .ant-table-tbody > tr:hover:not(.ant-table-expanded-row) > td.ant-table-cell-fix-end:not(.ant-table-cell-scrollbar),
.vm-page.lc-page .lc-list-table--nested .ant-table-tbody > tr:hover:not(.ant-table-expanded-row) > td.ant-table-cell-fix-right:not(.ant-table-cell-scrollbar) {
background: var(--ln-canvas-soft) !important;
}
.vm-page.lc-page .lc-list-table--nested .ant-table-cell-fix-end-shadow-show:not(.ant-table-cell-scrollbar)::after,
.vm-page.lc-page .lc-list-table--nested .ant-table-cell-fix-right-last::after {
box-shadow: inset -10px 0 8px -8px rgba(15, 23, 42, 0.12);
}
/* 盖住纵向滚动条占位缝,防止表头/表体滚动列从操作列右侧露出 */
.vm-page.lc-page .lc-list-table--nested .ant-table-thead > tr > th.ant-table-cell-fix-end:not(.ant-table-cell-scrollbar)::before,
.vm-page.lc-page .lc-list-table--nested .ant-table-tbody > tr > td.ant-table-cell-fix-end:not(.ant-table-cell-scrollbar)::before {
content: '';
position: absolute;
top: 0;
bottom: 0;
right: -17px;
width: 17px;
background: inherit;
pointer-events: none;
}
.vm-page.lc-page .lc-list-table--nested .ant-table-cell-scrollbar {
width: 0 !important;
min-width: 0 !important;
max-width: 0 !important;
padding: 0 !important;
border: none !important;
visibility: hidden;
}
.vm-page.lc-page .lc-list-table--nested .ant-table-thead > tr > th:not(.ant-table-cell-fix-start):not(.ant-table-cell-fix-end),
.vm-page.lc-page .lc-list-table--nested .ant-table-tbody > tr:not(.ant-table-expanded-row) > td:not(.ant-table-cell-fix-start):not(.ant-table-cell-fix-end) {
overflow: hidden;
}
.vm-page.lc-page .lc-list-table--nested .ant-table-container {
overflow: hidden;
}
.vm-page.lc-page .lc-list-table--nested .ant-table-expanded-row > td {
overflow: hidden;
}
.vm-page.lc-page .lc-list-table--nested .ant-table-expanded-row .lc-contract-vehicle-table-wrap {
max-width: 100%;
}
@media (max-width: 1280px) {
.vm-page.lc-page .vm-kpi-row {
grid-template-columns: repeat(3, minmax(0, 1fr));
@@ -1129,23 +1215,31 @@
}
}
/* 列表嵌套:展开列宽度与冻结 */
.vm-page.lc-page .lc-list-table--nested .ant-table-thead > tr > th.ant-table-row-expand-icon-cell,
.vm-page.lc-page .lc-list-table--nested .ant-table-tbody > tr > td.ant-table-row-expand-icon-cell,
.vm-page.lc-page .lc-list-table--nested col.ant-table-expand-icon-col {
width: 48px !important;
min-width: 48px !important;
max-width: 48px !important;
padding-left: 12px !important;
padding-right: 8px !important;
box-sizing: border-box;
/* 列表嵌套:项目信息列内嵌展开按钮(单列合并,左冻结 */
.vm-page.lc-page .lc-list-table--nested .lc-project-info-merged-cell {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
min-width: 0;
}
.vm-page.lc-page .lc-list-table--nested .ant-table-tbody > tr:hover > td.ant-table-row-expand-icon-cell.ant-table-cell-fix-start {
.vm-page.lc-page .lc-list-table--nested .lc-project-info-merged-cell .ant-table-row-expand-icon {
flex-shrink: 0;
margin: 0;
cursor: pointer;
}
.vm-page.lc-page .lc-list-table--nested .lc-project-info-merged-cell .lc-project-info-cell {
flex: 1;
min-width: 0;
}
.vm-page.lc-page .lc-list-table--nested .ant-table-tbody > tr:hover > td.lc-project-info-col.ant-table-cell-fix-start {
background: var(--ln-canvas-soft) !important;
}
.vm-page.lc-page .lc-list-table--nested .ant-table-expanded-row > td.ant-table-row-expand-icon-cell.ant-table-cell-fix-start {
.vm-page.lc-page .lc-list-table--nested .ant-table-expanded-row > td.lc-project-info-col.ant-table-cell-fix-start {
background: var(--ln-canvas-soft, #f8fafc);
z-index: 4;
}
@@ -1217,8 +1311,12 @@
width: 9%;
}
.vm-page.lc-page .lc-contract-vehicle-native-table__delivery-plan {
width: 11%;
}
.vm-page.lc-page .lc-contract-vehicle-native-table__mileage-req {
width: 18%;
width: 16%;
}
.vm-page.lc-page .lc-contract-vehicle-native-table__mileage-completion {
@@ -1235,6 +1333,7 @@
.vm-page.lc-page .lc-contract-vehicle-native-table tbody td > .lc-contract-pickup-payment,
.vm-page.lc-page .lc-contract-vehicle-native-table tbody td > .lc-contract-return-settlement,
.vm-page.lc-page .lc-contract-vehicle-native-table tbody td > .lc-contract-handover-situation,
.vm-page.lc-page .lc-contract-vehicle-native-table tbody td > .lc-delivery-cell,
.vm-page.lc-page .lc-contract-vehicle-native-table tbody td > .lc-contract-mileage-req-row,
.vm-page.lc-page .lc-contract-vehicle-native-table tbody td > .lc-contract-mileage-completion,
.vm-page.lc-page .lc-contract-vehicle-native-table tbody td > .lc-contract-vehicle-action-empty,
@@ -1270,6 +1369,32 @@
text-align: left;
}
.vm-page.lc-page .lc-contract-vehicle-native-table .lc-delivery-cell {
min-width: 0;
gap: 2px;
}
.vm-page.lc-page .lc-contract-vehicle-native-table .lc-delivery-cell__region {
font-size: 11px;
line-height: 1.3;
}
.vm-page.lc-page .lc-contract-vehicle-native-table .lc-delivery-cell__date {
font-size: 10px;
}
.vm-page.lc-page .lc-contract-vehicle-native-table .lc-delivery-cell__edit-btn {
width: 22px;
height: 22px;
}
.vm-page.lc-page .lc-delivery-change__subtitle {
margin-top: 2px;
font-size: 12px;
color: var(--ln-muted, #64748b);
line-height: 1.3;
}
/* 子表里程要求列:左右两栏(里程要求 | 当前里程) */
.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-mileage-req-row--dual {
flex-direction: row;
@@ -1321,18 +1446,14 @@
white-space: normal;
}
.vm-page.lc-page .lc-contract-vehicle-native-table .lc-vehicle-mileage-progress {
gap: 6px;
.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-mileage-completion__bar-row .lc-vehicle-mileage-progress__track {
min-width: 72px;
height: 8px;
}
.vm-page.lc-page .lc-contract-vehicle-native-table .lc-vehicle-mileage-progress__track {
min-width: 40px;
height: 6px;
}
.vm-page.lc-page .lc-contract-vehicle-native-table .lc-vehicle-mileage-progress__label {
font-size: 11px;
min-width: 30px;
.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-mileage-completion__bar-row .lc-vehicle-mileage-progress__label {
font-size: 12px;
min-width: 36px;
}
.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-handover-situation__line {
@@ -1623,26 +1744,34 @@
flex-shrink: 0;
}
.vm-page.lc-page .lc-contract-mileage-completion .lc-vehicle-mileage-progress {
width: 100%;
min-width: 0;
}
.vm-page.lc-page .lc-contract-mileage-completion__progress-row {
.vm-page.lc-page .lc-contract-mileage-completion__bar-row {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
width: 100%;
}
.vm-page.lc-page .lc-contract-mileage-completion__progress-main {
flex: 1 1 auto;
.vm-page.lc-page .lc-contract-mileage-completion__bar-row .lc-vehicle-mileage-progress__track {
flex: 1;
min-width: 72px;
height: 8px;
}
.vm-page.lc-page .lc-contract-mileage-completion__bar-row .lc-vehicle-mileage-progress__label {
flex-shrink: 0;
font-size: 12px;
min-width: 36px;
text-align: right;
}
.vm-page.lc-page .lc-contract-mileage-completion__range-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 8px;
min-width: 0;
max-width: 68%;
}
.vm-page.lc-page .lc-contract-mileage-completion__progress-main .lc-vehicle-mileage-progress__track {
min-width: 32px;
width: 100%;
}
.vm-page.lc-page .lc-contract-mileage-completion__range-side {
@@ -1655,13 +1784,13 @@
.vm-page.lc-page .lc-contract-mileage-completion__range-side--start {
align-items: flex-start;
text-align: left;
margin-top: 2px;
}
.vm-page.lc-page .lc-contract-mileage-completion__range-side--end {
flex: 0 0 auto;
align-items: flex-end;
text-align: right;
margin-left: auto;
}
.vm-page.lc-page .lc-contract-mileage-completion__range-date {
@@ -1677,18 +1806,6 @@
line-height: 1.25;
}
.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-mileage-completion__progress-main {
max-width: 72%;
}
.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-mileage-completion__progress-main .lc-vehicle-mileage-progress__track {
min-width: 36px;
}
.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-mileage-completion__range-side--end {
max-width: 28%;
}
.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-mileage-completion__range-date {
font-size: 10px;
}
@@ -2204,169 +2321,19 @@
overflow: auto;
}
.lc-fleet-detail-table-wrap {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.lc-fleet-detail-table {
width: 100%;
min-width: 880px;
border-collapse: collapse;
table-layout: fixed;
font-size: 0.8125rem;
}
.lc-fleet-detail-table thead th {
position: sticky;
top: 0;
z-index: 1;
padding: 10px 12px;
background: #f6f7f7;
color: #71717a;
font-size: 0.75rem;
font-weight: 600;
text-align: left;
white-space: nowrap;
border-bottom: 1px solid #e5e7eb;
}
.lc-fleet-detail-table tbody td {
padding: 11px 12px;
vertical-align: middle;
border-bottom: 1px solid #f4f4f5;
color: #52525b;
}
.lc-fleet-detail-table tbody tr:hover td {
background: rgba(50, 160, 110, 0.04);
}
.lc-fleet-detail-table tbody tr:last-child td {
border-bottom: none;
}
.lc-fleet-detail-table__col-plate { width: 88px; }
.lc-fleet-detail-table__col-vin { width: 148px; }
.lc-fleet-detail-table__col-type { width: 120px; }
.lc-fleet-detail-table__col-announce { width: 168px; }
.lc-fleet-detail-table__col-city { width: 96px; }
.lc-fleet-detail-table__col-mileage { width: 96px; }
.lc-fleet-detail-table__col-project { width: 180px; }
.lc-fleet-detail-table__col-action { width: 88px; }
.lc-fleet-detail-table__plate {
display: block;
font-size: 0.875rem;
font-weight: 700;
color: #32a06e;
line-height: 1.3;
white-space: nowrap;
}
.lc-fleet-detail-table__vin {
display: block;
font-size: 0.75rem;
line-height: 1.35;
color: #52525b;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.lc-fleet-detail-table__city {
display: block;
white-space: nowrap;
}
.lc-fleet-detail-table__type-pill {
display: inline-flex;
align-items: center;
max-width: 100%;
padding: 2px 8px;
border-radius: 999px;
background: #f4f4f5;
color: #71717a;
font-size: 0.6875rem;
line-height: 1.4;
white-space: normal;
word-break: break-word;
}
.lc-fleet-detail-table__announce {
display: block;
color: #52525b;
line-height: 1.4;
font-size: 0.75rem;
word-break: break-all;
}
.lc-fleet-detail-table__mileage {
display: inline-block;
font-weight: 600;
color: #18181b;
white-space: nowrap;
}
.lc-fleet-detail-table__project {
display: flex;
flex-direction: column;
gap: 3px;
min-width: 0;
}
.lc-fleet-detail-table__project-name {
font-weight: 600;
color: #18181b;
font-size: 0.8125rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.lc-fleet-detail-table__contract-link {
.lc-fleet-detail-modal .lc-fleet-detail-modal__table-host.vm-page.lc-page {
padding: 0;
border: none;
background: none;
color: #32a06e;
font-size: 0.75rem;
font-weight: 600;
text-align: left;
cursor: pointer;
white-space: nowrap;
min-height: 0;
background: transparent;
}
.lc-fleet-detail-table__contract-link:hover {
color: #3fb87c;
text-decoration: underline;
text-underline-offset: 2px;
.lc-fleet-detail-modal .lc-fleet-detail-modal__table-host .lc-contract-vehicle-table-wrap {
width: 100%;
max-width: 100%;
}
.lc-fleet-detail-table__customer {
font-size: 0.75rem;
color: #71717a;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.lc-fleet-detail-table__action {
min-height: 32px;
padding: 0 10px;
border: 1px solid rgba(50, 160, 110, 0.24);
border-radius: 8px;
background: rgba(50, 160, 110, 0.1);
color: #2b9260;
font-size: 0.75rem;
font-weight: 600;
cursor: pointer;
white-space: nowrap;
transition: background 0.15s ease, border-color 0.15s ease;
}
.lc-fleet-detail-table__action:hover {
background: rgba(50, 160, 110, 0.16);
border-color: #32a06e;
.lc-fleet-detail-modal .lc-fleet-detail-modal__vehicle-table {
min-width: 880px;
}
.lc-fleet-detail-modal__empty {
@@ -2401,10 +2368,6 @@
}
@media (prefers-reduced-motion: reduce) {
.lc-fleet-detail-table__action {
transition: none;
}
.vm-page.lc-page .lc-fleet-summary__card,
.vm-page.lc-page .lc-fleet-summary__card-bar-fill,
.vm-page.lc-page .lc-fleet-summary__toggle,

View File

@@ -0,0 +1,35 @@
# Prototype Review
- 审查目标:`src/prototypes/oneos-web-h2-station-site`
- 用户资料/参考资料:
- `src/prototypes/oneos-web-h2-station-site/.spec/requirements.md`
- `src/resources/oneos-web-legacy/加氢站管理/站点管理使用说明书.md`
- 页面实现:`src/prototypes/oneos-web-h2-station/pages/03-站点信息.jsx`
- 氢费共享 Store`src/common/h2VehicleLedgerBridge.js`
- 生成时间2026-07-01复审
## 总体点评
独立「站点信息」原型已完成 Review 项修复KPI 恢复高频/低频/无加氢筛选,补齐标注与 `.spec` 决策,增加角色切换演示,氢费明细统一对接「车辆氢费明细」共享 Store。可作为培训与高保真业务演示使用。
## P0-P3 优先级问题(复审状态)
| 级别 | 问题 | 状态 |
|------|------|------|
| P1 | KPI 与说明书不一致 | **已修复**KPI 为全部/高频/低频/无加氢 |
| P2 | 缺 annotation / `.spec` | **已修复**`annotation-source.json``.spec/requirements.md` |
| P2 | 角色权限无 UI 演示 | **已修复**:右上角角色切换 |
| P3 | 双入口未文档化 | **已修复**:标注与 requirements 中说明 |
| P3 | 氢费 Mock 未联动车辆氢费明细 | **已修复**`h2VehicleLedgerBridge.js` 统一数据源 |
## 验收建议
1. 打开「站点信息」,点击 KPI「高频/低频/无加氢」,列表应正确筛选。
2. 切换「加氢站账号」,编辑站点时系统账号字段应为只读。
3. 打开「车辆氢费明细」后再看站点加氢下钻/对账,数据应与台账一致(同 Store
4. 侧边栏标注目录可查看入口、KPI、对账联动说明。
## 剩余风险(低)
- 余额预警/欠费站点仍可通过「全站余额提醒设置」与行内余额下钻查看,但未再占用 KPI 主卡位;若产品需 KPI 同时展示余额类指标,需另行对齐口径。
- 车辆氢费明细未打开时,加氢站模块使用 canonical seed10 条),与台账页全量 mock 行数可能不同,属原型预期行为。

View File

@@ -0,0 +1,104 @@
# 加氢站管理 · 站点信息 — 列表页 PRD
| 项 | 说明 |
|---|---|
| 模块名称 | 加氢站管理 → 站点信息 |
| 所属系统 | ONE-OS Web 端 |
| 目标用户 | 加氢站运营、财务结算、平台管理员 |
| 核心任务 | 查询站点台账、维护主数据、查看运营指标、发起对账与充值 |
| 交互原型 | `/prototypes/oneos-web-h2-station-site` |
| 设计规范 | 与租赁合同管理一致的 vm 布局(筛选 / KPI / 表格 / 分页) |
---
## 1. 页面结构
用户进入「加氢站管理 → 站点信息」后,列表页自上而下为:
1. **筛选区** — 名称、签约、地区、营业状态
2. **KPI 分类** — 全部 / 签约·普通 / 高频 / 低频 / 无加氢
3. **工具栏** — 全站余额提醒、批量导入、发起充值单、新建站点
4. **站点列表** — 主数据 + 运营指标 + 操作列
5. **分页栏** — 每页 5/10/20/50 条
---
## 2. 筛选区
与租赁合同等模块一致的 **vm 筛选卡片** 布局:标题「筛选条件」+ 4 列栅格(标签在上、控件在下);底部分隔线 + 右对齐「重置」「查询」。
| 字段 | 控件 | 说明 |
|---|---|---|
| 加氢站名称 | 输入框 | 模糊匹配,支持回车查询 |
| 是否签约 | 下拉 | 签约站点 / 普通站点 |
| 地区(省-市) | 级联 | 省、市两级,可只选省 |
| 营业状态 | 下拉 | 营业中 / 暂停营业 / 停止营业 |
**交互**
- **查询**:写入「已应用筛选」,列表与 KPI 统计联动,回到第 1 页
- **重置**:清空筛选条件并刷新列表
---
## 3. KPI 统计卡片
与租赁合同一致的 **vm-kpi-card**:左侧图标 + 标题 + 数值,右上角 `?` 悬停说明,选中态绿色描边。
| 卡片 | 口径 |
|---|---|
| 全部加氢站 | 列表全部站点 |
| 签约站点 | 已签约,点击快捷筛选 |
| 普通站点 | 未签约,点击快捷筛选 |
| 高频加氢站 | 该站加氢次数 ≥ 3 |
| 低频加氢站 | 有加氢但次数 < 3 |
| 无加氢站点 | 加氢次数 = 0 |
加氢次数来自「车辆氢费明细」共享 Store按站点名称聚合。KPI 与筛选区签约条件互斥切换(点 KPI 分类会清除签约筛选,点签约卡会重置 KPI 为全部)。
---
## 4. 列表工具栏
| 按钮 | 行为 |
|---|---|
| 全站余额提醒设置 | 配置全局预付余额预警阈值 |
| 批量导入 | 下载模板 → 上传 CSV/Excel → 预览确认导入 |
| 发起充值单 | 为站点发起预付充值流程(原型演示) |
| 新建站点 | 进入同页内嵌新建视图 |
---
## 5. 列表字段说明
| 字段 | 说明 |
|---|---|
| 加氢站名称 | 含签约标签;下一行展示省市区 + 详细地址 |
| 合作时间 | 未签约可空 |
| 营业状态 / 营业时间 | 列表只读;在「更多 → 营业状态」维护 |
| 当前成本价格 | 元/kg受价格配置生效时间驱动多价时展示警示 |
| 加氢次数 / 加氢量 | 加氢量可点击下钻;表头支持排序 |
| 预付余额 | 可点击下钻流水;负值标红并显示「已欠费」 |
| 联系方式 | 联系人 + 电话 |
| 操作 | 查看 + 更多菜单 |
**更多菜单**:编辑、删除、营业状态、价格配置、生成对账单、查看对账记录等(按状态与权限显隐)。
**分页**:默认 10 条,可选 5/10/20/50。
---
## 6. 数据来源
- 氢费/加氢明细:统一来自 `src/common/h2VehicleLedgerBridge.js`,与「车辆氢费明细」共享 Store
- 先打开「车辆氢费明细」时台账全量同步;仅打开加氢站模块时使用 canonical seed
- 对账单提交经 `H2_STATION_STATEMENT_LEDGER_UPDATES` 回写车辆氢费明细
---
## 7. 双入口说明
| 入口 | 路径 |
|---|---|
| 独立原型(本页) | OneOS → 加氢站管理 → 站点信息 |
| 合包 | ONE-OS Web 端 → 加氢站管理 → 加氢订单 / 加氢记录 |

View File

@@ -0,0 +1,97 @@
# 加氢站管理 · 站点信息 — 对账与结算 PRD
> 本模块与「车辆氢费明细」配合完成**站点侧氢费结算闭环**。
---
## 1. 数据来源
对账单明细取自 **车辆氢费明细** 中同时满足以下条件的记录:
1. 对账状态 = **已对账**(台账已完成「完成对账」)
2. **尚未**被本站点历史对账单结算过(未绑定 `statementRecordId`
3. 加氢时间落在用户选择的 **[账单开始日期, 账单结束日期]** 内
4. 加氢站名称与当前操作站点一致
> 对账单明细**仅展示成本字段**(加氢量、成本单价、成本总价等),不展示加氢单价、加氢总价。
---
## 2. 两阶段交互
**阶段一 · 选期生成**
- 日期区下方展示:**上次对账单结束时间**(无历史则「暂无」)
- 默认开始日期建议 = 上次结束日 + 1 天
- 点击「生成对账单」后进入阶段二,日期锁定不可改
- 无符合条件记录时提示调整日期
**阶段二 · 结算提交**
统计卡(明细表上方):
| 指标 | 说明 |
|---|---|
| 加氢次数 | 本账单包含的已对账记录笔数 |
| 加氢总量 | kg 合计 |
| 成本总金额 | 成本总价合计 |
明细表下方**必填**结算项:
| 字段 | 规则 |
|---|---|
| 结算后加氢站预付款余额 | **只读**= 当前预付余额 成本总金额;不足为负并标「已欠费」 |
| 收票日期 | 必填 |
| 收票金额 | 必填,默认填入成本总金额 |
| 发票附件 | 必填,支持上传 |
---
## 3. 提交后的系统行为
提交成功后需**原子性**完成:
1. 写入对账历史(供「查看对账记录」查询)
2. 扣减站点预付余额
3. 标记台账记录已结算(绑定 `statementRecordId`
4. **回写车辆氢费明细**
| 车辆氢费明细字段 | 回写值 |
|---|---|
| 对账日期 | 操作完成时间 |
| 收票日期 | 操作完成日期 |
| 加氢站付款状态 | 已付款 |
原型通过 `H2_STATION_STATEMENT_LEDGER_UPDATES` / `h2VehicleLedgerBridge` 模拟跨页回写。
---
## 4. 查看对账记录
入口:列表操作列 → 更多 → **查看对账记录**
历史列表:对账日期、对账人、账单区间、加氢次数、加氢金额、结算后余额、查看明细。
查看明细:账单区间、统计三卡、收票信息、发票附件、成本明细表。
---
## 5. 校验与异常
| 场景 | 处理 |
|---|---|
| 日期未填 / 开始晚于结束 | 阻止生成 |
| 区间内无已对账未结算记录 | 阻止生成,提示调整日期 |
| 收票日期/金额/附件未填 | 阻止提交 |
| 收票金额 ≤ 0 | 阻止提交 |
| 预付余额结算后为负 | 允许提交,标「已欠费」 |
---
## 6. 研发要点
1. **对账单幂等**:同一笔已对账记录只能进入一次成功提交的对账单
2. **上次结束时间**:取该站点已成功提交对账单的最大 `endDate`
3. **余额计算**`balanceAfter = prepaidBalance - sum(costTotal)`
4. **跨模块同步**:生产对接统一台账;原型使用 Bridge Store
5. **明细字段范围**:对账弹窗不展示客户侧加氢单价/总价

View File

@@ -0,0 +1,163 @@
# 加氢站管理 · 站点信息 — 产品需求文档PRD
| 项 | 内容 |
|---|---|
| 文档版本 | v1.0 |
| 模块名称 | 加氢站管理 → 站点信息 |
| 所属系统 | ONE-OS Web 端 |
| 目标读者 | 前端 / 后端 / 测试 / 业务产品对接人 |
| 交互原型 | `/prototypes/oneos-web-h2-station-site` |
| 关联原型 | [车辆氢费明细](/prototypes/vehicle-h2-fee-ledger)、[加氢站合包](/prototypes/oneos-web-h2-station) |
| 文档状态 | 已对齐原型 |
---
## 1. 背景与目标
### 1.1 业务背景
加氢站是氢费成本结算的关键节点。运营侧需要维护站点主数据(签约、营业、成本价、预付余额),并在财务周期内按站点与加氢站完成对账、收票与付款闭环。
### 1.2 本期目标
- 站点台账的查询、新建、编辑、查看、删除与批量导入
- 营业状态、成本价格、预付余额等运营配置
- **按站点生成氢费对账单 → 填写收票信息 → 提交结算**,并与「车辆氢费明细」联动回写
### 1.3 用户角色
| 角色 | 典型诉求 |
|---|---|
| 加氢站运营 | 维护站点资料、营业与价格;查看加氢量与余额 |
| 财务/结算 | 按周期生成对账单、登记收票、完成站点付款闭环 |
| 管理员 | 批量导入站点、删除无效站点、配置系统账号 |
### 1.4 非目标(本期不做)
- 对账单审批流、ERP 自动过账
- 发票 OCR 识别与验真
- 加氢订单、加氢记录(仍在合包 `oneos-web-h2-station`
- 后端接口联调(原型使用 Mock + 共享 Store
---
## 2. 信息架构
```text
站点信息
├── 列表页
│ ├── 筛选区4 项)
│ ├── KPI 卡片6 张)
│ ├── 工具栏
│ └── 站点台账表 + 分页
├── 新建 / 编辑(同页内嵌视图)
│ ├── 基本信息
│ ├── 系统账号绑定
│ └── 供应商 / 付款信息
└── 弹窗 / 抽屉
├── 查看详情
├── 营业状态 / 价格配置
├── 加氢量下钻 / 预付余额下钻
├── 生成对账单(两阶段)
└── 查看对账记录
```
---
## 3. 列表与 KPI
> 详见 `.spec/requirements-prd-list.md`
- 筛选:名称、签约、地区、营业状态
- KPI全部 / 签约·普通 / 高频≥3 / 低频12 / 无加氢0
- 列表字段:名称地址、合作时间、营业、成本价、加氢次数/量、预付余额、联系方式、操作
- 设计规范:与租赁合同管理一致的 vm 布局与 Inter 字体
---
## 4. 站点主数据
### 4.1 新建站点
同页内嵌视图:基本信息、系统账号绑定(可选)、供应商信息(新建或关联)。
### 4.2 编辑 / 查看
| 模式 | 交互 |
|---|---|
| 编辑 | 整页表单;系统账号仅 admin 可改 |
| 查看 | 弹窗分块展示站点、账号、供应商、付款信息 |
### 4.3 批量导入
下载 CSV 模板 → 上传 → 解析预览 → 确认导入;站点名称不可重复。
---
## 5. 运营配置
### 5.1 营业状态
卡片式弹窗:营业状态、营业时间按钮组;非全天须配置时段;展示状态变更记录。
### 5.2 价格配置
配置成本价格(元/kg与生效时间到达生效时间后刷新列表「当前成本价格」展示价格调整记录。
### 5.3 下钻分析
- **加氢量下钻**:统计卡 + 明细表 + 导出,字段对齐车辆氢费明细
- **预付余额下钻**:充值/加氢流水 + 趋势图 + 导出
---
## 6. 对账与结算(重点)
> 详见 `.spec/requirements-prd-statement.md`
与「车辆氢费明细」配合完成站点侧氢费结算闭环:
1. 拉取已对账且未结算的记录
2. 两阶段:选期生成 → 填收票提交
3. 提交后更新预付余额、写对账历史、回写台账(对账日期、收票日期、已付款)
---
## 7. 数据与集成
| 能力 | 实现 |
|---|---|
| 氢费明细 | `src/common/h2VehicleLedgerBridge.js` |
| 对账回写 | `H2_STATION_STATEMENT_LEDGER_UPDATES` |
| 页面实现 | `src/prototypes/oneos-web-h2-station/pages/03-站点信息.jsx` |
| 独立入口 | `src/prototypes/oneos-web-h2-station-site/index.tsx` |
---
## 8. 验收标准
### 8.1 列表页
- [ ] 筛选与 KPI 六卡片联动正确
- [ ] 高频/低频/无加氢口径与车辆氢费明细一致
- [ ] 加氢量、预付余额下钻数据来自共享 Store
- [ ] 布局与租赁合同 vm 规范一致
### 8.2 对账流程
- [ ] 仅已对账未结算记录可进入对账单
- [ ] 两阶段交互与必填校验完整
- [ ] 提交后回写车辆氢费明细可跨页验收
### 8.3 标注与文档
- [ ] `@axhub/annotation` 已接入,关键区域有 `data-annotation-id`
- [ ] 右侧工具栏目录可阅 PRD 全文与页面标注
---
## 9. 修订记录
| 版本 | 日期 | 说明 |
|---|---|---|
| v1.0 | 2026-07-01 | 独立入口、vm 规范对齐、标注目录与 PRD 接入 |

View File

@@ -0,0 +1,32 @@
# 站点信息独立原型 — 范围与决策
## 背景
「站点信息」从 ONE-OS Web 合包 `oneos-web-h2-station` 拆出,作为 OneOS → 加氢站管理下的独立 Axhub 入口,便于与正式菜单路径对齐。
## 范围
| 包含 | 不包含 |
|------|--------|
| 站点列表、KPI、新建/编辑/查看/删除 | 加氢订单、加氢记录(仍在合包) |
| 营业状态、价格配置、余额下钻 | 后端接口联调 |
| 两阶段对账单、充值单、批量导入 | |
## 数据来源
- **氢费/加氢明细**:统一来自 `src/common/h2VehicleLedgerBridge.js`,与「车辆氢费明细」原型共享 Store。
- 先打开「车辆氢费明细」时,台账全量行会同步至 Store仅打开加氢站模块时使用 canonical seed10 条演示数据)。
- 对账单提交 patch 经 `window.H2_STATION_STATEMENT_LEDGER_UPDATES` 回写车辆氢费明细。
## KPI 口径(已确认)
全部 / 签约·普通 / **高频≥3 次)** / **低频12 次)** / **无加氢0 次)**,与《站点管理使用说明书》及页面内嵌 PRD 一致。
## 角色演示
页面右上角提供「平台管理员 / 加氢站账号」切换,影响编辑页系统账号字段可编辑性。
## 双入口说明
- **本原型**OneOS → 加氢站管理 → 站点信息
- **合包**ONE-OS Web 端 → 加氢站管理 → 加氢订单 / 加氢记录

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,85 @@
/**
* @name 站点信息
* 自 ONE-OS web端/加氢站管理/站点信息 独立预览
*/
import '../../common/oneosWebLegacy/legacyGlobals';
import '../../common/h2VehicleLedgerBridge.js';
import React, { useEffect, useMemo } from 'react';
import * as antd from 'antd';
import { ConfigProvider } from 'antd';
import 'antd/dist/reset.css';
import '../vehicle-management/style.css';
import '../lease-contract-management/styles/lease-contract.css';
import '../oneos-web-h2-station/styles/h2-station-vm-filter.css';
import {
AnnotationViewer,
type AnnotationSourceDocument,
type AnnotationViewerOptions,
} from '@axhub/annotation';
import { clearHostPrototypeRouteInfo } from '../../common/useHashPage';
import SiteInfoPage from '../oneos-web-h2-station/pages/03-站点信息.jsx';
import annotationSourceDocument from './annotation-source.json';
/** 与租赁合同管理 / vehicle-management 主题一致 */
const vmTheme = {
token: {
colorPrimary: '#32a06e',
colorLink: '#32a06e',
colorLinkHover: '#3fb87c',
borderRadius: 8,
fontFamily:
'Inter, -apple-system, BlinkMacSystemFont, "SF Pro Display", "PingFang SC", "Microsoft YaHei", sans-serif',
fontSize: 14,
colorText: '#18181b',
colorTextSecondary: '#52525b',
colorBorder: '#e5e7eb',
colorBgContainer: '#ffffff',
},
components: {
Table: {
headerBg: '#f6f7f7',
headerColor: '#71717a',
rowHoverBg: '#f6f7f7',
borderColor: '#e5e7eb',
cellPaddingBlock: 8,
cellPaddingInline: 12,
},
Card: {
borderRadiusLG: 12,
},
InputNumber: {
handleVisible: true,
},
},
};
window.React = React;
window.antd = antd;
export default function OneosWebH2StationSite() {
useEffect(() => {
clearHostPrototypeRouteInfo();
}, []);
const annotationOptions = useMemo<AnnotationViewerOptions>(
() => ({
showToolbar: true,
showThemeToggle: true,
showColorFilter: true,
emptyWhenNoData: false,
toolbarEdge: 'right',
currentPageId: 'site-info',
}),
[],
);
return (
<ConfigProvider theme={vmTheme}>
<SiteInfoPage />
<AnnotationViewer
source={annotationSourceDocument as AnnotationSourceDocument}
options={annotationOptions}
/>
</ConfigProvider>
);
}

View File

@@ -0,0 +1,267 @@
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const protoRoot = path.resolve(__dirname, '..');
const specRoot = path.join(protoRoot, '.spec');
const fullPrd = fs.readFileSync(path.join(specRoot, 'requirements-prd.md'), 'utf8');
const listPrd = fs.readFileSync(path.join(specRoot, 'requirements-prd-list.md'), 'utf8');
const statementPrd = fs.readFileSync(path.join(specRoot, 'requirements-prd-statement.md'), 'utf8');
const scopeMd = fs.readFileSync(path.join(specRoot, 'requirements.md'), 'utf8');
const NODE_DEFS = [
{
id: 'h2-site-filter',
title: '筛选条件',
pageId: 'site-info',
color: '#2563eb',
aiPrompt: '筛选字段与查询/重置行为。',
annotationText: 'vm 筛选卡片4 列栅格,标签在上控件在下。',
locator: {
selectors: ['.h2-station-page .vm-filter-card', '[data-annotation-id="h2-site-filter"]'],
fingerprint: 'filter|site-info',
},
},
{
id: 'h2-site-kpi',
title: 'KPI 快捷筛选',
pageId: 'site-info',
color: '#2563eb',
aiPrompt: '六张 KPI 卡片口径与点击筛选。',
annotationText: '全部 / 签约·普通 / 高频 / 低频 / 无加氢。',
locator: {
selectors: ['.h2-station-page .vm-kpi-row', '[data-annotation-id="h2-site-kpi"]'],
fingerprint: 'kpi|site-info',
},
},
{
id: 'h2-site-statement',
title: '工具栏与对账',
pageId: 'site-info',
color: '#d97706',
aiPrompt: '工具栏操作与对账单氢费联动。',
annotationText: '全站余额提醒、批量导入、充值单、新建站点;对账回写车辆氢费明细。',
locator: {
selectors: ['.h2-station-page .vm-table-toolbar', '[data-annotation-id="h2-site-toolbar"]'],
fingerprint: 'toolbar|statement',
},
},
{
id: 'h2-site-table',
title: '站点列表',
pageId: 'site-info',
color: '#32a06e',
aiPrompt: '台账表格列、下钻与行内操作。',
annotationText: '名称、营业、成本价、加氢量、预付余额、更多菜单。',
locator: {
selectors: ['.h2-station-page .vm-table-card', '[data-annotation-id="h2-site-table"]'],
fingerprint: 'table|site-info',
},
},
];
const NODE_PRD_MAP = {
'h2-site-filter': { file: 'list', heading: '## 2. 筛选区' },
'h2-site-kpi': { file: 'list', heading: '## 3. KPI 统计卡片' },
'h2-site-statement': { file: 'list', heading: '## 4. 列表工具栏', combine: ['## 4. 列表工具栏', '## 6. 数据来源'] },
'h2-site-table': { file: 'list', heading: '## 5. 列表字段说明' },
};
const prdByKey = { list: listPrd, statement: statementPrd, full: fullPrd };
function extractSection(md, startHeading) {
const lines = md.split('\n');
let start = lines.findIndex((line) => line.trim() === startHeading || line.startsWith(startHeading));
if (start < 0) return null;
const startLevel = (lines[start].match(/^(#+)/)?.[1] ?? '##').length;
let end = lines.length;
for (let i = start + 1; i < lines.length; i++) {
if (/^#{1,6}\s/.test(lines[i])) {
const level = lines[i].match(/^(#+)/)?.[1].length ?? 2;
if (level <= startLevel) {
end = i;
break;
}
}
}
return lines.slice(start, end).join('\n').trim();
}
function extractFromPrd({ file, heading, combine }) {
const md = prdByKey[file];
if (!md) return null;
if (combine?.length) {
return combine.map((h) => extractSection(md, h)).filter(Boolean).join('\n\n---\n\n') || null;
}
return extractSection(md, heading);
}
function buildFallbackMarkdown(node) {
const parts = [`# ${node.title}`, ''];
if (node.aiPrompt) parts.push(node.aiPrompt, '');
if (node.annotationText?.trim()) parts.push('## 说明', '', node.annotationText.trim());
return parts.join('\n').trim();
}
function buildNodeMarkdown(node) {
const mapping = NODE_PRD_MAP[node.id];
if (mapping) {
const body = extractFromPrd(mapping);
if (body) return body;
}
if (node.id === 'h2-site-statement') {
const toolbar = extractSection(listPrd, '## 4. 列表工具栏');
const stmt = extractSection(statementPrd, '## 1. 数据来源');
if (toolbar && stmt) return `${toolbar}\n\n---\n\n${stmt}`;
}
return buildFallbackMarkdown(node);
}
const now = Date.now();
const nodes = NODE_DEFS.map((def, index) => ({
id: def.id,
index: index + 1,
title: def.title,
pageId: def.pageId,
locator: { ...def.locator, path: [] },
aiPrompt: def.aiPrompt,
annotationText: def.annotationText,
hasMarkdown: true,
color: def.color,
images: [],
createdAt: now,
updatedAt: now,
controls: [],
}));
const markdownMap = Object.fromEntries(nodes.map((node) => [node.id, buildNodeMarkdown(node)]));
const overviewMd = [
'## 1. 模块定位',
'',
'| 项 | 说明 |',
'|---|---|',
'| 模块 | 加氢站管理 → 站点信息 |',
'| 用户 | 运营、财务结算、管理员 |',
'| 核心任务 | 站点主数据、运营配置、氢费对账结算 |',
'| 页面结构 | 筛选 + KPI + 工具栏 + 列表 + 分页 |',
'',
'## 2. 关联模块',
'',
'- **车辆氢费明细**:氢费数据来源与对账回写目标',
'- **加氢站合包**:加氢订单、加氢记录仍在合包入口',
'',
'## 3. 验收重点',
'',
'1. KPI 高频/低频/无加氢与氢费明细一致',
'2. 对账单两阶段流程与跨页回写',
'3. 列表布局与租赁合同 vm 规范一致',
].join('\n');
const acceptanceMd = extractSection(fullPrd, '## 8. 验收标准') ?? '## 验收标准\n\n见 PRD 全文。';
const source = {
documentVersion: 1,
format: 'axhub-annotation-source',
data: {
version: 2,
prototypeName: 'oneos-web-h2-station-site',
pageId: 'site-info',
updatedAt: now,
nodes,
},
markdownMap,
assetMap: {},
directory: {
nodes: [
{
type: 'folder',
id: 'h2-site-doc-root',
title: '站点信息说明',
defaultExpanded: true,
children: [
{
type: 'markdown',
id: 'h2-site-doc-overview',
title: '模块总览',
markdown: overviewMd,
},
{
type: 'link',
id: 'h2-site-link-vh2',
title: '车辆氢费明细',
href: '/prototypes/vehicle-h2-fee-ledger',
target: 'self',
},
{
type: 'link',
id: 'h2-site-link-bundle',
title: '加氢站管理(合包)',
href: '/prototypes/oneos-web-h2-station',
target: 'self',
},
{
type: 'folder',
id: 'h2-site-doc-prd',
title: 'PRD 全文',
defaultExpanded: true,
children: [
{
type: 'markdown',
id: 'h2-site-doc-prd-full',
title: 'PRD 完整文档',
markdown: fullPrd,
markdownPath: 'src/prototypes/oneos-web-h2-station-site/.spec/requirements-prd.md',
},
{
type: 'markdown',
id: 'h2-site-doc-prd-list',
title: '列表页 PRD',
markdown: listPrd,
markdownPath: 'src/prototypes/oneos-web-h2-station-site/.spec/requirements-prd-list.md',
},
{
type: 'markdown',
id: 'h2-site-doc-prd-statement',
title: '对账与结算 PRD',
markdown: statementPrd,
markdownPath: 'src/prototypes/oneos-web-h2-station-site/.spec/requirements-prd-statement.md',
},
{
type: 'markdown',
id: 'h2-site-doc-scope',
title: '范围与决策',
markdown: scopeMd,
markdownPath: 'src/prototypes/oneos-web-h2-station-site/.spec/requirements.md',
},
{
type: 'markdown',
id: 'h2-site-doc-acceptance',
title: '验收标准',
markdown: acceptanceMd,
},
],
},
{
type: 'folder',
id: 'h2-site-doc-page',
title: '页面标注',
defaultExpanded: true,
children: nodes.map((node) => ({
type: 'markdown',
id: `h2-site-doc-node-${node.id}`,
title: node.title,
markdown: markdownMap[node.id],
})),
},
],
},
],
},
};
const outPath = path.join(protoRoot, 'annotation-source.json');
fs.writeFileSync(outPath, `${JSON.stringify(source, null, 2)}\n`, 'utf8');
console.log(`Wrote ${outPath} (${nodes.length} nodes, ${Object.keys(markdownMap).length} markdown entries)`);

View File

@@ -1,18 +1,17 @@
/**
* @name 加氢站管理
* 自 ONE-OS web端 原稿复刻(3 个页面
* 自 ONE-OS web端 原稿复刻(加氢订单、加氢记录
*/
import '../../common/oneosWebLegacy/legacyGlobals';
import '../../common/h2VehicleLedgerBridge.js';
import React from 'react';
import Page1 from './pages/01-加氢订单.jsx';
import Page2 from './pages/02-加氢记录.jsx';
import Page3 from './pages/03-站点信息.jsx';
import { OneosWebLegacyShell } from '../../common/oneosWebLegacy/OneosWebLegacyShell';
const pages = [
{ id: 'u52a0u6c22u7ad9u7ba1u7406-u52a0u6c22u8ba2u5355', title: '加氢订单', component: Page1 },
{ id: 'u52a0u6c22u7ad9u7ba1u7406-u52a0u6c22u8bb0u5f55', title: '加氢记录', component: Page2 },
{ id: 'u52a0u6c22u7ad9u7ba1u7406-u7ad9u70b9u4fe1u606f', title: '站点信息', component: Page3 },
];
export default function OneosWebH2Station() {

View File

@@ -32,16 +32,36 @@ const Component = function () {
var requirementModalVisible = useState(false);
// 模拟:加氢站与加氢记录数据
var orderDataRevisionState = useState(0);
var orderDataRevision = orderDataRevisionState[0];
var setOrderDataRevision = orderDataRevisionState[1];
React.useEffect(function () {
var bridge = typeof window !== 'undefined' ? window.H2VehicleLedgerBridge : null;
if (!bridge || typeof bridge.subscribe !== 'function') return undefined;
return bridge.subscribe(function () {
setOrderDataRevision(function (v) { return v + 1; });
});
}, []);
// 模拟:加氢站与加氢记录数据(氢费明细来自车辆氢费明细共享 Store
var stationList = useMemo(function () {
var bridge = typeof window !== 'undefined' ? window.H2VehicleLedgerBridge : null;
if (bridge && typeof bridge.getStationList === 'function') {
return bridge.getStationList();
}
return [
{ value: 'JX-H2-001', label: '嘉兴加氢站(一期)' },
{ value: 'HZ-H2-002', label: '杭州临平加氢站' },
{ value: 'SH-H2-003', label: '上海宝山加氢站' }
];
}, []);
}, [orderDataRevision]);
var rawData = useMemo(function () {
var bridge = typeof window !== 'undefined' ? window.H2VehicleLedgerBridge : null;
if (bridge && typeof bridge.getOrderRecords === 'function') {
return bridge.getOrderRecords();
}
return [
{ id: 1, stationCode: 'JX-H2-001', stationName: '嘉兴加氢站(一期)', time: '2026-03-01 10:21', plateNo: '浙A12345', amountKg: 12.5, amountYuan: 625.00 },
{ id: 2, stationCode: 'JX-H2-001', stationName: '嘉兴加氢站(一期)', time: '2026-03-01 14:08', plateNo: '浙A67890', amountKg: 10.0, amountYuan: 500.00 },
@@ -49,7 +69,7 @@ const Component = function () {
{ id: 4, stationCode: 'SH-H2-003', stationName: '上海宝山加氢站', time: '2026-03-03 16:45', plateNo: '沪A88888', amountKg: 8.0, amountYuan: 400.00 },
{ id: 5, stationCode: 'HZ-H2-002', stationName: '杭州临平加氢站', time: '2026-03-03 18:10', plateNo: '浙B99999', amountKg: 18.2, amountYuan: 910.00 }
];
}, []);
}, [orderDataRevision]);
// 简单字符串时间过滤(示例用,实际可改为 dayjs 比较)
function inRange(timeStr, range) {

View File

@@ -217,7 +217,19 @@ function hrSettlementStatusLabel(value) {
return info ? info.label : '—';
}
var HR_INITIAL_RECORDS = [
function hrGetVehicleLedgerBridge() {
return typeof window !== 'undefined' ? window.H2VehicleLedgerBridge : null;
}
function hrBuildInitialRecordsFromLedger() {
var bridge = hrGetVehicleLedgerBridge();
if (bridge && typeof bridge.getHrRecords === 'function') {
return bridge.getHrRecords(HR_MOCK_FILLERS);
}
return HR_FALLBACK_RECORDS.map(function (r) { return Object.assign({}, r); });
}
var HR_FALLBACK_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' },
@@ -492,11 +504,19 @@ const Component = function () {
var setEditingId = editingIdState[1];
var listDataState = useState(function () {
return HR_INITIAL_RECORDS.map(function (r) { return Object.assign({}, r); });
return hrBuildInitialRecordsFromLedger();
});
var listData = listDataState[0];
var setListData = listDataState[1];
React.useEffect(function () {
var bridge = hrGetVehicleLedgerBridge();
if (!bridge || typeof bridge.subscribe !== 'function') return undefined;
return bridge.subscribe(function () {
setListData(hrBuildInitialRecordsFromLedger());
});
}, []);
var draftFiltersState = useState({ timeRange: [], plateNo: undefined, fillerName: undefined });
var draftFilters = draftFiltersState[0];
var setDraftFilters = draftFiltersState[1];

View File

@@ -10,8 +10,8 @@ var H2_REGION_CASCADER_OPTIONS = [
var H2_STATION_KPI_CARDS = [
{ key: 'all', type: 'total', title: '全部加氢站', desc: '纳入台账管理的全部站点' },
{ key: 'balanceAlert', type: 'warning', title: '预付余额预警站点', desc: '预付余额低于所设提醒阈值的站点' },
{ key: 'arrears', type: 'unuploaded', title: '已欠费站点', desc: '预付余额小于 0 的站点' },
{ key: 'high', type: 'normal', title: '高频加氢站', desc: '近周期内加氢次数 ≥ 3 次的站点' },
{ key: 'low', type: 'warning', title: '低频加氢站', desc: '近周期内有加氢但次数 < 3 次的站点' },
{ key: 'none', type: 'unuploaded', title: '无加氢站点', desc: '暂无加氢记录的站点' }
];
@@ -31,83 +31,50 @@ 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; }'
'.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; }',
'.h2-site-list-table.vm-list-table .ant-table-thead th,.h2-site-list-table.vm-list-table .ant-table-tbody td{white-space:nowrap;}'
];
function h2KpiHelpIcon() {
return React.createElement('svg', {
xmlns: 'http://www.w3.org/2000/svg',
width: 14,
height: 14,
viewBox: '0 0 24 24',
fill: 'none',
stroke: 'currentColor',
strokeWidth: 2,
strokeLinecap: 'round',
strokeLinejoin: 'round',
'aria-hidden': true
},
React.createElement('circle', { cx: 12, cy: 12, r: 10 }),
React.createElement('path', { d: 'M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3' }),
React.createElement('path', { d: 'M12 17h.01' })
);
}
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; }',
'.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-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: 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, .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; }',
'.h2-station-page .lc-alert-card-active { box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.25) !important; border-color: #10b981 !important; }',
'.h2-station-page .lc-table-section { margin-bottom: 0; flex: 1; display: flex; flex-direction: column; min-height: 0; }',
'.h2-station-page .lc-table-toolbar { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px 16px; margin-bottom: 8px; min-height: 32px; }',
'.h2-station-page .lc-table-legend-outer { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; padding: 6px 4px; font-size: 12px; color: #64748b; }',
'.h2-station-page .lc-table-legend-label { font-weight: 600; color: #64748b; }',
'.h2-station-page .lc-table-legend-item { display: inline-flex; align-items: center; gap: 6px; }',
'.h2-station-page .lc-table-legend-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }',
'.h2-station-page .lc-table-toolbar-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-left: auto; }',
'.h2-station-page .lc-table-card { background: #fff; border-radius: 16px; border: 1px solid #e2e8f0; box-shadow: 0 4px 20px -4px rgba(15, 23, 42, 0.03); overflow: hidden; flex: 1; overflow-y: auto; display: flex; flex-direction: column; }',
'.h2-station-page .lc-list-table .ant-table-wrapper, .h2-station-page .lc-list-table .ant-table { width: 100% !important; }',
'.h2-station-page .lc-table-card .ant-table-thead > tr > th { background: #f8fafc !important; color: #475569 !important; font-weight: 700 !important; font-size: 13px !important; border-bottom: 1px solid #e2e8f0 !important; padding: 12px 16px !important; }',
'.h2-station-page .lc-table-card .ant-table-tbody > tr.ant-table-measure-row, .h2-station-page .lc-table-card .ant-table-tbody > tr.ant-table-measure-row > td { height: 0 !important; max-height: 0 !important; padding: 0 !important; margin: 0 !important; border: none !important; line-height: 0 !important; font-size: 0 !important; overflow: hidden !important; visibility: hidden !important; }',
'.h2-station-page .lc-table-card .ant-table-tbody > tr:not(.ant-table-measure-row) > td { padding: 12px 16px !important; font-size: 13px; }',
'.h2-station-page .lc-table-card .ant-table-tbody > tr:not(.ant-table-measure-row):hover > td { background: #f8fafc !important; }',
'.h2-station-page .lc-table-card .ant-pagination { margin: 0 !important; padding: 12px 16px !important; border-top: 1px solid #f1f5f9; }',
'.h2-station-page .lc-action-btn { font-weight: 600 !important; color: #10b981 !important; padding: 0 !important; min-height: 44px; }',
'.h2-station-page .lc-action-btn-danger { color: #ef4444 !important; }',
'.h2-station-page { --h2-form-row-gap: 20px; --h2-form-col-gap: 24px; --h2-form-label-gap: 8px; font-family: var(--vm-font); font-size: 0.875rem; line-height: 1.5; letter-spacing: -0.01em; color: var(--ln-body); -webkit-font-smoothing: antialiased; padding: 20px 24px 24px; height: 100vh; display: flex; flex-direction: column; background: #f8fafc; overflow: hidden; box-sizing: border-box; }',
'.h2-station-page .h2-action-more-btn { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 8px; color: #64748b; cursor: pointer; transition: background 0.15s ease, color 0.15s ease; }',
'.h2-station-page .h2-action-more-btn:hover { background: #f1f5f9; color: #334155; }',
'.h2-station-page .h2-action-more-btn:focus-visible { outline: 2px solid #10b981; outline-offset: 2px; }',
'.h2-station-page .h2-row-actions { display: inline-flex; align-items: center; gap: 4px; }',
'.h2-station-page .lc-station-name { font-weight: 700; color: #0f172a; font-size: 13px; }',
'.h2-station-page .lc-station-name { font-weight: 500; color: var(--ln-ink); font-size: 0.8125rem; }',
'.h2-station-page .lc-station-name-cell { min-width: 0; }',
'.h2-station-page .h2-current-cost-price-cell { display: inline-flex; align-items: center; justify-content: flex-end; gap: 6px; max-width: 100%; flex-wrap: nowrap; }',
'.h2-station-page .h2-price-type-tag.ant-tag { margin: 0; font-size: 11px; line-height: 18px; padding: 0 6px; flex-shrink: 0; }',
'.h2-station-page .h2-price-type-tag.ant-tag { margin: 0; font-size: 0.75rem; font-weight: 400; line-height: 18px; padding: 0 6px; flex-shrink: 0; }',
'.h2-station-page .h2-price-multi-warn { display: inline-flex; align-items: center; justify-content: center; color: #f59e0b; cursor: help; flex-shrink: 0; line-height: 0; }',
'.h2-station-page .h2-price-multi-warn:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; border-radius: 4px; }',
'.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-station-address-line { margin-top: 4px; font-size: 0.8125rem; color: var(--ln-muted); 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-size: 0.75rem !important; font-weight: 500 !important; flex-shrink: 0; line-height: 20px !important; }',
'.h2-station-page .lc-refuel-kg-row { display: flex; align-items: center; justify-content: flex-end; gap: 8px; flex-wrap: wrap; min-width: 0; }',
'.h2-station-page .lc-refuel-freq-tag { margin: 0 !important; border-radius: 6px !important; font-weight: 600 !important; flex-shrink: 0; line-height: 20px !important; }',
'.h2-station-page .lc-station-region { color: #64748b; font-size: 12px; margin-bottom: 2px; }',
'.h2-station-page .lc-station-contact-name { font-weight: 600; color: #0f172a; font-size: 13px; line-height: 1.35; }',
'.h2-station-page .lc-station-contact-phone { color: #64748b; font-size: 12px; margin-top: 2px; line-height: 1.35; font-variant-numeric: tabular-nums; }',
'.h2-station-page .lc-refuel-freq-tag { margin: 0 !important; border-radius: 6px !important; font-size: 0.75rem !important; font-weight: 500 !important; flex-shrink: 0; line-height: 20px !important; }',
'.h2-station-page .lc-station-region { color: var(--ln-muted); font-size: 0.8125rem; margin-bottom: 2px; }',
'.h2-station-page .lc-station-contact-name { font-weight: 500; color: var(--ln-ink); font-size: 0.8125rem; line-height: 1.35; }',
'.h2-station-page .lc-station-contact-phone { color: var(--ln-muted); font-size: 0.8125rem; margin-top: 2px; line-height: 1.35; font-variant-numeric: tabular-nums; font-family: var(--vm-font-mono); }',
'.h2-station-page .h2-region-address { display: flex; flex-direction: column; gap: 12px; width: 100%; }',
'.h2-station-page .h2-region-address .ant-select, .h2-station-page .h2-region-address .ant-input { width: 100%; }',
'.h2-station-page .h2-region-address--inline { flex-direction: row; align-items: center; gap: 8px; }',
@@ -186,7 +153,7 @@ var H2_PAGE_STYLE = ONEOS_ANT_TABLE_GLOBAL_FIX.concat([
'.h2-station-page .h2-status-log-title { font-size: 14px; font-weight: 700; color: #0f172a; margin-bottom: 10px; }',
'.h2-station-page .h2-status-log-table .ant-table-thead > tr > th { background: #f8fafc !important; font-size: 12px !important; padding: 8px 12px !important; }',
'.h2-station-page .h2-status-log-table .ant-table-tbody > tr > td { font-size: 12px !important; padding: 8px 12px !important; }',
'.h2-station-page .h2-refuel-drill-link { border: none; background: none; padding: 0; font-weight: 700; color: #059669; cursor: pointer; font-variant-numeric: tabular-nums; font-size: 13px; }',
'.h2-station-page .h2-refuel-drill-link { border: none; background: none; padding: 0; font-weight: 600; color: var(--ln-primary); cursor: pointer; font-variant-numeric: tabular-nums; font-size: 0.8125rem; }',
'.h2-station-page .h2-refuel-drill-link:hover { color: #047857; text-decoration: underline; }',
'.h2-station-page .h2-refuel-drill-link:focus-visible { outline: 2px solid #10b981; outline-offset: 2px; border-radius: 4px; }',
'.h2-station-page .h2-ledger-totals-bar { display: flex; align-items: stretch; gap: 0; margin-bottom: 12px; border: 1px solid #bae6fd; border-radius: 10px; overflow: hidden; background: #f8fafc; box-shadow: 0 1px 0 rgba(15, 23, 42, 0.04); }',
@@ -244,10 +211,9 @@ var H2_PAGE_STYLE = ONEOS_ANT_TABLE_GLOBAL_FIX.concat([
'.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 { flex: 0 0 auto; white-space: nowrap; border: none; background: none; padding: 0; font: inherit; font-weight: 600; font-size: 0.8125rem; color: var(--ln-primary); cursor: pointer; text-decoration: underline; text-underline-offset: 2px; font-variant-numeric: tabular-nums; font-family: var(--vm-font-mono); }',
'.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-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; }',
@@ -2142,24 +2108,20 @@ function h2KpiCardIcon(key) {
{ tag: 'line', x1: 19, y1: 16, x2: 16, y2: 19 }
], size);
}
if (key === 'balanceAlert') {
if (key === 'high') {
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 }
{ d: 'M3 22h18' },
{ d: 'M6 22V12l4-2v12' },
{ d: 'M12 22V8l4-2v16' },
{ d: 'M18 22V14l3-1.5v9.5' }
], size);
}
if (key === 'arrears') {
if (key === 'low') {
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 }
{ d: 'M3 22h18' },
{ d: 'M8 22V16l3-1.5V22' },
{ d: 'M14 22V18l3-1.5V22' },
{ tag: 'line', x1: 6, y1: 22, x2: 20, y2: 22 }
], size);
}
if (key === 'none') {
@@ -2990,19 +2952,15 @@ function h2SettlementStatusLabel(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, 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' }
];
/** 氢费明细统一来自「车辆氢费明细」共享 Store见 common/h2VehicleLedgerBridge.js */
function h2GetVehicleLedgerBridge() {
return typeof window !== 'undefined' ? window.H2VehicleLedgerBridge : null;
}
function h2GetAllRefuelRecordsFromLedger() {
var bridge = h2GetVehicleLedgerBridge();
return bridge ? bridge.getAllRefuelRecords() : [];
}
function h2FormatKgNum(v) {
var n = typeof v === 'number' ? v : parseFloat(v);
@@ -3513,32 +3471,10 @@ 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
};
});
}
/** 台账加氢记录字段对齐车辆氢费明细costTotal 对应成本总价) */
function h2InitLedgerRecordsStore() {
return h2BuildLedgerReconciledRecords();
var bridge = h2GetVehicleLedgerBridge();
return bridge ? bridge.buildStationLedgerStore() : [];
}
function h2InitStatementRecordsStore() {
@@ -3627,10 +3563,9 @@ function h2ExportStatementCsv(stationName, startDate, endDate, records) {
}
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) {
var bridge = h2GetVehicleLedgerBridge();
var records = bridge ? bridge.getRefuelRecordsByStation(stationName) : [];
return records.map(function (r, index) {
return Object.assign({}, r, { orderNo: h2BuildRefuelOrderNo(r, index) });
});
}
@@ -4959,16 +4894,6 @@ var H2_PRIMARY_BTN_STYLE = {
border: 'none'
};
var H2_REQ_BTN_STYLE = {
borderRadius: 8,
border: '1px solid #cbd5e1',
fontWeight: 600,
display: 'inline-flex',
alignItems: 'center',
boxShadow: '0 1px 2px rgba(0,0,0,0.05)',
color: '#475569'
};
function h2ResolveRecordSupplierMode(record) {
if (!record) return 'link';
if (record.supplierMode === 'none') return 'none';
@@ -5705,7 +5630,6 @@ const Component = function () {
var useCallback = React.useCallback;
var antd = window.antd;
var Breadcrumb = antd.Breadcrumb;
var Card = antd.Card;
var Table = antd.Table;
var Button = antd.Button;
@@ -5760,14 +5684,6 @@ const Component = function () {
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];
@@ -5841,6 +5757,21 @@ const Component = function () {
var ledgerStore = ledgerStoreState[0];
var setLedgerStore = ledgerStoreState[1];
var ledgerDataRevisionState = useState(0);
var ledgerDataRevision = ledgerDataRevisionState[0];
var setLedgerDataRevision = ledgerDataRevisionState[1];
React.useEffect(function () {
var bridge = h2GetVehicleLedgerBridge();
if (!bridge || typeof bridge.subscribe !== 'function') return undefined;
return bridge.subscribe(function () {
setLedgerStore(h2InitLedgerRecordsStore());
setLedgerDataRevision(function (v) { return v + 1; });
});
}, []);
var isAdminUser = h2IsAdminUser();
var statementRecordsState = useState(function () { return h2InitStatementRecordsStore(); });
var statementRecords = statementRecordsState[0];
var setStatementRecords = statementRecordsState[1];
@@ -5924,8 +5855,6 @@ const Component = function () {
var editSupplier = editSupplierState[0];
var setEditSupplier = editSupplierState[1];
var isAdminUser = h2IsAdminUser();
var createSupplierReadonly = createSupplierMode === 'link' && !!createLinkedSupplierId;
var existingNameMap = useMemo(function () {
@@ -5937,15 +5866,16 @@ const Component = function () {
}, [listData]);
var categoryCounts = useMemo(function () {
var counts = { all: listData.length, balanceAlert: 0, arrears: 0, none: 0 };
var counts = { all: listData.length, high: 0, low: 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);
if (h2DeriveFrequencyByRefuelCount(stats.count) === 'none') counts.none += 1;
var freq = h2DeriveFrequencyByRefuelCount(stats.count);
if (freq === 'high') counts.high += 1;
else if (freq === 'low') counts.low += 1;
else counts.none += 1;
});
return counts;
}, [listData]);
}, [listData, ledgerDataRevision]);
var signedStats = useMemo(function () {
var signed = 0;
@@ -5957,12 +5887,11 @@ const Component = function () {
var list = listData.slice();
if (categoryTab !== 'all') {
list = list.filter(function (r) {
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';
}
var stats = h2CalcRefuelStats(r.name);
var freq = h2DeriveFrequencyByRefuelCount(stats.count);
if (categoryTab === 'high') return freq === 'high';
if (categoryTab === 'low') return freq === 'low';
if (categoryTab === 'none') return freq === 'none';
return true;
});
}
@@ -5984,7 +5913,7 @@ const Component = function () {
refuelFreqKey: h2DeriveFrequencyByRefuelCount(stats.count)
});
});
}, [listData, categoryTab, appliedFilters]);
}, [listData, categoryTab, appliedFilters, ledgerDataRevision]);
var sortedList = useMemo(function () {
var list = filteredList.slice();
@@ -6000,30 +5929,94 @@ const Component = function () {
}, [filteredList, refuelSort]);
var totalCount = sortedList.length;
var totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
var safePage = Math.min(page, totalPages);
var displayList = useMemo(function () {
var start = (page - 1) * pageSize;
var start = (safePage - 1) * pageSize;
return sortedList.slice(start, start + pageSize);
}, [sortedList, page, pageSize]);
}, [sortedList, safePage, pageSize]);
var tablePagination = useMemo(function () {
return {
current: page,
pageSize: pageSize,
total: totalCount,
showSizeChanger: true,
pageSizeOptions: ['5', '10', '20', '50'],
showTotal: function (t) { return '共 ' + t + ' 条'; },
onChange: function (p, size) {
setPage(p);
if (size && size !== pageSize) setPageSize(size);
}
};
}, [page, pageSize, totalCount]);
function h2BuildPageItems(current, pages) {
if (pages <= 7) {
var all = [];
var pi;
for (pi = 1; pi <= pages; pi++) all.push(pi);
return all;
}
var items = [1];
if (current > 3) items.push('ellipsis');
var startP = Math.max(2, current - 1);
var endP = Math.min(pages - 1, current + 1);
for (var pj = startP; pj <= endP; pj++) items.push(pj);
if (current < pages - 2) items.push('ellipsis');
if (pages > 1) items.push(pages);
return items;
}
var renderH2TablePagination = useCallback(function () {
if (totalCount === 0) return null;
var pageItems = h2BuildPageItems(safePage, totalPages);
var pageSizeOptions = [5, 10, 20, 50];
return React.createElement('div', { className: 'vm-pagination', role: 'navigation', 'aria-label': '表格分页' },
React.createElement('label', { className: 'vm-pagination-size' },
React.createElement('span', { className: 'vm-pagination-size-label' }, '每页'),
React.createElement('select', {
className: 'vm-pagination-select',
value: pageSize,
'aria-label': '每页显示条数',
onChange: function (e) {
setPageSize(Number(e.target.value));
setPage(1);
}
}, pageSizeOptions.map(function (size) {
return React.createElement('option', { key: size, value: size }, size);
})),
React.createElement('span', { className: 'vm-pagination-size-label' }, '条')
),
React.createElement('div', { className: 'vm-pagination-pages' },
React.createElement('button', {
type: 'button',
className: 'vm-pagination-nav',
disabled: safePage <= 1,
'aria-label': '上一页',
onClick: function () { setPage(safePage - 1); }
},
React.createElement('svg', { xmlns: 'http://www.w3.org/2000/svg', width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true },
React.createElement('path', { d: 'm15 18-6-6 6-6' })
)
),
pageItems.map(function (item, index) {
if (item === 'ellipsis') {
return React.createElement('span', { key: 'ellipsis-' + index, className: 'vm-pagination-ellipsis', 'aria-hidden': true }, '…');
}
return React.createElement('button', {
key: item,
type: 'button',
className: 'vm-pagination-page' + (safePage === item ? ' active' : ''),
'aria-current': safePage === item ? 'page' : undefined,
onClick: function () { setPage(item); }
}, item);
}),
React.createElement('button', {
type: 'button',
className: 'vm-pagination-nav',
disabled: safePage >= totalPages,
'aria-label': '下一页',
onClick: function () { setPage(safePage + 1); }
},
React.createElement('svg', { xmlns: 'http://www.w3.org/2000/svg', width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true },
React.createElement('path', { d: 'm9 18 6-6-6-6' })
)
)
),
React.createElement('div', { className: 'vm-table-total' }, '共 ' + totalCount + ' 条')
);
}, [totalCount, safePage, totalPages, pageSize]);
var handleKpiCardClick = useCallback(function (key) {
setCategoryTab(key);
if (key === 'all' || key === 'none') {
if (key === 'all' || key === 'high' || key === 'low' || key === 'none') {
setListFilters(function (p) { return Object.assign({}, p, { signed: undefined }); });
setAppliedFilters(function (p) { return Object.assign({}, p, { signed: undefined }); });
}
@@ -6072,49 +6065,40 @@ const Component = function () {
}, []);
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)
return React.createElement('label', { className: 'vm-filter-field' },
React.createElement('span', null, label),
control
);
}, []);
var renderAlertStatCard = useCallback(function (card, options) {
var renderVmKpiCard = useCallback(function (card, options) {
var active = options.active;
var count = options.count;
var onClick = options.onClick;
var icon = options.icon;
return React.createElement('div', {
return React.createElement('button', {
key: card.key,
role: 'button',
tabIndex: 0,
className: 'lc-alert-card lc-alert-card--' + card.type + ' lc-alert-card-clickable' + (active ? ' lc-alert-card-active' : ''),
type: 'button',
className: 'vm-kpi-card' + (active ? ' active' : ''),
onClick: onClick,
onKeyDown: function (e) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onClick();
}
},
'aria-pressed': active,
'aria-label': card.title + ',共 ' + count + ' 个'
},
React.createElement('div', { className: 'lc-alert-card-tip-anchor' },
React.createElement(Tooltip, { title: card.desc, placement: 'topRight', overlayStyle: { maxWidth: 320 } },
React.createElement('span', {
className: 'lc-alert-card-tip',
role: 'img',
'aria-label': card.title + '说明',
onClick: function (e) { e.stopPropagation(); },
onMouseDown: function (e) { e.stopPropagation(); }
},
h2SvgIcon([{ tag: 'circle', cx: 12, cy: 12, r: 10 }, { tag: 'line', x1: 12, y1: 16, x2: 12, y2: 12 }, { tag: 'line', x1: 12, y1: 8, x2: 12.01, y2: 8 }], 12)
)
)
React.createElement('span', {
className: 'vm-kpi-tip',
tabIndex: 0,
role: 'note',
'aria-label': card.title + '说明',
onClick: function (e) { e.stopPropagation(); },
onMouseDown: function (e) { e.stopPropagation(); }
},
h2KpiHelpIcon(),
React.createElement('span', { className: 'vm-kpi-tooltip', role: 'tooltip' }, card.desc)
),
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-title' }, card.title),
React.createElement('div', { className: 'lc-alert-card-val' }, count)
React.createElement('span', { className: 'vm-kpi-icon', 'aria-hidden': true }, icon),
React.createElement('span', { className: 'vm-kpi-main' },
React.createElement('span', { className: 'vm-kpi-eyebrow' }, card.title),
React.createElement('span', { className: 'vm-kpi-val' }, count)
)
);
}, []);
@@ -7034,7 +7018,7 @@ const Component = function () {
}
}
var allCustomerOpts = h2CollectCustomerOptions(H2_MOCK_REFUEL_RECORDS);
var allCustomerOpts = h2CollectCustomerOptions(h2GetAllRefuelRecordsFromLedger());
var customerNameMap = {};
allCustomerOpts.forEach(function (opt) { customerNameMap[opt.value] = opt.label; });
@@ -8663,7 +8647,8 @@ const Component = function () {
key: 'name',
width: 300,
fixed: 'left',
ellipsis: true,
className: 'h2-site-name-col',
onCell: function () { return { className: 'h2-site-name-col' }; },
render: function (v, r) {
var addressLine = h2FormatStationAddressLine(r);
return React.createElement('div', { className: 'lc-station-name-cell' },
@@ -8702,6 +8687,7 @@ const Component = function () {
key: 'currentCostPrice',
width: 168,
align: 'right',
onHeaderCell: function () { return { style: { textAlign: 'right' } }; },
render: function (_, r) {
return renderCurrentCostPriceCell(r);
}
@@ -8755,6 +8741,8 @@ const Component = function () {
title: '联系方式',
key: 'contactInfo',
width: 120,
className: 'h2-site-contact-col',
onCell: function () { return { className: 'h2-site-contact-col' }; },
render: function (_, r) {
var mobile = r.mobilePhone || r.phone || '';
var landline = r.landlinePhone || '';
@@ -8773,10 +8761,9 @@ const Component = function () {
fixed: 'right',
render: function (_, record) {
return React.createElement('div', { className: 'h2-row-actions' },
React.createElement(Button, {
type: 'link',
size: 'small',
className: 'lc-action-btn',
React.createElement('button', {
type: 'button',
className: 'vm-link',
onClick: function () { openView(record); }
}, '查看'),
React.createElement(Dropdown, {
@@ -8799,6 +8786,10 @@ const Component = function () {
}
];
var listTableScrollX = columns.reduce(function (sum, col) {
return sum + (Number(col.width) || 0);
}, 0);
var formItem = function (label, required, node, extra) {
return React.createElement(Form.Item, {
label: required ? React.createElement('span', null, React.createElement('span', { style: { color: '#ef4444', marginRight: 4 } }, '*'), label) : label,
@@ -8898,7 +8889,7 @@ const Component = function () {
var priceConfigs = priceModal.priceConfigs || [];
var priceType = priceModal.priceType || 'fixed';
var assignedCustomerIds = h2GetAssignedCustomerIds(priceConfigs, priceModal.editingConfigId);
var allCustomerOptions = h2CollectCustomerOptions(H2_MOCK_REFUEL_RECORDS);
var allCustomerOptions = h2CollectCustomerOptions(h2GetAllRefuelRecordsFromLedger());
var selectedCustomerIds = priceModal.selectedCustomerIds || [];
var enabledTierCustomerOptions = allCustomerOptions.filter(function (opt) {
return !assignedCustomerIds[opt.value];
@@ -9468,41 +9459,29 @@ const Component = function () {
return React.createElement('div', {
className: 'h2-station-page lc-edit-page'
+ (subView === 'list' ? ' h2-station-page--list' : '')
+ (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(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 },
React.createElement('div', { className: 'lc-filter-grid' },
subView === 'list' ? React.createElement('div', { className: 'vm-page lc-page h2-site-page h2-site-list-shell' },
React.createElement('section', {
className: 'vm-filter-card',
'data-annotation-id': 'h2-site-filter',
'aria-label': '筛选条件'
},
React.createElement('header', { className: 'vm-filter-header' },
React.createElement('h2', { className: 'vm-filter-title' }, '筛选条件')
),
React.createElement('div', { className: 'vm-filter-grid' },
renderFilterField('加氢站名称', React.createElement(Input, {
placeholder: '请输入加氢站名称',
value: listFilters.name,
onChange: function (e) { setListFilters(function (p) { return Object.assign({}, p, { name: e.target.value }); }); },
onPressEnter: handleListFilterQuery,
allowClear: true,
style: { width: '100%', height: 32, borderRadius: 8 }
style: { width: '100%' }
})),
renderFilterField('是否签约', React.createElement(Select, {
placeholder: '全部',
@@ -9510,8 +9489,7 @@ const Component = function () {
value: listFilters.signed,
onChange: function (v) { setListFilters(function (p) { return Object.assign({}, p, { signed: v }); }); },
options: [{ value: 'yes', label: '签约站点' }, { value: 'no', label: '普通站点' }],
style: { width: '100%' },
dropdownStyle: { borderRadius: 8 }
style: { width: '100%' }
})),
renderFilterField('地区(省-市)', React.createElement(Cascader, {
options: H2_REGION_CASCADER_OPTIONS,
@@ -9519,7 +9497,7 @@ const Component = function () {
onChange: function (v) { setListFilters(function (p) { return Object.assign({}, p, { region: v || undefined }); }); },
placeholder: '请选择省 / 市',
allowClear: true,
style: { width: '100%', borderRadius: 8 },
style: { width: '100%' },
changeOnSelect: true
})),
renderFilterField('营业状态', React.createElement(Select, {
@@ -9528,25 +9506,41 @@ const Component = function () {
value: listFilters.businessStatus,
onChange: function (v) { setListFilters(function (p) { return Object.assign({}, p, { businessStatus: v }); }); },
options: H2_BUSINESS_STATUS_OPTIONS,
style: { width: '100%' },
dropdownStyle: { borderRadius: 8 }
style: { width: '100%' }
}))
),
React.createElement('div', { className: 'lc-filter-actions' },
React.createElement(Button, { onClick: handleListFilterReset, style: { borderRadius: 8 } }, '重置'),
React.createElement(Button, { type: 'primary', onClick: handleListFilterQuery, style: H2_PRIMARY_BTN_STYLE }, '查询')
React.createElement('div', { className: 'vm-filter-actions' },
React.createElement('button', { type: 'button', className: 'vm-btn vm-btn-ghost', onClick: handleListFilterReset }, '重置'),
React.createElement('button', { type: 'button', className: 'vm-btn vm-btn-primary', onClick: handleListFilterQuery },
React.createElement('svg', {
xmlns: 'http://www.w3.org/2000/svg',
width: 16,
height: 16,
viewBox: '0 0 24 24',
fill: 'none',
stroke: 'currentColor',
strokeWidth: 2,
strokeLinecap: 'round',
strokeLinejoin: 'round',
'aria-hidden': true
},
React.createElement('circle', { cx: 11, cy: 11, r: 8 }),
React.createElement('path', { d: 'm21 21-4.3-4.3' })
),
'查询'
)
)
),
React.createElement('div', { className: 'lc-alert-stats-row' },
renderAlertStatCard(H2_STATION_KPI_CARDS[0], {
React.createElement('div', { className: 'vm-kpi-row', role: 'group', 'aria-label': '站点统计', 'data-annotation-id': 'h2-site-kpi' },
renderVmKpiCard(H2_STATION_KPI_CARDS[0], {
active: categoryTab === 'all' && !appliedFilters.signed,
count: categoryCounts.all || 0,
onClick: function () { handleKpiCardClick('all'); },
icon: h2KpiCardIcon('all')
}),
H2_SIGNED_FILTER_CARDS.map(function (card) {
return renderAlertStatCard(card, {
return renderVmKpiCard(card, {
active: appliedFilters.signed === card.key,
count: card.key === 'yes' ? signedStats.signed : signedStats.unsigned,
onClick: function () { handleSignedFilterCardClick(card.key); },
@@ -9554,65 +9548,63 @@ const Component = function () {
});
}),
H2_STATION_KPI_CARDS.slice(1).map(function (card) {
var isAlertKpi = card.key === 'balanceAlert' || card.key === 'arrears';
return renderAlertStatCard(card, {
active: isAlertKpi
? (alertStationModal.open && alertStationModal.type === card.key)
: (categoryTab === card.key && !appliedFilters.signed),
return renderVmKpiCard(card, {
active: categoryTab === card.key && !appliedFilters.signed,
count: categoryCounts[card.key] || 0,
onClick: function () {
if (isAlertKpi) openAlertStationModal(card.key);
else handleKpiCardClick(card.key);
},
onClick: function () { handleKpiCardClick(card.key); },
icon: h2KpiIcon(card.key)
});
})
),
React.createElement('div', { className: 'lc-table-section' },
React.createElement('div', { className: 'lc-table-toolbar' },
React.createElement('div', { className: 'lc-table-toolbar-actions' },
React.createElement(Button, {
type: 'default',
icon: H2_ICONS.none,
style: { borderRadius: 8, fontWeight: 600, borderColor: '#f59e0b', color: '#d97706' },
React.createElement('section', { className: 'vm-table-section' },
React.createElement('div', { className: 'vm-table-toolbar', 'data-annotation-id': 'h2-site-toolbar' },
React.createElement('div', { className: 'vm-table-actions' },
React.createElement('button', {
type: 'button',
className: 'vm-btn vm-btn-ghost',
onClick: openGlobalBalanceAlertSetting,
'aria-label': '全站余额提醒设置'
}, '全站余额提醒设置'),
React.createElement(Button, {
type: 'default',
icon: H2_ICONS.upload,
style: { borderRadius: 8, fontWeight: 600, borderColor: '#10b981', color: '#059669' },
React.createElement('button', {
type: 'button',
className: 'vm-btn vm-btn-ghost',
onClick: function () { setImportModalOpen(true); },
'aria-label': '批量导入加氢站点'
}, '批量导入'),
React.createElement(Button, {
type: 'default',
icon: H2_ICONS.doc,
style: { borderRadius: 8, fontWeight: 600, borderColor: '#10b981', color: '#059669' },
React.createElement('button', {
type: 'button',
className: 'vm-btn vm-btn-ghost',
onClick: function () { openRechargeModal(); },
'aria-label': '发起充值单'
}, '发起充值单'),
React.createElement(Button, {
type: 'primary',
style: H2_PRIMARY_BTN_STYLE,
React.createElement('button', {
type: 'button',
className: 'vm-btn vm-btn-primary',
onClick: openCreate,
'aria-label': '新建加氢站点'
}, '新建站点')
)
),
React.createElement('div', { className: 'lc-table-card' },
React.createElement('div', { className: 'vm-table-card', 'data-annotation-id': 'h2-site-table' },
React.createElement('div', {
className: 'vm-table-wrap h2-site-table-wrap',
style: { '--h2-table-scroll-x': listTableScrollX + 'px' }
},
React.createElement(Table, {
className: 'lc-list-table',
className: 'vm-list-table h2-site-list-table',
rowKey: 'id',
columns: columns,
dataSource: displayList,
pagination: tablePagination,
size: 'middle',
scroll: { x: 1290 },
locale: { emptyText: emptyNode },
onChange: handleListTableChange
})
pagination: false,
size: 'small',
tableLayout: 'fixed',
scroll: { x: listTableScrollX, y: 'calc(100vh - 640px)' },
locale: { emptyText: emptyNode },
onChange: handleListTableChange
})
),
React.createElement('div', { className: 'vm-table-footer' }, renderH2TablePagination())
)
)
) : null,
@@ -9634,30 +9626,6 @@ 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: 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 } }, '关闭')
}, renderH2StationRequirementDocPanel()),
React.createElement(Modal, {
title: '批量导入加氢站点',
open: importModalOpen,

View File

@@ -0,0 +1,254 @@
/**
* 加氢站站点信息 — 与租赁合同管理 vm 布局 / 字体对齐
*/
.vm-page.lc-page.h2-site-page {
font-family: var(--vm-font);
font-size: 0.875rem;
line-height: 1.5;
letter-spacing: -0.01em;
color: var(--ln-body);
-webkit-font-smoothing: antialiased;
}
.vm-page.lc-page.h2-site-page .vm-filter-title {
font-size: 1.125rem;
font-weight: 600;
letter-spacing: -0.02em;
color: var(--ln-ink);
}
.vm-page.lc-page.h2-site-page .vm-filter-field > span {
font-size: 0.75rem;
font-weight: 500;
color: var(--ln-muted);
}
.vm-page.lc-page.h2-site-page .vm-btn {
font-family: var(--vm-font);
font-size: 0.875rem;
font-weight: 500;
}
.vm-page.lc-page.h2-site-page .vm-kpi-eyebrow {
font-size: 0.75rem;
font-weight: 500;
color: var(--ln-muted);
}
.vm-page.lc-page.h2-site-page .vm-kpi-val {
font-size: 1.375rem;
font-weight: 600;
letter-spacing: -0.03em;
font-variant-numeric: tabular-nums;
color: var(--ln-ink);
}
.vm-page.lc-page.h2-site-page .vm-list-table .ant-table-thead > tr > th {
font-size: 0.75rem !important;
font-weight: 500 !important;
color: var(--ln-muted) !important;
}
.vm-page.lc-page.h2-site-page .vm-list-table .ant-table-tbody > tr:not(.ant-table-measure-row) > td {
font-size: 0.8125rem;
color: var(--ln-body);
}
.vm-page.lc-page.h2-site-page .vm-list-table .vm-link {
font-size: 0.8125rem;
font-weight: 600;
}
.vm-page.lc-page.h2-site-page .vm-table-total,
.vm-page.lc-page.h2-site-page .vm-pagination-size,
.vm-page.lc-page.h2-site-page .vm-pagination-nav,
.vm-page.lc-page.h2-site-page .vm-pagination-page,
.vm-page.lc-page.h2-site-page .vm-pagination-select {
font-family: var(--vm-font);
font-size: 0.875rem;
}
.vm-page.lc-page.h2-site-page .vm-table-total {
color: var(--ln-muted);
}
.vm-page.lc-page.h2-site-page .vm-filter-field .ant-select,
.vm-page.lc-page.h2-site-page .vm-filter-field .ant-picker,
.vm-page.lc-page.h2-site-page .vm-filter-field .ant-cascader {
width: 100%;
}
.vm-page.lc-page.h2-site-page .vm-filter-field .ant-select .ant-select-selector,
.vm-page.lc-page.h2-site-page .vm-filter-field .ant-picker,
.vm-page.lc-page.h2-site-page .vm-filter-field .ant-cascader .ant-select-selector,
.vm-page.lc-page.h2-site-page .vm-filter-field .ant-input,
.vm-page.lc-page.h2-site-page .vm-filter-field .ant-input-affix-wrapper {
min-height: var(--vm-control-height) !important;
border-radius: var(--ln-radius-control) !important;
border-color: var(--ln-hairline-strong) !important;
font-family: var(--vm-font);
font-size: 0.875rem;
}
.vm-page.lc-page.h2-site-page .vm-filter-field .ant-select .ant-select-selector,
.vm-page.lc-page.h2-site-page .vm-filter-field .ant-cascader .ant-select-selector {
align-items: center;
}
.vm-page.lc-page.h2-site-page .vm-filter-field .ant-input-affix-wrapper {
display: inline-flex;
align-items: center;
padding-top: 0;
padding-bottom: 0;
}
.vm-page.lc-page.h2-site-page .vm-filter-field .ant-input-affix-wrapper > input.ant-input {
height: calc(var(--vm-control-height) - 2px);
line-height: calc(var(--vm-control-height) - 2px);
}
.vm-page.lc-page.h2-site-page .vm-filter-card {
margin-bottom: 16px;
}
.vm-page.lc-page.h2-site-page .vm-kpi-row {
grid-template-columns: repeat(6, minmax(0, 1fr));
margin-bottom: 16px;
}
@media (max-width: 1400px) {
.vm-page.lc-page.h2-site-page .vm-kpi-row {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (max-width: 960px) {
.vm-page.lc-page.h2-site-page .vm-kpi-row {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 640px) {
.vm-page.lc-page.h2-site-page .vm-kpi-row {
grid-template-columns: 1fr;
}
}
.vm-page.lc-page.h2-site-page .vm-table-section {
min-width: 0;
max-width: 100%;
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.vm-page.lc-page.h2-site-page .vm-table-card > .vm-table-wrap {
overflow: hidden;
max-height: none;
min-width: 0;
flex: 1;
min-height: 0;
}
.vm-page.lc-page.h2-site-page .vm-table-card {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.vm-page.lc-page.h2-site-page .h2-site-table-wrap {
--h2-table-scroll-x: 1624px;
}
/* 锁定表头/表体同宽,避免容器宽于列宽合计时表体被拉伸导致错位(租赁合同 scroll.x 大于容器则无此问题) */
.vm-page.lc-page.h2-site-page .h2-site-list-table .ant-table-header > table,
.vm-page.lc-page.h2-site-page .h2-site-list-table .ant-table-body > table {
width: var(--h2-table-scroll-x) !important;
min-width: var(--h2-table-scroll-x) !important;
max-width: var(--h2-table-scroll-x) !important;
table-layout: fixed !important;
}
.vm-page.lc-page.h2-site-page .h2-site-list-table .ant-table-content {
overflow-x: auto !important;
}
.vm-page.lc-page.h2-site-page .h2-site-list-table .ant-table-cell-scrollbar {
width: 0 !important;
min-width: 0 !important;
max-width: 0 !important;
padding: 0 !important;
border: none !important;
}
.vm-page.lc-page.h2-site-page .vm-table-section .vm-table-toolbar {
display: flex;
justify-content: flex-end;
align-items: center;
min-height: var(--vm-btn-height);
padding: 0 0 16px;
margin-bottom: 0;
}
.vm-page.lc-page.h2-site-page .h2-site-list-table .ant-table-cell {
vertical-align: middle;
}
.vm-page.lc-page.h2-site-page .h2-site-list-table .h2-site-name-col,
.vm-page.lc-page.h2-site-page .h2-site-list-table .h2-site-contact-col {
white-space: normal !important;
}
.vm-page.lc-page.h2-site-page .h2-site-list-table .ant-table-cell-fix-left,
.vm-page.lc-page.h2-site-page .h2-site-list-table .ant-table-cell-fix-start,
.vm-page.lc-page.h2-site-page .h2-site-list-table .ant-table-cell-fix-right,
.vm-page.lc-page.h2-site-page .h2-site-list-table .ant-table-cell-fix-end {
background: var(--ln-surface-card);
}
.vm-page.lc-page.h2-site-page .h2-site-list-table .ant-table-tbody > tr:hover .ant-table-cell-fix-left,
.vm-page.lc-page.h2-site-page .h2-site-list-table .ant-table-tbody > tr:hover .ant-table-cell-fix-start,
.vm-page.lc-page.h2-site-page .h2-site-list-table .ant-table-tbody > tr:hover .ant-table-cell-fix-right,
.vm-page.lc-page.h2-site-page .h2-site-list-table .ant-table-tbody > tr:hover .ant-table-cell-fix-end {
background: var(--ln-canvas-soft) !important;
}
.vm-page.lc-page.h2-site-page .h2-site-list-table .ant-table-thead > tr > th.ant-table-cell-fix-left,
.vm-page.lc-page.h2-site-page .h2-site-list-table .ant-table-thead > tr > th.ant-table-cell-fix-start,
.vm-page.lc-page.h2-site-page .h2-site-list-table .ant-table-thead > tr > th.ant-table-cell-fix-right,
.vm-page.lc-page.h2-site-page .h2-site-list-table .ant-table-thead > tr > th.ant-table-cell-fix-end {
background: var(--ln-canvas-soft) !important;
}
.vm-page.lc-page.h2-site-page .vm-list-table .ant-table-tbody > tr:not(.ant-table-measure-row) > td.h2-cell-prepaid-balance {
overflow: hidden;
}
.vm-page.lc-page.h2-site-page .h2-site-list-table.ant-table-wrapper,
.vm-page.lc-page.h2-site-page .h2-site-list-table .ant-table-wrapper {
min-width: 0;
max-width: 100%;
}
.vm-page.lc-page.h2-site-page .vm-list-table .ant-table-tbody > tr:not(.ant-table-measure-row) > td.h2-cell-prepaid-balance {
overflow: visible !important;
}
.h2-station-page.h2-station-page--list {
padding: 0;
height: 100vh;
background: transparent;
overflow: hidden;
}
.vm-page.lc-page.h2-site-page.h2-site-list-shell {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
overflow: hidden;
box-sizing: border-box;
}

View File

@@ -2962,10 +2962,25 @@ const Component = function () {
setAllRows(function (prev) {
return recalcAllRows(applyStatementPatchesToRows(prev, patches));
});
if (window.H2_VEHICLE_LEDGER_STORE) {
window.H2_VEHICLE_LEDGER_STORE.applyPatches(patches);
}
},
getRows: function () {
return allRows;
},
syncRows: function (rows) {
if (!rows) return;
setAllRows(recalcAllRows(rows));
}
};
}
React.useEffect(function () {
if (typeof window === 'undefined' || !window.H2_VEHICLE_LEDGER_STORE) return;
window.H2_VEHICLE_LEDGER_STORE.setRows(allRows);
}, [allRows]);
var dateRangeDraftState = useState(null);
var dateRangeDraft = dateRangeDraftState[0];
var setDateRangeDraft = dateRangeDraftState[1];

View File

@@ -1,6 +1,7 @@
import React from 'react';
import * as antd from 'antd';
import dayjs from 'dayjs';
import '../../common/h2VehicleLedgerBridge.js';
declare global {
interface Window {

File diff suppressed because one or more lines are too long

View File

@@ -1,395 +0,0 @@
/**
* @name 车辆管理Clay 主题备份)
* 设计基底src/themes/clay/DESIGN.md · 功能与标注同 vehicle-management 原版
*/
import '../vehicle-management/style.css';
import './styles/clay-theme.css';
import React, { useCallback, useMemo, useState } from 'react';
import { Download, FileUp } from 'lucide-react';
import {
AnnotationViewer,
type AnnotationDirectoryRouteNode,
type AnnotationSourceDocument,
type AnnotationViewerOptions,
} from '@axhub/annotation';
import seedVehicles from '../vehicle-management/data/vehicles.json';
import seedInsurancePurchases from '../vehicle-management/data/insurance-purchases.json';
import { FilterPanel } from '../vehicle-management/components/FilterPanel';
import { ImportModal } from '../vehicle-management/components/ImportModal';
import { KpiCards } from '../vehicle-management/components/KpiCards';
import { OpsManagerModal } from '../vehicle-management/components/OpsManagerModal';
import { OperateCityEditModal } from '../vehicle-management/components/OperateCityEditModal';
import { LocationMapModal } from '../vehicle-management/components/LocationMapModal';
import { VehicleEditModal } from '../vehicle-management/components/VehicleEditModal';
import { PageDocToolbar } from '../vehicle-management/components/PageDocToolbar';
import { VehicleTable } from '../vehicle-management/components/VehicleTable';
import { TablePagination } from '../vehicle-management/components/TablePagination';
import { DetailPage } from '../vehicle-management/pages/DetailPage';
import {
EMPTY_FILTERS,
type KpiKey,
type VehicleFilters,
type VehicleRecord,
} from '../vehicle-management/types';
import type { InsurancePurchaseRecord } from '../vehicle-management/types';
import { applyFilters } from '../vehicle-management/utils/filters';
import { buildInsuranceExpireMap } from '../vehicle-management/utils/insurance';
import type { LeaseRecordRow } from '../vehicle-management/utils/leaseRecords';
import type { AccidentRecordRow } from '../vehicle-management/utils/accidentRecords';
import type { ViolationRecordRow } from '../vehicle-management/utils/violationRecords';
import type { MovementRecordRow } from '../vehicle-management/utils/movementRecords';
import type { TransferRecordRow } from '../vehicle-management/utils/transferRecords';
import type { AnnualReviewRecordRow } from '../vehicle-management/utils/annualReviewRecords';
import { downloadVehicleTestCasesExcel } from '../vehicle-management/utils/testCaseExport';
import { countKpi } from '../vehicle-management/utils/vehicle';
import annotationSourceDocument from './annotation-source.json';
const CAN_EDIT_OPS_MANAGER = true;
const IS_ADMIN = true;
const DEFAULT_PAGE_SIZE = 10;
function ClayBackupBanner() {
return (
<div className="clay-backup-banner" role="note" aria-label="Clay 主题备份说明">
<div className="clay-backup-banner-main">
<span className="clay-backup-eyebrow">Clay Design System</span>
<h1 className="clay-backup-title"> · Clay </h1>
<p className="clay-backup-desc">
12px 24px KPI
</p>
</div>
<a className="clay-backup-link" href="/prototypes/vehicle-management">
</a>
</div>
);
}
export default function VehicleManagementClayApp() {
const [records, setRecords] = useState<VehicleRecord[]>(seedVehicles as VehicleRecord[]);
const [pendingFilters, setPendingFilters] = useState<VehicleFilters>(EMPTY_FILTERS);
const [appliedFilters, setAppliedFilters] = useState<VehicleFilters>(EMPTY_FILTERS);
const [kpiTab, setKpiTab] = useState<KpiKey>('all');
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
const [detailRecord, setDetailRecord] = useState<VehicleRecord | null>(null);
const [importOpen, setImportOpen] = useState(false);
const [opsRecord, setOpsRecord] = useState<VehicleRecord | null>(null);
const [operateCityRecord, setOperateCityRecord] = useState<VehicleRecord | null>(null);
const [locationMapRecord, setLocationMapRecord] = useState<VehicleRecord | null>(null);
const [editRecord, setEditRecord] = useState<VehicleRecord | null>(null);
const [toast, setToast] = useState('');
const showToast = useCallback((msg: string) => {
setToast(msg);
window.setTimeout(() => setToast(''), 3000);
}, []);
const insuranceExpireMap = useMemo(
() => buildInsuranceExpireMap(seedInsurancePurchases as InsurancePurchaseRecord[]),
[],
);
const kpiCounts = useMemo(() => ({
all: countKpi(records, 'all'),
lease: countKpi(records, 'lease'),
self: countKpi(records, 'self'),
stock: countKpi(records, 'stock'),
nonOperating: countKpi(records, 'nonOperating'),
exit: countKpi(records, 'exit'),
licenseAbnormal: countKpi(records, 'licenseAbnormal'),
insuranceAbnormal: countKpi(records, 'insuranceAbnormal'),
}), [records]);
const filtered = useMemo(
() => applyFilters(records, appliedFilters, kpiTab),
[records, appliedFilters, kpiTab],
);
const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize));
const paged = useMemo(() => {
const start = (page - 1) * pageSize;
return filtered.slice(start, start + pageSize);
}, [filtered, page, pageSize]);
const handlePageSizeChange = (size: number) => {
setPageSize(size);
setPage(1);
};
const handleSearch = () => {
setAppliedFilters(pendingFilters);
setPage(1);
};
const handleReset = () => {
setPendingFilters(EMPTY_FILTERS);
setAppliedFilters(EMPTY_FILTERS);
setPage(1);
};
const handleExport = () => {
showToast(`导出功能(原型演示):当前列表共 ${filtered.length}`);
};
const handleImport = (file: File) => {
showToast(`已接收文件 ${file.name},导入校验为原型演示`);
setImportOpen(false);
};
const handleSaveOps = (managers: string[]) => {
if (!opsRecord) return;
setRecords((prev) => prev.map((r) => (
r.id === opsRecord.id ? { ...r, opsManagers: managers } : r
)));
setOpsRecord(null);
showToast('运维负责人已更新');
};
const handleSaveOperateCity = (location: string) => {
if (!operateCityRecord) return;
setRecords((prev) => prev.map((r) => (
r.id === operateCityRecord.id
? { ...r, location, operateCitySource: '人工' as const }
: r
)));
setOperateCityRecord(null);
showToast('运营城市已更新');
};
const handleOpenContract = (_record: VehicleRecord, contractNo: string) => {
showToast(`即将跳转至合同详情(原型演示):${contractNo}`);
};
const handleOpenLeaseContract = (record: VehicleRecord, contractNo: string) => {
showToast(`即将跳转至租赁合同详情(原型演示):${record.plateNo} · ${contractNo}`);
};
const handleOpenDeliveryRecord = (record: VehicleRecord, row: LeaseRecordRow) => {
showToast(`即将跳转至交车管理详情(原型演示):${record.plateNo} · ${row.contractNo} · ${row.deliveryDate}`);
};
const handleOpenReturnRecord = (record: VehicleRecord, row: LeaseRecordRow) => {
showToast(`即将跳转至还车管理详情(原型演示):${record.plateNo} · ${row.contractNo} · ${row.returnDate}`);
};
const handleOpenLicenseEdit = (record: VehicleRecord) => {
showToast(`即将跳转至证照管理编辑页(原型演示):${record.plateNo}`);
};
const handleOpenInsurancePurchase = (record: VehicleRecord, insuranceLabel: string) => {
showToast(`即将跳转至保险采购编辑页(原型演示):${record.plateNo} · ${insuranceLabel}`);
};
const handleViewFaultDetail = (record: VehicleRecord, row: { faultNo: string }) => {
showToast(`即将跳转至故障管理详情页(原型演示):${row.faultNo}`);
};
const handleEditFaultRecord = (record: VehicleRecord, row: { faultNo: string }) => {
showToast(`编辑故障记录(原型演示):${record.plateNo} · ${row.faultNo}`);
};
const handleOpenAccidentDetail = (record: VehicleRecord, row: AccidentRecordRow) => {
showToast(`即将跳转至事故管理详情页(原型演示):${row.accidentCode}`);
};
const handleOpenViolationDetail = (_record: VehicleRecord, row: ViolationRecordRow) => {
showToast(`即将跳转至违章管理详情页(原型演示):${row.violationTime}`);
};
const handleOpenMovementDetail = (_record: VehicleRecord, row: MovementRecordRow) => {
showToast(`即将跳转至异动管理详情页(原型演示):${row.startDate}`);
};
const handleOpenTransferDetail = (_record: VehicleRecord, row: TransferRecordRow) => {
showToast(`即将跳转至车辆调拨详情页(原型演示):${row.transferDate}`);
};
const handleOpenAnnualReviewDetail = (_record: VehicleRecord, row: AnnualReviewRecordRow) => {
showToast(`即将跳转至年审管理详情页(原型演示):${row.inspectionValidUntil}`);
};
const handlePreviewPolicyFile = (fileName: string) => {
showToast(`预览保单文件(原型演示):${fileName}`);
};
const handleDownloadPolicyFile = (fileName: string) => {
showToast(`已开始下载:${fileName}(原型演示)`);
};
const handleOpenDeliveryManagement = (record: VehicleRecord) => {
showToast(`即将跳转至交车管理(原型演示):${record.plateNo}`);
};
const handleOpenReturnManagement = (record: VehicleRecord) => {
showToast(`即将跳转至还车管理(原型演示):${record.plateNo}`);
};
const handleSaveEdit = (record: VehicleRecord) => {
setRecords((prev) => prev.map((r) => (r.id === record.id ? record : r)));
setEditRecord(null);
showToast('车辆信息已更新');
};
const handleDetailFieldSave = (recordId: string, patch: Partial<VehicleRecord>) => {
setRecords((prev) => prev.map((r) => (r.id === recordId ? { ...r, ...patch } : r)));
showToast('车辆信息已更新');
};
const handleDownloadTestCases = () => {
downloadVehicleTestCasesExcel(showToast);
};
const docToolbar = (
<PageDocToolbar onDownloadTestCases={handleDownloadTestCases} />
);
const activeDetail = detailRecord
? records.find((r) => r.id === detailRecord.id) ?? detailRecord
: null;
const handleDirectoryRoute = useCallback((node: AnnotationDirectoryRouteNode) => {
const payload = node.payload as { pageId?: string } | undefined;
const pageId = typeof payload?.pageId === 'string' ? payload.pageId : '';
if (pageId === 'list') {
setDetailRecord(null);
return;
}
if (pageId === 'detail') {
setDetailRecord((current) => current ?? filtered[0] ?? records[0] ?? null);
}
}, [filtered, records]);
const annotationOptions = useMemo<AnnotationViewerOptions>(
() => ({
showToolbar: true,
showThemeToggle: true,
showColorFilter: true,
emptyWhenNoData: false,
toolbarEdge: 'right',
currentPageId: activeDetail ? 'detail' : 'list',
onDirectoryRoute: handleDirectoryRoute,
}),
[activeDetail, handleDirectoryRoute],
);
return (
<>
<div className="vm-page clay-vm-page">
{docToolbar}
{!activeDetail ? <ClayBackupBanner /> : null}
{activeDetail ? (
<DetailPage
record={activeDetail}
onBack={() => setDetailRecord(null)}
canEditOpsManager={CAN_EDIT_OPS_MANAGER}
onOpenLicenseEdit={handleOpenLicenseEdit}
onSaveField={handleDetailFieldSave}
onOpenLeaseContract={handleOpenLeaseContract}
onOpenDeliveryRecord={handleOpenDeliveryRecord}
onOpenReturnRecord={handleOpenReturnRecord}
onOpenInsurancePurchase={handleOpenInsurancePurchase}
onViewFaultDetail={handleViewFaultDetail}
onEditFaultRecord={handleEditFaultRecord}
onOpenAccidentDetail={handleOpenAccidentDetail}
onOpenViolationDetail={handleOpenViolationDetail}
onOpenMovementDetail={handleOpenMovementDetail}
onOpenTransferDetail={handleOpenTransferDetail}
onOpenAnnualReviewDetail={handleOpenAnnualReviewDetail}
onPreviewPolicyFile={handlePreviewPolicyFile}
onDownloadPolicyFile={handleDownloadPolicyFile}
/>
) : (
<>
<FilterPanel
records={records}
filters={pendingFilters}
onChange={(patch) => setPendingFilters((prev) => ({ ...prev, ...patch }))}
onReset={handleReset}
onSearch={handleSearch}
/>
<KpiCards counts={kpiCounts} active={kpiTab} onChange={(key) => { setKpiTab(key); setPage(1); }} />
<section className="vm-table-section">
<div className="vm-table-toolbar" data-annotation-id="vm-table-toolbar">
<div className="vm-table-actions">
<button type="button" className="vm-btn vm-btn-ghost" onClick={handleExport}>
<Download size={16} aria-hidden />
</button>
<button type="button" className="vm-btn vm-btn-secondary" onClick={() => setImportOpen(true)}>
<FileUp size={16} aria-hidden />
</button>
</div>
</div>
<div className="vm-table-card">
<VehicleTable
records={paged}
insuranceExpireMap={insuranceExpireMap}
canEditOpsManager={CAN_EDIT_OPS_MANAGER}
isAdmin={IS_ADMIN}
onView={setDetailRecord}
onEdit={setEditRecord}
onEditOpsManager={setOpsRecord}
onEditOperateCity={setOperateCityRecord}
onOpenContract={handleOpenContract}
onOpenLicenseEdit={handleOpenLicenseEdit}
onOpenInsurancePurchase={handleOpenInsurancePurchase}
onOpenLocationMap={setLocationMapRecord}
onOpenDeliveryManagement={handleOpenDeliveryManagement}
onOpenReturnManagement={handleOpenReturnManagement}
/>
<div className="vm-table-footer">
<TablePagination
page={Math.min(page, totalPages)}
pageSize={pageSize}
total={filtered.length}
onPageChange={setPage}
onPageSizeChange={handlePageSizeChange}
/>
</div>
</div>
</section>
</>
)}
<ImportModal open={importOpen} onClose={() => setImportOpen(false)} onImport={handleImport} />
<OpsManagerModal
open={!!opsRecord}
record={opsRecord}
onClose={() => setOpsRecord(null)}
onSave={handleSaveOps}
/>
<OperateCityEditModal
open={!!operateCityRecord}
record={operateCityRecord}
records={records}
onClose={() => setOperateCityRecord(null)}
onSave={handleSaveOperateCity}
/>
<LocationMapModal
open={!!locationMapRecord}
record={locationMapRecord}
onClose={() => setLocationMapRecord(null)}
/>
<VehicleEditModal
open={!!editRecord}
record={editRecord}
records={records}
onClose={() => setEditRecord(null)}
onSave={handleSaveEdit}
/>
{toast && <div className="vm-toast" role="status" aria-live="polite">{toast}</div>}
</div>
<AnnotationViewer
source={annotationSourceDocument as AnnotationSourceDocument}
options={annotationOptions}
/>
</>
);
}

View File

@@ -1,391 +0,0 @@
/**
* 车辆管理 · Clay 主题备份
* 设计基底src/themes/clay/DESIGN.md
*/
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
.clay-vm-page {
/* Clay tokens → vm/ln aliases */
--clay-primary: #0a0a0a;
--clay-primary-active: #1f1f1f;
--clay-ink: #0a0a0a;
--clay-body: #3a3a3a;
--clay-muted: #6a6a6a;
--clay-muted-soft: #9a9a9a;
--clay-hairline: #e5e5e5;
--clay-canvas: #fffaf0;
--clay-surface-soft: #faf5e8;
--clay-surface-card: #f5f0e0;
--clay-surface-strong: #ebe6d6;
--clay-brand-pink: #ff4d8b;
--clay-brand-teal: #1a3a3a;
--clay-brand-lavender: #b8a4ed;
--clay-brand-peach: #ffb084;
--clay-brand-ochre: #e8b94a;
--clay-brand-mint: #a4d4c5;
--clay-brand-coral: #ff6b5a;
--clay-on-dark: #ffffff;
--clay-radius-control: 12px;
--clay-radius-card: 24px;
--clay-radius-pill: 9999px;
--ln-primary: var(--clay-primary);
--ln-primary-hover: var(--clay-primary-active);
--ln-primary-focus: var(--clay-primary-active);
--ln-primary-active: var(--clay-primary-active);
--ln-primary-soft: rgba(10, 10, 10, 0.06);
--ln-primary-soft-strong: rgba(10, 10, 10, 0.1);
--ln-ink: var(--clay-ink);
--ln-body: var(--clay-body);
--ln-body-strong: #1a1a1a;
--ln-muted: var(--clay-muted);
--ln-muted-soft: var(--clay-muted-soft);
--ln-hairline: var(--clay-hairline);
--ln-hairline-strong: #d4d4d4;
--ln-canvas: var(--clay-canvas);
--ln-canvas-parchment: var(--clay-canvas);
--ln-surface-pearl: var(--clay-surface-soft);
--ln-surface-card: var(--clay-surface-card);
--ln-surface-strong: var(--clay-surface-strong);
--ln-on-primary: var(--clay-on-dark);
--ln-success: #22c55e;
--ln-warning: #f59e0b;
--ln-error: #ef4444;
--ln-radius-xs: 6px;
--ln-radius-sm: 8px;
--ln-radius-control: var(--clay-radius-control);
--ln-radius-card: var(--clay-radius-card);
--ln-radius-xl: var(--clay-radius-card);
--ln-radius-pill: var(--clay-radius-pill);
--ln-shadow-soft: none;
--ln-shadow-hover: 0 8px 24px rgba(10, 10, 10, 0.08);
--ln-shadow-float: 0 12px 32px rgba(10, 10, 10, 0.1);
--ln-canvas-soft: var(--clay-surface-soft);
--ln-link: var(--clay-ink);
--vm-primary: var(--clay-primary);
--vm-primary-dark: var(--clay-primary-active);
--vm-primary-soft: var(--ln-primary-soft);
--vm-bg: var(--clay-canvas);
--vm-surface: var(--clay-surface-card);
--vm-border: var(--clay-hairline);
--vm-text: var(--clay-ink);
--vm-muted: var(--clay-muted);
--vm-link: var(--clay-ink);
--vm-btn-height: 44px;
--vm-control-height: 44px;
--vm-focus-border-color: var(--clay-ink);
--vm-font: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
background: var(--clay-canvas);
color: var(--clay-body);
letter-spacing: 0;
}
.clay-vm-page .clay-backup-banner {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 12px 16px;
margin-bottom: 24px;
padding: 14px 20px;
border: 1px solid var(--clay-hairline);
border-radius: var(--clay-radius-card);
background: linear-gradient(135deg, var(--clay-surface-soft) 0%, var(--clay-surface-card) 100%);
}
.clay-vm-page .clay-backup-banner-main {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.clay-vm-page .clay-backup-eyebrow {
font-size: 12px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--clay-muted);
}
.clay-vm-page .clay-backup-title {
margin: 0;
font-size: 1.125rem;
font-weight: 600;
letter-spacing: -0.02em;
color: var(--clay-ink);
}
.clay-vm-page .clay-backup-desc {
margin: 0;
font-size: 0.875rem;
line-height: 1.5;
color: var(--clay-muted);
}
.clay-vm-page .clay-backup-link {
display: inline-flex;
align-items: center;
min-height: 44px;
padding: 0 16px;
border: 1px solid var(--clay-hairline);
border-radius: var(--clay-radius-control);
background: var(--clay-canvas);
color: var(--clay-ink);
font-size: 0.875rem;
font-weight: 600;
text-decoration: none;
cursor: pointer;
white-space: nowrap;
}
.clay-vm-page .clay-backup-link:hover {
border-color: var(--clay-ink);
background: var(--clay-surface-soft);
}
.clay-vm-page .clay-backup-link:focus-visible {
outline: 2px solid var(--clay-ink);
outline-offset: 2px;
}
/* Filter */
.clay-vm-page .vm-filter-card {
border-radius: var(--clay-radius-card);
border-color: var(--clay-hairline);
background: var(--clay-surface-card);
box-shadow: none;
}
.clay-vm-page .vm-filter-title {
font-weight: 600;
letter-spacing: -0.02em;
color: var(--clay-ink);
}
.clay-vm-page .vm-input,
.clay-vm-page .vm-filter-picker-control,
.clay-vm-page .vm-plate-trigger,
.clay-vm-page .vm-filter-card .vm-input:not(textarea) {
min-height: 44px;
height: 44px;
border-radius: var(--clay-radius-control);
background: var(--clay-canvas);
}
/* Buttons */
.clay-vm-page .vm-btn {
min-height: 44px;
height: 44px;
border-radius: var(--clay-radius-control);
font-weight: 600;
}
.clay-vm-page .vm-btn-primary {
background: var(--clay-primary);
color: var(--clay-on-dark);
border-color: var(--clay-primary);
}
.clay-vm-page .vm-btn-primary:hover:not(:disabled) {
background: var(--clay-primary-active);
border-color: var(--clay-primary-active);
}
.clay-vm-page .vm-btn-secondary,
.clay-vm-page .vm-btn-ghost {
background: var(--clay-canvas);
border-color: var(--clay-hairline);
color: var(--clay-ink);
}
.clay-vm-page .vm-btn-link {
color: var(--clay-ink);
font-weight: 600;
}
/* KPI — Clay saturated card cycle */
.clay-vm-page .vm-kpi-row {
gap: 12px;
}
.clay-vm-page .vm-kpi-card {
border: none;
border-radius: var(--clay-radius-card);
min-height: 96px;
padding: 20px 36px 20px 20px;
box-shadow: none;
}
.clay-vm-page .vm-kpi-card:hover {
box-shadow: var(--ln-shadow-hover);
}
.clay-vm-page .vm-kpi-card.active {
box-shadow: 0 0 0 2px var(--clay-ink);
}
.clay-vm-page .vm-kpi-card:nth-child(1) { background: var(--clay-surface-card); color: var(--clay-ink); }
.clay-vm-page .vm-kpi-card:nth-child(2) { background: var(--clay-brand-pink); color: var(--clay-on-dark); }
.clay-vm-page .vm-kpi-card:nth-child(3) { background: var(--clay-brand-teal); color: var(--clay-on-dark); }
.clay-vm-page .vm-kpi-card:nth-child(4) { background: var(--clay-brand-lavender); color: var(--clay-ink); }
.clay-vm-page .vm-kpi-card:nth-child(5) { background: var(--clay-brand-peach); color: var(--clay-ink); }
.clay-vm-page .vm-kpi-card:nth-child(6) { background: var(--clay-brand-ochre); color: var(--clay-ink); }
.clay-vm-page .vm-kpi-card:nth-child(7) { background: var(--clay-brand-coral); color: var(--clay-on-dark); }
.clay-vm-page .vm-kpi-card:nth-child(8) { background: var(--clay-brand-mint); color: var(--clay-ink); }
.clay-vm-page .vm-kpi-card:nth-child(1) .vm-kpi-eyebrow,
.clay-vm-page .vm-kpi-card:nth-child(4) .vm-kpi-eyebrow,
.clay-vm-page .vm-kpi-card:nth-child(5) .vm-kpi-eyebrow,
.clay-vm-page .vm-kpi-card:nth-child(6) .vm-kpi-eyebrow,
.clay-vm-page .vm-kpi-card:nth-child(8) .vm-kpi-eyebrow { color: var(--clay-muted); }
.clay-vm-page .vm-kpi-card:nth-child(2) .vm-kpi-eyebrow,
.clay-vm-page .vm-kpi-card:nth-child(3) .vm-kpi-eyebrow,
.clay-vm-page .vm-kpi-card:nth-child(7) .vm-kpi-eyebrow { color: rgba(255, 255, 255, 0.82); }
.clay-vm-page .vm-kpi-card:nth-child(1) .vm-kpi-val,
.clay-vm-page .vm-kpi-card:nth-child(4) .vm-kpi-val,
.clay-vm-page .vm-kpi-card:nth-child(5) .vm-kpi-val,
.clay-vm-page .vm-kpi-card:nth-child(6) .vm-kpi-val,
.clay-vm-page .vm-kpi-card:nth-child(8) .vm-kpi-val { color: var(--clay-ink); }
.clay-vm-page .vm-kpi-card:nth-child(2) .vm-kpi-val,
.clay-vm-page .vm-kpi-card:nth-child(3) .vm-kpi-val,
.clay-vm-page .vm-kpi-card:nth-child(7) .vm-kpi-val { color: var(--clay-on-dark); }
.clay-vm-page .vm-kpi-card.active .vm-kpi-val { color: inherit; }
.clay-vm-page .vm-kpi-icon {
width: 40px;
height: 40px;
border-radius: var(--clay-radius-control);
background: rgba(255, 255, 255, 0.22);
color: inherit;
}
.clay-vm-page .vm-kpi-card:nth-child(1) .vm-kpi-icon {
background: rgba(10, 10, 10, 0.06);
color: var(--clay-ink);
}
.clay-vm-page .vm-kpi-tip {
color: inherit;
opacity: 0.72;
}
/* Table */
.clay-vm-page .vm-table-card {
border-radius: var(--clay-radius-card);
border-color: var(--clay-hairline);
background: var(--clay-canvas);
box-shadow: none;
}
.clay-vm-page .vm-table th {
background: var(--clay-surface-soft);
color: var(--clay-muted);
font-weight: 600;
}
.clay-vm-page .vm-table tbody tr:hover td {
background: var(--clay-surface-soft);
}
.clay-vm-page .vm-plate-link,
.clay-vm-page .vm-cell-link {
color: var(--clay-ink);
font-weight: 600;
}
.clay-vm-page .vm-plate-link:hover,
.clay-vm-page .vm-cell-link:hover {
color: var(--clay-primary-active);
}
/* Modals & toast */
.clay-vm-page .vm-modal {
border-radius: var(--clay-radius-card);
background: var(--clay-canvas);
}
.clay-vm-page .vm-toast {
border-radius: var(--clay-radius-control);
background: var(--clay-primary);
color: var(--clay-on-dark);
}
.clay-vm-page .vm-doc-toolbar-btn {
min-height: 44px;
border-radius: var(--clay-radius-control);
background: var(--clay-canvas);
}
/* Detail page */
.clay-vm-page .vm-detail-card {
border-radius: var(--clay-radius-card);
border-color: var(--clay-hairline);
background: var(--clay-canvas);
box-shadow: none;
}
.clay-vm-page .vm-detail-plate {
color: var(--clay-ink);
letter-spacing: -0.02em;
}
.clay-vm-page .vm-detail-stats {
border-color: var(--clay-hairline);
background: var(--clay-surface-soft);
border-radius: var(--clay-radius-control);
}
.clay-vm-page .vm-detail-stat {
border-color: var(--clay-hairline);
}
.clay-vm-page .vm-tabs {
border-color: var(--clay-hairline);
}
.clay-vm-page .vm-tab {
min-height: 44px;
color: var(--clay-muted);
font-weight: 600;
}
.clay-vm-page .vm-tab.active {
color: var(--clay-ink);
border-color: var(--clay-ink);
}
.clay-vm-page .vm-btn-back {
min-height: 44px;
border-radius: var(--clay-radius-control);
color: var(--clay-ink);
}
@media (max-width: 1280px) {
.clay-vm-page .vm-kpi-row {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
@media (max-width: 768px) {
.clay-vm-page .vm-kpi-row {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.clay-vm-page {
padding: 16px;
}
}
@media (prefers-reduced-motion: reduce) {
.clay-vm-page .vm-kpi-card,
.clay-vm-page .vm-btn {
transition: none;
}
}