迭代 ONE-OS 多原型:租赁台账与租赁合同增强,加氢站站点独立原型与氢费数据桥接
- 租赁业务台账:空值留白、排序/列宽拖拽、操作列与交互优化 - 租赁合同:列表展开列合并、在租明细弹窗、编辑回填与样式修复 - 加氢站:新增站点信息独立原型,订单/记录页与共享样式调整 - 车辆氢费明细对接 h2VehicleLedgerBridge;移除 vehicle-management-clay - 同步 sidebar-tree 与标注提取 output Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"updatedAt": "2026-07-01T02:59:09.593Z",
|
"updatedAt": "2026-07-01T09:58:50.712Z",
|
||||||
"prototypes": [
|
"prototypes": [
|
||||||
{
|
{
|
||||||
"id": "folder-1782874576229-r8atbu",
|
"id": "folder-1782874576229-r8atbu",
|
||||||
@@ -17,12 +17,6 @@
|
|||||||
"kind": "item",
|
"kind": "item",
|
||||||
"title": "车辆资产",
|
"title": "车辆资产",
|
||||||
"itemKey": "prototypes/vehicle-management"
|
"itemKey": "prototypes/vehicle-management"
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "item:prototypes:vehicle-management-clay",
|
|
||||||
"kind": "item",
|
|
||||||
"title": "车辆资产(Clay)",
|
|
||||||
"itemKey": "prototypes/vehicle-management-clay"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -69,6 +63,19 @@
|
|||||||
"itemKey": "prototypes/contract-template-management"
|
"itemKey": "prototypes/contract-template-management"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "folder-1782895924552-goclwo",
|
||||||
|
"kind": "folder",
|
||||||
|
"title": "加氢站管理",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "item:prototypes:oneos-web-h2-station-site",
|
||||||
|
"kind": "item",
|
||||||
|
"title": "站点信息",
|
||||||
|
"itemKey": "prototypes/oneos-web-h2-station-site"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
285
src/common/h2VehicleLedgerBridge.js
Normal file
285
src/common/h2VehicleLedgerBridge.js
Normal 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 seed(10 条) */
|
||||||
|
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
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -35,37 +35,52 @@ export function FilterPanel({ records, filters, onChange, onReset, onSearch }: F
|
|||||||
{ label: '付款日期', control: (
|
{ label: '付款日期', control: (
|
||||||
<div className="ldb-date-range">
|
<div className="ldb-date-range">
|
||||||
<input type="date" className="vm-input" value={filters.paymentFrom} aria-label="付款开始日期" onChange={(e) => onChange({ paymentFrom: e.target.value })} />
|
<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 })} />
|
<input type="date" className="vm-input" value={filters.paymentTo} aria-label="付款结束日期" onChange={(e) => onChange({ paymentTo: e.target.value })} />
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const visible = expanded ? fields : fields.slice(0, DEFAULT_VISIBLE);
|
const primaryFields = fields.slice(0, DEFAULT_VISIBLE);
|
||||||
|
const extraFields = fields.slice(DEFAULT_VISIBLE);
|
||||||
|
|
||||||
return (
|
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">
|
<header className="vm-filter-header">
|
||||||
<h1 className="ldb-page-title">租赁业务台账</h1>
|
<h1 className="ldb-page-title">租赁业务台账</h1>
|
||||||
</header>
|
</header>
|
||||||
<div className="vm-filter-grid">
|
<div className="vm-filter-grid">
|
||||||
{visible.map((field) => (
|
{primaryFields.map((field) => (
|
||||||
<label key={field.label} className="vm-filter-field">
|
<label key={field.label} className="vm-filter-field">
|
||||||
<span>{field.label}</span>
|
<span>{field.label}</span>
|
||||||
{field.control}
|
{field.control}
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
</div>
|
</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">
|
<div className="vm-filter-actions">
|
||||||
{fields.length > DEFAULT_VISIBLE && (
|
{extraFields.length > 0 && (
|
||||||
<button type="button" className="vm-btn vm-btn-link" onClick={() => setExpanded(!expanded)}>
|
<button type="button" className="vm-btn vm-btn-link ldb-filter-toggle" onClick={() => setExpanded(!expanded)} aria-expanded={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>
|
||||||
)}
|
)}
|
||||||
<button type="button" className="vm-btn vm-btn-ghost" onClick={onReset}>重置</button>
|
<button type="button" className="vm-btn vm-btn-ghost ldb-toolbar-btn" onClick={onReset}>重置</button>
|
||||||
<button type="button" className="vm-btn vm-btn-primary" onClick={onSearch}>
|
<button type="button" className="vm-btn vm-btn-primary ldb-toolbar-btn" onClick={onSearch}>
|
||||||
<Search size={16} aria-hidden />
|
<Search size={16} aria-hidden />
|
||||||
查询
|
查询
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,20 +1,24 @@
|
|||||||
import React from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { AlertTriangle, CircleHelp } from 'lucide-react';
|
import { ArrowDown, ArrowUp, ArrowUpDown, AlertTriangle, CircleHelp } from 'lucide-react';
|
||||||
import type { AccidentRecord, LeaseLedgerRow, MaintenanceOrder } from '../types';
|
import type { AccidentRecord, LeaseLedgerRow, MaintenanceOrder } from '../types';
|
||||||
import {
|
import {
|
||||||
calcMonthlyIncome,
|
calcMonthlyIncome,
|
||||||
calcUnreceived,
|
calcUnreceived,
|
||||||
canStaffDeleteRow,
|
canStaffDeleteRow,
|
||||||
canStaffEditRow,
|
canStaffEditRow,
|
||||||
|
displayMoney,
|
||||||
|
displayText,
|
||||||
formatMoney,
|
formatMoney,
|
||||||
|
isBlankDisplayValue,
|
||||||
needsAccidentLinkWarning,
|
needsAccidentLinkWarning,
|
||||||
needsMaintenanceLinkWarning,
|
needsMaintenanceLinkWarning,
|
||||||
} from '../utils/ledger';
|
} from '../utils/ledger';
|
||||||
import { LEASE_TABLE_COLUMNS, STATUS_CLASS } from './tableColumns';
|
import { LEASE_TABLE_COLUMNS, STATUS_CLASS, type LeaseTableColumn } from './tableColumns';
|
||||||
|
|
||||||
interface LeaseLedgerTableProps {
|
interface LeaseLedgerTableProps {
|
||||||
records: LeaseLedgerRow[];
|
records: LeaseLedgerRow[];
|
||||||
selectedIds: Set<string>;
|
selectedIds: Set<string>;
|
||||||
|
loading?: boolean;
|
||||||
maintenanceOrders: MaintenanceOrder[];
|
maintenanceOrders: MaintenanceOrder[];
|
||||||
accidentRecords: AccidentRecord[];
|
accidentRecords: AccidentRecord[];
|
||||||
onToggleRow: (id: string, checked: boolean) => void;
|
onToggleRow: (id: string, checked: boolean) => void;
|
||||||
@@ -31,6 +35,62 @@ interface LeaseLedgerTableProps {
|
|||||||
onDeductionDetailChange: (recordId: string, value: string) => void;
|
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 }) {
|
function IncomeWarning({ title, onClick }: { title: string; onClick: () => void }) {
|
||||||
return (
|
return (
|
||||||
<button type="button" className="ldb-income-warn" title={title} aria-label={title} onClick={onClick}>
|
<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 }) {
|
function HeaderWithTip({
|
||||||
if (!tip) return <>{title}</>;
|
title,
|
||||||
return (
|
tip,
|
||||||
<span className="ldb-th-with-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}
|
{title}
|
||||||
<span className="ldb-th-tip" tabIndex={0} aria-label={`${title}说明`}>
|
{tip && (
|
||||||
<CircleHelp size={14} aria-hidden />
|
<span className="ldb-th-tip" tabIndex={0} aria-label={`${title}说明`} onClick={(e) => e.stopPropagation()}>
|
||||||
<span className="ldb-kpi-tooltip" role="tooltip">{tip}</span>
|
<CircleHelp size={14} aria-hidden />
|
||||||
</span>
|
<span className="ldb-kpi-tooltip" role="tooltip">{tip}</span>
|
||||||
|
</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({
|
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(
|
function renderCell(
|
||||||
row: LeaseLedgerRow,
|
row: LeaseLedgerRow,
|
||||||
col: (typeof LEASE_TABLE_COLUMNS)[number],
|
col: LeaseTableColumn,
|
||||||
index: number,
|
index: number,
|
||||||
ctx: {
|
ctx: {
|
||||||
maintenanceOrders: MaintenanceOrder[];
|
maintenanceOrders: MaintenanceOrder[];
|
||||||
@@ -126,33 +239,33 @@ function renderCell(
|
|||||||
case 'stack-dept':
|
case 'stack-dept':
|
||||||
return (
|
return (
|
||||||
<div className="ldb-cell-stack">
|
<div className="ldb-cell-stack">
|
||||||
<span className="ldb-cell-primary">{row.dept}</span>
|
<span className="ldb-cell-primary">{displayText(row.dept)}</span>
|
||||||
<span className="ldb-cell-secondary">{row.salesman}</span>
|
<span className="ldb-cell-secondary">{displayText(row.salesman)}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case 'stack-plate': {
|
case 'stack-plate': {
|
||||||
const vehicleLine = row.vehicleType || [row.vehicleBrand, row.vehicleModel].filter(Boolean).join('');
|
const vehicleLine = row.vehicleType || [row.vehicleBrand, row.vehicleModel].filter(Boolean).join('');
|
||||||
return (
|
return (
|
||||||
<div className="ldb-cell-stack">
|
<div className="ldb-cell-stack">
|
||||||
<span className="ldb-cell-primary tabular-nums">{row.plateNo}</span>
|
<span className="ldb-cell-primary tabular-nums">{displayText(row.plateNo)}</span>
|
||||||
<span className="ldb-cell-secondary" title={vehicleLine}>{vehicleLine}</span>
|
<span className="ldb-cell-secondary" title={vehicleLine}>{displayText(vehicleLine)}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
case 'stack-customer':
|
case 'stack-customer':
|
||||||
return (
|
return (
|
||||||
<div className="ldb-cell-stack">
|
<div className="ldb-cell-stack">
|
||||||
<span className="ldb-cell-primary" title={row.customerName}>{row.customerName}</span>
|
<span className="ldb-cell-primary" title={row.customerName}>{displayText(row.customerName)}</span>
|
||||||
<span className="ldb-cell-secondary" title={row.projectName}>{row.projectName}</span>
|
<span className="ldb-cell-secondary" title={row.projectName}>{displayText(row.projectName)}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case 'deposit':
|
case 'deposit':
|
||||||
if (row.deposit === '/' || row.deposit === '') return '—';
|
if (isBlankDisplayValue(row.deposit)) return '';
|
||||||
return typeof row.deposit === 'number' ? formatMoney(row.deposit) : String(row.deposit);
|
return typeof row.deposit === 'number' ? formatMoney(row.deposit) : displayText(row.deposit);
|
||||||
case 'asset-owner':
|
case 'asset-owner':
|
||||||
return <span title={row.assetOwner}>{row.assetOwner || '—'}</span>;
|
return <span title={row.assetOwner}>{displayText(row.assetOwner)}</span>;
|
||||||
case 'signing-company':
|
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':
|
case 'monthly-income':
|
||||||
return (
|
return (
|
||||||
<span className="ldb-computed-val" title="租金 + 服务费 − 里程减免 − 其他减免">
|
<span className="ldb-computed-val" title="租金 + 服务费 − 里程减免 − 其他减免">
|
||||||
@@ -201,7 +314,7 @@ function renderCell(
|
|||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
case 'other-income':
|
case 'other-income':
|
||||||
if (!ctx.canEdit) return row.otherIncome ? formatMoney(row.otherIncome) : '—';
|
if (!ctx.canEdit) return displayMoney(row.otherIncome, true);
|
||||||
return (
|
return (
|
||||||
<MoneyInput
|
<MoneyInput
|
||||||
value={row.otherIncome}
|
value={row.otherIncome}
|
||||||
@@ -211,7 +324,7 @@ function renderCell(
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'other-cost':
|
case 'other-cost':
|
||||||
if (!ctx.canEdit) return row.otherCost ? formatMoney(row.otherCost) : '—';
|
if (!ctx.canEdit) return displayMoney(row.otherCost, true);
|
||||||
return (
|
return (
|
||||||
<MoneyInput
|
<MoneyInput
|
||||||
value={row.otherCost}
|
value={row.otherCost}
|
||||||
@@ -222,7 +335,7 @@ function renderCell(
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'brokerage-fee':
|
case 'brokerage-fee':
|
||||||
if (!ctx.canEdit) return row.brokerageFee ? formatMoney(row.brokerageFee) : '—';
|
if (!ctx.canEdit) return displayMoney(row.brokerageFee, true);
|
||||||
return (
|
return (
|
||||||
<MoneyInput
|
<MoneyInput
|
||||||
value={row.brokerageFee}
|
value={row.brokerageFee}
|
||||||
@@ -241,7 +354,7 @@ function renderCell(
|
|||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
case 'invoice-date':
|
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':
|
case 'vehicle-standard-cost':
|
||||||
return (
|
return (
|
||||||
<span className="ldb-cell-readonly" title="车型明细维护的固定标准成本">
|
<span className="ldb-cell-readonly" title="车型明细维护的固定标准成本">
|
||||||
@@ -257,7 +370,7 @@ function renderCell(
|
|||||||
case 'insurance-readonly':
|
case 'insurance-readonly':
|
||||||
return (
|
return (
|
||||||
<span className="ldb-cell-readonly" title="暂不可编辑与导入,后续可能自动计算">
|
<span className="ldb-cell-readonly" title="暂不可编辑与导入,后续可能自动计算">
|
||||||
{row.insuranceFee ? formatMoney(row.insuranceFee) : '—'}
|
{displayMoney(row.insuranceFee, true)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
case 'hydrogen-fee':
|
case 'hydrogen-fee':
|
||||||
@@ -266,17 +379,17 @@ function renderCell(
|
|||||||
className={row.hydrogenBorneBy === '我司承担' ? 'ldb-computed-val' : 'ldb-cell-readonly'}
|
className={row.hydrogenBorneBy === '我司承担' ? 'ldb-computed-val' : 'ldb-cell-readonly'}
|
||||||
title={row.hydrogenBorneBy === '我司承担' ? '我司承担氢费账单期汇总' : '客户承担或无氢费'}
|
title={row.hydrogenBorneBy === '我司承担' ? '我司承担氢费账单期汇总' : '客户承担或无氢费'}
|
||||||
>
|
>
|
||||||
{row.hydrogenFee ? formatMoney(row.hydrogenFee) : '—'}
|
{displayMoney(row.hydrogenFee, true)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
case 'gps-mileage':
|
case 'gps-mileage':
|
||||||
return (
|
return (
|
||||||
<span className="ldb-computed-val tabular-nums" title="BI 里程 · 账单时间段">
|
<span className="ldb-computed-val tabular-nums" title="BI 里程 · 账单时间段">
|
||||||
{row.gpsMileage || '—'}
|
{displayText(row.gpsMileage)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
case 'deduction-detail':
|
case 'deduction-detail':
|
||||||
if (!ctx.canEdit) return row.otherDeductionDetail || '—';
|
if (!ctx.canEdit) return displayText(row.otherDeductionDetail);
|
||||||
return (
|
return (
|
||||||
<TextInput
|
<TextInput
|
||||||
value={row.otherDeductionDetail}
|
value={row.otherDeductionDetail}
|
||||||
@@ -286,7 +399,7 @@ function renderCell(
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'remark-text':
|
case 'remark-text':
|
||||||
if (!ctx.canEdit) return row.remark || '—';
|
if (!ctx.canEdit) return displayText(row.remark);
|
||||||
return (
|
return (
|
||||||
<TextInput
|
<TextInput
|
||||||
value={row.remark}
|
value={row.remark}
|
||||||
@@ -312,13 +425,24 @@ function renderCell(
|
|||||||
|
|
||||||
const raw = col.key === '_index' ? index + 1 : row[col.key as keyof LeaseLedgerRow];
|
const raw = col.key === '_index' ? index + 1 : row[col.key as keyof LeaseLedgerRow];
|
||||||
if (col.money && typeof raw === 'number') return formatMoney(raw);
|
if (col.money && typeof raw === 'number') return formatMoney(raw);
|
||||||
if (raw === '' || raw === null || raw === undefined) return '—';
|
if (isBlankDisplayValue(raw)) return '';
|
||||||
return String(raw);
|
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({
|
export function LeaseLedgerTable({
|
||||||
records,
|
records,
|
||||||
selectedIds,
|
selectedIds,
|
||||||
|
loading = false,
|
||||||
maintenanceOrders,
|
maintenanceOrders,
|
||||||
accidentRecords,
|
accidentRecords,
|
||||||
onToggleRow,
|
onToggleRow,
|
||||||
@@ -334,47 +458,145 @@ export function LeaseLedgerTable({
|
|||||||
onRemarkChange,
|
onRemarkChange,
|
||||||
onDeductionDetailChange,
|
onDeductionDetailChange,
|
||||||
}: LeaseLedgerTableProps) {
|
}: 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>;
|
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 (
|
return (
|
||||||
<div className="vm-table-wrap ldb-table-wrap" data-annotation-id="lbl-table">
|
<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>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th className="ldb-col-check sticky-col-left">
|
<th className="ldb-col-check sticky-col-left">
|
||||||
<input type="checkbox" checked={allSelected} onChange={(e) => onToggleAll(e.target.checked)} aria-label="全选" />
|
<input type="checkbox" checked={allSelected} onChange={(e) => onToggleAll(e.target.checked)} aria-label="全选" />
|
||||||
</th>
|
</th>
|
||||||
{LEASE_TABLE_COLUMNS.map((col) => (
|
{LEASE_TABLE_COLUMNS.map((col) => {
|
||||||
<th
|
const sortable = col.sortable !== false && col.key !== '_index';
|
||||||
key={col.key}
|
return (
|
||||||
className={[
|
<th
|
||||||
col.money ? 'ldb-col-money' : '',
|
key={col.key}
|
||||||
col.className,
|
className={[
|
||||||
col.sticky === 'left' ? 'sticky-col-left' : '',
|
col.money ? 'ldb-col-money' : '',
|
||||||
].filter(Boolean).join(' ')}
|
col.className,
|
||||||
data-annotation-id={col.annotationId}
|
col.sticky === 'left' ? 'sticky-col-left' : '',
|
||||||
title={
|
'ldb-col-resizable',
|
||||||
col.key === 'opsFeeIncome'
|
].filter(Boolean).join(' ')}
|
||||||
? '收入侧运维费'
|
data-annotation-id={col.annotationId}
|
||||||
: col.key === 'opsFeeCost'
|
title={
|
||||||
? '成本侧运维费,与收入侧平进平出'
|
col.key === 'opsFeeIncome'
|
||||||
: undefined
|
? '收入侧运维费'
|
||||||
}
|
: col.key === 'opsFeeCost'
|
||||||
>
|
? '成本侧运维费,与收入侧平进平出'
|
||||||
<HeaderWithTip title={col.title} tip={col.headerTip} />
|
: undefined
|
||||||
</th>
|
}
|
||||||
))}
|
>
|
||||||
|
<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>
|
<th className="sticky-col-right ldb-col-actions">操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{records.map((row, index) => {
|
{sortedRecords.map((row, index) => {
|
||||||
const canEdit = canStaffEditRow(row);
|
const canEdit = canStaffEditRow(row);
|
||||||
const canDelete = canStaffDeleteRow(row);
|
const canDelete = canStaffDeleteRow(row);
|
||||||
|
const isDraft = row.status === '草稿';
|
||||||
const showReceiptLink = row.status === '已确认' || row.status === '部分收款';
|
const showReceiptLink = row.status === '已确认' || row.status === '部分收款';
|
||||||
const cellCtx = {
|
const cellCtx = {
|
||||||
maintenanceOrders,
|
maintenanceOrders,
|
||||||
@@ -412,37 +634,23 @@ export function LeaseLedgerTable({
|
|||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
<td className="sticky-col-right ldb-col-actions">
|
<td className="sticky-col-right ldb-col-actions">
|
||||||
{showReceiptLink && (
|
<div className="ldb-row-actions">
|
||||||
<>
|
{showReceiptLink && (
|
||||||
<button type="button" className="vm-btn vm-btn-link" onClick={() => onLinkReceipt(row)}>
|
<button type="button" className="ldb-action-btn ldb-action-btn--link" onClick={() => onLinkReceipt(row)}>
|
||||||
关联收款记录
|
关联收款
|
||||||
</button>
|
</button>
|
||||||
<span className="ldb-action-sep" aria-hidden>|</span>
|
)}
|
||||||
</>
|
{canEdit && (
|
||||||
)}
|
<button type="button" className="ldb-action-btn ldb-action-btn--edit" onClick={() => onEdit(row)}>
|
||||||
{needsMaintenanceLinkWarning(row, maintenanceOrders) && (
|
编辑
|
||||||
<>
|
|
||||||
<button type="button" className="vm-btn vm-btn-link" onClick={() => onLinkMaintenance(row)}>
|
|
||||||
关联维保
|
|
||||||
</button>
|
</button>
|
||||||
<span className="ldb-action-sep" aria-hidden>|</span>
|
)}
|
||||||
</>
|
{isDraft && canDelete && (
|
||||||
)}
|
<button type="button" className="ldb-action-btn ldb-action-btn--delete" onClick={() => onDelete(row)}>
|
||||||
{needsAccidentLinkWarning(row, accidentRecords) && (
|
删除
|
||||||
<>
|
|
||||||
<button type="button" className="vm-btn vm-btn-link" onClick={() => onLinkAccident(row)}>
|
|
||||||
关联事故
|
|
||||||
</button>
|
</button>
|
||||||
<span className="ldb-action-sep" aria-hidden>|</span>
|
)}
|
||||||
</>
|
</div>
|
||||||
)}
|
|
||||||
<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>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export interface LeaseTableColumn {
|
|||||||
className?: string;
|
className?: string;
|
||||||
annotationId?: string;
|
annotationId?: string;
|
||||||
headerTip?: string;
|
headerTip?: string;
|
||||||
|
sortable?: boolean;
|
||||||
|
defaultWidth?: number;
|
||||||
render?:
|
render?:
|
||||||
| 'stack-dept' | 'stack-plate' | 'stack-customer' | 'status' | 'deposit'
|
| 'stack-dept' | 'stack-plate' | 'stack-customer' | 'status' | 'deposit'
|
||||||
| 'asset-owner' | 'signing-company'
|
| 'asset-owner' | 'signing-company'
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { FilterPanel } from './components/FilterPanel';
|
|||||||
import { LedgerKpiRow } from './components/LedgerKpiRow';
|
import { LedgerKpiRow } from './components/LedgerKpiRow';
|
||||||
import { LeaseLedgerTable } from './components/LeaseLedgerTable';
|
import { LeaseLedgerTable } from './components/LeaseLedgerTable';
|
||||||
import { ConfirmMarkModal } from './components/ConfirmMarkModal';
|
import { ConfirmMarkModal } from './components/ConfirmMarkModal';
|
||||||
|
import { DeleteConfirmModal } from './components/DeleteConfirmModal';
|
||||||
import { ReceiptLinkModal } from './components/ReceiptLinkModal';
|
import { ReceiptLinkModal } from './components/ReceiptLinkModal';
|
||||||
import { MaintenanceLinkModal } from './components/MaintenanceLinkModal';
|
import { MaintenanceLinkModal } from './components/MaintenanceLinkModal';
|
||||||
import { AccidentLinkModal } from './components/AccidentLinkModal';
|
import { AccidentLinkModal } from './components/AccidentLinkModal';
|
||||||
@@ -77,6 +78,9 @@ export default function LeaseBusinessLedgerApp() {
|
|||||||
const [showUnpaidOnly, setShowUnpaidOnly] = useState(false);
|
const [showUnpaidOnly, setShowUnpaidOnly] = useState(false);
|
||||||
const [listViewMode, setListViewMode] = useState<ListViewMode>('all');
|
const [listViewMode, setListViewMode] = useState<ListViewMode>('all');
|
||||||
const [visibleCount, setVisibleCount] = useState(LOAD_BATCH_SIZE);
|
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) => {
|
const showToast = useCallback((msg: string) => {
|
||||||
setToast(msg);
|
setToast(msg);
|
||||||
@@ -109,8 +113,12 @@ export default function LeaseBusinessLedgerApp() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
setAppliedFilters(pendingFilters);
|
setTableLoading(true);
|
||||||
setSelectedIds(new Set());
|
window.setTimeout(() => {
|
||||||
|
setAppliedFilters(pendingFilters);
|
||||||
|
setSelectedIds(new Set());
|
||||||
|
setTableLoading(false);
|
||||||
|
}, 320);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
@@ -274,17 +282,17 @@ export default function LeaseBusinessLedgerApp() {
|
|||||||
仅显示未收款数据
|
仅显示未收款数据
|
||||||
</label>
|
</label>
|
||||||
<div className="vm-table-actions">
|
<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 />
|
<FileUp size={16} aria-hidden />
|
||||||
批量导入
|
批量导入
|
||||||
</button>
|
</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 />
|
<Download size={16} aria-hidden />
|
||||||
导出
|
导出
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="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"
|
data-annotation-id="lbl-btn-mark-confirmed"
|
||||||
onClick={handleMarkConfirmedClick}
|
onClick={handleMarkConfirmedClick}
|
||||||
>
|
>
|
||||||
@@ -293,8 +301,14 @@ export default function LeaseBusinessLedgerApp() {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="vm-btn vm-btn-primary"
|
className="vm-btn vm-btn-primary ldb-toolbar-btn"
|
||||||
onClick={() => showToast(`提交收款审批:已选 ${selectedIds.size} 条(原型演示)`)}
|
onClick={() => {
|
||||||
|
if (selectedIds.size === 0) {
|
||||||
|
showToast('请先勾选需要提交审批的记录');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSubmitApprovalOpen(true);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Send size={16} aria-hidden />
|
<Send size={16} aria-hidden />
|
||||||
提交收款审批
|
提交收款审批
|
||||||
@@ -305,6 +319,7 @@ export default function LeaseBusinessLedgerApp() {
|
|||||||
<div className="vm-table-card">
|
<div className="vm-table-card">
|
||||||
<LeaseLedgerTable
|
<LeaseLedgerTable
|
||||||
records={visibleRecords}
|
records={visibleRecords}
|
||||||
|
loading={tableLoading}
|
||||||
maintenanceOrders={MAINTENANCE_ORDERS}
|
maintenanceOrders={MAINTENANCE_ORDERS}
|
||||||
accidentRecords={ACCIDENT_RECORDS}
|
accidentRecords={ACCIDENT_RECORDS}
|
||||||
selectedIds={selectedIds}
|
selectedIds={selectedIds}
|
||||||
@@ -327,11 +342,8 @@ export default function LeaseBusinessLedgerApp() {
|
|||||||
showToast(`行内编辑:${row.plateNo}(原型演示)`);
|
showToast(`行内编辑:${row.plateNo}(原型演示)`);
|
||||||
}}
|
}}
|
||||||
onDelete={(row) => {
|
onDelete={(row) => {
|
||||||
if (!canStaffDeleteRow(row)) {
|
if (!canStaffDeleteRow(row)) return;
|
||||||
showToast('已确认记录仅业务服务组主管可删除');
|
setDeleteTarget(row);
|
||||||
return;
|
|
||||||
}
|
|
||||||
showToast(`删除:${row.plateNo}(原型演示)`);
|
|
||||||
}}
|
}}
|
||||||
onLinkReceipt={(row) => setReceiptRow(row)}
|
onLinkReceipt={(row) => setReceiptRow(row)}
|
||||||
onLinkMaintenance={(row) => setMaintenanceRow(row)}
|
onLinkMaintenance={(row) => setMaintenanceRow(row)}
|
||||||
@@ -379,6 +391,47 @@ export default function LeaseBusinessLedgerApp() {
|
|||||||
onCancel={() => setConfirmOpen(false)}
|
onCancel={() => setConfirmOpen(false)}
|
||||||
onConfirm={handleConfirmMark}
|
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
|
<ReceiptLinkModal
|
||||||
key={receiptRow?.id ?? 'closed'}
|
key={receiptRow?.id ?? 'closed'}
|
||||||
open={Boolean(receiptRow)}
|
open={Boolean(receiptRow)}
|
||||||
|
|||||||
@@ -40,24 +40,25 @@
|
|||||||
|
|
||||||
.ldb-kpi-card {
|
.ldb-kpi-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 14px 16px;
|
padding: 16px 18px;
|
||||||
border-radius: var(--ln-radius-card);
|
border-radius: var(--ln-radius-card);
|
||||||
border: 1px solid var(--ln-hairline);
|
border: 1px solid var(--ln-hairline);
|
||||||
background: var(--ln-surface-card);
|
background: var(--ln-surface-card);
|
||||||
box-shadow: var(--ln-shadow-soft);
|
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04), 0 4px 12px rgba(15, 23, 42, 0.03);
|
||||||
min-height: 72px;
|
min-height: 76px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ldb-kpi-label {
|
.ldb-kpi-label {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 0.75rem;
|
font-size: 0.6875rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--ln-muted);
|
color: var(--ln-muted-soft);
|
||||||
margin-bottom: 4px;
|
letter-spacing: 0.02em;
|
||||||
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ldb-kpi-val {
|
.ldb-kpi-val {
|
||||||
font-size: 1.25rem;
|
font-size: 1.375rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--ln-ink);
|
color: var(--ln-ink);
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
@@ -159,7 +160,26 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ldb-table-wrap { overflow: auto; }
|
.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 {
|
.ldb-cell-stack {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -187,7 +207,198 @@
|
|||||||
.ldb-col-money { text-align: right; white-space: nowrap; }
|
.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-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-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 {
|
.sticky-col-left {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
@@ -205,17 +416,19 @@
|
|||||||
|
|
||||||
.ldb-status-tag {
|
.ldb-status-tag {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
padding: 0 8px;
|
align-items: center;
|
||||||
|
padding: 2px 10px;
|
||||||
border-radius: var(--ln-radius-pill);
|
border-radius: var(--ln-radius-pill);
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
line-height: 22px;
|
line-height: 20px;
|
||||||
|
border: 1px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ldb-status--draft { background: #ecfdf5; color: #047857; }
|
.ldb-status--draft { background: #ecfdf5; color: #047857; border-color: #a7f3d0; }
|
||||||
.ldb-status--confirmed { background: #eff6ff; color: #1d4ed8; }
|
.ldb-status--confirmed { background: #eff6ff; color: #1d4ed8; border-color: #bfdbfe; }
|
||||||
.ldb-status--partial { background: #fff7ed; color: #c2410c; }
|
.ldb-status--partial { background: #fff7ed; color: #c2410c; border-color: #fed7aa; }
|
||||||
.ldb-status--paid { background: #f0fdf4; color: #15803d; }
|
.ldb-status--paid { background: #f0fdf4; color: #15803d; border-color: #bbf7d0; }
|
||||||
|
|
||||||
.ldb-confirm-text {
|
.ldb-confirm-text {
|
||||||
margin: 0 0 8px;
|
margin: 0 0 8px;
|
||||||
|
|||||||
@@ -94,6 +94,27 @@ export function formatMoney(value: number): string {
|
|||||||
return value.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
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 {
|
export function calcUnreceived(row: Pick<LeaseLedgerRow, 'receivableTotal' | 'receivedAmount'>): number {
|
||||||
return Math.round((row.receivableTotal - row.receivedAmount) * 100) / 100;
|
return Math.round((row.receivableTotal - row.receivedAmount) * 100) / 100;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
getContractTemplatePreviewBadge,
|
getContractTemplatePreviewBadge,
|
||||||
getContractTypeLabel,
|
getContractTypeLabel,
|
||||||
} from '../contract-template-management/contract-template-catalog.js';
|
} 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 { createDefaultLeaseOrderState, createDefaultPowerOfAttorneyState } from './lease-order-vars.js';
|
||||||
import { buildLeaseContractPreviewHtml } from './lease-contract-preview-build.js';
|
import { buildLeaseContractPreviewHtml } from './lease-contract-preview-build.js';
|
||||||
import {
|
import {
|
||||||
@@ -250,6 +251,40 @@ export default function LeaseContractCreate({ onBack, initialScrollSection, edit
|
|||||||
return function () { window.clearTimeout(timer); };
|
return function () { window.clearTimeout(timer); };
|
||||||
}, [initialScrollSection, editRecord && editRecord.id]);
|
}, [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 () {
|
var createSteps = useMemo(function () {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,26 +1,13 @@
|
|||||||
// 在租车辆概览 — 按品牌型号统计
|
// 在租车辆概览 — 按品牌型号统计
|
||||||
import { buildOnLeaseFleetByBrandModel, formatDeliveryRegion } from './lease-contract-list-data.js';
|
import { buildOnLeaseFleetByBrandModel } 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';
|
|
||||||
}
|
|
||||||
|
|
||||||
function FleetDetailModal(props) {
|
function FleetDetailModal(props) {
|
||||||
var Modal = props.Modal;
|
var Modal = props.Modal;
|
||||||
var item = props.item;
|
var item = props.item;
|
||||||
var open = props.open;
|
var open = props.open;
|
||||||
var onClose = props.onClose;
|
var onClose = props.onClose;
|
||||||
var onViewContract = props.onViewContract;
|
var renderVehicleNativeTableHeader = props.renderVehicleNativeTableHeader;
|
||||||
|
var renderVehicleNativeTableRow = props.renderVehicleNativeTableRow;
|
||||||
if (!Modal) return null;
|
if (!Modal) return null;
|
||||||
|
|
||||||
var titleNode = item
|
var titleNode = item
|
||||||
@@ -31,6 +18,14 @@ function FleetDetailModal(props) {
|
|||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
var canRenderTable = Boolean(
|
||||||
|
item
|
||||||
|
&& item.entries
|
||||||
|
&& item.entries.length
|
||||||
|
&& renderVehicleNativeTableHeader
|
||||||
|
&& renderVehicleNativeTableRow,
|
||||||
|
);
|
||||||
|
|
||||||
return React.createElement(Modal, {
|
return React.createElement(Modal, {
|
||||||
className: 'lc-fleet-detail-modal',
|
className: 'lc-fleet-detail-modal',
|
||||||
rootClassName: 'lc-fleet-detail-modal-root',
|
rootClassName: 'lc-fleet-detail-modal-root',
|
||||||
@@ -46,86 +41,29 @@ function FleetDetailModal(props) {
|
|||||||
centered: true,
|
centered: true,
|
||||||
destroyOnClose: 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-modal__body' },
|
||||||
React.createElement('div', { className: 'lc-fleet-detail-table-wrap' },
|
React.createElement('div', { className: 'lc-fleet-detail-modal__table-host vm-page lc-page' },
|
||||||
React.createElement('table', { className: 'lc-fleet-detail-table' },
|
React.createElement('div', { className: 'lc-contract-vehicle-table-wrap' },
|
||||||
React.createElement('thead', null,
|
React.createElement('table', { className: 'lc-contract-vehicle-native-table lc-fleet-detail-modal__vehicle-table' },
|
||||||
React.createElement('tr', null,
|
renderVehicleNativeTableHeader(),
|
||||||
React.createElement('th', { scope: 'col', className: 'lc-fleet-detail-table__col-plate' }, '车牌号'),
|
React.createElement('tbody', null,
|
||||||
React.createElement('th', { scope: 'col', className: 'lc-fleet-detail-table__col-vin' }, '识别代码'),
|
item.entries.map(function (entry, index) {
|
||||||
React.createElement('th', { scope: 'col', className: 'lc-fleet-detail-table__col-type' }, '车辆类型'),
|
var record = entry.record;
|
||||||
React.createElement('th', { scope: 'col', className: 'lc-fleet-detail-table__col-announce' }, '公告型号'),
|
var vehicle = entry.vehicle;
|
||||||
React.createElement('th', { scope: 'col', className: 'lc-fleet-detail-table__col-city' }, '运营城市'),
|
var vehicleIndex = (record && record.vehicles)
|
||||||
React.createElement('th', { scope: 'col', className: 'lc-fleet-detail-table__col-mileage' }, '里程数'),
|
? record.vehicles.indexOf(vehicle)
|
||||||
React.createElement('th', { scope: 'col', className: 'lc-fleet-detail-table__col-project' }, '所属项目'),
|
: -1;
|
||||||
React.createElement('th', { scope: 'col', className: 'lc-fleet-detail-table__col-action' }, '操作'),
|
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) {
|
export default function LeaseContractFleetSummary(props) {
|
||||||
var records = props.records || [];
|
var records = props.records || [];
|
||||||
var onViewContract = props.onViewContract || function () {};
|
|
||||||
var Modal = props.Modal;
|
var Modal = props.Modal;
|
||||||
|
var renderVehicleNativeTableHeader = props.renderVehicleNativeTableHeader;
|
||||||
|
var renderVehicleNativeTableRow = props.renderVehicleNativeTableRow;
|
||||||
|
|
||||||
var useState = React.useState;
|
var useState = React.useState;
|
||||||
var useMemo = React.useMemo;
|
var useMemo = React.useMemo;
|
||||||
@@ -299,7 +238,8 @@ export default function LeaseContractFleetSummary(props) {
|
|||||||
item: detailItem,
|
item: detailItem,
|
||||||
open: detailOpen,
|
open: detailOpen,
|
||||||
onClose: closeDetail,
|
onClose: closeDetail,
|
||||||
onViewContract: onViewContract,
|
renderVehicleNativeTableHeader: renderVehicleNativeTableHeader,
|
||||||
|
renderVehicleNativeTableRow: renderVehicleNativeTableRow,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,12 +40,13 @@ import {
|
|||||||
DELIVERY_DATE_TBD_LABEL,
|
DELIVERY_DATE_TBD_LABEL,
|
||||||
isDeliveryDateTbd,
|
isDeliveryDateTbd,
|
||||||
formatContractDeliveryDateLabel,
|
formatContractDeliveryDateLabel,
|
||||||
|
formatVehicleDeliveryPlanDateLabel,
|
||||||
|
resolveVehicleDeliveryDateTbd,
|
||||||
|
resolveVehiclePlannedDeliveryDate,
|
||||||
resolveContractStatusByVehicleLease,
|
resolveContractStatusByVehicleLease,
|
||||||
} from './lease-contract-list-data.js';
|
} from './lease-contract-list-data.js';
|
||||||
import { getLessorCompanies } from '../contract-template-management/contract-template-vars.js';
|
import { getLessorCompanies } from '../contract-template-management/contract-template-vars.js';
|
||||||
|
|
||||||
var LC_LIST_TABLE_EXPAND_WIDTH = 48;
|
|
||||||
|
|
||||||
const Component = function(props) {
|
const Component = function(props) {
|
||||||
var useEffect = React.useEffect;
|
var useEffect = React.useEffect;
|
||||||
var ONEOS_ANT_TABLE_GLOBAL_FIX = [
|
var ONEOS_ANT_TABLE_GLOBAL_FIX = [
|
||||||
@@ -70,6 +71,7 @@ const Component = function(props) {
|
|||||||
var Tag = antd.Tag;
|
var Tag = antd.Tag;
|
||||||
var Avatar = antd.Avatar;
|
var Avatar = antd.Avatar;
|
||||||
var Dropdown = antd.Dropdown;
|
var Dropdown = antd.Dropdown;
|
||||||
|
var Tooltip = antd.Tooltip;
|
||||||
var Modal = antd.Modal;
|
var Modal = antd.Modal;
|
||||||
var Upload = antd.Upload;
|
var Upload = antd.Upload;
|
||||||
var message = antd.message;
|
var message = antd.message;
|
||||||
@@ -119,6 +121,9 @@ const Component = function(props) {
|
|||||||
var _deliveryChangePopoverRecord = useState(null);
|
var _deliveryChangePopoverRecord = useState(null);
|
||||||
var _deliveryChangeDraftDate = useState(null);
|
var _deliveryChangeDraftDate = useState(null);
|
||||||
var _deliveryChangeDraftTbd = useState(false);
|
var _deliveryChangeDraftTbd = useState(false);
|
||||||
|
var _vehicleDeliveryChangeTarget = useState(null);
|
||||||
|
var _vehicleDeliveryChangeDraftDate = useState(null);
|
||||||
|
var _vehicleDeliveryChangeDraftTbd = useState(false);
|
||||||
var _delegatePopoverRecord = useState(null);
|
var _delegatePopoverRecord = useState(null);
|
||||||
var _authorizedModalVisible = useState(false);
|
var _authorizedModalVisible = useState(false);
|
||||||
var _authorizedModalRecord = useState(null);
|
var _authorizedModalRecord = useState(null);
|
||||||
@@ -139,6 +144,7 @@ const Component = function(props) {
|
|||||||
var _stampedUploadedOverride = useState({});
|
var _stampedUploadedOverride = useState({});
|
||||||
// 撤回等列表行状态覆盖(原型)
|
// 撤回等列表行状态覆盖(原型)
|
||||||
var _recordOverrides = useState({});
|
var _recordOverrides = useState({});
|
||||||
|
var _expandedRowKeys = useState([]);
|
||||||
|
|
||||||
useEffect(function() {
|
useEffect(function() {
|
||||||
if (props && props.onPageViewChange) {
|
if (props && props.onPageViewChange) {
|
||||||
@@ -1000,16 +1006,16 @@ const Component = function(props) {
|
|||||||
message.info('查看(跳转车辆租赁合同-查看)' + (code ? ':' + code : ''));
|
message.info('查看(跳转车辆租赁合同-查看)' + (code ? ':' + code : ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderMileageProgressCell(vehicle) {
|
function renderMileageProgressBarRow(percent, ariaLabel) {
|
||||||
var percent = Math.min(100, Math.max(0, Number(vehicle.mileageProgress) || 0));
|
percent = Math.min(100, Math.max(0, Number(percent) || 0));
|
||||||
return React.createElement('div', { className: 'lc-vehicle-mileage-progress' },
|
return React.createElement('div', { className: 'lc-contract-mileage-completion__bar-row' },
|
||||||
React.createElement('div', {
|
React.createElement('div', {
|
||||||
className: 'lc-vehicle-mileage-progress__track',
|
className: 'lc-vehicle-mileage-progress__track',
|
||||||
role: 'progressbar',
|
role: 'progressbar',
|
||||||
'aria-valuenow': percent,
|
'aria-valuenow': percent,
|
||||||
'aria-valuemin': 0,
|
'aria-valuemin': 0,
|
||||||
'aria-valuemax': 100,
|
'aria-valuemax': 100,
|
||||||
'aria-label': '里程完成 ' + percent + '%',
|
'aria-label': ariaLabel,
|
||||||
},
|
},
|
||||||
React.createElement('div', {
|
React.createElement('div', {
|
||||||
className: 'lc-vehicle-mileage-progress__bar',
|
className: 'lc-vehicle-mileage-progress__bar',
|
||||||
@@ -1261,15 +1267,17 @@ const Component = function(props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
var rangeData = getMileageCompletionRangeData(vehicle, contractRecord);
|
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' },
|
return React.createElement('div', { className: 'lc-contract-mileage-completion' },
|
||||||
remainingNode,
|
remainingNode,
|
||||||
React.createElement('div', { className: 'lc-contract-mileage-completion__progress-row' },
|
renderMileageProgressBarRow(percent, '里程完成 ' + percent + '%'),
|
||||||
React.createElement('div', { className: 'lc-contract-mileage-completion__progress-main' },
|
rangeRow,
|
||||||
renderMileageProgressCell(vehicle),
|
|
||||||
),
|
|
||||||
renderMileageCompletionRangeEnd(rangeData),
|
|
||||||
),
|
|
||||||
renderMileageCompletionRangeStart(rangeData),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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) {
|
function renderExpandedVehicleTable(record) {
|
||||||
var vehicles = record.vehicles || [];
|
var vehicles = record.vehicles || [];
|
||||||
if (!vehicles.length) {
|
if (!vehicles.length) {
|
||||||
@@ -1383,30 +1483,10 @@ const Component = function(props) {
|
|||||||
}
|
}
|
||||||
return React.createElement('div', { className: 'lc-contract-vehicle-table-wrap' },
|
return React.createElement('div', { className: 'lc-contract-vehicle-table-wrap' },
|
||||||
React.createElement('table', { className: 'lc-contract-vehicle-native-table' },
|
React.createElement('table', { className: 'lc-contract-vehicle-native-table' },
|
||||||
React.createElement('thead', null,
|
renderContractVehicleNativeTableHeader(),
|
||||||
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' }, '操作'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
React.createElement('tbody', null,
|
React.createElement('tbody', null,
|
||||||
vehicles.map(function (vehicle, index) {
|
vehicles.map(function (vehicle, index) {
|
||||||
return React.createElement('tr', { key: record.id + '-vehicle-' + index },
|
return renderContractVehicleNativeTableRow(record, 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)),
|
|
||||||
);
|
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1485,31 +1565,73 @@ const Component = function(props) {
|
|||||||
}, fullName);
|
}, 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) {
|
function renderProjectInfoCell(_, record) {
|
||||||
var status = record.contractStatus || '-';
|
var status = record.contractStatus || '-';
|
||||||
return React.createElement('div', { className: 'lc-project-info-cell' },
|
return React.createElement('div', { className: 'lc-project-info-merged-cell' },
|
||||||
React.createElement(
|
renderContractRowExpandIcon(record),
|
||||||
'button',
|
React.createElement('div', { className: 'lc-project-info-cell' },
|
||||||
{
|
|
||||||
type: 'button',
|
|
||||||
className: 'lc-project-info-btn',
|
|
||||||
onClick: function() { openContractView(record); },
|
|
||||||
title: '查看合同详情'
|
|
||||||
},
|
|
||||||
React.createElement(
|
React.createElement(
|
||||||
'div',
|
'button',
|
||||||
{ className: 'vm-stack' },
|
{
|
||||||
React.createElement('span', { className: 'primary' }, record.projectName || '-'),
|
type: 'button',
|
||||||
React.createElement('span', { className: 'code' }, record.contractCode || '-'),
|
className: 'lc-project-info-btn',
|
||||||
React.createElement('span', { className: 'sub' }, record.customerName || '-')
|
onClick: function() { openContractView(record); },
|
||||||
)
|
title: '查看合同详情'
|
||||||
),
|
},
|
||||||
React.createElement('div', { className: 'lc-project-info-cell__status' },
|
React.createElement(
|
||||||
React.createElement(StatusTag, {
|
'div',
|
||||||
label: status,
|
{ className: 'vm-stack' },
|
||||||
tone: contractStatusTone(status),
|
React.createElement('span', { className: 'primary' }, record.projectName || '-'),
|
||||||
title: status,
|
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);
|
_deliveryChangeDraftTbd[1](false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function closeVehicleDeliveryChangePopover() {
|
||||||
|
_vehicleDeliveryChangeTarget[1](null);
|
||||||
|
_vehicleDeliveryChangeDraftDate[1](null);
|
||||||
|
_vehicleDeliveryChangeDraftTbd[1](false);
|
||||||
|
}
|
||||||
|
|
||||||
function confirmDeliveryDateChange(record) {
|
function confirmDeliveryDateChange(record) {
|
||||||
var draftTbd = _deliveryChangeDraftTbd[0];
|
var draftTbd = _deliveryChangeDraftTbd[0];
|
||||||
var newDate = draftTbd ? DELIVERY_DATE_TBD_LABEL : _deliveryChangeDraftDate[0];
|
var newDate = draftTbd ? DELIVERY_DATE_TBD_LABEL : _deliveryChangeDraftDate[0];
|
||||||
@@ -1623,6 +1751,48 @@ const Component = function(props) {
|
|||||||
closeDeliveryChangePopover();
|
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) {
|
function renderDeliveryChangePanel(record) {
|
||||||
var dayjs = window.dayjs;
|
var dayjs = window.dayjs;
|
||||||
var logs = record.deliveryDateChangeLogs || [];
|
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() {
|
function renderDeliveryEditIcon() {
|
||||||
return React.createElement('svg', {
|
return React.createElement('svg', {
|
||||||
xmlns: 'http://www.w3.org/2000/svg',
|
xmlns: 'http://www.w3.org/2000/svg',
|
||||||
@@ -1871,6 +2105,7 @@ const Component = function(props) {
|
|||||||
closeDeliveryChangePopover();
|
closeDeliveryChangePopover();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
closeVehicleDeliveryChangePopover();
|
||||||
_vehiclePopoverRecord[1](null);
|
_vehiclePopoverRecord[1](null);
|
||||||
_deliveredPopoverRecord[1](null);
|
_deliveredPopoverRecord[1](null);
|
||||||
closeDelegatePopover();
|
closeDelegatePopover();
|
||||||
@@ -1983,6 +2218,20 @@ const Component = function(props) {
|
|||||||
return renderStackCell(record.updater, record.updateTime, 'code');
|
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) {
|
function getOperationButtons(record) {
|
||||||
var menuItems = getMoreMenuItems(record);
|
var menuItems = getMoreMenuItems(record);
|
||||||
var moreDropdown = menuItems.length > 0
|
var moreDropdown = menuItems.length > 0
|
||||||
@@ -1990,21 +2239,29 @@ const Component = function(props) {
|
|||||||
Dropdown,
|
Dropdown,
|
||||||
{
|
{
|
||||||
menu: { items: menuItems },
|
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;
|
: null;
|
||||||
return React.createElement(
|
return React.createElement(
|
||||||
Space,
|
'div',
|
||||||
{ size: 4, wrap: true },
|
{ className: 'lc-row-actions' },
|
||||||
React.createElement(Button, { type: 'link', size: 'small', onClick: function() { openContractView(record); } }, '查看'),
|
React.createElement(Button, { type: 'link', size: 'small', onClick: function() { openContractView(record); } }, '查看'),
|
||||||
moreDropdown
|
moreDropdown,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
var columns = [
|
var columns = [
|
||||||
Table.EXPAND_COLUMN,
|
|
||||||
{
|
{
|
||||||
title: '项目信息',
|
title: '项目信息',
|
||||||
key: 'projectInfo',
|
key: 'projectInfo',
|
||||||
@@ -2132,9 +2389,6 @@ const Component = function(props) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
var listTableScrollX = columns.reduce(function (sum, col) {
|
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);
|
return sum + (Number(col.width) || 0);
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
@@ -2358,6 +2612,8 @@ const Component = function(props) {
|
|||||||
React.createElement(LeaseContractFleetSummary, {
|
React.createElement(LeaseContractFleetSummary, {
|
||||||
records: fleetSourceList,
|
records: fleetSourceList,
|
||||||
Modal: Modal,
|
Modal: Modal,
|
||||||
|
renderVehicleNativeTableHeader: renderContractVehicleNativeTableHeader,
|
||||||
|
renderVehicleNativeTableRow: renderContractVehicleNativeTableRow,
|
||||||
onViewContract: function (record) {
|
onViewContract: function (record) {
|
||||||
message.info('查看合同(原型):' + (record.contractCode || record.projectName || ''));
|
message.info('查看合同(原型):' + (record.contractCode || record.projectName || ''));
|
||||||
},
|
},
|
||||||
@@ -2365,11 +2621,6 @@ const Component = function(props) {
|
|||||||
React.createElement('section', { className: 'vm-table-section' },
|
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-toolbar', 'data-annotation-id': 'lc-list-toolbar' },
|
||||||
React.createElement('div', { className: 'vm-table-actions' },
|
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', {
|
React.createElement('button', {
|
||||||
type: 'button',
|
type: 'button',
|
||||||
className: 'vm-btn vm-btn-ghost',
|
className: 'vm-btn vm-btn-ghost',
|
||||||
@@ -2412,8 +2663,10 @@ const Component = function(props) {
|
|||||||
size: 'small',
|
size: 'small',
|
||||||
pagination: false,
|
pagination: false,
|
||||||
expandable: {
|
expandable: {
|
||||||
fixed: 'left',
|
showExpandColumn: false,
|
||||||
columnWidth: LC_LIST_TABLE_EXPAND_WIDTH,
|
expandIconColumnIndex: -1,
|
||||||
|
expandedRowKeys: _expandedRowKeys[0],
|
||||||
|
onExpandedRowsChange: function (keys) { _expandedRowKeys[1](keys); },
|
||||||
expandedRowRender: renderExpandedVehicleTable,
|
expandedRowRender: renderExpandedVehicleTable,
|
||||||
rowExpandable: function (record) {
|
rowExpandable: function (record) {
|
||||||
return (record.vehicles || []).length > 0;
|
return (record.vehicles || []).length > 0;
|
||||||
|
|||||||
134
src/prototypes/lease-contract-management/lease-contract-edit-bridge.js
vendored
Normal file
134
src/prototypes/lease-contract-management/lease-contract-edit-bridge.js
vendored
Normal 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'],
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -375,25 +375,32 @@ var SAMPLE_AUTHORIZED_DELEGATES = [
|
|||||||
{ name: '周磊', contact: '13900139005', idNumber: '330102199005055678' },
|
{ 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) {
|
export function getAuthorizedDelegates(record) {
|
||||||
if (record.authorizedDelegates && record.authorizedDelegates.length) {
|
if (record.authorizedDelegates && record.authorizedDelegates.length) {
|
||||||
return record.authorizedDelegates;
|
return record.authorizedDelegates;
|
||||||
}
|
}
|
||||||
var count = record.delegateCount != null ? record.delegateCount : 0;
|
if (record.poaUploaded === true && (record.delegateCount || 0) > 0) {
|
||||||
if (count <= 0) return [];
|
return buildAuthorizedDelegatesSample(record.delegateCount);
|
||||||
var list = [];
|
|
||||||
for (var i = 0; i < count; i++) {
|
|
||||||
list.push(SAMPLE_AUTHORIZED_DELEGATES[i % SAMPLE_AUTHORIZED_DELEGATES.length]);
|
|
||||||
}
|
}
|
||||||
return list;
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 列表「受托人」列是否需提示补充授权委托书与受托人信息 */
|
/** 列表「受托人」列是否需提示补充授权委托书与受托人信息 */
|
||||||
export function needsContractDelegateSupplement(record) {
|
export function needsContractDelegateSupplement(record) {
|
||||||
if (!record) return false;
|
if (!record) return false;
|
||||||
if (record.delegateSupplementRequired === false) return false;
|
if (record.delegateSupplementRequired === false) return false;
|
||||||
if (record.poaUploaded === true && (record.delegateCount || 0) > 0) return false;
|
if (record.poaUploaded === true && (record.delegateCount || 0) > 0 && getAuthorizedDelegates(record).length > 0) {
|
||||||
if ((record.delegateCount || 0) > 0 && getAuthorizedDelegates(record).length > 0) return false;
|
return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -507,6 +514,7 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
|
|||||||
mileagePeriod: 'month',
|
mileagePeriod: 'month',
|
||||||
mileageTargetKm: 6000,
|
mileageTargetKm: 6000,
|
||||||
delegateCount: 0,
|
delegateCount: 0,
|
||||||
|
poaUploaded: false,
|
||||||
contractEndDate: '2026-02-16',
|
contractEndDate: '2026-02-16',
|
||||||
contactName: '张三',
|
contactName: '张三',
|
||||||
contactPhone: '13800138001',
|
contactPhone: '13800138001',
|
||||||
@@ -579,6 +587,12 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
|
|||||||
mileagePeriod: 'quarter',
|
mileagePeriod: 'quarter',
|
||||||
mileageTargetKm: 18000,
|
mileageTargetKm: 18000,
|
||||||
delegateCount: 2,
|
delegateCount: 2,
|
||||||
|
poaUploaded: true,
|
||||||
|
authorizedDelegates: [
|
||||||
|
{ name: '王明', contact: '13900139001', idNumber: '330102199001011234' },
|
||||||
|
{ name: '李华', contact: '13900139002', idNumber: '330102199002022345' },
|
||||||
|
],
|
||||||
|
poaRemark: '授权委托书已上传,两名受托人信息与现场核验一致。',
|
||||||
contractEndDate: '2026-06-30',
|
contractEndDate: '2026-06-30',
|
||||||
contactName: '王五',
|
contactName: '王五',
|
||||||
contactPhone: '13800138003',
|
contactPhone: '13800138003',
|
||||||
@@ -619,6 +633,11 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
|
|||||||
mileagePeriod: 'month',
|
mileagePeriod: 'month',
|
||||||
mileageTargetKm: 8000,
|
mileageTargetKm: 8000,
|
||||||
delegateCount: 1,
|
delegateCount: 1,
|
||||||
|
poaUploaded: true,
|
||||||
|
authorizedDelegates: [
|
||||||
|
{ name: '孙丽华', contact: '13900139011', idNumber: '330102198803033210' },
|
||||||
|
],
|
||||||
|
poaRemark: '授权委托书已归档,受托人孙丽华为客户指定接车人。',
|
||||||
contractEndDate: '2026-03-01',
|
contractEndDate: '2026-03-01',
|
||||||
contactName: '赵六',
|
contactName: '赵六',
|
||||||
contactPhone: '13900139001',
|
contactPhone: '13900139001',
|
||||||
@@ -703,6 +722,13 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
|
|||||||
mileagePeriod: 'year',
|
mileagePeriod: 'year',
|
||||||
mileageTargetKm: 120000,
|
mileageTargetKm: 120000,
|
||||||
delegateCount: 3,
|
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',
|
contractEndDate: '2026-12-31',
|
||||||
contactName: '周八',
|
contactName: '周八',
|
||||||
contactPhone: '13900139003',
|
contactPhone: '13900139003',
|
||||||
@@ -745,6 +771,11 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
|
|||||||
mileagePeriod: 'month',
|
mileagePeriod: 'month',
|
||||||
mileageTargetKm: null,
|
mileageTargetKm: null,
|
||||||
delegateCount: 1,
|
delegateCount: 1,
|
||||||
|
poaUploaded: true,
|
||||||
|
authorizedDelegates: [
|
||||||
|
{ name: '吴九', contact: '13900139004', idNumber: '320211199005051122' },
|
||||||
|
],
|
||||||
|
poaRemark: '试用合同授权委托书已维护,受托人吴九负责无锡区域接车。',
|
||||||
contractEndDate: '2025-07-10',
|
contractEndDate: '2025-07-10',
|
||||||
contactName: '吴九',
|
contactName: '吴九',
|
||||||
contactPhone: '13900139004',
|
contactPhone: '13900139004',
|
||||||
@@ -869,6 +900,11 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
|
|||||||
mileagePeriod: 'month',
|
mileagePeriod: 'month',
|
||||||
mileageTargetKm: null,
|
mileageTargetKm: null,
|
||||||
delegateCount: 1,
|
delegateCount: 1,
|
||||||
|
poaUploaded: true,
|
||||||
|
authorizedDelegates: [
|
||||||
|
{ name: '张三', contact: '13800138001', idNumber: '330102198501011234' },
|
||||||
|
],
|
||||||
|
poaRemark: '历史合同授权委托书已补录归档。',
|
||||||
contractEndDate: '2025-01-15',
|
contractEndDate: '2025-01-15',
|
||||||
contactName: '张三',
|
contactName: '张三',
|
||||||
contactPhone: '13800138001',
|
contactPhone: '13800138001',
|
||||||
@@ -944,6 +980,11 @@ export var LEASE_CONTRACT_LIST_RECORDS = [
|
|||||||
mileageTargetKm: 8000,
|
mileageTargetKm: 8000,
|
||||||
delegateCount: 2,
|
delegateCount: 2,
|
||||||
poaUploaded: true,
|
poaUploaded: true,
|
||||||
|
authorizedDelegates: [
|
||||||
|
{ name: '陈静', contact: '13900139004', idNumber: '330102199004044567' },
|
||||||
|
{ name: '周磊', contact: '13900139005', idNumber: '330102199005055678' },
|
||||||
|
],
|
||||||
|
poaRemark: '在租车队合同授权委托书已维护,两名受托人负责七车接还对接。',
|
||||||
contractEndDate: '2027-01-31',
|
contractEndDate: '2027-01-31',
|
||||||
contactName: '张三',
|
contactName: '张三',
|
||||||
contactPhone: '13800138001',
|
contactPhone: '13800138001',
|
||||||
@@ -1018,6 +1059,36 @@ export function formatContractDeliveryDateLabel(record) {
|
|||||||
return record.deliveryDate || '-';
|
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) {
|
function addMonthsToDate(date, months) {
|
||||||
var next = new Date(date.getTime());
|
var next = new Date(date.getTime());
|
||||||
next.setMonth(next.getMonth() + months);
|
next.setMonth(next.getMonth() + months);
|
||||||
@@ -1271,6 +1342,9 @@ export function enrichLeaseContractVehicle(vehicle, index, recordIndex, record)
|
|||||||
plannedDeliveryDate: vehicle.plannedDeliveryDate != null
|
plannedDeliveryDate: vehicle.plannedDeliveryDate != null
|
||||||
? vehicle.plannedDeliveryDate
|
? vehicle.plannedDeliveryDate
|
||||||
: (delivered ? null : (record && record.deliveryDate) || null),
|
: (delivered ? null : (record && record.deliveryDate) || null),
|
||||||
|
deliveryDateTbd: vehicle.deliveryDateTbd != null
|
||||||
|
? vehicle.deliveryDateTbd
|
||||||
|
: (delivered ? false : resolveVehicleDeliveryDateTbd(vehicle, record)),
|
||||||
dailyAvgMileage7d: dailyAvgMileage7d,
|
dailyAvgMileage7d: dailyAvgMileage7d,
|
||||||
});
|
});
|
||||||
if (delivered && hasMinMileage && record) {
|
if (delivered && hasMinMileage && record) {
|
||||||
|
|||||||
@@ -40,6 +40,34 @@
|
|||||||
margin-bottom: 0;
|
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 */
|
/* 列表区: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,
|
||||||
.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%;
|
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-start,
|
||||||
.vm-page.lc-page .lc-list-table--nested .ant-table-cell-fix-left {
|
.vm-page.lc-page .lc-list-table--nested .ant-table-cell-fix-left {
|
||||||
background: var(--ln-surface-card);
|
background: var(--ln-surface-card);
|
||||||
@@ -97,15 +125,6 @@
|
|||||||
background: var(--ln-canvas-soft) !important;
|
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-start,
|
||||||
.vm-page.lc-page .lc-list-table--nested .lc-project-info-col.ant-table-cell-fix-left {
|
.vm-page.lc-page .lc-list-table--nested .lc-project-info-col.ant-table-cell-fix-left {
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
@@ -121,6 +140,73 @@
|
|||||||
box-shadow: inset 10px 0 8px -8px rgba(15, 23, 42, 0.12);
|
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) {
|
@media (max-width: 1280px) {
|
||||||
.vm-page.lc-page .vm-kpi-row {
|
.vm-page.lc-page .vm-kpi-row {
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
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 .lc-project-info-merged-cell {
|
||||||
.vm-page.lc-page .lc-list-table--nested .ant-table-tbody > tr > td.ant-table-row-expand-icon-cell,
|
display: flex;
|
||||||
.vm-page.lc-page .lc-list-table--nested col.ant-table-expand-icon-col {
|
align-items: center;
|
||||||
width: 48px !important;
|
gap: 8px;
|
||||||
min-width: 48px !important;
|
width: 100%;
|
||||||
max-width: 48px !important;
|
min-width: 0;
|
||||||
padding-left: 12px !important;
|
|
||||||
padding-right: 8px !important;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.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;
|
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);
|
background: var(--ln-canvas-soft, #f8fafc);
|
||||||
z-index: 4;
|
z-index: 4;
|
||||||
}
|
}
|
||||||
@@ -1217,8 +1311,12 @@
|
|||||||
width: 9%;
|
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 {
|
.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 {
|
.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-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-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-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-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-mileage-completion,
|
||||||
.vm-page.lc-page .lc-contract-vehicle-native-table tbody td > .lc-contract-vehicle-action-empty,
|
.vm-page.lc-page .lc-contract-vehicle-native-table tbody td > .lc-contract-vehicle-action-empty,
|
||||||
@@ -1270,6 +1369,32 @@
|
|||||||
text-align: left;
|
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 {
|
.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-mileage-req-row--dual {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -1321,18 +1446,14 @@
|
|||||||
white-space: normal;
|
white-space: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vm-page.lc-page .lc-contract-vehicle-native-table .lc-vehicle-mileage-progress {
|
.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-mileage-completion__bar-row .lc-vehicle-mileage-progress__track {
|
||||||
gap: 6px;
|
min-width: 72px;
|
||||||
|
height: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vm-page.lc-page .lc-contract-vehicle-native-table .lc-vehicle-mileage-progress__track {
|
.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-mileage-completion__bar-row .lc-vehicle-mileage-progress__label {
|
||||||
min-width: 40px;
|
font-size: 12px;
|
||||||
height: 6px;
|
min-width: 36px;
|
||||||
}
|
|
||||||
|
|
||||||
.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-handover-situation__line {
|
.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-handover-situation__line {
|
||||||
@@ -1623,26 +1744,34 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vm-page.lc-page .lc-contract-mileage-completion .lc-vehicle-mileage-progress {
|
.vm-page.lc-page .lc-contract-mileage-completion__bar-row {
|
||||||
width: 100%;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vm-page.lc-page .lc-contract-mileage-completion__progress-row {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vm-page.lc-page .lc-contract-mileage-completion__progress-main {
|
.vm-page.lc-page .lc-contract-mileage-completion__bar-row .lc-vehicle-mileage-progress__track {
|
||||||
flex: 1 1 auto;
|
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;
|
min-width: 0;
|
||||||
max-width: 68%;
|
width: 100%;
|
||||||
}
|
|
||||||
|
|
||||||
.vm-page.lc-page .lc-contract-mileage-completion__progress-main .lc-vehicle-mileage-progress__track {
|
|
||||||
min-width: 32px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.vm-page.lc-page .lc-contract-mileage-completion__range-side {
|
.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 {
|
.vm-page.lc-page .lc-contract-mileage-completion__range-side--start {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
margin-top: 2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.vm-page.lc-page .lc-contract-mileage-completion__range-side--end {
|
.vm-page.lc-page .lc-contract-mileage-completion__range-side--end {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vm-page.lc-page .lc-contract-mileage-completion__range-date {
|
.vm-page.lc-page .lc-contract-mileage-completion__range-date {
|
||||||
@@ -1677,18 +1806,6 @@
|
|||||||
line-height: 1.25;
|
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 {
|
.vm-page.lc-page .lc-contract-vehicle-native-table .lc-contract-mileage-completion__range-date {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
@@ -2204,169 +2321,19 @@
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lc-fleet-detail-table-wrap {
|
.lc-fleet-detail-modal .lc-fleet-detail-modal__table-host.vm-page.lc-page {
|
||||||
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 {
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: none;
|
min-height: 0;
|
||||||
background: none;
|
background: transparent;
|
||||||
color: #32a06e;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
text-align: left;
|
|
||||||
cursor: pointer;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.lc-fleet-detail-table__contract-link:hover {
|
.lc-fleet-detail-modal .lc-fleet-detail-modal__table-host .lc-contract-vehicle-table-wrap {
|
||||||
color: #3fb87c;
|
width: 100%;
|
||||||
text-decoration: underline;
|
max-width: 100%;
|
||||||
text-underline-offset: 2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.lc-fleet-detail-table__customer {
|
.lc-fleet-detail-modal .lc-fleet-detail-modal__vehicle-table {
|
||||||
font-size: 0.75rem;
|
min-width: 880px;
|
||||||
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__empty {
|
.lc-fleet-detail-modal__empty {
|
||||||
@@ -2401,10 +2368,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@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,
|
||||||
.vm-page.lc-page .lc-fleet-summary__card-bar-fill,
|
.vm-page.lc-page .lc-fleet-summary__card-bar-fill,
|
||||||
.vm-page.lc-page .lc-fleet-summary__toggle,
|
.vm-page.lc-page .lc-fleet-summary__toggle,
|
||||||
|
|||||||
@@ -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 seed(10 条),与台账页全量 mock 行数可能不同,属原型预期行为。
|
||||||
@@ -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 端 → 加氢站管理 → 加氢订单 / 加氢记录 |
|
||||||
@@ -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. **明细字段范围**:对账弹窗不展示客户侧加氢单价/总价
|
||||||
@@ -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 / 低频1–2 / 无加氢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 接入 |
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# 站点信息独立原型 — 范围与决策
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
「站点信息」从 ONE-OS Web 合包 `oneos-web-h2-station` 拆出,作为 OneOS → 加氢站管理下的独立 Axhub 入口,便于与正式菜单路径对齐。
|
||||||
|
|
||||||
|
## 范围
|
||||||
|
|
||||||
|
| 包含 | 不包含 |
|
||||||
|
|------|--------|
|
||||||
|
| 站点列表、KPI、新建/编辑/查看/删除 | 加氢订单、加氢记录(仍在合包) |
|
||||||
|
| 营业状态、价格配置、余额下钻 | 后端接口联调 |
|
||||||
|
| 两阶段对账单、充值单、批量导入 | |
|
||||||
|
|
||||||
|
## 数据来源
|
||||||
|
|
||||||
|
- **氢费/加氢明细**:统一来自 `src/common/h2VehicleLedgerBridge.js`,与「车辆氢费明细」原型共享 Store。
|
||||||
|
- 先打开「车辆氢费明细」时,台账全量行会同步至 Store;仅打开加氢站模块时使用 canonical seed(10 条演示数据)。
|
||||||
|
- 对账单提交 patch 经 `window.H2_STATION_STATEMENT_LEDGER_UPDATES` 回写车辆氢费明细。
|
||||||
|
|
||||||
|
## KPI 口径(已确认)
|
||||||
|
|
||||||
|
全部 / 签约·普通 / **高频(≥3 次)** / **低频(1–2 次)** / **无加氢(0 次)**,与《站点管理使用说明书》及页面内嵌 PRD 一致。
|
||||||
|
|
||||||
|
## 角色演示
|
||||||
|
|
||||||
|
页面右上角提供「平台管理员 / 加氢站账号」切换,影响编辑页系统账号字段可编辑性。
|
||||||
|
|
||||||
|
## 双入口说明
|
||||||
|
|
||||||
|
- **本原型**:OneOS → 加氢站管理 → 站点信息
|
||||||
|
- **合包**:ONE-OS Web 端 → 加氢站管理 → 加氢订单 / 加氢记录
|
||||||
348
src/prototypes/oneos-web-h2-station-site/annotation-source.json
Normal file
348
src/prototypes/oneos-web-h2-station-site/annotation-source.json
Normal file
File diff suppressed because one or more lines are too long
85
src/prototypes/oneos-web-h2-station-site/index.tsx
Normal file
85
src/prototypes/oneos-web-h2-station-site/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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)`);
|
||||||
@@ -1,18 +1,17 @@
|
|||||||
/**
|
/**
|
||||||
* @name 加氢站管理
|
* @name 加氢站管理
|
||||||
* 自 ONE-OS web端 原稿复刻(3 个页面)
|
* 自 ONE-OS web端 原稿复刻(加氢订单、加氢记录)
|
||||||
*/
|
*/
|
||||||
import '../../common/oneosWebLegacy/legacyGlobals';
|
import '../../common/oneosWebLegacy/legacyGlobals';
|
||||||
|
import '../../common/h2VehicleLedgerBridge.js';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Page1 from './pages/01-加氢订单.jsx';
|
import Page1 from './pages/01-加氢订单.jsx';
|
||||||
import Page2 from './pages/02-加氢记录.jsx';
|
import Page2 from './pages/02-加氢记录.jsx';
|
||||||
import Page3 from './pages/03-站点信息.jsx';
|
|
||||||
import { OneosWebLegacyShell } from '../../common/oneosWebLegacy/OneosWebLegacyShell';
|
import { OneosWebLegacyShell } from '../../common/oneosWebLegacy/OneosWebLegacyShell';
|
||||||
|
|
||||||
const pages = [
|
const pages = [
|
||||||
{ id: 'u52a0u6c22u7ad9u7ba1u7406-u52a0u6c22u8ba2u5355', title: '加氢订单', component: Page1 },
|
{ id: 'u52a0u6c22u7ad9u7ba1u7406-u52a0u6c22u8ba2u5355', title: '加氢订单', component: Page1 },
|
||||||
{ id: 'u52a0u6c22u7ad9u7ba1u7406-u52a0u6c22u8bb0u5f55', title: '加氢记录', component: Page2 },
|
{ id: 'u52a0u6c22u7ad9u7ba1u7406-u52a0u6c22u8bb0u5f55', title: '加氢记录', component: Page2 },
|
||||||
{ id: 'u52a0u6c22u7ad9u7ba1u7406-u7ad9u70b9u4fe1u606f', title: '站点信息', component: Page3 },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function OneosWebH2Station() {
|
export default function OneosWebH2Station() {
|
||||||
|
|||||||
@@ -32,16 +32,36 @@ const Component = function () {
|
|||||||
|
|
||||||
var requirementModalVisible = useState(false);
|
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 stationList = useMemo(function () {
|
||||||
|
var bridge = typeof window !== 'undefined' ? window.H2VehicleLedgerBridge : null;
|
||||||
|
if (bridge && typeof bridge.getStationList === 'function') {
|
||||||
|
return bridge.getStationList();
|
||||||
|
}
|
||||||
return [
|
return [
|
||||||
{ value: 'JX-H2-001', label: '嘉兴加氢站(一期)' },
|
{ value: 'JX-H2-001', label: '嘉兴加氢站(一期)' },
|
||||||
{ value: 'HZ-H2-002', label: '杭州临平加氢站' },
|
{ value: 'HZ-H2-002', label: '杭州临平加氢站' },
|
||||||
{ value: 'SH-H2-003', label: '上海宝山加氢站' }
|
{ value: 'SH-H2-003', label: '上海宝山加氢站' }
|
||||||
];
|
];
|
||||||
}, []);
|
}, [orderDataRevision]);
|
||||||
|
|
||||||
var rawData = useMemo(function () {
|
var rawData = useMemo(function () {
|
||||||
|
var bridge = typeof window !== 'undefined' ? window.H2VehicleLedgerBridge : null;
|
||||||
|
if (bridge && typeof bridge.getOrderRecords === 'function') {
|
||||||
|
return bridge.getOrderRecords();
|
||||||
|
}
|
||||||
return [
|
return [
|
||||||
{ id: 1, stationCode: 'JX-H2-001', stationName: '嘉兴加氢站(一期)', time: '2026-03-01 10:21', plateNo: '浙A12345', amountKg: 12.5, amountYuan: 625.00 },
|
{ 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 },
|
{ 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: 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 }
|
{ id: 5, stationCode: 'HZ-H2-002', stationName: '杭州临平加氢站', time: '2026-03-03 18:10', plateNo: '浙B99999', amountKg: 18.2, amountYuan: 910.00 }
|
||||||
];
|
];
|
||||||
}, []);
|
}, [orderDataRevision]);
|
||||||
|
|
||||||
// 简单字符串时间过滤(示例用,实际可改为 dayjs 比较)
|
// 简单字符串时间过滤(示例用,实际可改为 dayjs 比较)
|
||||||
function inRange(timeStr, range) {
|
function inRange(timeStr, range) {
|
||||||
|
|||||||
@@ -217,7 +217,19 @@ function hrSettlementStatusLabel(value) {
|
|||||||
return info ? info.label : '—';
|
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-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-2', hydrogenTime: '2026-05-26 14:08:33', plateNo: '浙A67890F', hydrogenKg: 10.0, customerAmount: 450.0, mileageKm: 95230, fillerName: '李四', settlementStatus: 'internal' },
|
||||||
{ id: 'hr-3', hydrogenTime: '2026-05-22 09:15:00', plateNo: '浙A88888F', hydrogenKg: 18.3, customerAmount: 823.5, mileageKm: 201880, fillerName: '王静', settlementStatus: 'customer_self' },
|
{ id: 'hr-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 setEditingId = editingIdState[1];
|
||||||
|
|
||||||
var listDataState = useState(function () {
|
var listDataState = useState(function () {
|
||||||
return HR_INITIAL_RECORDS.map(function (r) { return Object.assign({}, r); });
|
return hrBuildInitialRecordsFromLedger();
|
||||||
});
|
});
|
||||||
var listData = listDataState[0];
|
var listData = listDataState[0];
|
||||||
var setListData = listDataState[1];
|
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 draftFiltersState = useState({ timeRange: [], plateNo: undefined, fillerName: undefined });
|
||||||
var draftFilters = draftFiltersState[0];
|
var draftFilters = draftFiltersState[0];
|
||||||
var setDraftFilters = draftFiltersState[1];
|
var setDraftFilters = draftFiltersState[1];
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ var H2_REGION_CASCADER_OPTIONS = [
|
|||||||
|
|
||||||
var H2_STATION_KPI_CARDS = [
|
var H2_STATION_KPI_CARDS = [
|
||||||
{ key: 'all', type: 'total', title: '全部加氢站', desc: '纳入台账管理的全部站点' },
|
{ key: 'all', type: 'total', title: '全部加氢站', desc: '纳入台账管理的全部站点' },
|
||||||
{ key: 'balanceAlert', type: 'warning', title: '预付余额预警站点', desc: '预付余额低于所设提醒阈值的站点' },
|
{ key: 'high', type: 'normal', title: '高频加氢站', desc: '近周期内加氢次数 ≥ 3 次的站点' },
|
||||||
{ key: 'arrears', type: 'unuploaded', title: '已欠费站点', desc: '预付余额小于 0 的站点' },
|
{ key: 'low', type: 'warning', title: '低频加氢站', desc: '近周期内有加氢但次数 < 3 次的站点' },
|
||||||
{ key: 'none', type: 'unuploaded', title: '无加氢站点', desc: '暂无加氢记录的站点' }
|
{ 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-header { margin-bottom: 0 !important; }',
|
||||||
'.ant-table-container .ant-table-body { margin-top: 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-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([
|
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 { --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 .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-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 { 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: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-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 .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 .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-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 { 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 .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 { 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-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-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-weight: 600 !important; flex-shrink: 0; line-height: 20px !important; }',
|
'.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-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-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: #64748b; font-size: 12px; margin-bottom: 2px; }',
|
'.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: 600; color: #0f172a; font-size: 13px; line-height: 1.35; }',
|
'.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: #64748b; font-size: 12px; margin-top: 2px; line-height: 1.35; font-variant-numeric: tabular-nums; }',
|
'.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 { 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 .ant-select, .h2-station-page .h2-region-address .ant-input { width: 100%; }',
|
||||||
'.h2-station-page .h2-region-address--inline { flex-direction: row; align-items: center; gap: 8px; }',
|
'.h2-station-page .h2-region-address--inline { 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-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-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-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: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-refuel-drill-link:focus-visible { outline: 2px solid #10b981; outline-offset: 2px; border-radius: 4px; }',
|
||||||
'.h2-station-page .h2-ledger-totals-bar { display: flex; align-items: stretch; gap: 0; margin-bottom: 12px; border: 1px solid #bae6fd; border-radius: 10px; overflow: hidden; background: #f8fafc; box-shadow: 0 1px 0 rgba(15, 23, 42, 0.04); }',
|
'.h2-station-page .h2-ledger-totals-bar { 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-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-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 { 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 .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 .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-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-header { padding: 18px 24px 14px !important; border-bottom: 1px solid #f1f5f9 !important; margin-bottom: 0 !important; }',
|
||||||
'.h2-balance-drill-modal .ant-modal-title { font-size: 17px !important; font-weight: 700 !important; color: #0f172a !important; }',
|
'.h2-balance-drill-modal .ant-modal-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 }
|
{ tag: 'line', x1: 19, y1: 16, x2: 16, y2: 19 }
|
||||||
], size);
|
], size);
|
||||||
}
|
}
|
||||||
if (key === 'balanceAlert') {
|
if (key === 'high') {
|
||||||
return h2SvgIcon([
|
return h2SvgIcon([
|
||||||
{ d: 'M12 2v4' },
|
{ d: 'M3 22h18' },
|
||||||
{ d: 'M12 18v4' },
|
{ d: 'M6 22V12l4-2v12' },
|
||||||
{ d: 'M4.93 4.93l2.83 2.83' },
|
{ d: 'M12 22V8l4-2v16' },
|
||||||
{ d: 'M16.24 16.24l2.83 2.83' },
|
{ d: 'M18 22V14l3-1.5v9.5' }
|
||||||
{ d: 'M2 12h4' },
|
|
||||||
{ d: 'M18 12h4' },
|
|
||||||
{ d: 'M4.93 19.07l2.83-2.83' },
|
|
||||||
{ d: 'M16.24 7.76l2.83-2.83' },
|
|
||||||
{ tag: 'circle', cx: 12, cy: 12, r: 4 }
|
|
||||||
], size);
|
], size);
|
||||||
}
|
}
|
||||||
if (key === 'arrears') {
|
if (key === 'low') {
|
||||||
return h2SvgIcon([
|
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' },
|
{ d: 'M3 22h18' },
|
||||||
{ tag: 'line', x1: 12, y1: 9, x2: 12, y2: 13 },
|
{ d: 'M8 22V16l3-1.5V22' },
|
||||||
{ tag: 'line', x1: 12, y1: 17, x2: 12.01, y2: 17 }
|
{ d: 'M14 22V18l3-1.5V22' },
|
||||||
|
{ tag: 'line', x1: 6, y1: 22, x2: 20, y2: 22 }
|
||||||
], size);
|
], size);
|
||||||
}
|
}
|
||||||
if (key === 'none') {
|
if (key === 'none') {
|
||||||
@@ -2990,19 +2952,15 @@ function h2SettlementStatusLabel(value) {
|
|||||||
return info ? info.label : '—';
|
return info ? info.label : '—';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 站点加氢记录 Mock(字段对齐车辆氢费明细) */
|
/** 氢费明细统一来自「车辆氢费明细」共享 Store(见 common/h2VehicleLedgerBridge.js) */
|
||||||
var H2_MOCK_REFUEL_RECORDS = [
|
function h2GetVehicleLedgerBridge() {
|
||||||
{ 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' },
|
return typeof window !== 'undefined' ? window.H2VehicleLedgerBridge : null;
|
||||||
{ 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' },
|
function h2GetAllRefuelRecordsFromLedger() {
|
||||||
{ 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' },
|
var bridge = h2GetVehicleLedgerBridge();
|
||||||
{ 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' },
|
return bridge ? bridge.getAllRefuelRecords() : [];
|
||||||
{ id: 'rf-7', stationName: '杭州临平加氢站', hydrogenTime: '2026-05-24 11:05:40', plateNo: '浙B58888F', customerName: '杭州临平城配中心', hydrogenKg: 11.8, costUnitPrice: 43.0, costAmount: 507.4, customerUnitPrice: 46, customerAmount: 542.8, settlementStatus: 'customer' },
|
}
|
||||||
{ id: 'rf-8', stationName: '上海宝山加氢站', hydrogenTime: '2026-04-20 16:45:18', plateNo: '沪A88888F', customerName: '上海羚牛氢运', hydrogenKg: 8.0, costUnitPrice: 44.0, costAmount: 352.0, customerUnitPrice: 47, customerAmount: 376.0, settlementStatus: 'internal' },
|
|
||||||
{ id: 'rf-9', stationName: '上海宝山加氢站', hydrogenTime: '2026-04-08 09:12:55', plateNo: '沪BDB9161F', customerName: '宝山园区试运车队', hydrogenKg: 9.5, costUnitPrice: 44.0, costAmount: 418.0, customerUnitPrice: 47, customerAmount: 446.5, settlementStatus: 'customer_self' },
|
|
||||||
{ id: 'rf-10', stationName: '苏州工业园区备用站', hydrogenTime: '2026-03-15 10:00:00', plateNo: '苏E33333F', customerName: '苏州试运客户', hydrogenKg: 6.2, costUnitPrice: 41.0, costAmount: 254.2, customerUnitPrice: 44, customerAmount: 272.8, settlementStatus: 'customer' }
|
|
||||||
];
|
|
||||||
|
|
||||||
function h2FormatKgNum(v) {
|
function h2FormatKgNum(v) {
|
||||||
var n = typeof v === 'number' ? v : parseFloat(v);
|
var n = typeof v === 'number' ? v : parseFloat(v);
|
||||||
@@ -3513,32 +3471,10 @@ function h2FormatOperateDateOnly() {
|
|||||||
return h2OperateTimestamp().slice(0, 10);
|
return h2OperateTimestamp().slice(0, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 台账加氢记录 Mock(字段对齐车辆氢费明细,costTotal 对应成本总价) */
|
/** 台账加氢记录(字段对齐车辆氢费明细,costTotal 对应成本总价) */
|
||||||
function h2BuildLedgerReconciledRecords() {
|
|
||||||
return H2_MOCK_REFUEL_RECORDS.map(function (r, index) {
|
|
||||||
var isReconciled = index < 9;
|
|
||||||
return {
|
|
||||||
id: r.id,
|
|
||||||
stationName: r.stationName,
|
|
||||||
hydrogenTime: r.hydrogenTime,
|
|
||||||
plateNo: r.plateNo,
|
|
||||||
customerName: r.customerName,
|
|
||||||
hydrogenKg: r.hydrogenKg,
|
|
||||||
costUnitPrice: r.costUnitPrice,
|
|
||||||
costTotal: r.costAmount,
|
|
||||||
settlementStatus: r.settlementStatus,
|
|
||||||
orderNo: h2BuildRefuelOrderNo(r, index),
|
|
||||||
reconcileStatus: isReconciled ? H2_RECONCILE_RECONCILED : H2_RECONCILE_PENDING,
|
|
||||||
statementRecordId: null,
|
|
||||||
reconcileDate: null,
|
|
||||||
receiptDate: null,
|
|
||||||
paymentStatus: isReconciled ? H2_PAYMENT_STATUS_UNPAID : null
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function h2InitLedgerRecordsStore() {
|
function h2InitLedgerRecordsStore() {
|
||||||
return h2BuildLedgerReconciledRecords();
|
var bridge = h2GetVehicleLedgerBridge();
|
||||||
|
return bridge ? bridge.buildStationLedgerStore() : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function h2InitStatementRecordsStore() {
|
function h2InitStatementRecordsStore() {
|
||||||
@@ -3627,10 +3563,9 @@ function h2ExportStatementCsv(stationName, startDate, endDate, records) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function h2GetRefuelRecordsByStation(stationName) {
|
function h2GetRefuelRecordsByStation(stationName) {
|
||||||
var name = String(stationName || '').trim();
|
var bridge = h2GetVehicleLedgerBridge();
|
||||||
return H2_MOCK_REFUEL_RECORDS.filter(function (r) { return r.stationName === name; }).slice().sort(function (a, b) {
|
var records = bridge ? bridge.getRefuelRecordsByStation(stationName) : [];
|
||||||
return String(b.hydrogenTime || '').localeCompare(String(a.hydrogenTime || ''));
|
return records.map(function (r, index) {
|
||||||
}).map(function (r, index) {
|
|
||||||
return Object.assign({}, r, { orderNo: h2BuildRefuelOrderNo(r, index) });
|
return Object.assign({}, r, { orderNo: h2BuildRefuelOrderNo(r, index) });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -4959,16 +4894,6 @@ var H2_PRIMARY_BTN_STYLE = {
|
|||||||
border: 'none'
|
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) {
|
function h2ResolveRecordSupplierMode(record) {
|
||||||
if (!record) return 'link';
|
if (!record) return 'link';
|
||||||
if (record.supplierMode === 'none') return 'none';
|
if (record.supplierMode === 'none') return 'none';
|
||||||
@@ -5705,7 +5630,6 @@ const Component = function () {
|
|||||||
var useCallback = React.useCallback;
|
var useCallback = React.useCallback;
|
||||||
|
|
||||||
var antd = window.antd;
|
var antd = window.antd;
|
||||||
var Breadcrumb = antd.Breadcrumb;
|
|
||||||
var Card = antd.Card;
|
var Card = antd.Card;
|
||||||
var Table = antd.Table;
|
var Table = antd.Table;
|
||||||
var Button = antd.Button;
|
var Button = antd.Button;
|
||||||
@@ -5760,14 +5684,6 @@ const Component = function () {
|
|||||||
var filePreviewModal = filePreviewModalState[0];
|
var filePreviewModal = filePreviewModalState[0];
|
||||||
var setFilePreviewModal = filePreviewModalState[1];
|
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 importModalState = useState(false);
|
||||||
var importModalOpen = importModalState[0];
|
var importModalOpen = importModalState[0];
|
||||||
var setImportModalOpen = importModalState[1];
|
var setImportModalOpen = importModalState[1];
|
||||||
@@ -5841,6 +5757,21 @@ const Component = function () {
|
|||||||
var ledgerStore = ledgerStoreState[0];
|
var ledgerStore = ledgerStoreState[0];
|
||||||
var setLedgerStore = ledgerStoreState[1];
|
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 statementRecordsState = useState(function () { return h2InitStatementRecordsStore(); });
|
||||||
var statementRecords = statementRecordsState[0];
|
var statementRecords = statementRecordsState[0];
|
||||||
var setStatementRecords = statementRecordsState[1];
|
var setStatementRecords = statementRecordsState[1];
|
||||||
@@ -5924,8 +5855,6 @@ const Component = function () {
|
|||||||
var editSupplier = editSupplierState[0];
|
var editSupplier = editSupplierState[0];
|
||||||
var setEditSupplier = editSupplierState[1];
|
var setEditSupplier = editSupplierState[1];
|
||||||
|
|
||||||
var isAdminUser = h2IsAdminUser();
|
|
||||||
|
|
||||||
var createSupplierReadonly = createSupplierMode === 'link' && !!createLinkedSupplierId;
|
var createSupplierReadonly = createSupplierMode === 'link' && !!createLinkedSupplierId;
|
||||||
|
|
||||||
var existingNameMap = useMemo(function () {
|
var existingNameMap = useMemo(function () {
|
||||||
@@ -5937,15 +5866,16 @@ const Component = function () {
|
|||||||
}, [listData]);
|
}, [listData]);
|
||||||
|
|
||||||
var categoryCounts = useMemo(function () {
|
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) {
|
listData.forEach(function (r) {
|
||||||
if (h2IsArrearsStation(r)) counts.arrears += 1;
|
|
||||||
else if (h2IsBalanceAlertStation(r)) counts.balanceAlert += 1;
|
|
||||||
var stats = h2CalcRefuelStats(r.name);
|
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;
|
return counts;
|
||||||
}, [listData]);
|
}, [listData, ledgerDataRevision]);
|
||||||
|
|
||||||
var signedStats = useMemo(function () {
|
var signedStats = useMemo(function () {
|
||||||
var signed = 0;
|
var signed = 0;
|
||||||
@@ -5957,12 +5887,11 @@ const Component = function () {
|
|||||||
var list = listData.slice();
|
var list = listData.slice();
|
||||||
if (categoryTab !== 'all') {
|
if (categoryTab !== 'all') {
|
||||||
list = list.filter(function (r) {
|
list = list.filter(function (r) {
|
||||||
if (categoryTab === 'balanceAlert') return h2IsBalanceAlertStation(r);
|
var stats = h2CalcRefuelStats(r.name);
|
||||||
if (categoryTab === 'arrears') return h2IsArrearsStation(r);
|
var freq = h2DeriveFrequencyByRefuelCount(stats.count);
|
||||||
if (categoryTab === 'none') {
|
if (categoryTab === 'high') return freq === 'high';
|
||||||
var stats = h2CalcRefuelStats(r.name);
|
if (categoryTab === 'low') return freq === 'low';
|
||||||
return h2DeriveFrequencyByRefuelCount(stats.count) === 'none';
|
if (categoryTab === 'none') return freq === 'none';
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -5984,7 +5913,7 @@ const Component = function () {
|
|||||||
refuelFreqKey: h2DeriveFrequencyByRefuelCount(stats.count)
|
refuelFreqKey: h2DeriveFrequencyByRefuelCount(stats.count)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}, [listData, categoryTab, appliedFilters]);
|
}, [listData, categoryTab, appliedFilters, ledgerDataRevision]);
|
||||||
|
|
||||||
var sortedList = useMemo(function () {
|
var sortedList = useMemo(function () {
|
||||||
var list = filteredList.slice();
|
var list = filteredList.slice();
|
||||||
@@ -6000,30 +5929,94 @@ const Component = function () {
|
|||||||
}, [filteredList, refuelSort]);
|
}, [filteredList, refuelSort]);
|
||||||
|
|
||||||
var totalCount = sortedList.length;
|
var totalCount = sortedList.length;
|
||||||
|
var totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
|
||||||
|
var safePage = Math.min(page, totalPages);
|
||||||
|
|
||||||
var displayList = useMemo(function () {
|
var displayList = useMemo(function () {
|
||||||
var start = (page - 1) * pageSize;
|
var start = (safePage - 1) * pageSize;
|
||||||
return sortedList.slice(start, start + pageSize);
|
return sortedList.slice(start, start + pageSize);
|
||||||
}, [sortedList, page, pageSize]);
|
}, [sortedList, safePage, pageSize]);
|
||||||
|
|
||||||
var tablePagination = useMemo(function () {
|
function h2BuildPageItems(current, pages) {
|
||||||
return {
|
if (pages <= 7) {
|
||||||
current: page,
|
var all = [];
|
||||||
pageSize: pageSize,
|
var pi;
|
||||||
total: totalCount,
|
for (pi = 1; pi <= pages; pi++) all.push(pi);
|
||||||
showSizeChanger: true,
|
return all;
|
||||||
pageSizeOptions: ['5', '10', '20', '50'],
|
}
|
||||||
showTotal: function (t) { return '共 ' + t + ' 条'; },
|
var items = [1];
|
||||||
onChange: function (p, size) {
|
if (current > 3) items.push('ellipsis');
|
||||||
setPage(p);
|
var startP = Math.max(2, current - 1);
|
||||||
if (size && size !== pageSize) setPageSize(size);
|
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');
|
||||||
}, [page, pageSize, totalCount]);
|
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) {
|
var handleKpiCardClick = useCallback(function (key) {
|
||||||
setCategoryTab(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 }); });
|
setListFilters(function (p) { return Object.assign({}, p, { signed: undefined }); });
|
||||||
setAppliedFilters(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) {
|
var renderFilterField = useCallback(function (label, control) {
|
||||||
return React.createElement('div', { className: 'lc-filter-field' },
|
return React.createElement('label', { className: 'vm-filter-field' },
|
||||||
React.createElement('span', { className: 'lc-filter-field-label' }, label),
|
React.createElement('span', null, label),
|
||||||
React.createElement('div', { className: 'lc-filter-field-control' }, control)
|
control
|
||||||
);
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
var renderAlertStatCard = useCallback(function (card, options) {
|
var renderVmKpiCard = useCallback(function (card, options) {
|
||||||
var active = options.active;
|
var active = options.active;
|
||||||
var count = options.count;
|
var count = options.count;
|
||||||
var onClick = options.onClick;
|
var onClick = options.onClick;
|
||||||
var icon = options.icon;
|
var icon = options.icon;
|
||||||
return React.createElement('div', {
|
return React.createElement('button', {
|
||||||
key: card.key,
|
key: card.key,
|
||||||
role: 'button',
|
type: 'button',
|
||||||
tabIndex: 0,
|
className: 'vm-kpi-card' + (active ? ' active' : ''),
|
||||||
className: 'lc-alert-card lc-alert-card--' + card.type + ' lc-alert-card-clickable' + (active ? ' lc-alert-card-active' : ''),
|
|
||||||
onClick: onClick,
|
onClick: onClick,
|
||||||
onKeyDown: function (e) {
|
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
|
||||||
e.preventDefault();
|
|
||||||
onClick();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'aria-pressed': active,
|
'aria-pressed': active,
|
||||||
'aria-label': card.title + ',共 ' + count + ' 个'
|
'aria-label': card.title + ',共 ' + count + ' 个'
|
||||||
},
|
},
|
||||||
React.createElement('div', { className: 'lc-alert-card-tip-anchor' },
|
React.createElement('span', {
|
||||||
React.createElement(Tooltip, { title: card.desc, placement: 'topRight', overlayStyle: { maxWidth: 320 } },
|
className: 'vm-kpi-tip',
|
||||||
React.createElement('span', {
|
tabIndex: 0,
|
||||||
className: 'lc-alert-card-tip',
|
role: 'note',
|
||||||
role: 'img',
|
'aria-label': card.title + '说明',
|
||||||
'aria-label': card.title + '说明',
|
onClick: function (e) { e.stopPropagation(); },
|
||||||
onClick: function (e) { e.stopPropagation(); },
|
onMouseDown: function (e) { e.stopPropagation(); }
|
||||||
onMouseDown: function (e) { e.stopPropagation(); }
|
},
|
||||||
},
|
h2KpiHelpIcon(),
|
||||||
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-tooltip', role: 'tooltip' }, card.desc)
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
React.createElement('div', { className: 'lc-alert-card-icon', 'aria-hidden': true }, icon),
|
React.createElement('span', { className: 'vm-kpi-icon', 'aria-hidden': true }, icon),
|
||||||
React.createElement('div', { className: 'lc-alert-card-main' },
|
React.createElement('span', { className: 'vm-kpi-main' },
|
||||||
React.createElement('div', { className: 'lc-alert-card-title' }, card.title),
|
React.createElement('span', { className: 'vm-kpi-eyebrow' }, card.title),
|
||||||
React.createElement('div', { className: 'lc-alert-card-val' }, count)
|
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 = {};
|
var customerNameMap = {};
|
||||||
allCustomerOpts.forEach(function (opt) { customerNameMap[opt.value] = opt.label; });
|
allCustomerOpts.forEach(function (opt) { customerNameMap[opt.value] = opt.label; });
|
||||||
|
|
||||||
@@ -8663,7 +8647,8 @@ const Component = function () {
|
|||||||
key: 'name',
|
key: 'name',
|
||||||
width: 300,
|
width: 300,
|
||||||
fixed: 'left',
|
fixed: 'left',
|
||||||
ellipsis: true,
|
className: 'h2-site-name-col',
|
||||||
|
onCell: function () { return { className: 'h2-site-name-col' }; },
|
||||||
render: function (v, r) {
|
render: function (v, r) {
|
||||||
var addressLine = h2FormatStationAddressLine(r);
|
var addressLine = h2FormatStationAddressLine(r);
|
||||||
return React.createElement('div', { className: 'lc-station-name-cell' },
|
return React.createElement('div', { className: 'lc-station-name-cell' },
|
||||||
@@ -8702,6 +8687,7 @@ const Component = function () {
|
|||||||
key: 'currentCostPrice',
|
key: 'currentCostPrice',
|
||||||
width: 168,
|
width: 168,
|
||||||
align: 'right',
|
align: 'right',
|
||||||
|
onHeaderCell: function () { return { style: { textAlign: 'right' } }; },
|
||||||
render: function (_, r) {
|
render: function (_, r) {
|
||||||
return renderCurrentCostPriceCell(r);
|
return renderCurrentCostPriceCell(r);
|
||||||
}
|
}
|
||||||
@@ -8755,6 +8741,8 @@ const Component = function () {
|
|||||||
title: '联系方式',
|
title: '联系方式',
|
||||||
key: 'contactInfo',
|
key: 'contactInfo',
|
||||||
width: 120,
|
width: 120,
|
||||||
|
className: 'h2-site-contact-col',
|
||||||
|
onCell: function () { return { className: 'h2-site-contact-col' }; },
|
||||||
render: function (_, r) {
|
render: function (_, r) {
|
||||||
var mobile = r.mobilePhone || r.phone || '';
|
var mobile = r.mobilePhone || r.phone || '';
|
||||||
var landline = r.landlinePhone || '';
|
var landline = r.landlinePhone || '';
|
||||||
@@ -8773,10 +8761,9 @@ const Component = function () {
|
|||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
render: function (_, record) {
|
render: function (_, record) {
|
||||||
return React.createElement('div', { className: 'h2-row-actions' },
|
return React.createElement('div', { className: 'h2-row-actions' },
|
||||||
React.createElement(Button, {
|
React.createElement('button', {
|
||||||
type: 'link',
|
type: 'button',
|
||||||
size: 'small',
|
className: 'vm-link',
|
||||||
className: 'lc-action-btn',
|
|
||||||
onClick: function () { openView(record); }
|
onClick: function () { openView(record); }
|
||||||
}, '查看'),
|
}, '查看'),
|
||||||
React.createElement(Dropdown, {
|
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) {
|
var formItem = function (label, required, node, extra) {
|
||||||
return React.createElement(Form.Item, {
|
return React.createElement(Form.Item, {
|
||||||
label: required ? React.createElement('span', null, React.createElement('span', { style: { color: '#ef4444', marginRight: 4 } }, '*'), label) : label,
|
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 priceConfigs = priceModal.priceConfigs || [];
|
||||||
var priceType = priceModal.priceType || 'fixed';
|
var priceType = priceModal.priceType || 'fixed';
|
||||||
var assignedCustomerIds = h2GetAssignedCustomerIds(priceConfigs, priceModal.editingConfigId);
|
var assignedCustomerIds = h2GetAssignedCustomerIds(priceConfigs, priceModal.editingConfigId);
|
||||||
var allCustomerOptions = h2CollectCustomerOptions(H2_MOCK_REFUEL_RECORDS);
|
var allCustomerOptions = h2CollectCustomerOptions(h2GetAllRefuelRecordsFromLedger());
|
||||||
var selectedCustomerIds = priceModal.selectedCustomerIds || [];
|
var selectedCustomerIds = priceModal.selectedCustomerIds || [];
|
||||||
var enabledTierCustomerOptions = allCustomerOptions.filter(function (opt) {
|
var enabledTierCustomerOptions = allCustomerOptions.filter(function (opt) {
|
||||||
return !assignedCustomerIds[opt.value];
|
return !assignedCustomerIds[opt.value];
|
||||||
@@ -9468,41 +9459,29 @@ const Component = function () {
|
|||||||
|
|
||||||
return React.createElement('div', {
|
return React.createElement('div', {
|
||||||
className: 'h2-station-page lc-edit-page'
|
className: 'h2-station-page lc-edit-page'
|
||||||
|
+ (subView === 'list' ? ' h2-station-page--list' : '')
|
||||||
+ (subView === 'create' || subView === 'edit' ? ' h2-station-page--create' : '')
|
+ (subView === 'create' || subView === 'edit' ? ' h2-station-page--create' : '')
|
||||||
},
|
},
|
||||||
React.createElement('style', null, H2_PAGE_STYLE),
|
React.createElement('style', null, H2_PAGE_STYLE),
|
||||||
createViewNode,
|
createViewNode,
|
||||||
editViewNode,
|
editViewNode,
|
||||||
subView === 'list' ? React.createElement('div', { style: { display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' } },
|
subView === 'list' ? React.createElement('div', { className: 'vm-page lc-page h2-site-page h2-site-list-shell' },
|
||||||
React.createElement('div', { style: { marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 12 } },
|
React.createElement('section', {
|
||||||
React.createElement(Breadcrumb, { items: [{ title: '加氢站管理' }, { title: '站点信息' }] }),
|
className: 'vm-filter-card',
|
||||||
React.createElement(Space, { size: 8 },
|
'data-annotation-id': 'h2-site-filter',
|
||||||
React.createElement(Button, {
|
'aria-label': '筛选条件'
|
||||||
type: 'default',
|
},
|
||||||
icon: H2_ICONS.doc,
|
React.createElement('header', { className: 'vm-filter-header' },
|
||||||
style: H2_REQ_BTN_STYLE,
|
React.createElement('h2', { className: 'vm-filter-title' }, '筛选条件')
|
||||||
onClick: function () { setUserManualModalOpen(true); },
|
),
|
||||||
'aria-label': '查看使用说明书'
|
React.createElement('div', { className: 'vm-filter-grid' },
|
||||||
}, '查看使用说明书'),
|
|
||||||
React.createElement(Button, {
|
|
||||||
type: 'default',
|
|
||||||
icon: H2_ICONS.doc,
|
|
||||||
style: H2_REQ_BTN_STYLE,
|
|
||||||
onClick: function () { setRequirementModalOpen(true); },
|
|
||||||
'aria-label': '查看需求说明'
|
|
||||||
}, '查看需求说明')
|
|
||||||
)
|
|
||||||
),
|
|
||||||
|
|
||||||
React.createElement(Card, { className: 'lc-filter-card', title: '筛选条件', bordered: false },
|
|
||||||
React.createElement('div', { className: 'lc-filter-grid' },
|
|
||||||
renderFilterField('加氢站名称', React.createElement(Input, {
|
renderFilterField('加氢站名称', React.createElement(Input, {
|
||||||
placeholder: '请输入加氢站名称',
|
placeholder: '请输入加氢站名称',
|
||||||
value: listFilters.name,
|
value: listFilters.name,
|
||||||
onChange: function (e) { setListFilters(function (p) { return Object.assign({}, p, { name: e.target.value }); }); },
|
onChange: function (e) { setListFilters(function (p) { return Object.assign({}, p, { name: e.target.value }); }); },
|
||||||
onPressEnter: handleListFilterQuery,
|
onPressEnter: handleListFilterQuery,
|
||||||
allowClear: true,
|
allowClear: true,
|
||||||
style: { width: '100%', height: 32, borderRadius: 8 }
|
style: { width: '100%' }
|
||||||
})),
|
})),
|
||||||
renderFilterField('是否签约', React.createElement(Select, {
|
renderFilterField('是否签约', React.createElement(Select, {
|
||||||
placeholder: '全部',
|
placeholder: '全部',
|
||||||
@@ -9510,8 +9489,7 @@ const Component = function () {
|
|||||||
value: listFilters.signed,
|
value: listFilters.signed,
|
||||||
onChange: function (v) { setListFilters(function (p) { return Object.assign({}, p, { signed: v }); }); },
|
onChange: function (v) { setListFilters(function (p) { return Object.assign({}, p, { signed: v }); }); },
|
||||||
options: [{ value: 'yes', label: '签约站点' }, { value: 'no', label: '普通站点' }],
|
options: [{ value: 'yes', label: '签约站点' }, { value: 'no', label: '普通站点' }],
|
||||||
style: { width: '100%' },
|
style: { width: '100%' }
|
||||||
dropdownStyle: { borderRadius: 8 }
|
|
||||||
})),
|
})),
|
||||||
renderFilterField('地区(省-市)', React.createElement(Cascader, {
|
renderFilterField('地区(省-市)', React.createElement(Cascader, {
|
||||||
options: H2_REGION_CASCADER_OPTIONS,
|
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 }); }); },
|
onChange: function (v) { setListFilters(function (p) { return Object.assign({}, p, { region: v || undefined }); }); },
|
||||||
placeholder: '请选择省 / 市',
|
placeholder: '请选择省 / 市',
|
||||||
allowClear: true,
|
allowClear: true,
|
||||||
style: { width: '100%', borderRadius: 8 },
|
style: { width: '100%' },
|
||||||
changeOnSelect: true
|
changeOnSelect: true
|
||||||
})),
|
})),
|
||||||
renderFilterField('营业状态', React.createElement(Select, {
|
renderFilterField('营业状态', React.createElement(Select, {
|
||||||
@@ -9528,25 +9506,41 @@ const Component = function () {
|
|||||||
value: listFilters.businessStatus,
|
value: listFilters.businessStatus,
|
||||||
onChange: function (v) { setListFilters(function (p) { return Object.assign({}, p, { businessStatus: v }); }); },
|
onChange: function (v) { setListFilters(function (p) { return Object.assign({}, p, { businessStatus: v }); }); },
|
||||||
options: H2_BUSINESS_STATUS_OPTIONS,
|
options: H2_BUSINESS_STATUS_OPTIONS,
|
||||||
style: { width: '100%' },
|
style: { width: '100%' }
|
||||||
dropdownStyle: { borderRadius: 8 }
|
|
||||||
}))
|
}))
|
||||||
),
|
),
|
||||||
React.createElement('div', { className: 'lc-filter-actions' },
|
React.createElement('div', { className: 'vm-filter-actions' },
|
||||||
React.createElement(Button, { onClick: handleListFilterReset, style: { borderRadius: 8 } }, '重置'),
|
React.createElement('button', { type: 'button', className: 'vm-btn vm-btn-ghost', onClick: handleListFilterReset }, '重置'),
|
||||||
React.createElement(Button, { type: 'primary', onClick: handleListFilterQuery, style: H2_PRIMARY_BTN_STYLE }, '查询')
|
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' },
|
React.createElement('div', { className: 'vm-kpi-row', role: 'group', 'aria-label': '站点统计', 'data-annotation-id': 'h2-site-kpi' },
|
||||||
renderAlertStatCard(H2_STATION_KPI_CARDS[0], {
|
renderVmKpiCard(H2_STATION_KPI_CARDS[0], {
|
||||||
active: categoryTab === 'all' && !appliedFilters.signed,
|
active: categoryTab === 'all' && !appliedFilters.signed,
|
||||||
count: categoryCounts.all || 0,
|
count: categoryCounts.all || 0,
|
||||||
onClick: function () { handleKpiCardClick('all'); },
|
onClick: function () { handleKpiCardClick('all'); },
|
||||||
icon: h2KpiCardIcon('all')
|
icon: h2KpiCardIcon('all')
|
||||||
}),
|
}),
|
||||||
H2_SIGNED_FILTER_CARDS.map(function (card) {
|
H2_SIGNED_FILTER_CARDS.map(function (card) {
|
||||||
return renderAlertStatCard(card, {
|
return renderVmKpiCard(card, {
|
||||||
active: appliedFilters.signed === card.key,
|
active: appliedFilters.signed === card.key,
|
||||||
count: card.key === 'yes' ? signedStats.signed : signedStats.unsigned,
|
count: card.key === 'yes' ? signedStats.signed : signedStats.unsigned,
|
||||||
onClick: function () { handleSignedFilterCardClick(card.key); },
|
onClick: function () { handleSignedFilterCardClick(card.key); },
|
||||||
@@ -9554,65 +9548,63 @@ const Component = function () {
|
|||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
H2_STATION_KPI_CARDS.slice(1).map(function (card) {
|
H2_STATION_KPI_CARDS.slice(1).map(function (card) {
|
||||||
var isAlertKpi = card.key === 'balanceAlert' || card.key === 'arrears';
|
return renderVmKpiCard(card, {
|
||||||
return renderAlertStatCard(card, {
|
active: categoryTab === card.key && !appliedFilters.signed,
|
||||||
active: isAlertKpi
|
|
||||||
? (alertStationModal.open && alertStationModal.type === card.key)
|
|
||||||
: (categoryTab === card.key && !appliedFilters.signed),
|
|
||||||
count: categoryCounts[card.key] || 0,
|
count: categoryCounts[card.key] || 0,
|
||||||
onClick: function () {
|
onClick: function () { handleKpiCardClick(card.key); },
|
||||||
if (isAlertKpi) openAlertStationModal(card.key);
|
|
||||||
else handleKpiCardClick(card.key);
|
|
||||||
},
|
|
||||||
icon: h2KpiIcon(card.key)
|
icon: h2KpiIcon(card.key)
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
|
|
||||||
React.createElement('div', { className: 'lc-table-section' },
|
React.createElement('section', { className: 'vm-table-section' },
|
||||||
React.createElement('div', { className: 'lc-table-toolbar' },
|
React.createElement('div', { className: 'vm-table-toolbar', 'data-annotation-id': 'h2-site-toolbar' },
|
||||||
React.createElement('div', { className: 'lc-table-toolbar-actions' },
|
React.createElement('div', { className: 'vm-table-actions' },
|
||||||
React.createElement(Button, {
|
React.createElement('button', {
|
||||||
type: 'default',
|
type: 'button',
|
||||||
icon: H2_ICONS.none,
|
className: 'vm-btn vm-btn-ghost',
|
||||||
style: { borderRadius: 8, fontWeight: 600, borderColor: '#f59e0b', color: '#d97706' },
|
|
||||||
onClick: openGlobalBalanceAlertSetting,
|
onClick: openGlobalBalanceAlertSetting,
|
||||||
'aria-label': '全站余额提醒设置'
|
'aria-label': '全站余额提醒设置'
|
||||||
}, '全站余额提醒设置'),
|
}, '全站余额提醒设置'),
|
||||||
React.createElement(Button, {
|
React.createElement('button', {
|
||||||
type: 'default',
|
type: 'button',
|
||||||
icon: H2_ICONS.upload,
|
className: 'vm-btn vm-btn-ghost',
|
||||||
style: { borderRadius: 8, fontWeight: 600, borderColor: '#10b981', color: '#059669' },
|
|
||||||
onClick: function () { setImportModalOpen(true); },
|
onClick: function () { setImportModalOpen(true); },
|
||||||
'aria-label': '批量导入加氢站点'
|
'aria-label': '批量导入加氢站点'
|
||||||
}, '批量导入'),
|
}, '批量导入'),
|
||||||
React.createElement(Button, {
|
React.createElement('button', {
|
||||||
type: 'default',
|
type: 'button',
|
||||||
icon: H2_ICONS.doc,
|
className: 'vm-btn vm-btn-ghost',
|
||||||
style: { borderRadius: 8, fontWeight: 600, borderColor: '#10b981', color: '#059669' },
|
|
||||||
onClick: function () { openRechargeModal(); },
|
onClick: function () { openRechargeModal(); },
|
||||||
'aria-label': '发起充值单'
|
'aria-label': '发起充值单'
|
||||||
}, '发起充值单'),
|
}, '发起充值单'),
|
||||||
React.createElement(Button, {
|
React.createElement('button', {
|
||||||
type: 'primary',
|
type: 'button',
|
||||||
style: H2_PRIMARY_BTN_STYLE,
|
className: 'vm-btn vm-btn-primary',
|
||||||
onClick: openCreate,
|
onClick: openCreate,
|
||||||
'aria-label': '新建加氢站点'
|
'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, {
|
React.createElement(Table, {
|
||||||
className: 'lc-list-table',
|
className: 'vm-list-table h2-site-list-table',
|
||||||
rowKey: 'id',
|
rowKey: 'id',
|
||||||
columns: columns,
|
columns: columns,
|
||||||
dataSource: displayList,
|
dataSource: displayList,
|
||||||
pagination: tablePagination,
|
pagination: false,
|
||||||
size: 'middle',
|
size: 'small',
|
||||||
scroll: { x: 1290 },
|
tableLayout: 'fixed',
|
||||||
locale: { emptyText: emptyNode },
|
scroll: { x: listTableScrollX, y: 'calc(100vh - 640px)' },
|
||||||
onChange: handleListTableChange
|
locale: { emptyText: emptyNode },
|
||||||
})
|
onChange: handleListTableChange
|
||||||
|
})
|
||||||
|
),
|
||||||
|
React.createElement('div', { className: 'vm-table-footer' }, renderH2TablePagination())
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
) : null,
|
) : null,
|
||||||
@@ -9634,30 +9626,6 @@ const Component = function () {
|
|||||||
cancelText: '取消'
|
cancelText: '取消'
|
||||||
}, '确定删除站点「' + ((deleteModal.record && deleteModal.record.name) || '') + '」吗?此操作不可撤销。'),
|
}, '确定删除站点「' + ((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, {
|
React.createElement(Modal, {
|
||||||
title: '批量导入加氢站点',
|
title: '批量导入加氢站点',
|
||||||
open: importModalOpen,
|
open: importModalOpen,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -2962,10 +2962,25 @@ const Component = function () {
|
|||||||
setAllRows(function (prev) {
|
setAllRows(function (prev) {
|
||||||
return recalcAllRows(applyStatementPatchesToRows(prev, patches));
|
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 dateRangeDraftState = useState(null);
|
||||||
var dateRangeDraft = dateRangeDraftState[0];
|
var dateRangeDraft = dateRangeDraftState[0];
|
||||||
var setDateRangeDraft = dateRangeDraftState[1];
|
var setDateRangeDraft = dateRangeDraftState[1];
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import * as antd from 'antd';
|
import * as antd from 'antd';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import '../../common/h2VehicleLedgerBridge.js';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -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}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user