迭代加氢订单、站点记录与台账链路,同步审批组件与导航注册,并下线旧工作台入口。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -47,6 +47,7 @@ const MORE_ACTION_ICONS: Record<string, LucideIcon> = {
|
||||
manage: Settings2,
|
||||
preview: Eye,
|
||||
download: Download,
|
||||
failDetail: FileText,
|
||||
};
|
||||
|
||||
function renderMenuItemLabel(item: OperationActionItem) {
|
||||
@@ -95,7 +96,7 @@ function ViewIcon() {
|
||||
}
|
||||
|
||||
function MoreIcon() {
|
||||
return <MoreHorizontal className="vm-op-action__icon" aria-hidden />;
|
||||
return <MoreHorizontal className="vm-op-more-btn__icon" aria-hidden strokeWidth={1.75} />;
|
||||
}
|
||||
|
||||
const PrimaryActionButton = React.forwardRef<
|
||||
@@ -121,18 +122,22 @@ const PrimaryActionButton = React.forwardRef<
|
||||
},
|
||||
ref,
|
||||
) {
|
||||
const isMore = icon === 'more';
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
type="button"
|
||||
className={['vm-op-action', className].filter(Boolean).join(' ')}
|
||||
className={[
|
||||
isMore ? 'vm-op-more-btn' : 'vm-op-action',
|
||||
className,
|
||||
].filter(Boolean).join(' ')}
|
||||
aria-label={ariaLabel}
|
||||
aria-haspopup={ariaHasPopup ? 'menu' : undefined}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
>
|
||||
{icon === 'view' ? <ViewIcon /> : <MoreIcon />}
|
||||
<span className="vm-op-action__label">{label}</span>
|
||||
{isMore ? <MoreIcon /> : <ViewIcon />}
|
||||
{isMore ? null : <span className="vm-op-action__label">{label}</span>}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
@@ -236,9 +241,9 @@ export function OperationActions({
|
||||
<PrimaryActionButton
|
||||
icon="more"
|
||||
label={moreLabel}
|
||||
ariaLabel={moreLabel + '操作'}
|
||||
ariaLabel="更多操作"
|
||||
ariaHasPopup
|
||||
className={moreOpen ? 'vm-op-action--open' : undefined}
|
||||
className={moreOpen ? 'vm-op-more-btn--open' : undefined}
|
||||
/>
|
||||
</Dropdown>
|
||||
) : null}
|
||||
|
||||
@@ -11,6 +11,41 @@ var H2_VERIFY_UNVERIFIED = 'unverified';
|
||||
var H2_RECORD_SOURCE_STATION = 'station';
|
||||
var H2_RECORD_SOURCE_LINGNIU = 'lingniu';
|
||||
|
||||
/**
|
||||
* 原型本地「系统车辆列表」车牌(不含尾缀 F)。命中 → 羚牛车辆,否则 → 非羚牛车辆。
|
||||
* 正式环境应改为车辆管理接口;未接真实 API。
|
||||
*/
|
||||
var H2_FLEET_PLATE_KEYS = [
|
||||
'浙A55666', '浙A77888', '浙A99001', '浙A12345', '浙A67890', '浙A88888', '浙A03561',
|
||||
'浙B23456', '浙B99999', '浙B58888',
|
||||
'沪A88888', '沪BDB9161', '沪ADB9161',
|
||||
'苏E33333',
|
||||
'浙A88H201', '浙F00688', '浙F07588', '浙F06618',
|
||||
'粤AGR8556', '粤AGP5156',
|
||||
/* 车辆氢费明细演示车牌 */
|
||||
'浙AD12345', '浙AH55660', '浙BK33210', '粤BK33210', '沪AD12345', '苏EF99887', '京CN88771', '川AL55602'
|
||||
];
|
||||
|
||||
var H2_FLEET_PLATE_SET = (function () {
|
||||
var set = {};
|
||||
H2_FLEET_PLATE_KEYS.forEach(function (p) {
|
||||
set[String(p).toUpperCase()] = true;
|
||||
});
|
||||
return set;
|
||||
})();
|
||||
|
||||
/** 车牌规范化键:去尾缀 F、大写、去空白 */
|
||||
function h2BridgeNormalizePlateKey(plateNo) {
|
||||
return String(plateNo || '').trim().toUpperCase().replace(/F$/u, '');
|
||||
}
|
||||
|
||||
/** 是否羚牛车辆(系统车辆列表命中) */
|
||||
function h2BridgeIsLingniuVehicle(plateNo) {
|
||||
var key = h2BridgeNormalizePlateKey(plateNo);
|
||||
if (!key) return false;
|
||||
return Boolean(H2_FLEET_PLATE_SET[key]);
|
||||
}
|
||||
|
||||
var H2_STATION_CODE_MAP = {
|
||||
'中国石油中油高新能源牙谷加油加氢站': 'JX-H2-001',
|
||||
'杭州临平加氢站': 'HZ-H2-002',
|
||||
@@ -41,7 +76,9 @@ var H2_CANONICAL_LEDGER_SEED = [
|
||||
{ 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: '李四', verifyStatus: H2_VERIFY_VERIFIED, verifiedAt: '2026-05-25 10:00:00', reconcileStatus: H2_RECONCILE_RECONCILED, reconcileDate: '2026-05-25 10:00:00' },
|
||||
{ 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: '王静', verifyStatus: H2_VERIFY_VERIFIED, verifiedAt: '2026-04-21 10:00:00', reconcileStatus: H2_RECONCILE_RECONCILED, reconcileDate: '2026-04-21 10:00:00' },
|
||||
{ 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: '张三', verifyStatus: H2_VERIFY_VERIFIED, verifiedAt: '2026-04-09 10:00:00', reconcileStatus: H2_RECONCILE_PENDING, reconcileDate: null },
|
||||
{ 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: '赵敏', verifyStatus: H2_VERIFY_UNVERIFIED, verifiedAt: null, reconcileStatus: H2_RECONCILE_PENDING }
|
||||
{ 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: '赵敏', verifyStatus: H2_VERIFY_UNVERIFIED, verifiedAt: null, reconcileStatus: H2_RECONCILE_PENDING },
|
||||
/* 非羚牛车辆演示:车牌不在系统车辆列表 → 列表核对/对账字段应显示为空 */
|
||||
{ key: 'rf-ext-1', id: 'rf-ext-1', stationId: 'JX-H2-001', stationName: '中国石油中油高新能源牙谷加油加氢站', hydrogenTime: '2026-07-13 14:22:00', plateNo: '浙C77801F', customerName: '中国石油中油高新能源牙谷加油加氢站·站端上报', hydrogenKg: 8.5, costUnitPrice: 42.5, costTotal: 361.25, customerUnitPrice: 42.5, customerAmount: 361.25, settlementStatus: 'customer', mileageKm: 88000, creatorName: '站端账号', verifyStatus: H2_VERIFY_UNVERIFIED, verifiedAt: null, reconcileStatus: H2_RECONCILE_PENDING }
|
||||
];
|
||||
|
||||
function h2BridgeFormatDateTime(value) {
|
||||
@@ -263,26 +300,28 @@ function h2BridgeResolveStationId(stationId, stationName) {
|
||||
return a || '';
|
||||
}
|
||||
|
||||
/** 手工台账种子:昨日已上传;今日默认未上传,便于演示拦截新增 */
|
||||
/** 手工台账种子:前日已上传;昨日默认未上传,便于演示「前一日缺失则今日禁新增」 */
|
||||
function h2BridgeBuildInitialManualLedgers() {
|
||||
var yesterday = h2BridgeYesterdayDateKey();
|
||||
var d = new Date();
|
||||
d.setDate(d.getDate() - 2);
|
||||
var dayBeforeYesterday = h2BridgeTodayDateKey(d);
|
||||
return [
|
||||
{
|
||||
id: 'ml-seed-yesterday',
|
||||
id: 'ml-seed-day-before-yesterday',
|
||||
stationId: 'JX-H2-001',
|
||||
stationName: '中国石油中油高新能源牙谷加油加氢站',
|
||||
ledgerDate: yesterday,
|
||||
ledgerDate: dayBeforeYesterday,
|
||||
images: [
|
||||
{
|
||||
id: 'ml-img-seed-1',
|
||||
name: '昨日手工台账.jpg',
|
||||
name: '前日手工台账.jpg',
|
||||
dataUrl: '',
|
||||
placeholder: true,
|
||||
archived: true,
|
||||
uploadedAt: yesterday + ' 18:30:00'
|
||||
uploadedAt: dayBeforeYesterday + ' 18:30:00'
|
||||
}
|
||||
],
|
||||
updatedAt: yesterday + ' 18:30:00'
|
||||
updatedAt: dayBeforeYesterday + ' 18:30:00'
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -507,6 +546,9 @@ function h2BridgeMapToStationLedgerRow(row, index) {
|
||||
costTotal: costTotal,
|
||||
settlementStatus: row.settlementStatus,
|
||||
orderNo: h2BridgeBuildRefuelOrderNo(row, index),
|
||||
/* 对账单筛选依赖:须透出核对状态,否则站点侧会把全部行当成未核对 */
|
||||
verifyStatus: row.verifyStatus === H2_VERIFY_VERIFIED ? H2_VERIFY_VERIFIED : H2_VERIFY_UNVERIFIED,
|
||||
verifiedAt: row.verifiedAt || null,
|
||||
reconcileStatus: row.reconcileStatus || (isReconciled ? H2_RECONCILE_RECONCILED : H2_RECONCILE_PENDING),
|
||||
statementRecordId: row.statementRecordId || null,
|
||||
reconcileDate: row.reconcileDate || null,
|
||||
@@ -732,6 +774,7 @@ function h2BridgeInit() {
|
||||
totalAmountMismatch: h2BridgeTotalAmountMismatch,
|
||||
lookupStationUnitPrice: h2BridgeLookupStationUnitPrice,
|
||||
todayDateKey: h2BridgeTodayDateKey,
|
||||
yesterdayDateKey: h2BridgeYesterdayDateKey,
|
||||
getManualLedger: function (stationIdOrName, ledgerDate) {
|
||||
return store.getManualLedger(stationIdOrName, ledgerDate);
|
||||
},
|
||||
@@ -744,9 +787,30 @@ function h2BridgeInit() {
|
||||
upsertManualLedger: function (input) {
|
||||
return store.upsertManualLedger(input);
|
||||
},
|
||||
/** 前一日台账未上传(且昨日≥本站首笔加氢日)→ 今日禁新增 */
|
||||
isManualLedgerGateBlocked: function (stationIdOrName) {
|
||||
var station = String(stationIdOrName || '').trim();
|
||||
if (!station) return false;
|
||||
var todayKey = h2BridgeTodayDateKey();
|
||||
var yesterdayKey = h2BridgeYesterdayDateKey();
|
||||
var rows = store.getRows() || [];
|
||||
var earliest = null;
|
||||
rows.forEach(function (row) {
|
||||
var sid = String(row.stationId || '').trim();
|
||||
var sn = String(row.stationName || '').trim();
|
||||
if (sid !== station && sn !== station) return;
|
||||
var m = String(row.hydrogenTime || '').match(/^(\d{4}-\d{2}-\d{2})/);
|
||||
if (!m) return;
|
||||
if (!earliest || m[1] < earliest) earliest = m[1];
|
||||
});
|
||||
if (!earliest || yesterdayKey > todayKey || yesterdayKey < earliest) return false;
|
||||
return !store.hasManualLedgerForDate(station, yesterdayKey);
|
||||
},
|
||||
upsertRow: function (row) { return store.upsertRow(row); },
|
||||
removeRow: function (id) { return store.removeRow(id); },
|
||||
subscribe: function (fn) { return store.subscribe(fn); }
|
||||
subscribe: function (fn) { return store.subscribe(fn); },
|
||||
isLingniuVehicle: h2BridgeIsLingniuVehicle,
|
||||
normalizePlateKey: h2BridgeNormalizePlateKey
|
||||
};
|
||||
}
|
||||
return store;
|
||||
@@ -776,5 +840,7 @@ export {
|
||||
h2BridgeFindDuplicateRow,
|
||||
h2BridgeTotalAmountMismatch,
|
||||
h2BridgeLookupStationUnitPrice,
|
||||
h2BridgeInferRecordSource
|
||||
h2BridgeInferRecordSource,
|
||||
h2BridgeIsLingniuVehicle,
|
||||
h2BridgeNormalizePlateKey
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ export interface ApprovalCardProps {
|
||||
|
||||
function statusClassName(status: ApprovalCardItem['status']): string {
|
||||
if (status === 'approved') return 'ap-status ap-status--approved';
|
||||
if (status === 'rejected') return 'ap-status ap-status--rejected';
|
||||
if (status === 'rejected' || status === 'terminated') return 'ap-status ap-status--rejected';
|
||||
return 'ap-status ap-status--pending';
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,9 @@ export interface ApprovalDetailPlaceholderProps {
|
||||
|
||||
function statusBadgeClass(status: ApprovalCardItem['status']): string {
|
||||
if (status === 'approved') return 'ap-detail__badge ap-detail__badge--approved';
|
||||
if (status === 'rejected') return 'ap-detail__badge ap-detail__badge--rejected';
|
||||
if (status === 'rejected' || status === 'terminated') {
|
||||
return 'ap-detail__badge ap-detail__badge--rejected';
|
||||
}
|
||||
return 'ap-detail__badge ap-detail__badge--pending';
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,8 @@ const STATUS_LABEL_MAP: Record<ApprovalStatus, string> = {
|
||||
pending: '审批中',
|
||||
processing: '审批中',
|
||||
approved: '已通过',
|
||||
rejected: '已驳回',
|
||||
rejected: '被驳回',
|
||||
terminated: '被终止',
|
||||
};
|
||||
|
||||
export function getApprovalTypeLabel(type: ApprovalTypeKey): string {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type ApprovalStatus = 'pending' | 'processing' | 'approved' | 'rejected';
|
||||
export type ApprovalStatus = 'pending' | 'processing' | 'approved' | 'rejected' | 'terminated';
|
||||
|
||||
export type ApprovalTabKey = 'todo' | 'done' | 'initiated' | 'cc';
|
||||
|
||||
|
||||
@@ -84,6 +84,12 @@
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--ln-primary, #32a06e) 35%, transparent);
|
||||
}
|
||||
|
||||
.vm-op-more-btn--open,
|
||||
.vm-op-more-btn[aria-expanded="true"] {
|
||||
color: var(--ln-ink, #0f172a);
|
||||
background: var(--ln-canvas-soft, #f4f4f5);
|
||||
}
|
||||
|
||||
.vm-op-more-btn__icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
|
||||
@@ -43,7 +43,10 @@ const moment = dayjs;
|
||||
const ANCHOR_TODAY = '2026-06-01';
|
||||
const IPC_STORAGE_KEY = 'oneos_ipc_insurance_v1';
|
||||
const IPC_COMPARE_SHEETS_KEY = 'oneos_ipc_compare_sheets_v1';
|
||||
const IPC_POLICY_RECOGN_TASKS_KEY = 'oneos_ipc_policy_recogn_tasks_v2';
|
||||
const IPC_POLICY_RECOGN_TASKS_KEY = 'oneos_ipc_policy_recogn_tasks_v3';
|
||||
const POLICY_RECOGN_FAIL_REASON_BLUR = 'OCR 识别失败,请检查文件清晰度或重新上传';
|
||||
const POLICY_RECOGN_FAIL_REASON_FORMAT = '文件格式或版式无法解析,请上传清晰 PDF / 图片保单';
|
||||
const POLICY_RECOGN_FAIL_REASON_CORRUPT = '文件损坏或内容为空,请重新导出后上传';
|
||||
const IPC_INSURANCE_HISTORY_EDITS_KEY = 'oneos_ipc_insurance_history_edits_v1';
|
||||
const IPC_EDIT_PLATE_KEY = 'oneos_ipc_edit_plate';
|
||||
const NO_PLATE_LABEL = '暂无车牌';
|
||||
@@ -1084,17 +1087,35 @@ const buildMockOcrResults = (files, mode, insuranceTypeLabel, allInsurance) => (
|
||||
mode
|
||||
);
|
||||
if (files.length >= 2 && idx === files.length - 1) {
|
||||
return {
|
||||
...result,
|
||||
recognSuccess: false,
|
||||
matched: false,
|
||||
matchTip: 'OCR 识别失败,请检查文件清晰度或重新上传',
|
||||
};
|
||||
return markRecognResultFailed(result, POLICY_RECOGN_FAIL_REASON_BLUR);
|
||||
}
|
||||
return result;
|
||||
})
|
||||
);
|
||||
|
||||
/** 识别失败原因:优先 failReason,回退 matchTip */
|
||||
const getPolicyRecognFailReason = (result) => (
|
||||
String(result?.failReason || result?.matchTip || '').trim() || '识别失败,原因未知'
|
||||
);
|
||||
|
||||
const getPolicyRecognFailResults = (results) => (
|
||||
(results || [])
|
||||
.filter((r) => r.recognSuccess === false)
|
||||
.map((r) => ({
|
||||
id: r.id || r.fileUid || r.fileName,
|
||||
fileName: r.fileName || '未命名文件',
|
||||
failReason: getPolicyRecognFailReason(r),
|
||||
}))
|
||||
);
|
||||
|
||||
const markRecognResultFailed = (result, reason) => ({
|
||||
...result,
|
||||
recognSuccess: false,
|
||||
matched: false,
|
||||
matchTip: reason,
|
||||
failReason: reason,
|
||||
});
|
||||
|
||||
const recognResultToPolicyDetail = (result, taskMode = 'policy') => {
|
||||
const mode = resolvePolicyRecognEffectiveMode(taskMode, result);
|
||||
const pd = result.policyDetail || {};
|
||||
@@ -2561,6 +2582,13 @@ const createMockPolicyRecognTasks = () => {
|
||||
];
|
||||
const resultsCompleted1 = buildMockOcrResults(filesCompleted1, 'policy', '交强险', insMap);
|
||||
if (resultsCompleted1[0]) resultsCompleted1[0].confirmed = true;
|
||||
// 末条失败:清晰度问题(buildMockOcrResults 已标记);再给另一张不同失败原因
|
||||
if (resultsCompleted1[1]) {
|
||||
resultsCompleted1[1] = markRecognResultFailed(
|
||||
resultsCompleted1[1],
|
||||
POLICY_RECOGN_FAIL_REASON_FORMAT,
|
||||
);
|
||||
}
|
||||
|
||||
const filesCompleted2 = [
|
||||
{ uid: 'demo-c4', name: '粤B88888_复驶批单.pdf', status: 'done' },
|
||||
@@ -2572,6 +2600,14 @@ const createMockPolicyRecognTasks = () => {
|
||||
resultsCompleted2.forEach((r) => {
|
||||
if (r.recognSuccess !== false) r.confirmed = true;
|
||||
});
|
||||
// 末条已是清晰度失败;确保 failReason 字段齐全
|
||||
const lastResumeFail = resultsCompleted2[resultsCompleted2.length - 1];
|
||||
if (lastResumeFail && lastResumeFail.recognSuccess === false) {
|
||||
resultsCompleted2[resultsCompleted2.length - 1] = markRecognResultFailed(
|
||||
lastResumeFail,
|
||||
POLICY_RECOGN_FAIL_REASON_BLUR,
|
||||
);
|
||||
}
|
||||
|
||||
const filesSuspend = [
|
||||
{ uid: 'demo-s1', name: '沪A06192F_商业险_停保批单.pdf', status: 'done' },
|
||||
@@ -2579,6 +2615,18 @@ const createMockPolicyRecognTasks = () => {
|
||||
];
|
||||
const resultsSuspend = buildMockOcrResults(filesSuspend, 'suspend', '', insMap);
|
||||
|
||||
// 全部失败任务:便于演示「无成功仍可看失败明细」
|
||||
const filesFailOnly = [
|
||||
{ uid: 'demo-f1', name: '损坏文件_保单.pdf', status: 'done' },
|
||||
{ uid: 'demo-f2', name: '非保单截图.jpg', status: 'done' },
|
||||
];
|
||||
const resultsFailOnly = buildMockOcrResults(filesFailOnly, 'policy', '商业险', insMap).map((r, idx) => (
|
||||
markRecognResultFailed(
|
||||
r,
|
||||
idx === 0 ? POLICY_RECOGN_FAIL_REASON_CORRUPT : POLICY_RECOGN_FAIL_REASON_FORMAT,
|
||||
)
|
||||
));
|
||||
|
||||
return [
|
||||
buildPolicyRecognTaskRecord({
|
||||
id: 'TASK-83892906',
|
||||
@@ -2616,6 +2664,18 @@ const createMockPolicyRecognTasks = () => {
|
||||
totalFileCount: filesSuspend.length,
|
||||
recognDoneCount: filesSuspend.length,
|
||||
}),
|
||||
buildPolicyRecognTaskRecord({
|
||||
id: 'TASK-84450001',
|
||||
entry: 'ocr',
|
||||
mode: 'policy',
|
||||
insuranceType: '商业险',
|
||||
results: resultsFailOnly,
|
||||
createdAt: '2026-06-03 11:05:00',
|
||||
completedAt: '2026-06-03 11:06:20',
|
||||
phase: 'results',
|
||||
totalFileCount: filesFailOnly.length,
|
||||
recognDoneCount: filesFailOnly.length,
|
||||
}),
|
||||
buildPolicyRecognTaskRecord({
|
||||
id: 'TASK-84210588',
|
||||
entry: 'ocr',
|
||||
@@ -3317,6 +3377,8 @@ const Component = function () {
|
||||
return stored && stored.length ? stored : createMockPolicyRecognTasks();
|
||||
});
|
||||
const [policyRecognTasksOpen, setPolicyRecognTasksOpen] = useState(false);
|
||||
const [policyRecognFailDetailOpen, setPolicyRecognFailDetailOpen] = useState(false);
|
||||
const [policyRecognFailDetailTask, setPolicyRecognFailDetailTask] = useState(null);
|
||||
const [policyRecognTasksFilters, setPolicyRecognTasksFilters] = useState(() => ({ ...DEFAULT_POLICY_RECOGN_TASK_FILTERS }));
|
||||
const [appliedPolicyRecognTasksFilters, setAppliedPolicyRecognTasksFilters] = useState(() => ({ ...DEFAULT_POLICY_RECOGN_TASK_FILTERS }));
|
||||
const [policyAddOpen, setPolicyAddOpen] = useState(false);
|
||||
@@ -4602,10 +4664,27 @@ const Component = function () {
|
||||
if (preferred) {
|
||||
setPolicyRecognActiveResultId(preferred.id);
|
||||
setPolicyRecognConfirmDraft(buildPolicyRecognConfirmDraft(preferred, task.mode || 'policy'));
|
||||
showPolicyRecognResultPreview(preferred);
|
||||
} else {
|
||||
setPolicyRecognActiveResultId('');
|
||||
setPolicyRecognConfirmDraft({ ...EMPTY_POLICY_DETAIL });
|
||||
}
|
||||
};
|
||||
|
||||
const openPolicyRecognFailDetail = (task) => {
|
||||
const fails = getPolicyRecognFailResults(task?.results);
|
||||
if (!fails.length) {
|
||||
message.info('该任务没有识别失败记录');
|
||||
return;
|
||||
}
|
||||
setPolicyRecognFailDetailTask(task);
|
||||
setPolicyRecognFailDetailOpen(true);
|
||||
};
|
||||
|
||||
const policyRecognFailDetailRows = useMemo(
|
||||
() => getPolicyRecognFailResults(policyRecognFailDetailTask?.results),
|
||||
[policyRecognFailDetailTask],
|
||||
);
|
||||
|
||||
const closePolicyRecogn = () => {
|
||||
const syncedResults = policyRecognPhase === 'results'
|
||||
? persistActiveRecognDraft()
|
||||
@@ -8120,13 +8199,31 @@ const Component = function () {
|
||||
{
|
||||
title: '识别失败数',
|
||||
dataIndex: 'recognFailCount',
|
||||
width: 96,
|
||||
width: 108,
|
||||
align: 'center',
|
||||
render: (val) => (
|
||||
<span style={{ fontVariantNumeric: 'tabular-nums', color: val > 0 ? '#dc2626' : '#64748b', fontWeight: 600 }}>
|
||||
{val ?? 0}
|
||||
</span>
|
||||
),
|
||||
render: (val, record) => {
|
||||
const count = val ?? 0;
|
||||
if (count > 0) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="lc-policy-recogn-fail-count-btn"
|
||||
aria-label={`查看 ${count} 条识别失败明细`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openPolicyRecognFailDetail(record);
|
||||
}}
|
||||
>
|
||||
{count}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span style={{ fontVariantNumeric: 'tabular-nums', color: '#64748b', fontWeight: 600 }}>
|
||||
0
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '识别进度',
|
||||
@@ -8171,31 +8268,112 @@ const Component = function () {
|
||||
fixed: 'right',
|
||||
render: (_, record) => {
|
||||
const recognizing = isPolicyRecognTaskRecognizing(record);
|
||||
const noSuccess = !(record.recognSuccessCount > 0);
|
||||
const disabled = recognizing || noSuccess;
|
||||
const action = (
|
||||
<OperationActions
|
||||
view={{
|
||||
label: '确认识别结果',
|
||||
disabled,
|
||||
onClick: () => openPolicyRecognTaskRecord(record),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
const hasSuccess = (record.recognSuccessCount || 0) > 0;
|
||||
const hasFail = (record.recognFailCount || 0) > 0;
|
||||
if (recognizing) {
|
||||
return (
|
||||
<Tooltip title="请等待识别完成后操作">
|
||||
<span>{action}</span>
|
||||
<span>
|
||||
<OperationActions
|
||||
view={{
|
||||
label: '确认识别结果',
|
||||
disabled: true,
|
||||
onClick: () => {},
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return action;
|
||||
if (!hasSuccess && hasFail) {
|
||||
return (
|
||||
<OperationActions
|
||||
view={{
|
||||
label: '查看失败明细',
|
||||
onClick: () => openPolicyRecognFailDetail(record),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<OperationActions
|
||||
view={{
|
||||
label: '确认识别结果',
|
||||
disabled: !hasSuccess,
|
||||
onClick: () => openPolicyRecognTaskRecord(record),
|
||||
}}
|
||||
more={hasFail ? [{
|
||||
key: 'failDetail',
|
||||
label: '查看失败明细',
|
||||
onClick: () => openPolicyRecognFailDetail(record),
|
||||
}] : undefined}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
className="lc-policy-recogn-fail-detail-modal"
|
||||
open={policyRecognFailDetailOpen}
|
||||
title={policyRecognFailDetailTask
|
||||
? `识别失败明细(${policyRecognFailDetailRows.length})`
|
||||
: '识别失败明细'}
|
||||
width={720}
|
||||
centered
|
||||
footer={(
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
setPolicyRecognFailDetailOpen(false);
|
||||
setPolicyRecognFailDetailTask(null);
|
||||
}}
|
||||
>
|
||||
关闭
|
||||
</Button>
|
||||
)}
|
||||
onCancel={() => {
|
||||
setPolicyRecognFailDetailOpen(false);
|
||||
setPolicyRecognFailDetailTask(null);
|
||||
}}
|
||||
>
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
style={{ marginBottom: 14, borderRadius: 8 }}
|
||||
message="以下文件识别未成功,不会进入确认写入台账;可核对文件后重新发起识别。"
|
||||
/>
|
||||
<Table
|
||||
size="middle"
|
||||
rowKey="id"
|
||||
dataSource={policyRecognFailDetailRows}
|
||||
pagination={policyRecognFailDetailRows.length > 8
|
||||
? { pageSize: 8, showSizeChanger: false }
|
||||
: false}
|
||||
locale={{ emptyText: '暂无失败记录' }}
|
||||
columns={[
|
||||
{
|
||||
title: '文件/保单名',
|
||||
dataIndex: 'fileName',
|
||||
width: 280,
|
||||
ellipsis: true,
|
||||
render: (val) => (
|
||||
<span style={{ fontWeight: 600, color: '#0f172a' }}>{val || '—'}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '失败原因',
|
||||
dataIndex: 'failReason',
|
||||
render: (val) => (
|
||||
<span style={{ color: '#b91c1c', lineHeight: 1.45 }}>{val || '—'}</span>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
className="lc-policy-recogn-modal"
|
||||
title={
|
||||
|
||||
@@ -460,6 +460,46 @@
|
||||
margin-bottom: var(--oneos-space-3);
|
||||
}
|
||||
|
||||
/* 识别任务:失败数可点击入口 */
|
||||
.lc-policy-recogn-fail-count-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 44px;
|
||||
min-height: 44px;
|
||||
padding: 0 8px;
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: #dc2626;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1.2;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
transition: background-color 0.15s ease, color 0.15s ease;
|
||||
}
|
||||
|
||||
.lc-policy-recogn-fail-count-btn:hover,
|
||||
.lc-policy-recogn-fail-count-btn:focus-visible {
|
||||
color: #b91c1c;
|
||||
background: color-mix(in srgb, #ef4444 10%, transparent);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.lc-policy-recogn-fail-count-btn:focus-visible {
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, #ef4444 35%, transparent);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.lc-policy-recogn-fail-count-btn {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
.vm-page.ipc-page .lc-alert-stats-row {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
BookOpen,
|
||||
Building2,
|
||||
Calendar,
|
||||
CalendarClock,
|
||||
Car,
|
||||
CircleDollarSign,
|
||||
ClipboardCheck,
|
||||
@@ -772,34 +773,53 @@ export const SELF_OPERATED_MODULES: LineModule[] = [
|
||||
id: 'self-contract',
|
||||
step: 1,
|
||||
title: '自营合同',
|
||||
subtitle: '关键信息录入 · 上传合同 · 自动交车',
|
||||
subtitle: '关键信息录入 · 业务确认生效 · 可派车',
|
||||
shortLabel: '合同',
|
||||
accent: 'blue',
|
||||
icon: FileSignature,
|
||||
roles: ['业务管理组', '业务服务组'],
|
||||
start: '创建自营合同,记录客户方、车型、车辆数、交车时间与地点等关键信息,并上传合同附件。',
|
||||
start: '创建自营运力服务合同,记录客户方、车型、车辆数、交车时间与地点等关键信息,并上传合同附件。',
|
||||
process: [
|
||||
'录入自营项目与客户方、车型、车辆数量等合同要素。',
|
||||
'填写交车时间、交车地点,上传自营合同扫描件。',
|
||||
'合同提交后自动生成交车任务,推送运维条线处理交付。',
|
||||
'业务确认生效后可创建轻量调度任务,并提示运维交车交付。',
|
||||
],
|
||||
closure: '合同信息与交车任务联动,自营项目从签约起即可进入交付与运营环节。',
|
||||
prototypeHref: '/prototypes/oneos-web-lease-contract',
|
||||
prototypeLabel: '打开车辆租赁合同',
|
||||
closure: '合同界定项目边界,成为调度派车与交车的上游凭证。',
|
||||
prototypeHref: '/prototypes/self-operated-contract',
|
||||
prototypeLabel: '打开自营合同',
|
||||
},
|
||||
{
|
||||
id: 'self-dispatch',
|
||||
step: 2,
|
||||
title: '调度任务',
|
||||
subtitle: '轻量派车 · 交还车校验 · 办结出车',
|
||||
shortLabel: '调度',
|
||||
accent: 'indigo',
|
||||
icon: CalendarClock,
|
||||
roles: ['业务服务组', '调度岗', '运维部'],
|
||||
start: '在生效自营合同下创建出车任务,派车派司机;非 TMS,不做配载与承运商调度。',
|
||||
process: [
|
||||
'选择合同、计划出车日、线路说明、是否多趟与计价口径。',
|
||||
'派车派司机;车辆未交车时先完成运维交车(原型可模拟交车)。',
|
||||
'办结确认实际出车后,自动生成物流业务明细一行。',
|
||||
],
|
||||
closure: '出车事实可追溯到合同与车牌,并驱动台账落账。',
|
||||
prototypeHref: '/prototypes/self-operated-dispatch-task',
|
||||
prototypeLabel: '打开调度任务',
|
||||
},
|
||||
{
|
||||
id: 'self-ledger',
|
||||
step: 2,
|
||||
step: 3,
|
||||
title: '自营台账',
|
||||
subtitle: '导入台账 · 成本利润核算 · 项目盈亏',
|
||||
subtitle: '任务自动生成 · 补录导入 · 盈亏核算',
|
||||
shortLabel: '台账',
|
||||
accent: 'emerald',
|
||||
icon: BookOpen,
|
||||
roles: ['业务服务组', '财务部'],
|
||||
start: '根据导入的自营台账与账单信息,汇总项目侧收入与成本数据。',
|
||||
start: '承接调度办结自动生成的出车明细,并以导入/行编辑补录异常与历史数据。',
|
||||
process: [
|
||||
'导入或维护自营运营台账、账单明细。',
|
||||
'自动计算利润侧数据,以及司机成本、车辆成本侧数据。',
|
||||
'调度办结后自动写入出车营收侧字段,并计算金额/总成本/盈亏。',
|
||||
'氢费、ETC 等成本一期可手工补录;导入模板保留为补录通道。',
|
||||
'汇集至项目盈亏表,支撑项目经营分析。',
|
||||
'演进阶段关联财务「收款记录」,形成业财记录闭环。',
|
||||
],
|
||||
@@ -924,26 +944,26 @@ export const BUSINESS_LINES: BusinessLineConfig[] = [
|
||||
shortLabel: '自营',
|
||||
hero: {
|
||||
title: '自营业务条线',
|
||||
tagline: '自营物流条线合同->账单业财闭环',
|
||||
tagline: '自营合同 → 轻量调度 → 交还车 → 台账自动生成',
|
||||
summary:
|
||||
'两大模块串联自营合同与自营台账:合同驱动交车任务,台账导入后自动核算司机、车辆成本与项目盈亏,逐步对接财务收款形成业财闭环。',
|
||||
'三大模块串联自营合同、轻量调度任务与自营台账:合同界定项目,调度派车办结驱动明细自动生成,运维交还车保障运营态,逐步对接财务收款形成业财闭环。',
|
||||
stats: [
|
||||
{ value: '2', label: '业务模块' },
|
||||
{ value: '3', label: '业务模块' },
|
||||
{ value: '4+', label: '协同部门' },
|
||||
{ value: '1', label: '业财闭环' },
|
||||
{ value: '1', label: '经营闭环' },
|
||||
],
|
||||
},
|
||||
modules: SELF_OPERATED_MODULES,
|
||||
loopTitle: '合同到台账,自营经营可核算',
|
||||
loopTitle: '合同到调度再到台账,自营经营可核算',
|
||||
loopSummary:
|
||||
'自营合同录入并生成交车任务 → 运营数据导入自营台账 → 自动汇总利润与司机、车辆成本 → 汇集项目盈亏表 → 关联收款记录完成业财闭环。',
|
||||
'自营合同生效 → 轻量调度派车(交车后可办结)→ 办结自动生成物流业务明细 → 自动汇总盈亏 → 汇集项目盈亏表 → 演进关联收款记录完成业财闭环。',
|
||||
evolution: {
|
||||
currentLabel: '现阶段 · 人工核算台账',
|
||||
currentLabel: '现阶段 · 合同 + 调度办结落账',
|
||||
currentText:
|
||||
'人工发起自营合同,人工计算车辆成本、司机成本与自营台账,项目盈亏依赖线下表格汇总与人工核对。',
|
||||
'自营合同业务确认生效;轻量调度派车办结(须交车);台账以自动生成为主、导入为辅,盈亏在台账内可算。',
|
||||
targetLabel: '演进方向 · 业财记录闭环',
|
||||
targetText:
|
||||
'仍人工发起合同;司机在线管理、司机成本自动计算;自营台账人工导入后自动汇总,关联财务「收款记录」模块,形成业财记录闭环。',
|
||||
'成本侧氢费/ETC/日成本自动回填;司机在线管理;台账关联财务「收款记录」,形成业财记录闭环;不做完整 TMS。',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
# 加氢订单(H5)— 已确认需求与设计决策
|
||||
|
||||
> **状态:已收敛 / 部分作废(2026-07-18)**
|
||||
> 现行权威规格以 [requirements-prd.md](./requirements-prd.md) 及同目录功能规格为准。
|
||||
> 下文保留为 2026-07-14 对齐快照;下列条目**已被后续决策覆盖,勿再按本文件验收**:
|
||||
>
|
||||
> | 快照原文 | 现行口径 |
|
||||
> |---|---|
|
||||
> | 顶栏演示可切换示例站 | **H5 无切站**,固定登录本站 |
|
||||
> | 列表展示对账状态 / 对账时间 | 列表只展示**核对**;对账见详情 |
|
||||
> | 已对账才只读 | **羚牛**已核对或已对账即锁定;非羚牛不锁 |
|
||||
> | 无底 Tab | 已有底栏 Tab:加氢订单 / 手工台账 |
|
||||
> | 分步 OCR 向导 | 已改为单表单 + OCR 拍摄区 |
|
||||
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 日期 | 2026-07-14 |
|
||||
| 日期 | 2026-07-14(快照);覆盖说明 2026-07-18 |
|
||||
| 原型 ID | `oneos-h5-h2-order` |
|
||||
| 菜单位置 | OneOS → 加氢站管理 → 加氢订单 |
|
||||
| 文档性质 | 决策快照(对齐当时确认,不随实现逐行同步) |
|
||||
| 文档性质 | 历史决策快照(**不随实现同步**;冲突处以 PRD 为准) |
|
||||
|
||||
---
|
||||
|
||||
@@ -14,8 +26,8 @@
|
||||
| 问题 | 用户选择 |
|
||||
|---|---|
|
||||
| 列表数据范围 | **A. 仅本站**(当前演示站点绑定的加氢站) |
|
||||
| 登录态 | **A. 已登录态**;顶栏展示站名,演示可切换示例站 |
|
||||
| 已对账记录 | **A. 不可改**(只读);未对账可编辑 / 删除 |
|
||||
| 登录态 | **A. 已登录态**;顶栏展示站名 ~~演示可切换示例站~~ → **已收敛:无切站** |
|
||||
| 已对账记录 | **A. 不可改**(只读);未对账可编辑 / 删除 → **已收敛:羚牛·已核对或已对账均锁** |
|
||||
| OCR 演示 | **A. 拍照或选图 → 约 1s 模拟识别 → 可校正**(已改为单表单,非三步) |
|
||||
|
||||
### 1.1 目标与范围
|
||||
@@ -61,8 +73,8 @@
|
||||
| 预览壳 | 居中 390×844 手机框;真机浏览器可全宽 |
|
||||
| 首屏 | 列表为主 + 悬浮「+」 |
|
||||
| 卡片优先级 | 车牌、状态、时间、量与金额;单价 / 对账时间次级 |
|
||||
| 无底 Tab | 本模块单入口,不套小羚羚全局 Tab |
|
||||
| 演示切换 | 顶栏站点下拉,仅演示,不代表正式鉴权 |
|
||||
| 无底 Tab | ~~本模块单入口~~ → **已收敛:底栏「加氢订单 / 手工台账」** |
|
||||
| 演示切换 | ~~顶栏站点下拉~~ → **已收敛:H5 固定本站、无切站** |
|
||||
|
||||
### 2.2 信息架构
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## 目标
|
||||
|
||||
查看单笔加氢记录完整字段;在「未核对且未对账」时可编辑或删除。
|
||||
查看单笔加氢记录完整字段;在「未核对且未对账」时可编辑或删除(**锁定仅适用于羚牛车辆**)。
|
||||
|
||||
## 详情展示
|
||||
|
||||
@@ -11,15 +11,18 @@
|
||||
- **识别原图**:车牌照片、加氢机面板照片(均为拍摄时叠加时间/地点水印后的原图;种子数据无实拍时用演示水印图)
|
||||
- 明细行:站点、车牌、里程、单价、核对/对账状态与时间
|
||||
|
||||
> 非羚牛车辆:核对/对账字段展示「—」,不展示核对待办语义(见 [fleet-plate-tag.md](./fleet-plate-tag.md))。
|
||||
|
||||
## 锁定规则
|
||||
|
||||
| 条件 | 结果 |
|
||||
|------|------|
|
||||
| 已对账 | 不可编辑/删除;提示已纳入站点对账单 |
|
||||
| 已核对(未对账) | 不可编辑/删除;提示能源部已核对 |
|
||||
| 未核对且未对账 | 可编辑、可删除 |
|
||||
| 优先级 | 条件 | 结果 |
|
||||
|--------|------|------|
|
||||
| 1 | **非羚牛车辆** | **不锁定**(不以核对/对账状态禁改删) |
|
||||
| 2 | 羚牛 ∧ 已对账 | 不可编辑/删除;提示已纳入站点对账单 |
|
||||
| 3 | 羚牛 ∧ 已核对(未对账) | 不可编辑/删除;提示能源部已核对 |
|
||||
| 4 | 羚牛 ∧ 未核对且未对账 | 可编辑、可删除 |
|
||||
|
||||
判定函数:`isOrderLocked`(核对或对账任一成立即锁)。
|
||||
判定函数:`isOrderLocked`(先判羚牛;再核「核对或对账任一成立即锁」)。
|
||||
|
||||
## 编辑
|
||||
|
||||
@@ -27,11 +30,12 @@
|
||||
|
||||
## 删除
|
||||
|
||||
二次确认后从 Bridge 移除该行。
|
||||
二次确认文案:「确认删除车牌 … 的未锁定记录?」;确认后从 Bridge 移除该行。
|
||||
|
||||
## 验收
|
||||
|
||||
1. 未核对未对账:底部有删除/编辑
|
||||
2. 已核对或已对账:无操作栏,有锁定说明
|
||||
3. 详情含对账信息;列表卡片不含对账
|
||||
4. 详情展示车牌与面板带水印原图(新上报提交后为实拍;种子行为演示图)
|
||||
1. 羚牛 ∧ 未核对未对账:底部有删除/编辑
|
||||
2. 羚牛 ∧ 已核对或已对账:无操作栏,有锁定说明
|
||||
3. 非羚牛:可改删(不因核对/对账锁定);核对/对账展示「—」
|
||||
4. 详情含对账信息;列表卡片不含对账
|
||||
5. 详情展示车牌与面板带水印原图(新上报提交后为实拍;种子行为演示图)
|
||||
|
||||
@@ -9,14 +9,14 @@
|
||||
| 项 | 规则 |
|
||||
|---|---|
|
||||
| 入口 | H5:底部 Tab「手工台账」;Web:顶栏「手工台账」 |
|
||||
| 站点范围 | 仅展示当前登录用户角色对应站点;单站人员仅 1 个、多站可切换、超级管理员可选全部(原型用标注状态切换演示,未接真实登录) |
|
||||
| 站点范围 | **H5 固定本站**(无切站);Web 可按角色多站/超管切换(标注状态演示,未接真实登录) |
|
||||
| 上传形态 | 仅图片(拍照/相册),可多张 |
|
||||
| 维度 | **加氢站 + 自然日(本地日历日)** 一份 |
|
||||
| 日历 | 月历自本站**首笔加氢记录日**起算:绿=已传、橙=未传;首笔之前无需补传;禁选未来日 |
|
||||
| 补传 | 允许为过往未传日期补传;已传日期仅可**追加**新图 |
|
||||
| 归档 | **确认上传后的图片进入已归档状态,不可删除**;未确认的草稿图仍可删除 |
|
||||
| 二次确认 | Web 点「确认上传/补传」先弹窗:「提交后将无法修改,是否确认手工台账照片无误」;确认后才写入 |
|
||||
| 拦截 | **未上传今日手工台账 → 禁止新增加氢记录**(编辑已有记录不拦;补传历史不替代今日校验) |
|
||||
| 二次确认 | H5 / Web 确认上传前弹窗:「提交后将无法修改,是否确认手工台账照片无误」;确认后才写入 |
|
||||
| 拦截 | **前一日手工台账未上传 → 今日禁止新增加氢记录**(编辑已有记录不拦;补传更早历史日不替代昨日校验) |
|
||||
| 数据 | 共享 `H2VehicleLedgerBridge` 内存 Store(未接真实 API);站名/编码解析见 Bridge.`h2BridgeResolveStationId` |
|
||||
|
||||
## 判定顺序(新增拦截)
|
||||
@@ -24,8 +24,9 @@
|
||||
| 优先级 | 条件 | 结果 |
|
||||
|--------|------|------|
|
||||
| 1 | 操作=编辑已有记录 | 不校验手工台账 |
|
||||
| 2 | 操作=新增,且目标站今日已有 ≥1 张台账图 | 允许进入新增 |
|
||||
| 3 | 操作=新增,今日未上传 | 提示并跳转手工台账 Tab |
|
||||
| 2 | 操作=新增,且昨日早于本站首笔加氢日(或不要求昨日台账) | 允许进入新增 |
|
||||
| 3 | 操作=新增,且目标站昨日已有 ≥1 张台账图 | 允许进入新增 |
|
||||
| 4 | 操作=新增,昨日未上传 | 顶部/Toast 提示「手工台账有缺失,作为对账重要依据,请先上传手工台账」,并**直接跳转**手工台账页且选中昨日 |
|
||||
|
||||
## 日历与补传
|
||||
|
||||
@@ -36,26 +37,27 @@
|
||||
| 未传计数 | 本月内 `max(月初, 首笔日)`~min(月末, 今日) 中未上传的天数 |
|
||||
| 点选 | 仅 `dateKey ≤ 今日`;选中后右侧/下方展示该日草稿图与上传按钮 |
|
||||
| 归档不可删 | Store 已有图强制保留;`upsertManualLedger` 仅追加新图,忽略删除已有图的请求 |
|
||||
| 与拦截关系 | 仅 **今日** 影响「禁止新增」;历史补传解决核对缺口,不放宽今日门禁 |
|
||||
| 与拦截关系 | 仅 **昨日** 影响「禁止新增」;历史补传解决核对缺口;今日台账仍可按日常上传,但不作为新增门禁 |
|
||||
|
||||
## 用户可见
|
||||
|
||||
- 今日状态双反馈(Web 放在右侧「当日台账」卡片内):未上传「今日尚未上传手工台账(YYYY-MM-DD)」异常态;已上传「今日已上传手工台账(YYYY-MM-DD)」正常态
|
||||
- 今日未上传:橙/黄提示 + Tab 红点
|
||||
- 今日状态双反馈(H5 台账页顶部卡片 / Web 右侧「当日台账」):未上传「今日尚未上传…」异常态;已上传「今日已上传…」正常态。**日常上传反馈 ≠ 新增门禁**;H5 文案须写明「今日不作为新增门禁,仅要求前一日已上传」
|
||||
- 昨日缺失(H5):**列表顶栏可点橙条** + 底栏 FAB 旁提示 + Tab 红点;台账页同样展示橙条并可选中昨日;文案固定为「手工台账有缺失,作为对账重要依据,请先上传手工台账」
|
||||
- 日历:自首笔加氢日起绿/橙标记;首笔之前无橙点(无需补传);本月未传天数摘要
|
||||
- Web 右侧面板标题「手工台账」:空态提示;有图可点进全屏翻页预览并角标「已归档/待提交」;预览区内上传;底部确认前二次弹窗;提交后立即展示已归档缩略图;左右卡片等高
|
||||
|
||||
## 代码路径
|
||||
|
||||
- Bridge:`src/common/h2VehicleLedgerBridge.js`(`hasManualLedgerForDate` / `upsertManualLedger` / `listManualLedgersByStation`)
|
||||
- H5:`oneos-h5-h2-order` · `ManualLedgerPanel` + `utils/manual-ledger.ts`(`buildManualMonthCells` / `getFirstHydrogenDateKey`)
|
||||
- Web:`oneos-web-h2-station/pages/02-加氢记录.jsx`(手工台账 Tab 月历)
|
||||
- Bridge:`src/common/h2VehicleLedgerBridge.js`(`hasManualLedgerForDate` / `isManualLedgerGateBlocked` / `upsertManualLedger` / `listManualLedgersByStation`)
|
||||
- H5:`oneos-h5-h2-order` · `ManualLedgerPanel` + `utils/manual-ledger.ts`(`isManualLedgerGateBlocked` / `buildManualMonthCells` / `getFirstHydrogenDateKey`)
|
||||
- Web:`oneos-web-h2-station/pages/02-加氢记录.jsx`(`hrIsManualLedgerGateBlocked` / 跳转选中昨日)
|
||||
|
||||
## 验收
|
||||
|
||||
1. 今日未上传时,H5/Web 点「新增」均被拦截并引导上传
|
||||
2. 上传至少一张图并确认后,可新增电子记录
|
||||
1. 昨日未上传时,H5/Web 列表顶栏提示固定文案;点「新增」被拦截并直接进入手工台账(选中昨日)
|
||||
2. 补传昨日台账并确认后,可新增加氢记录
|
||||
3. 日历自首笔加氢日起区分已传/未传;首笔之前不标未传、不计入补传;可点选范围内未传日补传
|
||||
4. 补传历史日后,该日日历变为已传;今日仍未传时新增仍被拦截
|
||||
4. 仅补传更早历史日、昨日仍未传时,新增仍被拦截
|
||||
5. 确认上传后的图显示已归档且不可删除;仅可追加补传
|
||||
6. H5 与 Web 共用同一 Store,一端上传另一端可见(同会话)
|
||||
7. Web 导出 CSV 列含「车辆来源」(羚牛车辆/非羚牛车辆)、「数据来源」(站端上传/羚牛上传),与列表标签口径一致
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
原型本地判定,未接真实车辆管理 API。
|
||||
|
||||
全文(含核对/对账字段空展示规则,跨 Web / 台账 / H5):
|
||||
[../../oneos-web-h2-station/.spec/fleet-vehicle-verify.md](../../oneos-web-h2-station/.spec/fleet-vehicle-verify.md)
|
||||
|
||||
## 判定顺序
|
||||
|
||||
| 优先级 | 条件 | 结果 |
|
||||
@@ -17,14 +20,14 @@
|
||||
|
||||
## 数据源
|
||||
|
||||
- `utils/fleet-plates.ts` 内本地种子车牌集合(含加氢 Bridge 演示车牌与部分车辆管理样例)。
|
||||
- 正式环境应改为查询车辆管理 / 系统车辆列表接口。
|
||||
- 优先 `H2VehicleLedgerBridge.isLingniuVehicle`;本地回退见 `utils/fleet-plates.ts`(与 Bridge `H2_FLEET_PLATE_KEYS` 同步)。
|
||||
|
||||
## 用户可见结果
|
||||
|
||||
- 绿色标签「羚牛车辆」或橙色标签「非羚牛车辆」。
|
||||
- **非羚牛车辆**:列表/详情中核对状态、核对时间、对账状态、对账时间显示「—」,不展示核对待办语义。
|
||||
- 不拦截提交(仅提示归属)。
|
||||
|
||||
## 代码路径
|
||||
|
||||
- `getVehicleTag` / `isLingniuVehicle` → `CreateWizard` 车牌行右侧标签。
|
||||
- `getVehicleTag` / `isLingniuVehicle` → `CreateWizard`、`OrderCard`、`OrderDetail`
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|---|---|
|
||||
| 代码路径 | Store:`src/common/h2VehicleLedgerBridge.js`;对账单:`oneos-web-h2-station-site`;PC 加氢记录:`oneos-web-h2-station`;车辆氢费明细:`vehicle-h2-fee-ledger` |
|
||||
| 规格全文 | [../vehicle-h2-fee-ledger/.spec/verify-reconcile.md](../vehicle-h2-fee-ledger/.spec/verify-reconcile.md) |
|
||||
| 车辆标签 | [fleet-plate-tag.md](./fleet-plate-tag.md) |
|
||||
| 数据源 | 原型本地种子 + 内存共享 Store(**未接真实 API**) |
|
||||
|
||||
---
|
||||
@@ -17,6 +18,8 @@
|
||||
|
||||
> `reconciledAt` 仅作完成核对时的兼容写入,**不得**再用于判定「已对账」。
|
||||
|
||||
---
|
||||
|
||||
## 2. 数据流
|
||||
|
||||
```text
|
||||
@@ -26,23 +29,29 @@
|
||||
|
||||
能源部 · 完成核对
|
||||
→ verifyStatus=verified + verifiedAt
|
||||
→ 站端同步「已核对」,禁改删
|
||||
→ 站端同步「已核对」;羚牛车辆禁改删
|
||||
|
||||
站点信息 · 对账单提交(仅已核对 ∧ 未对账)
|
||||
→ reconcileStatus=reconciled + reconcileDate + statementRecordId
|
||||
→ 站端 / 台账同步「已对账」
|
||||
→ 站端 / 台账同步「已对账」;羚牛车辆禁改删
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 用户可见结果(H5)
|
||||
|
||||
| 结果 | 行为 |
|
||||
|---|---|
|
||||
| 未核对 | 可编辑、删除 |
|
||||
| 已核对未对账 | 只读;提示能源部已核对 |
|
||||
| 已对账 | 只读;提示已纳入对账单 |
|
||||
| 非羚牛车辆 | 核对/对账展示「—」;**不锁定**改删 |
|
||||
| 羚牛 · 未核对 | 可编辑、删除 |
|
||||
| 羚牛 · 已核对未对账 | 只读;提示能源部已核对 |
|
||||
| 羚牛 · 已对账 | 只读;提示已纳入对账单 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 边界
|
||||
|
||||
- 站端上报默认未核对、未对账
|
||||
- **站端改删锁定仅适用于羚牛车辆**(与 Web / 台账车辆标签口径一致)
|
||||
- 对账时间只认 `reconcileDate`(对账单回写)
|
||||
- 跨页联动依赖同浏览器会话共享 Store
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 文档版本 | v1.5 |
|
||||
| 文档版本 | v1.7 |
|
||||
| 模块名称 | 加氢站管理 → 加氢订单 |
|
||||
| 所属系统 | ONE-OS(加氢站手机浏览器) |
|
||||
| 交互原型 | `/prototypes/oneos-h5-h2-order` |
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
采购创建加氢站并绑定供应商、开放系统账号后,加氢站人员通过手机浏览器登录 OneOS,在「加氢订单」模块上报加氢记录。Web「加氢记录」为同一业务的 PC 入口(共享 Bridge 数据;PC 另有导出与预约加氢)。
|
||||
|
||||
每日须上传**手工台账**照片,作为电子记录的核对依据;未上传当日手工台账时不可新增加氢记录。手工台账页提供月历,标出已传/未传日期,支持补传过往缺口。
|
||||
每日须上传**手工台账**照片,作为电子记录的核对依据;**前一日**手工台账未上传时,今日不可新增加氢记录。手工台账页提供月历,标出已传/未传日期,支持补传过往缺口。
|
||||
|
||||
### 1.1 目标用户
|
||||
|
||||
@@ -46,14 +46,15 @@
|
||||
|
||||
1. 菜单位于 OneOS → 加氢站管理 → 加氢订单
|
||||
2. 默认已登录本站;列表仅本站(无切站)
|
||||
3. 底栏 Tab:加氢订单 / 手工台账;月历可区分已传/未传并补传过往日;未上传今日手工台账不可新增;上传后可新增;时间默认点击新增时刻(到分)
|
||||
4. 已核对或已对账不可编辑/删除;未核对未对账可
|
||||
5. 能源部完成核对后同步「已核对」;站点对账单提交后同步「已对账」+ 对账时间
|
||||
3. 底栏 Tab:加氢订单 / 手工台账;月历可区分已传/未传并补传过往日;前一日未上传时提示「手工台账有缺失,作为对账重要依据,请先上传手工台账」且点新增直达台账页;补传昨日后可新增;时间默认点击新增时刻(到分)
|
||||
4. **羚牛车辆**已核对或已对账不可编辑/删除;非羚牛不锁定;未核对未对账(羚牛)可改删
|
||||
5. 能源部完成核对后同步「已核对」;站点对账单提交后同步「已对账」+ 对账时间(锁定仅羚牛)
|
||||
6. 同站同车牌同加氢时间重复保存被拦截
|
||||
7. Web「加氢记录」与 H5「加氢订单」手工台账逻辑一致(同 Store)
|
||||
8. 车牌/面板拍摄预览含水印(时间、地点=本站名称);里程可从车机获取(Mock)
|
||||
9. 面板 OCR 可展示站点信息维护的加氢机品牌型号线索
|
||||
10. 详情页展示车牌与面板带水印原图
|
||||
11. 昨日台账缺失时:列表顶栏橙条 + 底栏提示 + Tab 红点;台账页不把「今日未传」写成新增门禁
|
||||
|
||||
---
|
||||
|
||||
@@ -61,11 +62,11 @@
|
||||
|
||||
| 主题 | 摘要 | 全文 |
|
||||
|---|---|---|
|
||||
| 对账 / 核对 | 核对≠对账;触发方不同 | [reconcile-linkage.md](./reconcile-linkage.md)、[车辆氢费 verify-reconcile](../vehicle-h2-fee-ledger/.spec/verify-reconcile.md) |
|
||||
| 对账 / 核对 | 核对≠对账;**锁定仅羚牛** | [reconcile-linkage.md](./reconcile-linkage.md)、[车辆氢费 verify-reconcile](../vehicle-h2-fee-ledger/.spec/verify-reconcile.md) |
|
||||
| 重复键 | 与 Web 共用 Bridge | [Web 数据模型](../oneos-web-h2-station/.spec/record-data-model.md) |
|
||||
| 羚牛车辆标签 | 本地系统车辆集合判定 | [fleet-plate-tag.md](./fleet-plate-tag.md) |
|
||||
| 总额 | 单价×加氢量自动算,界面不展示公式 | [feature-create.md](./feature-create.md) |
|
||||
| 手工台账 | 按站+自然日上传图片;月历标已传/未传可补传;未上传今日禁新增 | [feature-manual-ledger.md](./feature-manual-ledger.md) |
|
||||
| 手工台账 | 按站+自然日上传图片;月历标已传/未传可补传;**昨日未传则今日禁新增** | [feature-manual-ledger.md](./feature-manual-ledger.md) |
|
||||
|
||||
---
|
||||
|
||||
@@ -77,4 +78,16 @@
|
||||
| `create` | 时间选择、站名、车牌键盘、面板 OCR、金额字段 |
|
||||
| `detail` | 详情字段、锁定说明、编辑/删除 |
|
||||
|
||||
右侧工具栏「原型目录」结构:
|
||||
|
||||
| 目录 | 内容 |
|
||||
|---|---|
|
||||
| PRD 全文 | requirements-prd.md |
|
||||
| 需求角色 | roles.md |
|
||||
| 用户故事 | user-stories.md |
|
||||
| 关键逻辑 | key-logic + 台账门禁 / 对账 / 车辆标签 |
|
||||
| 流程图 | flows.md(Mermaid) |
|
||||
| 功能说明 | 列表 / 新增 / 台账 / 详情 / 车辆 / 对账 |
|
||||
| 按页面 | list / create / detail 要点 |
|
||||
|
||||
标注数据:`annotation-source.json`(由 `scripts/build-annotation-source.mjs` 从本目录 Markdown 生成)。
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
"version": 2,
|
||||
"prototypeName": "oneos-h5-h2-order",
|
||||
"pageId": "list",
|
||||
"updatedAt": 1784280448632,
|
||||
"updatedAt": 1784378211658,
|
||||
"nodes": [
|
||||
{
|
||||
"images": [],
|
||||
"controls": [],
|
||||
"createdAt": 1784280448632,
|
||||
"updatedAt": 1784280448632,
|
||||
"createdAt": 1784378211658,
|
||||
"updatedAt": 1784378211658,
|
||||
"hasMarkdown": true,
|
||||
"annotationText": "",
|
||||
"id": "h5-hero",
|
||||
@@ -31,8 +31,8 @@
|
||||
{
|
||||
"images": [],
|
||||
"controls": [],
|
||||
"createdAt": 1784280448632,
|
||||
"updatedAt": 1784280448632,
|
||||
"createdAt": 1784378211658,
|
||||
"updatedAt": 1784378211658,
|
||||
"hasMarkdown": true,
|
||||
"annotationText": "",
|
||||
"id": "h5-summary",
|
||||
@@ -52,8 +52,8 @@
|
||||
{
|
||||
"images": [],
|
||||
"controls": [],
|
||||
"createdAt": 1784280448632,
|
||||
"updatedAt": 1784280448632,
|
||||
"createdAt": 1784378211658,
|
||||
"updatedAt": 1784378211658,
|
||||
"hasMarkdown": true,
|
||||
"annotationText": "",
|
||||
"id": "h5-card",
|
||||
@@ -73,8 +73,8 @@
|
||||
{
|
||||
"images": [],
|
||||
"controls": [],
|
||||
"createdAt": 1784280448632,
|
||||
"updatedAt": 1784280448632,
|
||||
"createdAt": 1784378211658,
|
||||
"updatedAt": 1784378211658,
|
||||
"hasMarkdown": true,
|
||||
"annotationText": "",
|
||||
"id": "h5-fab",
|
||||
@@ -94,8 +94,8 @@
|
||||
{
|
||||
"images": [],
|
||||
"controls": [],
|
||||
"createdAt": 1784280448632,
|
||||
"updatedAt": 1784280448632,
|
||||
"createdAt": 1784378211658,
|
||||
"updatedAt": 1784378211658,
|
||||
"hasMarkdown": true,
|
||||
"annotationText": "",
|
||||
"id": "h5-time",
|
||||
@@ -115,8 +115,8 @@
|
||||
{
|
||||
"images": [],
|
||||
"controls": [],
|
||||
"createdAt": 1784280448632,
|
||||
"updatedAt": 1784280448632,
|
||||
"createdAt": 1784378211658,
|
||||
"updatedAt": 1784378211658,
|
||||
"hasMarkdown": true,
|
||||
"annotationText": "",
|
||||
"id": "h5-station",
|
||||
@@ -136,8 +136,8 @@
|
||||
{
|
||||
"images": [],
|
||||
"controls": [],
|
||||
"createdAt": 1784280448632,
|
||||
"updatedAt": 1784280448632,
|
||||
"createdAt": 1784378211658,
|
||||
"updatedAt": 1784378211658,
|
||||
"hasMarkdown": true,
|
||||
"annotationText": "",
|
||||
"id": "h5-plate",
|
||||
@@ -157,8 +157,8 @@
|
||||
{
|
||||
"images": [],
|
||||
"controls": [],
|
||||
"createdAt": 1784280448632,
|
||||
"updatedAt": 1784280448632,
|
||||
"createdAt": 1784378211658,
|
||||
"updatedAt": 1784378211658,
|
||||
"hasMarkdown": true,
|
||||
"annotationText": "",
|
||||
"id": "h5-mileage",
|
||||
@@ -178,8 +178,8 @@
|
||||
{
|
||||
"images": [],
|
||||
"controls": [],
|
||||
"createdAt": 1784280448632,
|
||||
"updatedAt": 1784280448632,
|
||||
"createdAt": 1784378211658,
|
||||
"updatedAt": 1784378211658,
|
||||
"hasMarkdown": true,
|
||||
"annotationText": "",
|
||||
"id": "h5-dispenser-brands",
|
||||
@@ -199,8 +199,8 @@
|
||||
{
|
||||
"images": [],
|
||||
"controls": [],
|
||||
"createdAt": 1784280448632,
|
||||
"updatedAt": 1784280448632,
|
||||
"createdAt": 1784378211658,
|
||||
"updatedAt": 1784378211658,
|
||||
"hasMarkdown": true,
|
||||
"annotationText": "",
|
||||
"id": "h5-price",
|
||||
@@ -220,8 +220,8 @@
|
||||
{
|
||||
"images": [],
|
||||
"controls": [],
|
||||
"createdAt": 1784280448632,
|
||||
"updatedAt": 1784280448632,
|
||||
"createdAt": 1784378211658,
|
||||
"updatedAt": 1784378211658,
|
||||
"hasMarkdown": true,
|
||||
"annotationText": "",
|
||||
"id": "h5-kg",
|
||||
@@ -241,8 +241,8 @@
|
||||
{
|
||||
"images": [],
|
||||
"controls": [],
|
||||
"createdAt": 1784280448632,
|
||||
"updatedAt": 1784280448632,
|
||||
"createdAt": 1784378211658,
|
||||
"updatedAt": 1784378211658,
|
||||
"hasMarkdown": true,
|
||||
"annotationText": "",
|
||||
"id": "h5-total-label",
|
||||
@@ -262,8 +262,8 @@
|
||||
{
|
||||
"images": [],
|
||||
"controls": [],
|
||||
"createdAt": 1784280448632,
|
||||
"updatedAt": 1784280448632,
|
||||
"createdAt": 1784378211658,
|
||||
"updatedAt": 1784378211658,
|
||||
"hasMarkdown": true,
|
||||
"annotationText": "",
|
||||
"id": "h5-detail",
|
||||
@@ -283,8 +283,8 @@
|
||||
{
|
||||
"images": [],
|
||||
"controls": [],
|
||||
"createdAt": 1784280448632,
|
||||
"updatedAt": 1784280448632,
|
||||
"createdAt": 1784378211658,
|
||||
"updatedAt": 1784378211658,
|
||||
"hasMarkdown": true,
|
||||
"annotationText": "",
|
||||
"id": "h5-detail-photos",
|
||||
@@ -304,8 +304,8 @@
|
||||
{
|
||||
"images": [],
|
||||
"controls": [],
|
||||
"createdAt": 1784280448632,
|
||||
"updatedAt": 1784280448632,
|
||||
"createdAt": 1784378211658,
|
||||
"updatedAt": 1784378211658,
|
||||
"hasMarkdown": true,
|
||||
"annotationText": "",
|
||||
"id": "h5-manual",
|
||||
@@ -325,8 +325,8 @@
|
||||
{
|
||||
"images": [],
|
||||
"controls": [],
|
||||
"createdAt": 1784280448632,
|
||||
"updatedAt": 1784280448632,
|
||||
"createdAt": 1784378211658,
|
||||
"updatedAt": 1784378211658,
|
||||
"hasMarkdown": true,
|
||||
"annotationText": "",
|
||||
"id": "h5-manual-calendar",
|
||||
@@ -352,16 +352,16 @@
|
||||
"h5-fab": "## 底栏新增\n\n固定在列表底部,不遮挡卡片滚动区。点击进入新增表单,加氢时间默认=点击时刻。\n\n详见功能说明「新增加氢记录」。",
|
||||
"h5-time": "## 加氢时间(必填)\n\n默认=点击「新增」时的当前时间;点击后底部弹层选择日期 + 时 + 分。",
|
||||
"h5-station": "## 加氢站名称\n\n只读。默认显示当前登录账号对应加氢站名称,不可切换。",
|
||||
"h5-plate": "# 车牌「羚牛车辆 / 非羚牛车辆」标签\n\n原型本地判定,未接真实车辆管理 API。\n\n## 判定顺序\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 车牌为空 | 不展示标签 |\n| 2 | 规范化后车牌 ∈ 本地系统车辆集合 | **羚牛车辆** |\n| 3 | 有车牌但不在集合中 | **非羚牛车辆** |\n\n## 前置条件\n\n- 识别(OCR)或键盘输入得到车牌后才展示。\n- 比较前去掉尾缀 `F`,统一大写。\n\n## 数据源\n\n- `utils/fleet-plates.ts` 内本地种子车牌集合(含加氢 Bridge 演示车牌与部分车辆管理样例)。\n- 正式环境应改为查询车辆管理 / 系统车辆列表接口。\n\n## 用户可见结果\n\n- 绿色标签「羚牛车辆」或橙色标签「非羚牛车辆」。\n- 不拦截提交(仅提示归属)。\n\n## 代码路径\n\n- `getVehicleTag` / `isLingniuVehicle` → `CreateWizard` 车牌行右侧标签。\n",
|
||||
"h5-plate": "# 车牌「羚牛车辆 / 非羚牛车辆」标签\n\n原型本地判定,未接真实车辆管理 API。\n\n全文(含核对/对账字段空展示规则,跨 Web / 台账 / H5): \n[../../oneos-web-h2-station/.spec/fleet-vehicle-verify.md](../../oneos-web-h2-station/.spec/fleet-vehicle-verify.md)\n\n## 判定顺序\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 车牌为空 | 不展示标签 |\n| 2 | 规范化后车牌 ∈ 本地系统车辆集合 | **羚牛车辆** |\n| 3 | 有车牌但不在集合中 | **非羚牛车辆** |\n\n## 前置条件\n\n- 识别(OCR)或键盘输入得到车牌后才展示。\n- 比较前去掉尾缀 `F`,统一大写。\n\n## 数据源\n\n- 优先 `H2VehicleLedgerBridge.isLingniuVehicle`;本地回退见 `utils/fleet-plates.ts`(与 Bridge `H2_FLEET_PLATE_KEYS` 同步)。\n\n## 用户可见结果\n\n- 绿色标签「羚牛车辆」或橙色标签「非羚牛车辆」。\n- **非羚牛车辆**:列表/详情中核对状态、核对时间、对账状态、对账时间显示「—」,不展示核对待办语义。\n- 不拦截提交(仅提示归属)。\n\n## 代码路径\n\n- `getVehicleTag` / `isLingniuVehicle` → `CreateWizard`、`OrderCard`、`OrderDetail`\n",
|
||||
"h5-mileage": "## 里程(km)\n\n选填。支持「从车机获取」(原型 Mock);识别车牌后可自动回填。正式环境对接车机 / T-Box。\n\n详见 [新增加氢记录](feature-create)。",
|
||||
"h5-dispenser-brands": "## 加氢机品牌参考\n\n展示站点信息「加氢机品牌管理」维护的品牌·型号,供面板 OCR 模型判断参考。数据源:`h2DispenserBrandStore`。",
|
||||
"h5-price": "## 氢气单价(必填)\n\n可 OCR 识别后校正;变更后自动重算加氢总额(单价×加氢量)。",
|
||||
"h5-kg": "## 加氢量(必填)\n\n可 OCR 识别后校正;变更后自动重算加氢总额。",
|
||||
"h5-total-label": "## 加氢总额(元)\n\n只读展示;由单价×加氢量自动计算(两位小数)。界面标签不展示公式文案。",
|
||||
"h5-detail": "# 功能:详情、编辑与删除\n\n## 目标\n\n查看单笔加氢记录完整字段;在「未核对且未对账」时可编辑或删除。\n\n## 详情展示\n\n- 顶部:车牌、加氢时间、核对标签、对账标签 \n- 指标条:加氢总额、加氢量 \n- **识别原图**:车牌照片、加氢机面板照片(均为拍摄时叠加时间/地点水印后的原图;种子数据无实拍时用演示水印图) \n- 明细行:站点、车牌、里程、单价、核对/对账状态与时间 \n\n## 锁定规则\n\n| 条件 | 结果 |\n|------|------|\n| 已对账 | 不可编辑/删除;提示已纳入站点对账单 |\n| 已核对(未对账) | 不可编辑/删除;提示能源部已核对 |\n| 未核对且未对账 | 可编辑、可删除 |\n\n判定函数:`isOrderLocked`(核对或对账任一成立即锁)。\n\n## 编辑\n\n进入与「新增」同一套表单,预填当前值(含已存水印照片);提交走更新逻辑(仍校验重复键)。\n\n## 删除\n\n二次确认后从 Bridge 移除该行。\n\n## 验收\n\n1. 未核对未对账:底部有删除/编辑 \n2. 已核对或已对账:无操作栏,有锁定说明 \n3. 详情含对账信息;列表卡片不含对账 \n4. 详情展示车牌与面板带水印原图(新上报提交后为实拍;种子行为演示图) \n",
|
||||
"h5-detail": "# 功能:详情、编辑与删除\n\n## 目标\n\n查看单笔加氢记录完整字段;在「未核对且未对账」时可编辑或删除(**锁定仅适用于羚牛车辆**)。\n\n## 详情展示\n\n- 顶部:车牌、加氢时间、核对标签、对账标签 \n- 指标条:加氢总额、加氢量 \n- **识别原图**:车牌照片、加氢机面板照片(均为拍摄时叠加时间/地点水印后的原图;种子数据无实拍时用演示水印图) \n- 明细行:站点、车牌、里程、单价、核对/对账状态与时间 \n\n> 非羚牛车辆:核对/对账字段展示「—」,不展示核对待办语义(见 [fleet-plate-tag.md](./fleet-plate-tag.md))。\n\n## 锁定规则\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | **非羚牛车辆** | **不锁定**(不以核对/对账状态禁改删) |\n| 2 | 羚牛 ∧ 已对账 | 不可编辑/删除;提示已纳入站点对账单 |\n| 3 | 羚牛 ∧ 已核对(未对账) | 不可编辑/删除;提示能源部已核对 |\n| 4 | 羚牛 ∧ 未核对且未对账 | 可编辑、可删除 |\n\n判定函数:`isOrderLocked`(先判羚牛;再核「核对或对账任一成立即锁」)。\n\n## 编辑\n\n进入与「新增」同一套表单,预填当前值(含已存水印照片);提交走更新逻辑(仍校验重复键)。\n\n## 删除\n\n二次确认文案:「确认删除车牌 … 的未锁定记录?」;确认后从 Bridge 移除该行。\n\n## 验收\n\n1. 羚牛 ∧ 未核对未对账:底部有删除/编辑 \n2. 羚牛 ∧ 已核对或已对账:无操作栏,有锁定说明 \n3. 非羚牛:可改删(不因核对/对账锁定);核对/对账展示「—」 \n4. 详情含对账信息;列表卡片不含对账 \n5. 详情展示车牌与面板带水印原图(新上报提交后为实拍;种子行为演示图) \n",
|
||||
"h5-detail-photos": "## 识别原图(含水印)\n\n详情展示车牌识别、加氢机面板的原始照片;照片在拍摄/选图时已叠加时间与地点水印并随记录保存。种子数据无实拍时用演示水印图。\n\n详见 [详情编辑删除](feature-detail)。",
|
||||
"h5-manual": "# 功能:手工台账上传\n\n## 目标\n\n站端每日上传纸质**手工台账照片**,作为系统电子加氢记录的核对依据。多日未操作时,可通过日历查看缺口并补传过往日期。\n\n## 产品口径\n\n| 项 | 规则 |\n|---|---|\n| 入口 | H5:底部 Tab「手工台账」;Web:顶栏「手工台账」 |\n| 上传形态 | 仅图片(拍照/相册),可多张 |\n| 维度 | **加氢站 + 自然日(本地日历日)** 一份 |\n| 日历 | 月历自本站**首笔加氢记录日**起算:绿=已传、橙=未传;首笔之前无需补传;禁选未来日 |\n| 补传 | 允许为过往未传日期补传;已传日期仅可**追加**新图 |\n| 归档 | **确认上传后的图片进入已归档状态,不可删除**;未确认的草稿图仍可删除 |\n| 拦截 | **未上传今日手工台账 → 禁止新增加氢记录**(编辑已有记录不拦;补传历史不替代今日校验) |\n| 数据 | 共享 `H2VehicleLedgerBridge` 内存 Store(未接真实 API) |\n\n## 判定顺序(新增拦截)\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 操作=编辑已有记录 | 不校验手工台账 |\n| 2 | 操作=新增,且目标站今日已有 ≥1 张台账图 | 允许进入新增 |\n| 3 | 操作=新增,今日未上传 | 提示并跳转手工台账 Tab |\n\n## 日历与补传\n\n| 规则 | 说明 |\n|------|------|\n| 数据源 | `listManualLedgersByStation`;当日有 ≥1 张图视为已上传 |\n| 业务起点 | 取本站 Bridge 加氢记录中最早 `hydrogenTime` 自然日;无记录则历史日均不要求补传 |\n| 未传计数 | 本月内 `max(月初, 首笔日)`~min(月末, 今日) 中未上传的天数 |\n| 点选 | 仅 `dateKey ≤ 今日`;选中后右侧/下方展示该日草稿图与上传按钮 |\n| 归档不可删 | Store 已有图强制保留;`upsertManualLedger` 仅追加新图,忽略删除已有图的请求 |\n| 与拦截关系 | 仅 **今日** 影响「禁止新增」;历史补传解决核对缺口,不放宽今日门禁 |\n\n## 用户可见\n\n- 今日未上传:橙/黄提示条 + Tab 红点 \n- 日历:自首笔加氢日起绿/橙标记;首笔之前无橙点(无需补传);本月未传天数摘要 \n- 选中日:上传区标题为该日期;已归档图显示「已归档」无删除;仅未确认草稿可删;已有台账时主按钮为「确认补传」\n\n## 代码路径\n\n- Bridge:`src/common/h2VehicleLedgerBridge.js`(`hasManualLedgerForDate` / `upsertManualLedger` / `listManualLedgersByStation`) \n- H5:`oneos-h5-h2-order` · `ManualLedgerPanel` + `utils/manual-ledger.ts`(`buildManualMonthCells` / `getFirstHydrogenDateKey`) \n- Web:`oneos-web-h2-station/pages/02-加氢记录.jsx`(手工台账 Tab 月历) \n\n## 验收\n\n1. 今日未上传时,H5/Web 点「新增」均被拦截并引导上传 \n2. 上传至少一张图并确认后,可新增电子记录 \n3. 日历自首笔加氢日起区分已传/未传;首笔之前不标未传、不计入补传;可点选范围内未传日补传 \n4. 补传历史日后,该日日历变为已传;今日仍未传时新增仍被拦截 \n5. 确认上传后的图显示已归档且不可删除;仅可追加补传 \n6. H5 与 Web 共用同一 Store,一端上传另一端可见(同会话) \n",
|
||||
"h5-manual-calendar": "## 台账日历\n\n自本站首笔加氢记录日起:绿=已上传,橙=未上传;首笔之前无需补传。禁选未来日。点选范围内未传日可补传。仅「今日」是否上传影响新增加氢记录。\n\n详见 [手工台账上传](feature-manual-ledger)。"
|
||||
"h5-manual": "# 功能:手工台账上传\n\n## 目标\n\n站端每日上传纸质**手工台账照片**,作为系统电子加氢记录的核对依据。多日未操作时,可通过日历查看缺口并补传过往日期。\n\n## 产品口径\n\n| 项 | 规则 |\n|---|---|\n| 入口 | H5:底部 Tab「手工台账」;Web:顶栏「手工台账」 |\n| 站点范围 | **H5 固定本站**(无切站);Web 可按角色多站/超管切换(标注状态演示,未接真实登录) |\n| 上传形态 | 仅图片(拍照/相册),可多张 |\n| 维度 | **加氢站 + 自然日(本地日历日)** 一份 |\n| 日历 | 月历自本站**首笔加氢记录日**起算:绿=已传、橙=未传;首笔之前无需补传;禁选未来日 |\n| 补传 | 允许为过往未传日期补传;已传日期仅可**追加**新图 |\n| 归档 | **确认上传后的图片进入已归档状态,不可删除**;未确认的草稿图仍可删除 |\n| 二次确认 | H5 / Web 确认上传前弹窗:「提交后将无法修改,是否确认手工台账照片无误」;确认后才写入 |\n| 拦截 | **前一日手工台账未上传 → 今日禁止新增加氢记录**(编辑已有记录不拦;补传更早历史日不替代昨日校验) |\n| 数据 | 共享 `H2VehicleLedgerBridge` 内存 Store(未接真实 API);站名/编码解析见 Bridge.`h2BridgeResolveStationId` |\n\n## 判定顺序(新增拦截)\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 操作=编辑已有记录 | 不校验手工台账 |\n| 2 | 操作=新增,且昨日早于本站首笔加氢日(或不要求昨日台账) | 允许进入新增 |\n| 3 | 操作=新增,且目标站昨日已有 ≥1 张台账图 | 允许进入新增 |\n| 4 | 操作=新增,昨日未上传 | 顶部/Toast 提示「手工台账有缺失,作为对账重要依据,请先上传手工台账」,并**直接跳转**手工台账页且选中昨日 |\n\n## 日历与补传\n\n| 规则 | 说明 |\n|------|------|\n| 数据源 | `listManualLedgersByStation`;当日有 ≥1 张图视为已上传 |\n| 业务起点 | 取本站 Bridge 加氢记录中最早 `hydrogenTime` 自然日;无记录则历史日均不要求补传 |\n| 未传计数 | 本月内 `max(月初, 首笔日)`~min(月末, 今日) 中未上传的天数 |\n| 点选 | 仅 `dateKey ≤ 今日`;选中后右侧/下方展示该日草稿图与上传按钮 |\n| 归档不可删 | Store 已有图强制保留;`upsertManualLedger` 仅追加新图,忽略删除已有图的请求 |\n| 与拦截关系 | 仅 **昨日** 影响「禁止新增」;历史补传解决核对缺口;今日台账仍可按日常上传,但不作为新增门禁 |\n\n## 用户可见\n\n- 今日状态双反馈(H5 台账页顶部卡片 / Web 右侧「当日台账」):未上传「今日尚未上传…」异常态;已上传「今日已上传…」正常态。**日常上传反馈 ≠ 新增门禁**;H5 文案须写明「今日不作为新增门禁,仅要求前一日已上传」 \n- 昨日缺失(H5):**列表顶栏可点橙条** + 底栏 FAB 旁提示 + Tab 红点;台账页同样展示橙条并可选中昨日;文案固定为「手工台账有缺失,作为对账重要依据,请先上传手工台账」 \n- 日历:自首笔加氢日起绿/橙标记;首笔之前无橙点(无需补传);本月未传天数摘要 \n- Web 右侧面板标题「手工台账」:空态提示;有图可点进全屏翻页预览并角标「已归档/待提交」;预览区内上传;底部确认前二次弹窗;提交后立即展示已归档缩略图;左右卡片等高\n\n## 代码路径\n\n- Bridge:`src/common/h2VehicleLedgerBridge.js`(`hasManualLedgerForDate` / `isManualLedgerGateBlocked` / `upsertManualLedger` / `listManualLedgersByStation`) \n- H5:`oneos-h5-h2-order` · `ManualLedgerPanel` + `utils/manual-ledger.ts`(`isManualLedgerGateBlocked` / `buildManualMonthCells` / `getFirstHydrogenDateKey`) \n- Web:`oneos-web-h2-station/pages/02-加氢记录.jsx`(`hrIsManualLedgerGateBlocked` / 跳转选中昨日) \n\n## 验收\n\n1. 昨日未上传时,H5/Web 列表顶栏提示固定文案;点「新增」被拦截并直接进入手工台账(选中昨日) \n2. 补传昨日台账并确认后,可新增加氢记录 \n3. 日历自首笔加氢日起区分已传/未传;首笔之前不标未传、不计入补传;可点选范围内未传日补传 \n4. 仅补传更早历史日、昨日仍未传时,新增仍被拦截 \n5. 确认上传后的图显示已归档且不可删除;仅可追加补传 \n6. H5 与 Web 共用同一 Store,一端上传另一端可见(同会话) \n7. Web 导出 CSV 列含「车辆来源」(羚牛车辆/非羚牛车辆)、「数据来源」(站端上传/羚牛上传),与列表标签口径一致 \n",
|
||||
"h5-manual-calendar": "## 台账日历\n\n自本站首笔加氢记录日起:绿=已上传,橙=未上传;首笔之前无需补传。禁选未来日。点选范围内未传日可补传。**前一日**未上传时禁止新增加氢记录(今日台账状态不作为门禁)。\n\n详见 [手工台账上传](feature-manual-ledger)。"
|
||||
},
|
||||
"assetMap": {},
|
||||
"directory": {
|
||||
@@ -376,9 +376,90 @@
|
||||
"type": "markdown",
|
||||
"id": "h5-doc-prd",
|
||||
"title": "PRD 全文",
|
||||
"markdown": "# 加氢订单(H5)— 产品需求文档(PRD)\n\n| 项 | 内容 |\n|---|---|\n| 文档版本 | v1.5 |\n| 模块名称 | 加氢站管理 → 加氢订单 |\n| 所属系统 | ONE-OS(加氢站手机浏览器) |\n| 交互原型 | `/prototypes/oneos-h5-h2-order` |\n| 设计基底 | 小羚羚 `xll-miniapp/DESIGN.md` |\n| 关联模块 | [站点信息](/prototypes/oneos-web-h2-station-site)、[加氢记录 Web](/prototypes/oneos-web-h2-station)、[车辆氢费明细](/prototypes/vehicle-h2-fee-ledger) |\n\n---\n\n## 1. 背景与目标\n\n采购创建加氢站并绑定供应商、开放系统账号后,加氢站人员通过手机浏览器登录 OneOS,在「加氢订单」模块上报加氢记录。Web「加氢记录」为同一业务的 PC 入口(共享 Bridge 数据;PC 另有导出与预约加氢)。\n\n每日须上传**手工台账**照片,作为电子记录的核对依据;未上传当日手工台账时不可新增加氢记录。手工台账页提供月历,标出已传/未传日期,支持补传过往缺口。\n\n### 1.1 目标用户\n\n加氢站操作人员(户外 / 站场手机使用)。\n\n### 1.2 功能清单\n\n| # | 功能 | 说明文档 |\n|---|---|---|\n| 1 | 本站列表与 KPI | [feature-list.md](./feature-list.md) |\n| 2 | 新增加氢记录 | [feature-create.md](./feature-create.md) |\n| 3 | 详情 / 编辑 / 删除 | [feature-detail.md](./feature-detail.md) |\n| 4 | **手工台账上传** | [feature-manual-ledger.md](./feature-manual-ledger.md) |\n| 5 | 羚牛车辆标签 | [fleet-plate-tag.md](./fleet-plate-tag.md) |\n| 6 | 核对与对账联动 | [reconcile-linkage.md](./reconcile-linkage.md) |\n\n### 1.3 列表字段\n\n加氢时间、车牌号、氢气单价、加氢量、加氢总额、核对状态;已核对时展示核对时间。里程在新增/详情展示(选填),列表不展示。列表不展示对账状态。\n\n### 1.4 不做\n\n真登录、真 OCR SDK、PLC、原生 App、后端联调。\n\n---\n\n## 2. 验收项\n\n1. 菜单位于 OneOS → 加氢站管理 → 加氢订单 \n2. 默认已登录本站;列表仅本站(无切站) \n3. 底栏 Tab:加氢订单 / 手工台账;月历可区分已传/未传并补传过往日;未上传今日手工台账不可新增;上传后可新增;时间默认点击新增时刻(到分) \n4. 已核对或已对账不可编辑/删除;未核对未对账可 \n5. 能源部完成核对后同步「已核对」;站点对账单提交后同步「已对账」+ 对账时间 \n6. 同站同车牌同加氢时间重复保存被拦截 \n7. Web「加氢记录」与 H5「加氢订单」手工台账逻辑一致(同 Store) \n8. 车牌/面板拍摄预览含水印(时间、地点=本站名称);里程可从车机获取(Mock) \n9. 面板 OCR 可展示站点信息维护的加氢机品牌型号线索 \n10. 详情页展示车牌与面板带水印原图 \n\n---\n\n## 3. 复杂逻辑摘要\n\n| 主题 | 摘要 | 全文 |\n|---|---|---|\n| 对账 / 核对 | 核对≠对账;触发方不同 | [reconcile-linkage.md](./reconcile-linkage.md)、[车辆氢费 verify-reconcile](../vehicle-h2-fee-ledger/.spec/verify-reconcile.md) |\n| 重复键 | 与 Web 共用 Bridge | [Web 数据模型](../oneos-web-h2-station/.spec/record-data-model.md) |\n| 羚牛车辆标签 | 本地系统车辆集合判定 | [fleet-plate-tag.md](./fleet-plate-tag.md) |\n| 总额 | 单价×加氢量自动算,界面不展示公式 | [feature-create.md](./feature-create.md) |\n| 手工台账 | 按站+自然日上传图片;月历标已传/未传可补传;未上传今日禁新增 | [feature-manual-ledger.md](./feature-manual-ledger.md) |\n\n---\n\n## 4. 页面与标注对照\n\n| 页面 pageId | 主要能力 |\n|---|---|\n| `list` | 站点头、KPI、订单卡片、底栏新增 |\n| `create` | 时间选择、站名、车牌键盘、面板 OCR、金额字段 |\n| `detail` | 详情字段、锁定说明、编辑/删除 |\n\n标注数据:`annotation-source.json`(由 `scripts/build-annotation-source.mjs` 从本目录 Markdown 生成)。\n",
|
||||
"markdown": "# 加氢订单(H5)— 产品需求文档(PRD)\n\n| 项 | 内容 |\n|---|---|\n| 文档版本 | v1.7 |\n| 模块名称 | 加氢站管理 → 加氢订单 |\n| 所属系统 | ONE-OS(加氢站手机浏览器) |\n| 交互原型 | `/prototypes/oneos-h5-h2-order` |\n| 设计基底 | 小羚羚 `xll-miniapp/DESIGN.md` |\n| 关联模块 | [站点信息](/prototypes/oneos-web-h2-station-site)、[加氢记录 Web](/prototypes/oneos-web-h2-station)、[车辆氢费明细](/prototypes/vehicle-h2-fee-ledger) |\n\n---\n\n## 1. 背景与目标\n\n采购创建加氢站并绑定供应商、开放系统账号后,加氢站人员通过手机浏览器登录 OneOS,在「加氢订单」模块上报加氢记录。Web「加氢记录」为同一业务的 PC 入口(共享 Bridge 数据;PC 另有导出与预约加氢)。\n\n每日须上传**手工台账**照片,作为电子记录的核对依据;**前一日**手工台账未上传时,今日不可新增加氢记录。手工台账页提供月历,标出已传/未传日期,支持补传过往缺口。\n\n### 1.1 目标用户\n\n加氢站操作人员(户外 / 站场手机使用)。\n\n### 1.2 功能清单\n\n| # | 功能 | 说明文档 |\n|---|---|---|\n| 1 | 本站列表与 KPI | [feature-list.md](./feature-list.md) |\n| 2 | 新增加氢记录 | [feature-create.md](./feature-create.md) |\n| 3 | 详情 / 编辑 / 删除 | [feature-detail.md](./feature-detail.md) |\n| 4 | **手工台账上传** | [feature-manual-ledger.md](./feature-manual-ledger.md) |\n| 5 | 羚牛车辆标签 | [fleet-plate-tag.md](./fleet-plate-tag.md) |\n| 6 | 核对与对账联动 | [reconcile-linkage.md](./reconcile-linkage.md) |\n\n### 1.3 列表字段\n\n加氢时间、车牌号、氢气单价、加氢量、加氢总额、核对状态;已核对时展示核对时间。里程在新增/详情展示(选填),列表不展示。列表不展示对账状态。\n\n### 1.4 不做\n\n真登录、真 OCR SDK、PLC、原生 App、后端联调。\n\n---\n\n## 2. 验收项\n\n1. 菜单位于 OneOS → 加氢站管理 → 加氢订单 \n2. 默认已登录本站;列表仅本站(无切站) \n3. 底栏 Tab:加氢订单 / 手工台账;月历可区分已传/未传并补传过往日;前一日未上传时提示「手工台账有缺失,作为对账重要依据,请先上传手工台账」且点新增直达台账页;补传昨日后可新增;时间默认点击新增时刻(到分) \n4. **羚牛车辆**已核对或已对账不可编辑/删除;非羚牛不锁定;未核对未对账(羚牛)可改删 \n5. 能源部完成核对后同步「已核对」;站点对账单提交后同步「已对账」+ 对账时间(锁定仅羚牛) \n6. 同站同车牌同加氢时间重复保存被拦截 \n7. Web「加氢记录」与 H5「加氢订单」手工台账逻辑一致(同 Store) \n8. 车牌/面板拍摄预览含水印(时间、地点=本站名称);里程可从车机获取(Mock) \n9. 面板 OCR 可展示站点信息维护的加氢机品牌型号线索 \n10. 详情页展示车牌与面板带水印原图 \n11. 昨日台账缺失时:列表顶栏橙条 + 底栏提示 + Tab 红点;台账页不把「今日未传」写成新增门禁 \n\n---\n\n## 3. 复杂逻辑摘要\n\n| 主题 | 摘要 | 全文 |\n|---|---|---|\n| 对账 / 核对 | 核对≠对账;**锁定仅羚牛** | [reconcile-linkage.md](./reconcile-linkage.md)、[车辆氢费 verify-reconcile](../vehicle-h2-fee-ledger/.spec/verify-reconcile.md) |\n| 重复键 | 与 Web 共用 Bridge | [Web 数据模型](../oneos-web-h2-station/.spec/record-data-model.md) |\n| 羚牛车辆标签 | 本地系统车辆集合判定 | [fleet-plate-tag.md](./fleet-plate-tag.md) |\n| 总额 | 单价×加氢量自动算,界面不展示公式 | [feature-create.md](./feature-create.md) |\n| 手工台账 | 按站+自然日上传图片;月历标已传/未传可补传;**昨日未传则今日禁新增** | [feature-manual-ledger.md](./feature-manual-ledger.md) |\n\n---\n\n## 4. 页面与标注对照\n\n| 页面 pageId | 主要能力 |\n|---|---|\n| `list` | 站点头、KPI、订单卡片、底栏新增 |\n| `create` | 时间选择、站名、车牌键盘、面板 OCR、金额字段 |\n| `detail` | 详情字段、锁定说明、编辑/删除 |\n\n右侧工具栏「原型目录」结构:\n\n| 目录 | 内容 |\n|---|---|\n| PRD 全文 | requirements-prd.md |\n| 需求角色 | roles.md |\n| 用户故事 | user-stories.md |\n| 关键逻辑 | key-logic + 台账门禁 / 对账 / 车辆标签 |\n| 流程图 | flows.md(Mermaid) |\n| 功能说明 | 列表 / 新增 / 台账 / 详情 / 车辆 / 对账 |\n| 按页面 | list / create / detail 要点 |\n\n标注数据:`annotation-source.json`(由 `scripts/build-annotation-source.mjs` 从本目录 Markdown 生成)。\n",
|
||||
"markdownPath": ".spec/requirements-prd.md"
|
||||
},
|
||||
{
|
||||
"type": "folder",
|
||||
"id": "h5-doc-roles",
|
||||
"title": "需求角色",
|
||||
"defaultExpanded": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "markdown",
|
||||
"id": "h5-doc-roles-md",
|
||||
"title": "角色说明",
|
||||
"markdown": "# 需求角色\n\n| 项 | 说明 |\n|---|---|\n| 适用原型 | 加氢订单(H5)`oneos-h5-h2-order` |\n| 关联 Web | 加氢记录 `oneos-web-h2-station`(同一业务、不同端) |\n| 数据 | 原型本地种子 + 共享 Bridge(未接真实登录 / API) |\n\n---\n\n## 1. 角色一览\n\n| 角色 | 端 | 核心诉求 | 本原型是否为主用户 |\n|---|---|---|---|\n| **加氢站操作人员** | H5(手机浏览器) | 在站场快速上报本站加氢流水;补传手工台账照片 | **是** |\n| **加氢站站长 / 多站管理员** | Web 为主,H5 可看本站 | 多站台账、导出、手工台账补传与督导 | 间接(H5 固定单站演示) |\n| **能源部核对人员** | 车辆氢费明细 | 完成核对,回写「已核对」 | 否(触发方在台账) |\n| **站点对账人员** | 站点信息 · 对账单 | 提交对账单,回写「已对账」 | 否(触发方在站点信息) |\n| **超级管理员** | Web | 查看全部站点流水与台账 | 否(Web 标注状态演示) |\n\n---\n\n## 2. 本原型主角色:加氢站操作人员\n\n| 项 | 说明 |\n|---|---|\n| 场景 | 户外 / 站场,手机竖屏 |\n| 权限边界 | 仅本站数据;不可切站(原型固定演示站) |\n| 日常动作 | 看列表 KPI → 上传昨日手工台账(若缺失)→ 新增加氢记录 → 查看/编辑未锁定记录 |\n| 约束 | 前一日手工台账未上传则今日不可新增;已核对或已对账不可改删 |\n\n---\n\n## 3. 角色边界(本期不做)\n\n- 真登录与组织权限中心对接 \n- 能源部 / 对账人员在 H5 内操作核对或对账 \n- 跨站汇总与导出(属 Web「加氢记录」) \n",
|
||||
"markdownPath": ".spec/roles.md"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "folder",
|
||||
"id": "h5-doc-stories",
|
||||
"title": "用户故事",
|
||||
"defaultExpanded": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "markdown",
|
||||
"id": "h5-doc-stories-md",
|
||||
"title": "用户故事",
|
||||
"markdown": "# 用户故事\n\n> 以加氢站操作人员为主;验收口径见 [requirements-prd.md](./requirements-prd.md)。\n\n---\n\n## US-01 查看本站加氢订单\n\n**作为** 加氢站操作人员, \n**我希望** 打开加氢订单即看到本站列表与 KPI, \n**以便** 快速了解今日/近期上报情况。\n\n**验收要点**\n\n- 默认已登录本站;无切站 \n- KPI:订单数 / 加氢量 / 加氢金额随列表汇总 \n- 卡片展示加氢时间、车牌、量、额、核对状态等 \n\n---\n\n## US-02 补传缺失的手工台账后再新增\n\n**作为** 加氢站操作人员, \n**我希望** 在前一日手工台账未上传时被明确拦截,并直达台账页补传, \n**以便** 保证纸质台账作为对账依据不缺失。\n\n**验收要点**\n\n- 顶栏橙条 + 底栏提示:「手工台账有缺失,作为对账重要依据,请先上传手工台账」 \n- 点「新增」或顶栏橙条跳转手工台账并选中昨日 \n- 补传昨日确认后可新增 \n\n---\n\n## US-03 新增加氢记录\n\n**作为** 加氢站操作人员, \n**我希望** 用车牌键盘 + 面板拍照/OCR 快速填单, \n**以便** 在站场少打字完成上报。\n\n**验收要点**\n\n- 加氢时间默认=点击新增时刻(到分) \n- 站名只读为本站 \n- 总额 = 单价 × 加氢量(界面不展示公式) \n- 同站同车牌同时间重复保存拦截 \n\n---\n\n## US-04 查看详情并编辑/删除未锁定记录\n\n**作为** 加氢站操作人员, \n**我希望** 打开详情查看字段与识别原图,并对未核对未对账记录改删, \n**以便** 纠正录入错误。\n\n**验收要点**\n\n- 羚牛车辆已核对或已对账:不可编辑/删除,有锁定说明;非羚牛不锁定 \n- 详情展示车牌/面板含水印原图 \n\n---\n\n## US-05 按日历管理手工台账\n\n**作为** 加氢站操作人员, \n**我希望** 在月历上看已传/未传并补传过往日, \n**以便** 多日未操作后仍能补齐缺口。\n\n**验收要点**\n\n- 自首笔加氢日起绿/橙标记;首笔前不要求 \n- 禁选未来日;已归档图不可删,仅可追加 \n\n---\n\n## US-06 识别羚牛车辆\n\n**作为** 加氢站操作人员, \n**我希望** 在列表/详情看到是否羚牛车辆, \n**以便** 理解后续核对对账是否适用该车。\n\n**验收要点**\n\n- 标签:羚牛车辆 / 非羚牛车辆(本地车辆集合判定) \n",
|
||||
"markdownPath": ".spec/user-stories.md"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "folder",
|
||||
"id": "h5-doc-logic",
|
||||
"title": "关键逻辑",
|
||||
"defaultExpanded": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "markdown",
|
||||
"id": "h5-doc-logic-index",
|
||||
"title": "逻辑索引",
|
||||
"markdown": "# 关键逻辑索引\n\n本页汇总加氢订单(H5)复杂判定入口;细则见各规格全文。原型数据均为本地种子 / 内存 Store,**未接真实 API**。\n\n---\n\n## 1. 判定主题一览\n\n| 主题 | 一句话 | 全文 |\n|---|---|---|\n| 手工台账门禁 | **昨日**未传 → 今日禁新增;固定提示文案并直达台账 | [feature-manual-ledger.md](./feature-manual-ledger.md) |\n| 核对 ≠ 对账 | 能源部核对 / 站点对账单提交,触发方不同 | [reconcile-linkage.md](./reconcile-linkage.md) |\n| 记录锁定 | **仅羚牛**:已核对或已对账 → 站端不可改删;非羚牛不锁 | [feature-detail.md](./feature-detail.md)、[reconcile-linkage.md](./reconcile-linkage.md)、[fleet-plate-tag.md](./fleet-plate-tag.md) |\n| 重复键 | 同站 + 同车牌 + 同加氢时间 | Bridge / [Web record-data-model](../oneos-web-h2-station/.spec/record-data-model.md) |\n| 总额计算 | 总额 = 单价 × 量(两位小数) | [feature-create.md](./feature-create.md) |\n| 羚牛车辆 | 本地系统车牌集合 | [fleet-plate-tag.md](./fleet-plate-tag.md) |\n\n---\n\n## 2. 手工台账门禁(摘要)\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 编辑已有记录 | 不校验 |\n| 2 | 昨日早于本站首笔加氢日 | 不要求昨日台账,可新增 |\n| 3 | 昨日已有 ≥1 张台账图 | 可新增 |\n| 4 | 昨日未上传 | 提示并跳转手工台账(选中昨日) |\n\n用户可见文案:`手工台账有缺失,作为对账重要依据,请先上传手工台账`\n\n---\n\n## 3. 核对 / 对账(摘要)\n\n| 状态 | 谁触发 | 站端影响 |\n|---|---|---|\n| 已核对 | 车辆氢费明细「完成核对」 | **羚牛**不可改删 |\n| 已对账 | 站点信息对账单提交 | **羚牛**不可改删;回写对账时间 |\n| 非羚牛 | — | 不参与核对/对账展示与锁定 |\n\nH5 列表主要展示核对状态(仅羚牛);对账细节见联动规格。\n\n---\n\n## 4. 代码路径\n\n| 能力 | 路径 |\n|---|---|\n| Bridge | `src/common/h2VehicleLedgerBridge.js` |\n| 门禁工具 | `utils/manual-ledger.ts` → `isManualLedgerGateBlocked` |\n| 订单 CRUD | `utils/orders.ts` |\n| 页面 | `index.tsx` / `ManualLedgerPanel` / `CreateWizard` |\n",
|
||||
"markdownPath": ".spec/key-logic.md"
|
||||
},
|
||||
{
|
||||
"type": "markdown",
|
||||
"id": "h5-doc-logic-manual",
|
||||
"title": "手工台账门禁",
|
||||
"markdown": "# 功能:手工台账上传\n\n## 目标\n\n站端每日上传纸质**手工台账照片**,作为系统电子加氢记录的核对依据。多日未操作时,可通过日历查看缺口并补传过往日期。\n\n## 产品口径\n\n| 项 | 规则 |\n|---|---|\n| 入口 | H5:底部 Tab「手工台账」;Web:顶栏「手工台账」 |\n| 站点范围 | **H5 固定本站**(无切站);Web 可按角色多站/超管切换(标注状态演示,未接真实登录) |\n| 上传形态 | 仅图片(拍照/相册),可多张 |\n| 维度 | **加氢站 + 自然日(本地日历日)** 一份 |\n| 日历 | 月历自本站**首笔加氢记录日**起算:绿=已传、橙=未传;首笔之前无需补传;禁选未来日 |\n| 补传 | 允许为过往未传日期补传;已传日期仅可**追加**新图 |\n| 归档 | **确认上传后的图片进入已归档状态,不可删除**;未确认的草稿图仍可删除 |\n| 二次确认 | H5 / Web 确认上传前弹窗:「提交后将无法修改,是否确认手工台账照片无误」;确认后才写入 |\n| 拦截 | **前一日手工台账未上传 → 今日禁止新增加氢记录**(编辑已有记录不拦;补传更早历史日不替代昨日校验) |\n| 数据 | 共享 `H2VehicleLedgerBridge` 内存 Store(未接真实 API);站名/编码解析见 Bridge.`h2BridgeResolveStationId` |\n\n## 判定顺序(新增拦截)\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 操作=编辑已有记录 | 不校验手工台账 |\n| 2 | 操作=新增,且昨日早于本站首笔加氢日(或不要求昨日台账) | 允许进入新增 |\n| 3 | 操作=新增,且目标站昨日已有 ≥1 张台账图 | 允许进入新增 |\n| 4 | 操作=新增,昨日未上传 | 顶部/Toast 提示「手工台账有缺失,作为对账重要依据,请先上传手工台账」,并**直接跳转**手工台账页且选中昨日 |\n\n## 日历与补传\n\n| 规则 | 说明 |\n|------|------|\n| 数据源 | `listManualLedgersByStation`;当日有 ≥1 张图视为已上传 |\n| 业务起点 | 取本站 Bridge 加氢记录中最早 `hydrogenTime` 自然日;无记录则历史日均不要求补传 |\n| 未传计数 | 本月内 `max(月初, 首笔日)`~min(月末, 今日) 中未上传的天数 |\n| 点选 | 仅 `dateKey ≤ 今日`;选中后右侧/下方展示该日草稿图与上传按钮 |\n| 归档不可删 | Store 已有图强制保留;`upsertManualLedger` 仅追加新图,忽略删除已有图的请求 |\n| 与拦截关系 | 仅 **昨日** 影响「禁止新增」;历史补传解决核对缺口;今日台账仍可按日常上传,但不作为新增门禁 |\n\n## 用户可见\n\n- 今日状态双反馈(H5 台账页顶部卡片 / Web 右侧「当日台账」):未上传「今日尚未上传…」异常态;已上传「今日已上传…」正常态。**日常上传反馈 ≠ 新增门禁**;H5 文案须写明「今日不作为新增门禁,仅要求前一日已上传」 \n- 昨日缺失(H5):**列表顶栏可点橙条** + 底栏 FAB 旁提示 + Tab 红点;台账页同样展示橙条并可选中昨日;文案固定为「手工台账有缺失,作为对账重要依据,请先上传手工台账」 \n- 日历:自首笔加氢日起绿/橙标记;首笔之前无橙点(无需补传);本月未传天数摘要 \n- Web 右侧面板标题「手工台账」:空态提示;有图可点进全屏翻页预览并角标「已归档/待提交」;预览区内上传;底部确认前二次弹窗;提交后立即展示已归档缩略图;左右卡片等高\n\n## 代码路径\n\n- Bridge:`src/common/h2VehicleLedgerBridge.js`(`hasManualLedgerForDate` / `isManualLedgerGateBlocked` / `upsertManualLedger` / `listManualLedgersByStation`) \n- H5:`oneos-h5-h2-order` · `ManualLedgerPanel` + `utils/manual-ledger.ts`(`isManualLedgerGateBlocked` / `buildManualMonthCells` / `getFirstHydrogenDateKey`) \n- Web:`oneos-web-h2-station/pages/02-加氢记录.jsx`(`hrIsManualLedgerGateBlocked` / 跳转选中昨日) \n\n## 验收\n\n1. 昨日未上传时,H5/Web 列表顶栏提示固定文案;点「新增」被拦截并直接进入手工台账(选中昨日) \n2. 补传昨日台账并确认后,可新增加氢记录 \n3. 日历自首笔加氢日起区分已传/未传;首笔之前不标未传、不计入补传;可点选范围内未传日补传 \n4. 仅补传更早历史日、昨日仍未传时,新增仍被拦截 \n5. 确认上传后的图显示已归档且不可删除;仅可追加补传 \n6. H5 与 Web 共用同一 Store,一端上传另一端可见(同会话) \n7. Web 导出 CSV 列含「车辆来源」(羚牛车辆/非羚牛车辆)、「数据来源」(站端上传/羚牛上传),与列表标签口径一致 \n",
|
||||
"markdownPath": ".spec/feature-manual-ledger.md"
|
||||
},
|
||||
{
|
||||
"type": "markdown",
|
||||
"id": "h5-doc-logic-reconcile",
|
||||
"title": "核对与对账联动",
|
||||
"markdown": "# 加氢订单(H5)· 对账 / 核对联动规则\n\n| 项 | 说明 |\n|---|---|\n| 代码路径 | Store:`src/common/h2VehicleLedgerBridge.js`;对账单:`oneos-web-h2-station-site`;PC 加氢记录:`oneos-web-h2-station`;车辆氢费明细:`vehicle-h2-fee-ledger` |\n| 规格全文 | [../vehicle-h2-fee-ledger/.spec/verify-reconcile.md](../vehicle-h2-fee-ledger/.spec/verify-reconcile.md) |\n| 车辆标签 | [fleet-plate-tag.md](./fleet-plate-tag.md) |\n| 数据源 | 原型本地种子 + 内存共享 Store(**未接真实 API**) |\n\n---\n\n## 1. 两套状态(判定优先级)\n\n| 优先级 | 状态 | 字段 | 谁触发 | 用户可见 |\n|---|---|---|---|---|\n| 1 | **核对** | `verifyStatus` / `verifiedAt` | 车辆氢费明细 · **完成核对** | 未核对 / 已核对 + 核对时间 |\n| 2 | **对账** | `reconcileStatus` / `reconcileDate` | 站点信息 · **对账单提交** | 未对账 / 已对账 + 对账时间 |\n\n> `reconciledAt` 仅作完成核对时的兼容写入,**不得**再用于判定「已对账」。\n\n---\n\n## 2. 数据流\n\n```text\n站端 H5 / Web 新增加氢记录\n → Bridge.upsertRow(verify=unverified,reconcile=pending;成本单价/量/总额)\n → 车辆氢费明细可见\n\n能源部 · 完成核对\n → verifyStatus=verified + verifiedAt\n → 站端同步「已核对」;羚牛车辆禁改删\n\n站点信息 · 对账单提交(仅已核对 ∧ 未对账)\n → reconcileStatus=reconciled + reconcileDate + statementRecordId\n → 站端 / 台账同步「已对账」;羚牛车辆禁改删\n```\n\n---\n\n## 3. 用户可见结果(H5)\n\n| 结果 | 行为 |\n|---|---|\n| 非羚牛车辆 | 核对/对账展示「—」;**不锁定**改删 |\n| 羚牛 · 未核对 | 可编辑、删除 |\n| 羚牛 · 已核对未对账 | 只读;提示能源部已核对 |\n| 羚牛 · 已对账 | 只读;提示已纳入对账单 |\n\n---\n\n## 4. 边界\n\n- 站端上报默认未核对、未对账\n- **站端改删锁定仅适用于羚牛车辆**(与 Web / 台账车辆标签口径一致)\n- 对账时间只认 `reconcileDate`(对账单回写)\n- 跨页联动依赖同浏览器会话共享 Store\n",
|
||||
"markdownPath": ".spec/reconcile-linkage.md"
|
||||
},
|
||||
{
|
||||
"type": "markdown",
|
||||
"id": "h5-doc-logic-fleet",
|
||||
"title": "羚牛车辆标签",
|
||||
"markdown": "# 车牌「羚牛车辆 / 非羚牛车辆」标签\n\n原型本地判定,未接真实车辆管理 API。\n\n全文(含核对/对账字段空展示规则,跨 Web / 台账 / H5): \n[../../oneos-web-h2-station/.spec/fleet-vehicle-verify.md](../../oneos-web-h2-station/.spec/fleet-vehicle-verify.md)\n\n## 判定顺序\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 车牌为空 | 不展示标签 |\n| 2 | 规范化后车牌 ∈ 本地系统车辆集合 | **羚牛车辆** |\n| 3 | 有车牌但不在集合中 | **非羚牛车辆** |\n\n## 前置条件\n\n- 识别(OCR)或键盘输入得到车牌后才展示。\n- 比较前去掉尾缀 `F`,统一大写。\n\n## 数据源\n\n- 优先 `H2VehicleLedgerBridge.isLingniuVehicle`;本地回退见 `utils/fleet-plates.ts`(与 Bridge `H2_FLEET_PLATE_KEYS` 同步)。\n\n## 用户可见结果\n\n- 绿色标签「羚牛车辆」或橙色标签「非羚牛车辆」。\n- **非羚牛车辆**:列表/详情中核对状态、核对时间、对账状态、对账时间显示「—」,不展示核对待办语义。\n- 不拦截提交(仅提示归属)。\n\n## 代码路径\n\n- `getVehicleTag` / `isLingniuVehicle` → `CreateWizard`、`OrderCard`、`OrderDetail`\n",
|
||||
"markdownPath": ".spec/fleet-plate-tag.md"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "folder",
|
||||
"id": "h5-doc-flows",
|
||||
"title": "流程图",
|
||||
"defaultExpanded": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "markdown",
|
||||
"id": "h5-doc-flows-md",
|
||||
"title": "操作流程图",
|
||||
"markdown": "# 流程图\n\n> 标注面板支持 Mermaid 渲染。下列流程与当前原型行为一致(本地 Store,未接真实 API)。\n\n---\n\n## 1. 主路径:从打开到上报\n\n```mermaid\nflowchart TD\n A[打开加氢订单 H5] --> B[查看本站列表与 KPI]\n B --> C{前一日手工台账已上传?}\n C -->|否| D[提示:手工台账有缺失…]\n D --> E[进入手工台账 · 选中昨日]\n E --> F[拍照/相册上传并确认]\n F --> C\n C -->|是| G[点新增]\n G --> H[填写时间/车牌/面板/量价]\n H --> I{同站同车牌同时间已存在?}\n I -->|是| J[拦截提示]\n J --> H\n I -->|否| K[保存写入 Bridge]\n K --> B\n```\n\n---\n\n## 2. 手工台账补传\n\n```mermaid\nflowchart LR\n A[手工台账 Tab] --> B[月历:绿已传 / 橙未传]\n B --> C[点选 ≤今日 且 ≥首笔日]\n C --> D[上传图片]\n D --> E[二次确认]\n E --> F[归档不可删 · 可追加]\n```\n\n---\n\n## 3. 编辑 / 删除与锁定\n\n```mermaid\nflowchart TD\n A[打开详情] --> L{羚牛车辆?}\n L -->|否| D[可编辑 / 可删除]\n L -->|是| B{已核对或已对账?}\n B -->|是| C[只读 · 禁改删]\n B -->|否| D\n D --> E[写回 Bridge]\n```\n\n---\n\n## 4. 与 Web / 台账联动(概念)\n\n```mermaid\nsequenceDiagram\n participant H5 as 加氢订单 H5\n participant BR as Bridge Store\n participant WEB as 加氢记录 Web\n participant LED as 车辆氢费明细\n participant SITE as 站点对账单\n H5->>BR: 新增/改删流水 · 上传台账\n WEB->>BR: 同 Store 读写 · 导出\n LED->>BR: 完成核对 → 已核对\n SITE->>BR: 提交对账单 → 已对账\n```\n",
|
||||
"markdownPath": ".spec/flows.md"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "folder",
|
||||
"id": "h5-doc-features",
|
||||
@@ -403,28 +484,28 @@
|
||||
"type": "markdown",
|
||||
"id": "h5-doc-manual",
|
||||
"title": "手工台账上传",
|
||||
"markdown": "# 功能:手工台账上传\n\n## 目标\n\n站端每日上传纸质**手工台账照片**,作为系统电子加氢记录的核对依据。多日未操作时,可通过日历查看缺口并补传过往日期。\n\n## 产品口径\n\n| 项 | 规则 |\n|---|---|\n| 入口 | H5:底部 Tab「手工台账」;Web:顶栏「手工台账」 |\n| 上传形态 | 仅图片(拍照/相册),可多张 |\n| 维度 | **加氢站 + 自然日(本地日历日)** 一份 |\n| 日历 | 月历自本站**首笔加氢记录日**起算:绿=已传、橙=未传;首笔之前无需补传;禁选未来日 |\n| 补传 | 允许为过往未传日期补传;已传日期仅可**追加**新图 |\n| 归档 | **确认上传后的图片进入已归档状态,不可删除**;未确认的草稿图仍可删除 |\n| 拦截 | **未上传今日手工台账 → 禁止新增加氢记录**(编辑已有记录不拦;补传历史不替代今日校验) |\n| 数据 | 共享 `H2VehicleLedgerBridge` 内存 Store(未接真实 API) |\n\n## 判定顺序(新增拦截)\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 操作=编辑已有记录 | 不校验手工台账 |\n| 2 | 操作=新增,且目标站今日已有 ≥1 张台账图 | 允许进入新增 |\n| 3 | 操作=新增,今日未上传 | 提示并跳转手工台账 Tab |\n\n## 日历与补传\n\n| 规则 | 说明 |\n|------|------|\n| 数据源 | `listManualLedgersByStation`;当日有 ≥1 张图视为已上传 |\n| 业务起点 | 取本站 Bridge 加氢记录中最早 `hydrogenTime` 自然日;无记录则历史日均不要求补传 |\n| 未传计数 | 本月内 `max(月初, 首笔日)`~min(月末, 今日) 中未上传的天数 |\n| 点选 | 仅 `dateKey ≤ 今日`;选中后右侧/下方展示该日草稿图与上传按钮 |\n| 归档不可删 | Store 已有图强制保留;`upsertManualLedger` 仅追加新图,忽略删除已有图的请求 |\n| 与拦截关系 | 仅 **今日** 影响「禁止新增」;历史补传解决核对缺口,不放宽今日门禁 |\n\n## 用户可见\n\n- 今日未上传:橙/黄提示条 + Tab 红点 \n- 日历:自首笔加氢日起绿/橙标记;首笔之前无橙点(无需补传);本月未传天数摘要 \n- 选中日:上传区标题为该日期;已归档图显示「已归档」无删除;仅未确认草稿可删;已有台账时主按钮为「确认补传」\n\n## 代码路径\n\n- Bridge:`src/common/h2VehicleLedgerBridge.js`(`hasManualLedgerForDate` / `upsertManualLedger` / `listManualLedgersByStation`) \n- H5:`oneos-h5-h2-order` · `ManualLedgerPanel` + `utils/manual-ledger.ts`(`buildManualMonthCells` / `getFirstHydrogenDateKey`) \n- Web:`oneos-web-h2-station/pages/02-加氢记录.jsx`(手工台账 Tab 月历) \n\n## 验收\n\n1. 今日未上传时,H5/Web 点「新增」均被拦截并引导上传 \n2. 上传至少一张图并确认后,可新增电子记录 \n3. 日历自首笔加氢日起区分已传/未传;首笔之前不标未传、不计入补传;可点选范围内未传日补传 \n4. 补传历史日后,该日日历变为已传;今日仍未传时新增仍被拦截 \n5. 确认上传后的图显示已归档且不可删除;仅可追加补传 \n6. H5 与 Web 共用同一 Store,一端上传另一端可见(同会话) \n",
|
||||
"markdown": "# 功能:手工台账上传\n\n## 目标\n\n站端每日上传纸质**手工台账照片**,作为系统电子加氢记录的核对依据。多日未操作时,可通过日历查看缺口并补传过往日期。\n\n## 产品口径\n\n| 项 | 规则 |\n|---|---|\n| 入口 | H5:底部 Tab「手工台账」;Web:顶栏「手工台账」 |\n| 站点范围 | **H5 固定本站**(无切站);Web 可按角色多站/超管切换(标注状态演示,未接真实登录) |\n| 上传形态 | 仅图片(拍照/相册),可多张 |\n| 维度 | **加氢站 + 自然日(本地日历日)** 一份 |\n| 日历 | 月历自本站**首笔加氢记录日**起算:绿=已传、橙=未传;首笔之前无需补传;禁选未来日 |\n| 补传 | 允许为过往未传日期补传;已传日期仅可**追加**新图 |\n| 归档 | **确认上传后的图片进入已归档状态,不可删除**;未确认的草稿图仍可删除 |\n| 二次确认 | H5 / Web 确认上传前弹窗:「提交后将无法修改,是否确认手工台账照片无误」;确认后才写入 |\n| 拦截 | **前一日手工台账未上传 → 今日禁止新增加氢记录**(编辑已有记录不拦;补传更早历史日不替代昨日校验) |\n| 数据 | 共享 `H2VehicleLedgerBridge` 内存 Store(未接真实 API);站名/编码解析见 Bridge.`h2BridgeResolveStationId` |\n\n## 判定顺序(新增拦截)\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 操作=编辑已有记录 | 不校验手工台账 |\n| 2 | 操作=新增,且昨日早于本站首笔加氢日(或不要求昨日台账) | 允许进入新增 |\n| 3 | 操作=新增,且目标站昨日已有 ≥1 张台账图 | 允许进入新增 |\n| 4 | 操作=新增,昨日未上传 | 顶部/Toast 提示「手工台账有缺失,作为对账重要依据,请先上传手工台账」,并**直接跳转**手工台账页且选中昨日 |\n\n## 日历与补传\n\n| 规则 | 说明 |\n|------|------|\n| 数据源 | `listManualLedgersByStation`;当日有 ≥1 张图视为已上传 |\n| 业务起点 | 取本站 Bridge 加氢记录中最早 `hydrogenTime` 自然日;无记录则历史日均不要求补传 |\n| 未传计数 | 本月内 `max(月初, 首笔日)`~min(月末, 今日) 中未上传的天数 |\n| 点选 | 仅 `dateKey ≤ 今日`;选中后右侧/下方展示该日草稿图与上传按钮 |\n| 归档不可删 | Store 已有图强制保留;`upsertManualLedger` 仅追加新图,忽略删除已有图的请求 |\n| 与拦截关系 | 仅 **昨日** 影响「禁止新增」;历史补传解决核对缺口;今日台账仍可按日常上传,但不作为新增门禁 |\n\n## 用户可见\n\n- 今日状态双反馈(H5 台账页顶部卡片 / Web 右侧「当日台账」):未上传「今日尚未上传…」异常态;已上传「今日已上传…」正常态。**日常上传反馈 ≠ 新增门禁**;H5 文案须写明「今日不作为新增门禁,仅要求前一日已上传」 \n- 昨日缺失(H5):**列表顶栏可点橙条** + 底栏 FAB 旁提示 + Tab 红点;台账页同样展示橙条并可选中昨日;文案固定为「手工台账有缺失,作为对账重要依据,请先上传手工台账」 \n- 日历:自首笔加氢日起绿/橙标记;首笔之前无橙点(无需补传);本月未传天数摘要 \n- Web 右侧面板标题「手工台账」:空态提示;有图可点进全屏翻页预览并角标「已归档/待提交」;预览区内上传;底部确认前二次弹窗;提交后立即展示已归档缩略图;左右卡片等高\n\n## 代码路径\n\n- Bridge:`src/common/h2VehicleLedgerBridge.js`(`hasManualLedgerForDate` / `isManualLedgerGateBlocked` / `upsertManualLedger` / `listManualLedgersByStation`) \n- H5:`oneos-h5-h2-order` · `ManualLedgerPanel` + `utils/manual-ledger.ts`(`isManualLedgerGateBlocked` / `buildManualMonthCells` / `getFirstHydrogenDateKey`) \n- Web:`oneos-web-h2-station/pages/02-加氢记录.jsx`(`hrIsManualLedgerGateBlocked` / 跳转选中昨日) \n\n## 验收\n\n1. 昨日未上传时,H5/Web 列表顶栏提示固定文案;点「新增」被拦截并直接进入手工台账(选中昨日) \n2. 补传昨日台账并确认后,可新增加氢记录 \n3. 日历自首笔加氢日起区分已传/未传;首笔之前不标未传、不计入补传;可点选范围内未传日补传 \n4. 仅补传更早历史日、昨日仍未传时,新增仍被拦截 \n5. 确认上传后的图显示已归档且不可删除;仅可追加补传 \n6. H5 与 Web 共用同一 Store,一端上传另一端可见(同会话) \n7. Web 导出 CSV 列含「车辆来源」(羚牛车辆/非羚牛车辆)、「数据来源」(站端上传/羚牛上传),与列表标签口径一致 \n",
|
||||
"markdownPath": ".spec/feature-manual-ledger.md"
|
||||
},
|
||||
{
|
||||
"type": "markdown",
|
||||
"id": "h5-doc-detail",
|
||||
"title": "详情编辑删除",
|
||||
"markdown": "# 功能:详情、编辑与删除\n\n## 目标\n\n查看单笔加氢记录完整字段;在「未核对且未对账」时可编辑或删除。\n\n## 详情展示\n\n- 顶部:车牌、加氢时间、核对标签、对账标签 \n- 指标条:加氢总额、加氢量 \n- **识别原图**:车牌照片、加氢机面板照片(均为拍摄时叠加时间/地点水印后的原图;种子数据无实拍时用演示水印图) \n- 明细行:站点、车牌、里程、单价、核对/对账状态与时间 \n\n## 锁定规则\n\n| 条件 | 结果 |\n|------|------|\n| 已对账 | 不可编辑/删除;提示已纳入站点对账单 |\n| 已核对(未对账) | 不可编辑/删除;提示能源部已核对 |\n| 未核对且未对账 | 可编辑、可删除 |\n\n判定函数:`isOrderLocked`(核对或对账任一成立即锁)。\n\n## 编辑\n\n进入与「新增」同一套表单,预填当前值(含已存水印照片);提交走更新逻辑(仍校验重复键)。\n\n## 删除\n\n二次确认后从 Bridge 移除该行。\n\n## 验收\n\n1. 未核对未对账:底部有删除/编辑 \n2. 已核对或已对账:无操作栏,有锁定说明 \n3. 详情含对账信息;列表卡片不含对账 \n4. 详情展示车牌与面板带水印原图(新上报提交后为实拍;种子行为演示图) \n",
|
||||
"markdown": "# 功能:详情、编辑与删除\n\n## 目标\n\n查看单笔加氢记录完整字段;在「未核对且未对账」时可编辑或删除(**锁定仅适用于羚牛车辆**)。\n\n## 详情展示\n\n- 顶部:车牌、加氢时间、核对标签、对账标签 \n- 指标条:加氢总额、加氢量 \n- **识别原图**:车牌照片、加氢机面板照片(均为拍摄时叠加时间/地点水印后的原图;种子数据无实拍时用演示水印图) \n- 明细行:站点、车牌、里程、单价、核对/对账状态与时间 \n\n> 非羚牛车辆:核对/对账字段展示「—」,不展示核对待办语义(见 [fleet-plate-tag.md](./fleet-plate-tag.md))。\n\n## 锁定规则\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | **非羚牛车辆** | **不锁定**(不以核对/对账状态禁改删) |\n| 2 | 羚牛 ∧ 已对账 | 不可编辑/删除;提示已纳入站点对账单 |\n| 3 | 羚牛 ∧ 已核对(未对账) | 不可编辑/删除;提示能源部已核对 |\n| 4 | 羚牛 ∧ 未核对且未对账 | 可编辑、可删除 |\n\n判定函数:`isOrderLocked`(先判羚牛;再核「核对或对账任一成立即锁」)。\n\n## 编辑\n\n进入与「新增」同一套表单,预填当前值(含已存水印照片);提交走更新逻辑(仍校验重复键)。\n\n## 删除\n\n二次确认文案:「确认删除车牌 … 的未锁定记录?」;确认后从 Bridge 移除该行。\n\n## 验收\n\n1. 羚牛 ∧ 未核对未对账:底部有删除/编辑 \n2. 羚牛 ∧ 已核对或已对账:无操作栏,有锁定说明 \n3. 非羚牛:可改删(不因核对/对账锁定);核对/对账展示「—」 \n4. 详情含对账信息;列表卡片不含对账 \n5. 详情展示车牌与面板带水印原图(新上报提交后为实拍;种子行为演示图) \n",
|
||||
"markdownPath": ".spec/feature-detail.md"
|
||||
},
|
||||
{
|
||||
"type": "markdown",
|
||||
"id": "h5-doc-fleet",
|
||||
"title": "羚牛车辆标签",
|
||||
"markdown": "# 车牌「羚牛车辆 / 非羚牛车辆」标签\n\n原型本地判定,未接真实车辆管理 API。\n\n## 判定顺序\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 车牌为空 | 不展示标签 |\n| 2 | 规范化后车牌 ∈ 本地系统车辆集合 | **羚牛车辆** |\n| 3 | 有车牌但不在集合中 | **非羚牛车辆** |\n\n## 前置条件\n\n- 识别(OCR)或键盘输入得到车牌后才展示。\n- 比较前去掉尾缀 `F`,统一大写。\n\n## 数据源\n\n- `utils/fleet-plates.ts` 内本地种子车牌集合(含加氢 Bridge 演示车牌与部分车辆管理样例)。\n- 正式环境应改为查询车辆管理 / 系统车辆列表接口。\n\n## 用户可见结果\n\n- 绿色标签「羚牛车辆」或橙色标签「非羚牛车辆」。\n- 不拦截提交(仅提示归属)。\n\n## 代码路径\n\n- `getVehicleTag` / `isLingniuVehicle` → `CreateWizard` 车牌行右侧标签。\n",
|
||||
"markdown": "# 车牌「羚牛车辆 / 非羚牛车辆」标签\n\n原型本地判定,未接真实车辆管理 API。\n\n全文(含核对/对账字段空展示规则,跨 Web / 台账 / H5): \n[../../oneos-web-h2-station/.spec/fleet-vehicle-verify.md](../../oneos-web-h2-station/.spec/fleet-vehicle-verify.md)\n\n## 判定顺序\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 车牌为空 | 不展示标签 |\n| 2 | 规范化后车牌 ∈ 本地系统车辆集合 | **羚牛车辆** |\n| 3 | 有车牌但不在集合中 | **非羚牛车辆** |\n\n## 前置条件\n\n- 识别(OCR)或键盘输入得到车牌后才展示。\n- 比较前去掉尾缀 `F`,统一大写。\n\n## 数据源\n\n- 优先 `H2VehicleLedgerBridge.isLingniuVehicle`;本地回退见 `utils/fleet-plates.ts`(与 Bridge `H2_FLEET_PLATE_KEYS` 同步)。\n\n## 用户可见结果\n\n- 绿色标签「羚牛车辆」或橙色标签「非羚牛车辆」。\n- **非羚牛车辆**:列表/详情中核对状态、核对时间、对账状态、对账时间显示「—」,不展示核对待办语义。\n- 不拦截提交(仅提示归属)。\n\n## 代码路径\n\n- `getVehicleTag` / `isLingniuVehicle` → `CreateWizard`、`OrderCard`、`OrderDetail`\n",
|
||||
"markdownPath": ".spec/fleet-plate-tag.md"
|
||||
},
|
||||
{
|
||||
"type": "markdown",
|
||||
"id": "h5-doc-reconcile",
|
||||
"title": "核对与对账联动",
|
||||
"markdown": "# 加氢订单(H5)· 对账 / 核对联动规则\n\n| 项 | 说明 |\n|---|---|\n| 代码路径 | Store:`src/common/h2VehicleLedgerBridge.js`;对账单:`oneos-web-h2-station-site`;PC 加氢记录:`oneos-web-h2-station`;车辆氢费明细:`vehicle-h2-fee-ledger` |\n| 规格全文 | [../vehicle-h2-fee-ledger/.spec/verify-reconcile.md](../vehicle-h2-fee-ledger/.spec/verify-reconcile.md) |\n| 数据源 | 原型本地种子 + 内存共享 Store(**未接真实 API**) |\n\n---\n\n## 1. 两套状态(判定优先级)\n\n| 优先级 | 状态 | 字段 | 谁触发 | 用户可见 |\n|---|---|---|---|---|\n| 1 | **核对** | `verifyStatus` / `verifiedAt` | 车辆氢费明细 · **完成核对** | 未核对 / 已核对 + 核对时间 |\n| 2 | **对账** | `reconcileStatus` / `reconcileDate` | 站点信息 · **对账单提交** | 未对账 / 已对账 + 对账时间 |\n\n> `reconciledAt` 仅作完成核对时的兼容写入,**不得**再用于判定「已对账」。\n\n## 2. 数据流\n\n```text\n站端 H5 / Web 新增加氢记录\n → Bridge.upsertRow(verify=unverified,reconcile=pending;成本单价/量/总额)\n → 车辆氢费明细可见\n\n能源部 · 完成核对\n → verifyStatus=verified + verifiedAt\n → 站端同步「已核对」,禁改删\n\n站点信息 · 对账单提交(仅已核对 ∧ 未对账)\n → reconcileStatus=reconciled + reconcileDate + statementRecordId\n → 站端 / 台账同步「已对账」\n```\n\n## 3. 用户可见结果(H5)\n\n| 结果 | 行为 |\n|---|---|\n| 未核对 | 可编辑、删除 |\n| 已核对未对账 | 只读;提示能源部已核对 |\n| 已对账 | 只读;提示已纳入对账单 |\n\n## 4. 边界\n\n- 站端上报默认未核对、未对账\n- 对账时间只认 `reconcileDate`(对账单回写)\n- 跨页联动依赖同浏览器会话共享 Store\n",
|
||||
"markdown": "# 加氢订单(H5)· 对账 / 核对联动规则\n\n| 项 | 说明 |\n|---|---|\n| 代码路径 | Store:`src/common/h2VehicleLedgerBridge.js`;对账单:`oneos-web-h2-station-site`;PC 加氢记录:`oneos-web-h2-station`;车辆氢费明细:`vehicle-h2-fee-ledger` |\n| 规格全文 | [../vehicle-h2-fee-ledger/.spec/verify-reconcile.md](../vehicle-h2-fee-ledger/.spec/verify-reconcile.md) |\n| 车辆标签 | [fleet-plate-tag.md](./fleet-plate-tag.md) |\n| 数据源 | 原型本地种子 + 内存共享 Store(**未接真实 API**) |\n\n---\n\n## 1. 两套状态(判定优先级)\n\n| 优先级 | 状态 | 字段 | 谁触发 | 用户可见 |\n|---|---|---|---|---|\n| 1 | **核对** | `verifyStatus` / `verifiedAt` | 车辆氢费明细 · **完成核对** | 未核对 / 已核对 + 核对时间 |\n| 2 | **对账** | `reconcileStatus` / `reconcileDate` | 站点信息 · **对账单提交** | 未对账 / 已对账 + 对账时间 |\n\n> `reconciledAt` 仅作完成核对时的兼容写入,**不得**再用于判定「已对账」。\n\n---\n\n## 2. 数据流\n\n```text\n站端 H5 / Web 新增加氢记录\n → Bridge.upsertRow(verify=unverified,reconcile=pending;成本单价/量/总额)\n → 车辆氢费明细可见\n\n能源部 · 完成核对\n → verifyStatus=verified + verifiedAt\n → 站端同步「已核对」;羚牛车辆禁改删\n\n站点信息 · 对账单提交(仅已核对 ∧ 未对账)\n → reconcileStatus=reconciled + reconcileDate + statementRecordId\n → 站端 / 台账同步「已对账」;羚牛车辆禁改删\n```\n\n---\n\n## 3. 用户可见结果(H5)\n\n| 结果 | 行为 |\n|---|---|\n| 非羚牛车辆 | 核对/对账展示「—」;**不锁定**改删 |\n| 羚牛 · 未核对 | 可编辑、删除 |\n| 羚牛 · 已核对未对账 | 只读;提示能源部已核对 |\n| 羚牛 · 已对账 | 只读;提示已纳入对账单 |\n\n---\n\n## 4. 边界\n\n- 站端上报默认未核对、未对账\n- **站端改删锁定仅适用于羚牛车辆**(与 Web / 台账车辆标签口径一致)\n- 对账时间只认 `reconcileDate`(对账单回写)\n- 跨页联动依赖同浏览器会话共享 Store\n",
|
||||
"markdownPath": ".spec/reconcile-linkage.md"
|
||||
}
|
||||
]
|
||||
@@ -433,7 +514,7 @@
|
||||
"type": "folder",
|
||||
"id": "h5-doc-pages",
|
||||
"title": "按页面",
|
||||
"defaultExpanded": true,
|
||||
"defaultExpanded": false,
|
||||
"children": [
|
||||
{
|
||||
"type": "folder",
|
||||
@@ -500,7 +581,7 @@
|
||||
"type": "markdown",
|
||||
"id": "h5-page-detail-md",
|
||||
"title": "详情页要点",
|
||||
"markdown": "# 功能:详情、编辑与删除\n\n## 目标\n\n查看单笔加氢记录完整字段;在「未核对且未对账」时可编辑或删除。\n\n## 详情展示\n\n- 顶部:车牌、加氢时间、核对标签、对账标签 \n- 指标条:加氢总额、加氢量 \n- **识别原图**:车牌照片、加氢机面板照片(均为拍摄时叠加时间/地点水印后的原图;种子数据无实拍时用演示水印图) \n- 明细行:站点、车牌、里程、单价、核对/对账状态与时间 \n\n## 锁定规则\n\n| 条件 | 结果 |\n|------|------|\n| 已对账 | 不可编辑/删除;提示已纳入站点对账单 |\n| 已核对(未对账) | 不可编辑/删除;提示能源部已核对 |\n| 未核对且未对账 | 可编辑、可删除 |\n\n判定函数:`isOrderLocked`(核对或对账任一成立即锁)。\n\n## 编辑\n\n进入与「新增」同一套表单,预填当前值(含已存水印照片);提交走更新逻辑(仍校验重复键)。\n\n## 删除\n\n二次确认后从 Bridge 移除该行。\n\n## 验收\n\n1. 未核对未对账:底部有删除/编辑 \n2. 已核对或已对账:无操作栏,有锁定说明 \n3. 详情含对账信息;列表卡片不含对账 \n4. 详情展示车牌与面板带水印原图(新上报提交后为实拍;种子行为演示图) \n"
|
||||
"markdown": "# 功能:详情、编辑与删除\n\n## 目标\n\n查看单笔加氢记录完整字段;在「未核对且未对账」时可编辑或删除(**锁定仅适用于羚牛车辆**)。\n\n## 详情展示\n\n- 顶部:车牌、加氢时间、核对标签、对账标签 \n- 指标条:加氢总额、加氢量 \n- **识别原图**:车牌照片、加氢机面板照片(均为拍摄时叠加时间/地点水印后的原图;种子数据无实拍时用演示水印图) \n- 明细行:站点、车牌、里程、单价、核对/对账状态与时间 \n\n> 非羚牛车辆:核对/对账字段展示「—」,不展示核对待办语义(见 [fleet-plate-tag.md](./fleet-plate-tag.md))。\n\n## 锁定规则\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | **非羚牛车辆** | **不锁定**(不以核对/对账状态禁改删) |\n| 2 | 羚牛 ∧ 已对账 | 不可编辑/删除;提示已纳入站点对账单 |\n| 3 | 羚牛 ∧ 已核对(未对账) | 不可编辑/删除;提示能源部已核对 |\n| 4 | 羚牛 ∧ 未核对且未对账 | 可编辑、可删除 |\n\n判定函数:`isOrderLocked`(先判羚牛;再核「核对或对账任一成立即锁」)。\n\n## 编辑\n\n进入与「新增」同一套表单,预填当前值(含已存水印照片);提交走更新逻辑(仍校验重复键)。\n\n## 删除\n\n二次确认文案:「确认删除车牌 … 的未锁定记录?」;确认后从 Bridge 移除该行。\n\n## 验收\n\n1. 羚牛 ∧ 未核对未对账:底部有删除/编辑 \n2. 羚牛 ∧ 已核对或已对账:无操作栏,有锁定说明 \n3. 非羚牛:可改删(不因核对/对账锁定);核对/对账展示「—」 \n4. 详情含对账信息;列表卡片不含对账 \n5. 详情展示车牌与面板带水印原图(新上报提交后为实拍;种子行为演示图) \n"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -7,9 +7,11 @@ import {
|
||||
getFirstHydrogenDateKey,
|
||||
getManualLedger,
|
||||
isArchivedManualImage,
|
||||
isManualLedgerGateBlocked,
|
||||
listManualLedgersByStation,
|
||||
todayDateKey,
|
||||
upsertManualLedger,
|
||||
yesterdayDateKey,
|
||||
type ManualLedgerImage,
|
||||
} from '../utils/manual-ledger';
|
||||
import { IconCamera, IconCheck } from './Icons';
|
||||
@@ -19,21 +21,31 @@ type Props = {
|
||||
tick: number;
|
||||
onUploaded: () => void;
|
||||
showToast: (msg: string) => void;
|
||||
/** 打开面板时优先选中该日(如昨日缺口) */
|
||||
focusDate?: string;
|
||||
};
|
||||
|
||||
const WEEK_LABELS = ['日', '一', '二', '三', '四', '五', '六'];
|
||||
|
||||
export function ManualLedgerPanel({ station, tick, onUploaded, showToast }: Props) {
|
||||
export function ManualLedgerPanel({ station, tick, onUploaded, showToast, focusDate }: Props) {
|
||||
const today = todayDateKey();
|
||||
const fileRef = useRef<HTMLInputElement>(null);
|
||||
const [draftImages, setDraftImages] = useState<ManualLedgerImage[]>([]);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [selectedDate, setSelectedDate] = useState(today);
|
||||
const initialDate = focusDate && focusDate <= today ? focusDate : today;
|
||||
const [selectedDate, setSelectedDate] = useState(initialDate);
|
||||
const [cursorYm, setCursorYm] = useState(() => {
|
||||
const [y, m] = today.split('-').map(Number);
|
||||
const [y, m] = initialDate.split('-').map(Number);
|
||||
return { y, m };
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!focusDate || focusDate > today) return;
|
||||
setSelectedDate(focusDate);
|
||||
const [y, m] = focusDate.split('-').map(Number);
|
||||
setCursorYm({ y, m });
|
||||
}, [focusDate, today]);
|
||||
|
||||
const stationKey = station.stationId || station.stationName;
|
||||
|
||||
const allEntries = useMemo(
|
||||
@@ -127,7 +139,7 @@ export function ManualLedgerPanel({ station, tick, onUploaded, showToast }: Prop
|
||||
showToast('请至少上传一张手工台账照片');
|
||||
return;
|
||||
}
|
||||
const ok = window.confirm('确认上传后该记录将归档无法修改,是否确认无误');
|
||||
const ok = window.confirm('提交后将无法修改,是否确认手工台账照片无误');
|
||||
if (!ok) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
@@ -174,8 +186,24 @@ export function ManualLedgerPanel({ station, tick, onUploaded, showToast }: Prop
|
||||
setSelectedDate(dateKey);
|
||||
}
|
||||
|
||||
const yesterday = yesterdayDateKey();
|
||||
const gateBlocked = isManualLedgerGateBlocked(stationKey);
|
||||
|
||||
return (
|
||||
<div className="h5-manual" data-annotation-id="h5-manual">
|
||||
{gateBlocked ? (
|
||||
<button
|
||||
type="button"
|
||||
className="h5-banner-warn"
|
||||
onClick={() => {
|
||||
setSelectedDate(yesterday);
|
||||
const [y, m] = yesterday.split('-').map(Number);
|
||||
setCursorYm({ y, m });
|
||||
}}
|
||||
>
|
||||
手工台账有缺失,作为对账重要依据,请先上传手工台账(选中昨日 {yesterday})
|
||||
</button>
|
||||
) : null}
|
||||
<div
|
||||
className={`h5-manual-status${
|
||||
uploadedSet.has(today) ? ' h5-manual-status--ok' : ''
|
||||
@@ -186,7 +214,9 @@ export function ManualLedgerPanel({ station, tick, onUploaded, showToast }: Prop
|
||||
<strong>{uploadedSet.has(today) ? '今日已上传' : '今日尚未上传'}</strong>
|
||||
<p>
|
||||
{today} · {station.stationName}
|
||||
{!uploadedSet.has(today) ? '。上传今日台账后才可新增加氢记录。' : ''}
|
||||
{!uploadedSet.has(today)
|
||||
? '。今日台账可按日常上传,不作为新增门禁;新增加氢记录仅要求前一日已上传。'
|
||||
: ''}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import type { H5OrderRecord } from '../types';
|
||||
import { formatDateTimeMinute, formatKg, formatMoney, formatPlateDisplay } from '../utils/format';
|
||||
import { getVehicleTag } from '../utils/fleet-plates';
|
||||
import { IconCheck, IconClock } from './Icons';
|
||||
|
||||
type Props = {
|
||||
@@ -10,22 +11,34 @@ type Props = {
|
||||
};
|
||||
|
||||
export function OrderCard({ order, onClick, index = 0 }: Props) {
|
||||
const verified = order.verifyStatus === 'verified';
|
||||
const vehicleTag = getVehicleTag(order.plateNo);
|
||||
const isLingniu = vehicleTag === 'lingniu';
|
||||
const verified = isLingniu && order.verifyStatus === 'verified';
|
||||
const verifyLabel = !isLingniu ? null : verified ? '已核对' : '未核对';
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={`h5-card${verified ? ' h5-card--done' : ' h5-card--pending'}`}
|
||||
className={`h5-card${isLingniu ? (verified ? ' h5-card--done' : ' h5-card--pending') : ''}`}
|
||||
data-annotation-id="h5-card"
|
||||
onClick={onClick}
|
||||
style={{ animationDelay: `${Math.min(index, 8) * 40}ms` }}
|
||||
aria-label={`${formatPlateDisplay(order.plateNo)},${verified ? '已核对' : '未核对'}`}
|
||||
aria-label={`${formatPlateDisplay(order.plateNo)}${verifyLabel ? `,${verifyLabel}` : ''}`}
|
||||
>
|
||||
<div className="h5-card-top">
|
||||
<div className="h5-plate">{formatPlateDisplay(order.plateNo)}</div>
|
||||
<span className={`h5-tag ${verified ? 'h5-tag--done' : 'h5-tag--pending'}`}>
|
||||
{verified ? <IconCheck size={12} /> : <IconClock size={12} />}
|
||||
{verified ? '已核对' : '未核对'}
|
||||
</span>
|
||||
<div className="h5-plate-wrap">
|
||||
<div className="h5-plate">{formatPlateDisplay(order.plateNo)}</div>
|
||||
{vehicleTag === 'lingniu' ? (
|
||||
<span className="h5-vehicle-tag h5-vehicle-tag--in">羚牛车辆</span>
|
||||
) : vehicleTag === 'external' ? (
|
||||
<span className="h5-vehicle-tag h5-vehicle-tag--out">非羚牛车辆</span>
|
||||
) : null}
|
||||
</div>
|
||||
{verifyLabel ? (
|
||||
<span className={`h5-tag ${verified ? 'h5-tag--done' : 'h5-tag--pending'}`}>
|
||||
{verified ? <IconCheck size={12} /> : <IconClock size={12} />}
|
||||
{verifyLabel}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="h5-card-meta">
|
||||
<IconClock size={13} />
|
||||
@@ -46,7 +59,7 @@ export function OrderCard({ order, onClick, index = 0 }: Props) {
|
||||
</div>
|
||||
<div className="h5-card-foot">
|
||||
<span>单价 {formatMoney(order.unitPrice)} 元/kg</span>
|
||||
{verified ? (
|
||||
{isLingniu && verified ? (
|
||||
<span className="h5-card-foot__verify">核对 {formatDateTimeMinute(order.verifiedAt)}</span>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
|
||||
import type { H5OrderRecord } from '../types';
|
||||
import { buildDemoWatermarkedPhoto } from '../utils/demo-photo';
|
||||
import { formatDateTimeMinute, formatKg, formatMileage, formatMoney, formatPlateDisplay } from '../utils/format';
|
||||
import { getVehicleTag } from '../utils/fleet-plates';
|
||||
import { IconCheck, IconChevronLeft, IconClock } from './Icons';
|
||||
import { PhoneShell } from './PhoneShell';
|
||||
import { isOrderLocked } from '../utils/orders';
|
||||
@@ -14,8 +15,10 @@ type Props = {
|
||||
};
|
||||
|
||||
export function OrderDetail({ order, onBack, onEdit, onDelete }: Props) {
|
||||
const reconciled = order.reconcileStatus === 'reconciled';
|
||||
const verified = order.verifyStatus === 'verified';
|
||||
const vehicleTag = getVehicleTag(order.plateNo);
|
||||
const isLingniu = vehicleTag === 'lingniu';
|
||||
const reconciled = isLingniu && order.reconcileStatus === 'reconciled';
|
||||
const verified = isLingniu && order.verifyStatus === 'verified';
|
||||
const locked = isOrderLocked(order);
|
||||
const [plateSrc, setPlateSrc] = useState(order.platePhotoUrl || '');
|
||||
const [panelSrc, setPanelSrc] = useState(order.panelPhotoUrl || '');
|
||||
@@ -87,21 +90,30 @@ export function OrderDetail({ order, onBack, onEdit, onDelete }: Props) {
|
||||
<section className="h5-panel h5-panel--detail" data-annotation-id="h5-detail">
|
||||
<div className="h5-detail-hero">
|
||||
<div>
|
||||
<div className="h5-plate">{formatPlateDisplay(order.plateNo)}</div>
|
||||
<div className="h5-plate-wrap">
|
||||
<div className="h5-plate">{formatPlateDisplay(order.plateNo)}</div>
|
||||
{vehicleTag === 'lingniu' ? (
|
||||
<span className="h5-vehicle-tag h5-vehicle-tag--in">羚牛车辆</span>
|
||||
) : vehicleTag === 'external' ? (
|
||||
<span className="h5-vehicle-tag h5-vehicle-tag--out">非羚牛车辆</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="h5-detail-hero__time tabular-nums">
|
||||
{formatDateTimeMinute(order.hydrogenTime)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="h5-tag-row">
|
||||
<span className={`h5-tag ${verified ? 'h5-tag--done' : 'h5-tag--pending'}`}>
|
||||
{verified ? <IconCheck size={12} /> : <IconClock size={12} />}
|
||||
{verified ? '已核对' : '未核对'}
|
||||
</span>
|
||||
<span className={`h5-tag ${reconciled ? 'h5-tag--done' : 'h5-tag--muted'}`}>
|
||||
{reconciled ? <IconCheck size={12} /> : <IconClock size={12} />}
|
||||
{reconciled ? '已对账' : '未对账'}
|
||||
</span>
|
||||
</div>
|
||||
{isLingniu ? (
|
||||
<div className="h5-tag-row">
|
||||
<span className={`h5-tag ${verified ? 'h5-tag--done' : 'h5-tag--pending'}`}>
|
||||
{verified ? <IconCheck size={12} /> : <IconClock size={12} />}
|
||||
{verified ? '已核对' : '未核对'}
|
||||
</span>
|
||||
<span className={`h5-tag ${reconciled ? 'h5-tag--done' : 'h5-tag--muted'}`}>
|
||||
{reconciled ? <IconCheck size={12} /> : <IconClock size={12} />}
|
||||
{reconciled ? '已对账' : '未对账'}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="h5-detail-metrics" aria-label="加氢金额与用量">
|
||||
<div>
|
||||
@@ -152,19 +164,19 @@ export function OrderDetail({ order, onBack, onEdit, onDelete }: Props) {
|
||||
</div>
|
||||
<div className="h5-detail-row">
|
||||
<span>核对状态</span>
|
||||
<strong>{verified ? '已核对' : '未核对'}</strong>
|
||||
<strong>{isLingniu ? (verified ? '已核对' : '未核对') : '—'}</strong>
|
||||
</div>
|
||||
<div className="h5-detail-row">
|
||||
<span>核对时间</span>
|
||||
<strong>{verified ? formatDateTimeMinute(order.verifiedAt) : '—'}</strong>
|
||||
<strong>{isLingniu && verified ? formatDateTimeMinute(order.verifiedAt) : '—'}</strong>
|
||||
</div>
|
||||
<div className="h5-detail-row">
|
||||
<span>对账状态</span>
|
||||
<strong>{reconciled ? '已对账' : '未对账'}</strong>
|
||||
<strong>{isLingniu ? (reconciled ? '已对账' : '未对账') : '—'}</strong>
|
||||
</div>
|
||||
<div className="h5-detail-row">
|
||||
<span>对账时间</span>
|
||||
<strong>{reconciled ? formatDateTimeMinute(order.reconcileTime) : '—'}</strong>
|
||||
<strong>{isLingniu && reconciled ? formatDateTimeMinute(order.reconcileTime) : '—'}</strong>
|
||||
</div>
|
||||
</section>
|
||||
{reconciled ? (
|
||||
|
||||
@@ -31,7 +31,7 @@ import { OrderDetail } from './components/OrderDetail';
|
||||
import { PhoneShell } from './components/PhoneShell';
|
||||
import type { AppView, CreateDraft, StationOption } from './types';
|
||||
import { formatDateTimeMinute, formatKg, formatMoney, nowDateTimeMinute } from './utils/format';
|
||||
import { hasManualLedgerForDate } from './utils/manual-ledger';
|
||||
import { isManualLedgerGateBlocked, yesterdayDateKey } from './utils/manual-ledger';
|
||||
import {
|
||||
findDuplicateStationOrder,
|
||||
getTotalAmountMismatch,
|
||||
@@ -72,6 +72,7 @@ export default function OneosH5H2Order() {
|
||||
const [tick, setTick] = useState(0);
|
||||
const [view, setView] = useState<AppView>({ name: 'list' });
|
||||
const [mainTab, setMainTab] = useState<'orders' | 'manual'>('orders');
|
||||
const [manualFocusDate, setManualFocusDate] = useState<string | undefined>(undefined);
|
||||
const [toast, setToast] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
@@ -90,8 +91,10 @@ export default function OneosH5H2Order() {
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
if (!hasManualLedgerForDate(station.stationId || station.stationName)) {
|
||||
showToast('请先上传今日手工台账');
|
||||
const stationKey = station.stationId || station.stationName;
|
||||
if (isManualLedgerGateBlocked(stationKey)) {
|
||||
showToast('手工台账有缺失,作为对账重要依据,请先上传手工台账');
|
||||
setManualFocusDate(yesterdayDateKey());
|
||||
setMainTab('manual');
|
||||
setView({ name: 'list' });
|
||||
return;
|
||||
@@ -193,7 +196,7 @@ export default function OneosH5H2Order() {
|
||||
function handleDelete(id: string) {
|
||||
const order = orders.find((item) => item.id === id);
|
||||
if (!order || isOrderLocked(order)) return;
|
||||
if (!window.confirm(`确认删除车牌 ${order.plateNo.replace(/F$/u, '')} 的未对账记录?`)) return;
|
||||
if (!window.confirm(`确认删除车牌 ${order.plateNo.replace(/F$/u, '')} 的未锁定记录?`)) return;
|
||||
removeStationOrder(id);
|
||||
setTick((n) => n + 1);
|
||||
setView({ name: 'list' });
|
||||
@@ -267,7 +270,7 @@ export default function OneosH5H2Order() {
|
||||
</PhoneShell>
|
||||
);
|
||||
} else {
|
||||
const todayUploaded = hasManualLedgerForDate(station.stationId || station.stationName);
|
||||
const gateBlocked = isManualLedgerGateBlocked(station.stationId || station.stationName);
|
||||
content = (
|
||||
<PhoneShell
|
||||
title={mainTab === 'manual' ? '手工台账' : '加氢订单'}
|
||||
@@ -276,8 +279,8 @@ export default function OneosH5H2Order() {
|
||||
<>
|
||||
{mainTab === 'orders' ? (
|
||||
<div className="h5-fab-dock" data-annotation-id="h5-fab-dock">
|
||||
{!todayUploaded ? (
|
||||
<p className="h5-fab-dock__hint">今日未上传手工台账,无法新增电子记录</p>
|
||||
{gateBlocked ? (
|
||||
<p className="h5-fab-dock__hint">手工台账有缺失,作为对账重要依据,请先上传手工台账</p>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
@@ -307,7 +310,7 @@ export default function OneosH5H2Order() {
|
||||
>
|
||||
<IconCamera size={18} />
|
||||
手工台账
|
||||
{!todayUploaded ? <span className="h5-tabbar-dot" aria-label="今日未上传" /> : null}
|
||||
{gateBlocked ? <span className="h5-tabbar-dot" aria-label="前一日手工台账未上传" /> : null}
|
||||
</button>
|
||||
</nav>
|
||||
{toast ? (
|
||||
@@ -322,11 +325,29 @@ export default function OneosH5H2Order() {
|
||||
<ManualLedgerPanel
|
||||
station={station}
|
||||
tick={tick}
|
||||
focusDate={manualFocusDate}
|
||||
showToast={showToast}
|
||||
onUploaded={() => setTick((n) => n + 1)}
|
||||
onUploaded={() => {
|
||||
setTick((n) => n + 1);
|
||||
setManualFocusDate(undefined);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{gateBlocked ? (
|
||||
<button
|
||||
type="button"
|
||||
className="h5-banner-warn"
|
||||
data-annotation-id="h5-ledger-gate-banner"
|
||||
onClick={() => {
|
||||
setManualFocusDate(yesterdayDateKey());
|
||||
setMainTab('manual');
|
||||
}}
|
||||
>
|
||||
手工台账有缺失,作为对账重要依据,请先上传手工台账
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
<div className="h5-hero" data-annotation-id="h5-hero" aria-label="当前站点">
|
||||
<div className="h5-hero-label">
|
||||
<IconMapPin size={13} />
|
||||
|
||||
@@ -35,6 +35,10 @@ const mdDetail = readMd('feature-detail.md');
|
||||
const mdManual = readMd('feature-manual-ledger.md');
|
||||
const mdFleet = readMd('fleet-plate-tag.md');
|
||||
const mdReconcile = readMd('reconcile-linkage.md');
|
||||
const mdRoles = readMd('roles.md');
|
||||
const mdStories = readMd('user-stories.md');
|
||||
const mdLogic = readMd('key-logic.md');
|
||||
const mdFlows = readMd('flows.md');
|
||||
|
||||
const source = {
|
||||
documentVersion: 1,
|
||||
@@ -267,7 +271,7 @@ const source = {
|
||||
'## 识别原图(含水印)\n\n详情展示车牌识别、加氢机面板的原始照片;照片在拍摄/选图时已叠加时间与地点水印并随记录保存。种子数据无实拍时用演示水印图。\n\n详见 [详情编辑删除](feature-detail)。',
|
||||
'h5-manual': mdManual,
|
||||
'h5-manual-calendar':
|
||||
'## 台账日历\n\n自本站首笔加氢记录日起:绿=已上传,橙=未上传;首笔之前无需补传。禁选未来日。点选范围内未传日可补传。仅「今日」是否上传影响新增加氢记录。\n\n详见 [手工台账上传](feature-manual-ledger)。',
|
||||
'## 台账日历\n\n自本站首笔加氢记录日起:绿=已上传,橙=未上传;首笔之前无需补传。禁选未来日。点选范围内未传日可补传。**前一日**未上传时禁止新增加氢记录(今日台账状态不作为门禁)。\n\n详见 [手工台账上传](feature-manual-ledger)。',
|
||||
},
|
||||
assetMap: {},
|
||||
directory: {
|
||||
@@ -285,6 +289,87 @@ const source = {
|
||||
markdown: mdPrd,
|
||||
markdownPath: '.spec/requirements-prd.md',
|
||||
},
|
||||
{
|
||||
type: 'folder',
|
||||
id: 'h5-doc-roles',
|
||||
title: '需求角色',
|
||||
defaultExpanded: true,
|
||||
children: [
|
||||
{
|
||||
type: 'markdown',
|
||||
id: 'h5-doc-roles-md',
|
||||
title: '角色说明',
|
||||
markdown: mdRoles,
|
||||
markdownPath: '.spec/roles.md',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'folder',
|
||||
id: 'h5-doc-stories',
|
||||
title: '用户故事',
|
||||
defaultExpanded: true,
|
||||
children: [
|
||||
{
|
||||
type: 'markdown',
|
||||
id: 'h5-doc-stories-md',
|
||||
title: '用户故事',
|
||||
markdown: mdStories,
|
||||
markdownPath: '.spec/user-stories.md',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'folder',
|
||||
id: 'h5-doc-logic',
|
||||
title: '关键逻辑',
|
||||
defaultExpanded: true,
|
||||
children: [
|
||||
{
|
||||
type: 'markdown',
|
||||
id: 'h5-doc-logic-index',
|
||||
title: '逻辑索引',
|
||||
markdown: mdLogic,
|
||||
markdownPath: '.spec/key-logic.md',
|
||||
},
|
||||
{
|
||||
type: 'markdown',
|
||||
id: 'h5-doc-logic-manual',
|
||||
title: '手工台账门禁',
|
||||
markdown: mdManual,
|
||||
markdownPath: '.spec/feature-manual-ledger.md',
|
||||
},
|
||||
{
|
||||
type: 'markdown',
|
||||
id: 'h5-doc-logic-reconcile',
|
||||
title: '核对与对账联动',
|
||||
markdown: mdReconcile,
|
||||
markdownPath: '.spec/reconcile-linkage.md',
|
||||
},
|
||||
{
|
||||
type: 'markdown',
|
||||
id: 'h5-doc-logic-fleet',
|
||||
title: '羚牛车辆标签',
|
||||
markdown: mdFleet,
|
||||
markdownPath: '.spec/fleet-plate-tag.md',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'folder',
|
||||
id: 'h5-doc-flows',
|
||||
title: '流程图',
|
||||
defaultExpanded: true,
|
||||
children: [
|
||||
{
|
||||
type: 'markdown',
|
||||
id: 'h5-doc-flows-md',
|
||||
title: '操作流程图',
|
||||
markdown: mdFlows,
|
||||
markdownPath: '.spec/flows.md',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'folder',
|
||||
id: 'h5-doc-features',
|
||||
@@ -339,7 +424,7 @@ const source = {
|
||||
type: 'folder',
|
||||
id: 'h5-doc-pages',
|
||||
title: '按页面',
|
||||
defaultExpanded: true,
|
||||
defaultExpanded: false,
|
||||
children: [
|
||||
{
|
||||
type: 'folder',
|
||||
|
||||
@@ -373,6 +373,13 @@
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.h5-plate-wrap {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.h5-tag {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
/** 原型本地「系统车辆列表」车牌(不含尾缀 F),用于羚牛/非羚牛判定 */
|
||||
/** 原型本地「系统车辆列表」车牌(不含尾缀 F),用于羚牛/非羚牛判定。
|
||||
* 与 `src/common/h2VehicleLedgerBridge.js` 的 H2_FLEET_PLATE_KEYS 保持同步。
|
||||
*/
|
||||
const FLEET_PLATES = new Set(
|
||||
[
|
||||
'浙A55666',
|
||||
@@ -21,10 +23,21 @@ const FLEET_PLATES = new Set(
|
||||
'浙F06618',
|
||||
'粤AGR8556',
|
||||
'粤AGP5156',
|
||||
'浙AD12345',
|
||||
'浙AH55660',
|
||||
'浙BK33210',
|
||||
'粤BK33210',
|
||||
'沪AD12345',
|
||||
'苏EF99887',
|
||||
'京CN88771',
|
||||
'川AL55602',
|
||||
].map((p) => p.toUpperCase()),
|
||||
);
|
||||
|
||||
export function normalizePlateKey(plateNo: string): string {
|
||||
if (typeof window !== 'undefined' && window.H2VehicleLedgerBridge?.normalizePlateKey) {
|
||||
return window.H2VehicleLedgerBridge.normalizePlateKey(plateNo);
|
||||
}
|
||||
return String(plateNo || '')
|
||||
.trim()
|
||||
.toUpperCase()
|
||||
@@ -33,6 +46,9 @@ export function normalizePlateKey(plateNo: string): string {
|
||||
|
||||
/** 车牌是否在系统车辆列表中 → 羚牛车辆 */
|
||||
export function isLingniuVehicle(plateNo: string): boolean {
|
||||
if (typeof window !== 'undefined' && window.H2VehicleLedgerBridge?.isLingniuVehicle) {
|
||||
return window.H2VehicleLedgerBridge.isLingniuVehicle(plateNo);
|
||||
}
|
||||
const key = normalizePlateKey(plateNo);
|
||||
if (!key) return false;
|
||||
return FLEET_PLATES.has(key);
|
||||
|
||||
@@ -70,6 +70,32 @@ export function todayDateKey(): string {
|
||||
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
|
||||
}
|
||||
|
||||
export function yesterdayDateKey(): string {
|
||||
const b = bridge();
|
||||
if (b?.yesterdayDateKey) return b.yesterdayDateKey();
|
||||
const d = new Date();
|
||||
d.setDate(d.getDate() - 1);
|
||||
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 前一日手工台账未上传 → 今日不可新增加氢记录。
|
||||
* 若昨日早于本站首笔加氢日(或无首笔),不要求昨日台账。
|
||||
*/
|
||||
export function isManualLedgerGateBlocked(stationIdOrName: string): boolean {
|
||||
const b = bridge();
|
||||
if (b?.isManualLedgerGateBlocked) {
|
||||
return Boolean(b.isManualLedgerGateBlocked(stationIdOrName));
|
||||
}
|
||||
const station = String(stationIdOrName || '').trim();
|
||||
if (!station) return false;
|
||||
const todayKey = todayDateKey();
|
||||
const yKey = yesterdayDateKey();
|
||||
const firstKey = getFirstHydrogenDateKey(station);
|
||||
if (!cellNeedsLedger(yKey, todayKey, firstKey)) return false;
|
||||
return !hasManualLedgerForDate(station, yKey);
|
||||
}
|
||||
|
||||
export function hasManualLedgerForDate(
|
||||
stationIdOrName: string,
|
||||
ledgerDate?: string,
|
||||
@@ -191,6 +217,8 @@ declare global {
|
||||
interface Window {
|
||||
H2VehicleLedgerBridge?: {
|
||||
todayDateKey?: () => string;
|
||||
yesterdayDateKey?: () => string;
|
||||
isManualLedgerGateBlocked?: (stationIdOrName: string) => boolean;
|
||||
getRows?: () => Array<{ stationId?: string; stationName?: string; hydrogenTime?: string }>;
|
||||
hasManualLedgerForDate?: (stationIdOrName: string, ledgerDate?: string) => boolean;
|
||||
getManualLedger?: (stationIdOrName: string, ledgerDate?: string) => ManualLedgerEntry | null;
|
||||
|
||||
@@ -18,6 +18,8 @@ declare global {
|
||||
totalAmount: number,
|
||||
toleranceYuan?: number,
|
||||
) => { expected: number; actual: number; diff: number } | null;
|
||||
isLingniuVehicle?: (plateNo: string) => boolean;
|
||||
normalizePlateKey?: (plateNo: string) => string;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -85,6 +87,12 @@ export function mapRowToOrder(row: Record<string, unknown>): H5OrderRecord {
|
||||
}
|
||||
|
||||
export function isOrderLocked(order: H5OrderRecord): boolean {
|
||||
/* 非羚牛车辆不参与核对/对账业务属性,不以核对对账状态锁定 */
|
||||
const bridge = typeof window !== 'undefined' ? window.H2VehicleLedgerBridge : undefined;
|
||||
const isLingniu = bridge?.isLingniuVehicle
|
||||
? bridge.isLingniuVehicle(order.plateNo)
|
||||
: true;
|
||||
if (!isLingniu) return false;
|
||||
return order.verifyStatus === 'verified' || order.reconcileStatus === 'reconciled';
|
||||
}
|
||||
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
"title": "OneOS",
|
||||
"children": [
|
||||
{
|
||||
"id": "item:prototypes:oneos-web-workbench",
|
||||
"id": "item:prototypes:oneos-web-workbench-new",
|
||||
"kind": "item",
|
||||
"title": "工作台",
|
||||
"itemKey": "prototypes/oneos-web-workbench"
|
||||
"itemKey": "prototypes/oneos-web-workbench-new"
|
||||
},
|
||||
{
|
||||
"id": "folder-oneos-approval",
|
||||
@@ -78,6 +78,12 @@
|
||||
"title": "租赁合同",
|
||||
"itemKey": "prototypes/lease-contract-management"
|
||||
},
|
||||
{
|
||||
"id": "item:prototypes:self-operated-contract",
|
||||
"kind": "item",
|
||||
"title": "自营合同",
|
||||
"itemKey": "prototypes/self-operated-contract"
|
||||
},
|
||||
{
|
||||
"id": "item:prototypes:insurance-procurement",
|
||||
"kind": "item",
|
||||
@@ -221,6 +227,12 @@
|
||||
"kind": "item",
|
||||
"title": "任务工单",
|
||||
"itemKey": "prototypes/task-work-order"
|
||||
},
|
||||
{
|
||||
"id": "item:prototypes:self-operated-dispatch-task",
|
||||
"kind": "item",
|
||||
"title": "调度任务",
|
||||
"itemKey": "prototypes/self-operated-dispatch-task"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -228,14 +240,7 @@
|
||||
"id": "folder-1784341576489-spa7ms",
|
||||
"kind": "folder",
|
||||
"title": "审批中心",
|
||||
"children": [
|
||||
{
|
||||
"id": "item:prototypes:oneos-web-approval",
|
||||
"kind": "item",
|
||||
"title": "审批中心",
|
||||
"itemKey": "prototypes/oneos-web-approval"
|
||||
}
|
||||
]
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "item:prototypes:lease-business-line-overview",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* @name 审批中心
|
||||
*/
|
||||
import { ApprovalApp } from '../../common/oneos-web-approval/ApprovalApp';
|
||||
|
||||
export default function OneosWebApproval() {
|
||||
return <ApprovalApp pageTitle="审批中心" showTabs />;
|
||||
}
|
||||
@@ -37,8 +37,8 @@
|
||||
|
||||
1. 选择**账单开始日期**与**账单结束日期**后,从**车辆氢费明细**获取该时间段内、对应当前站点、且为**已核对且尚未结算(未对账)**的加氢记录。
|
||||
2. 点击「**生成对账单**」时弹出**二次确认**,提示:生成并提交后,车辆氢费明细中对应记录将标记为**已对账**且无法修改。
|
||||
3. 确认后进入结算阶段,填写**收票日期**、**收票金额**及**发票附件**(对账日期在提交时由系统自动回写)。
|
||||
4. 点击「**提交对账单**」后,本批明细在车辆氢费明细中锁定为**已对账**并绑定本对账单,不可再进入后续对账单。
|
||||
3. 确认后进入结算阶段,填写**收票日期**、**收票金额**、**发票附件**及**对账单盖章附件**(对账日期在提交时由系统自动回写)。
|
||||
4. 点击「**提交对账单**」并二次确认后,本批明细在**加氢记录(Web)、加氢订单(H5)、车辆氢费明细**三端同步锁定为**已对账**并绑定本对账单,不可再进入后续对账单。
|
||||
|
||||
**阶段一 · 选期生成**
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
| 收票日期 | 必填 |
|
||||
| 收票金额 | 必填,默认填入成本总金额 |
|
||||
| 发票附件 | 必填,支持上传 |
|
||||
| 对账单盖章附件 | **必填**;支持盖章 PDF、图片(`accept=.pdf,image/*`) |
|
||||
|
||||
---
|
||||
|
||||
@@ -72,19 +73,18 @@
|
||||
|
||||
提交成功后需**原子性**完成:
|
||||
|
||||
1. 写入对账历史(供「查看对账记录」查询)
|
||||
1. 写入对账历史(供「查看对账记录」查询;含发票附件与盖章附件)
|
||||
2. 扣减站点预付余额
|
||||
3. 标记台账记录已结算(绑定 `statementRecordId`)
|
||||
4. **回写车辆氢费明细**:
|
||||
4. **经共享 Bridge 回写三端**(同浏览器会话):
|
||||
|
||||
| 车辆氢费明细字段 | 回写值 |
|
||||
| 模块 | 回写结果 |
|
||||
|---|---|
|
||||
| 对账状态 | **已对账**(`reconcileStatus = reconciled`) |
|
||||
| 对账日期 | 操作完成时间 |
|
||||
| 收票日期 | 操作完成日期 |
|
||||
| 加氢站付款状态 | 已付款 |
|
||||
| 车辆氢费明细 | 对账状态 → **已对账**;对账日期 / 收票日期 / 付款状态 / 对账人 |
|
||||
| 加氢记录(Web) | 对账状态 → **已对账**(订阅 Bridge) |
|
||||
| 加氢订单(H5) | 对账状态 → **已对账**(订阅 Bridge) |
|
||||
|
||||
原型通过 `H2_STATION_STATEMENT_LEDGER_UPDATES` / `h2VehicleLedgerBridge` 模拟跨页回写。
|
||||
原型通过 `H2_STATION_STATEMENT_LEDGER_UPDATES` / `h2VehicleLedgerBridge.applyPatches` 模拟跨页回写(**未接真实 API**)。
|
||||
|
||||
> 「完成核对」只改变核对状态,**不会**把记录写成已对账;已对账仅由本页对账单提交产生。
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
|
||||
历史列表:对账日期、对账人、账单区间、加氢次数、加氢金额、结算后余额、查看明细。
|
||||
|
||||
查看明细:账单区间、统计三卡、收票信息、发票附件、成本明细表。
|
||||
查看明细:账单区间、统计三卡、收票信息、发票附件、对账单盖章附件、成本明细表。
|
||||
|
||||
---
|
||||
|
||||
@@ -108,8 +108,10 @@
|
||||
| 区间内无「已核对且未对账未结算」记录 | 阻止生成,提示调整日期(或先完成核对) |
|
||||
| 区间内存在未核对记录 | **不纳入**对账单;不单独弹窗枚举(静默排除) |
|
||||
| 点击「生成对账单」未确认 | 不进入结算阶段 |
|
||||
| 收票日期/金额/附件未填 | 阻止提交 |
|
||||
| 收票日期/金额/发票附件/盖章附件未填 | 阻止提交 |
|
||||
| 盖章附件非 PDF/图片 | 拒绝该文件并提示 |
|
||||
| 收票金额 ≤ 0 | 阻止提交 |
|
||||
| 点击「提交对账单」未二次确认 | 不执行回写 |
|
||||
| 预付余额结算后为负 | 允许提交,标「已欠费」 |
|
||||
|
||||
---
|
||||
|
||||
@@ -145,8 +145,8 @@
|
||||
| 步骤 | 说明 |
|
||||
|---|---|
|
||||
| 1. 拉取明细 | 按账期从车辆氢费明细取 **已核对** ∧ **未对账** ∧ 未结算 ∧ 本站 的记录;**未核对不纳入** |
|
||||
| 2. 两阶段 | 选期生成 → 填收票提交 |
|
||||
| 3. 提交回写 | 更新预付余额、写对账历史;台账变为 **已对账**(对账日期、收票日期、已付款) |
|
||||
| 2. 两阶段 | 选期生成 → 填收票 / 发票附件 / **对账单盖章附件** → 二次确认提交 |
|
||||
| 3. 提交回写 | 更新预付余额、写对账历史;**加氢记录 Web / H5 / 车辆氢费明细**三端同步为 **已对账** |
|
||||
|
||||
> **核对 ≠ 对账**:核对由车辆氢费明细「完成核对」产生;对账仅由本页「提交对账单」回写。
|
||||
|
||||
@@ -175,8 +175,8 @@
|
||||
### 8.2 对账流程
|
||||
|
||||
- [ ] 仅 **已核对** 且未对账、未结算的记录可进入对账单;未核对明细即使在账期内也不出现
|
||||
- [ ] 两阶段交互与必填校验完整
|
||||
- [ ] 提交后回写车辆氢费明细为 **已对账**,可跨页验收
|
||||
- [ ] 两阶段交互与必填校验完整(含对账单盖章附件:PDF/图片)
|
||||
- [ ] 提交二次确认后,加氢记录(Web)、加氢订单(H5)、车辆氢费明细三端同步为 **已对账**
|
||||
|
||||
### 8.3 期末余额设置
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
| 规则 | 代码 |
|
||||
|---|---|
|
||||
| 筛选入口 | `h2GetAvailableStatementLedgerRows(store, stationName, startDate, endDate)` |
|
||||
| Store 映射 | `h2BridgeMapToStationLedgerRow` **必须透出** `verifyStatus`(否则站点侧会全部当成未核对) |
|
||||
| 已核对 | `verifyStatus === 'verified'`(缺省按 `unverified`) |
|
||||
| 未对账 | `reconcileStatus === 'pending'`(且 ≠ `reconciled`) |
|
||||
| 账期 | `h2ParseDateTimeMs(hydrogenTime)` 落在起止日闭区间 |
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -2329,16 +2329,27 @@ function h2ResolveUploadPreview(file, variant) {
|
||||
};
|
||||
}
|
||||
|
||||
/** 盖章附件:仅允许 PDF / 常见图片 */
|
||||
function h2IsSealAttachmentFile(file) {
|
||||
var name = String((file && (file.name || file.fileName)) || '').toLowerCase();
|
||||
var type = String((file && file.type) || '').toLowerCase();
|
||||
if (type.indexOf('pdf') >= 0 || type.indexOf('image/') === 0) return true;
|
||||
return /\.(pdf|png|jpe?g|gif|webp|bmp)$/i.test(name);
|
||||
}
|
||||
|
||||
/** 合同附件:按钮上传 + 预览/下载 */
|
||||
function ContractFilesUpload(props) {
|
||||
var antd = window.antd;
|
||||
var Upload = antd.Upload;
|
||||
var Button = antd.Button;
|
||||
var messageApi = antd.message;
|
||||
var fileList = props.fileList || [];
|
||||
var disabled = props.disabled;
|
||||
var showHint = props.showHint !== false;
|
||||
var wrapClass = 'h2-contract-upload' + (props.wrapClassName ? ' ' + props.wrapClassName : '');
|
||||
var uploadClass = props.uploadClassName || 'h2-create-upload-btn';
|
||||
var accept = props.accept || undefined;
|
||||
var validateFile = props.validateFile;
|
||||
|
||||
return React.createElement('div', { className: wrapClass },
|
||||
React.createElement('div', { className: 'h2-contract-upload-actions' },
|
||||
@@ -2347,8 +2358,24 @@ function ContractFilesUpload(props) {
|
||||
multiple: true,
|
||||
disabled: disabled,
|
||||
fileList: fileList,
|
||||
beforeUpload: function () { return false; },
|
||||
onChange: props.onChange,
|
||||
accept: accept,
|
||||
beforeUpload: function (file) {
|
||||
if (typeof validateFile === 'function' && !validateFile(file)) {
|
||||
if (messageApi && messageApi.warning) {
|
||||
messageApi.warning(props.rejectMessage || '文件格式不支持');
|
||||
}
|
||||
return Upload.LIST_IGNORE || false;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
onChange: function (info) {
|
||||
if (!props.onChange) return;
|
||||
var nextList = info.fileList || [];
|
||||
if (typeof validateFile === 'function') {
|
||||
nextList = nextList.filter(function (f) { return validateFile(f); });
|
||||
}
|
||||
props.onChange({ fileList: nextList });
|
||||
},
|
||||
showUploadList: false
|
||||
},
|
||||
React.createElement(Button, {
|
||||
@@ -2359,7 +2386,7 @@ function ContractFilesUpload(props) {
|
||||
style: props.buttonStyle
|
||||
}, props.uploadLabel || '上传附件')
|
||||
),
|
||||
showHint ? React.createElement('span', { style: { fontSize: 12, color: '#94a3b8' } }, '点击上传,可多选') : null
|
||||
showHint ? React.createElement('span', { style: { fontSize: 12, color: '#94a3b8' } }, props.hintText || '点击上传,可多选') : null
|
||||
),
|
||||
fileList.length
|
||||
? React.createElement('div', { className: 'h2-contract-file-list' },
|
||||
@@ -5992,7 +6019,8 @@ const Component = function () {
|
||||
balanceAfterSettlement: 0,
|
||||
receiptDate: '',
|
||||
receiptAmount: '',
|
||||
invoiceFiles: []
|
||||
invoiceFiles: [],
|
||||
sealFiles: []
|
||||
});
|
||||
var statementModal = statementModalState[0];
|
||||
var setStatementModal = statementModalState[1];
|
||||
@@ -6753,13 +6781,14 @@ const Component = function () {
|
||||
open: true,
|
||||
record: record,
|
||||
startDate: defaultStart,
|
||||
endDate: '2026-05-31',
|
||||
endDate: '2026-07-31',
|
||||
phase: 'select',
|
||||
draftRows: [],
|
||||
balanceAfterSettlement: h2NumOrZero(record.prepaidBalance),
|
||||
receiptDate: '',
|
||||
receiptAmount: '',
|
||||
invoiceFiles: []
|
||||
invoiceFiles: [],
|
||||
sealFiles: []
|
||||
});
|
||||
}, [statementRecords]);
|
||||
|
||||
@@ -6774,7 +6803,8 @@ const Component = function () {
|
||||
balanceAfterSettlement: 0,
|
||||
receiptDate: '',
|
||||
receiptAmount: '',
|
||||
invoiceFiles: []
|
||||
invoiceFiles: [],
|
||||
sealFiles: []
|
||||
});
|
||||
}, []);
|
||||
|
||||
@@ -6829,7 +6859,8 @@ const Component = function () {
|
||||
balanceAfterSettlement: balanceAfter,
|
||||
receiptAmount: summary.totalCost ? h2FormatYuanNum(summary.totalCost) : '',
|
||||
receiptDate: '',
|
||||
invoiceFiles: []
|
||||
invoiceFiles: [],
|
||||
sealFiles: []
|
||||
});
|
||||
});
|
||||
message.success('已生成对账记录,共 ' + summary.count + ' 笔,请填写结算信息并提交');
|
||||
@@ -6857,71 +6888,88 @@ const Component = function () {
|
||||
message.warning('请上传发票附件');
|
||||
return;
|
||||
}
|
||||
var summary = h2CalcStatementSummary(rows);
|
||||
var operateTime = h2OperateTimestamp();
|
||||
var operateDate = h2FormatOperateDateOnly();
|
||||
var statementId = 'stmt-' + Date.now();
|
||||
var recordIds = rows.map(function (r) { return r.id; });
|
||||
var stationRecord = statementModal.record;
|
||||
var balanceAfter = h2NumOrZero(statementModal.balanceAfterSettlement);
|
||||
var newStatement = {
|
||||
id: statementId,
|
||||
stationId: stationRecord.id,
|
||||
stationName: stationRecord.name,
|
||||
reconcileDate: operateTime,
|
||||
reconciler: H2_CURRENT_OPERATOR,
|
||||
startDate: statementModal.startDate,
|
||||
endDate: statementModal.endDate,
|
||||
refuelCount: summary.count,
|
||||
totalKg: summary.totalKg,
|
||||
totalCost: summary.totalCost,
|
||||
balanceAfterSettlement: balanceAfter,
|
||||
receiptDate: statementModal.receiptDate,
|
||||
receiptAmount: receiptAmountNum,
|
||||
invoiceFiles: (statementModal.invoiceFiles || []).slice(),
|
||||
recordIds: recordIds
|
||||
};
|
||||
var patches = [];
|
||||
setLedgerStore(function (prev) {
|
||||
return prev.map(function (r) {
|
||||
if (recordIds.indexOf(r.id) < 0) return r;
|
||||
patches.push({
|
||||
ledgerId: r.id,
|
||||
orderNo: r.orderNo,
|
||||
stationName: r.stationName,
|
||||
plateNo: r.plateNo,
|
||||
hydrogenTime: r.hydrogenTime,
|
||||
reconcileDate: operateTime,
|
||||
receiptDate: operateDate,
|
||||
paymentStatus: H2_PAYMENT_STATUS_PAID,
|
||||
statementRecordId: statementId,
|
||||
reconcileStatus: H2_RECONCILE_RECONCILED
|
||||
});
|
||||
return Object.assign({}, r, {
|
||||
statementRecordId: statementId,
|
||||
reconcileStatus: H2_RECONCILE_RECONCILED,
|
||||
reconcileDate: operateTime,
|
||||
receiptDate: operateDate,
|
||||
paymentStatus: H2_PAYMENT_STATUS_PAID
|
||||
});
|
||||
});
|
||||
});
|
||||
setStatementRecords(function (prev) { return [newStatement].concat(prev); });
|
||||
setListData(function (prev) {
|
||||
return prev.map(function (item) {
|
||||
if (item.id !== stationRecord.id) return item;
|
||||
return Object.assign({}, item, {
|
||||
prepaidBalance: balanceAfter,
|
||||
updateTime: operateTime
|
||||
});
|
||||
});
|
||||
});
|
||||
h2PushStatementLedgerPatches(patches);
|
||||
if (typeof window !== 'undefined' && window.H2_VEHICLE_LEDGER_API && typeof window.H2_VEHICLE_LEDGER_API.applyPatches === 'function') {
|
||||
window.H2_VEHICLE_LEDGER_API.applyPatches(patches);
|
||||
if (!statementModal.sealFiles || !statementModal.sealFiles.length) {
|
||||
message.warning('请上传对账单盖章附件');
|
||||
return;
|
||||
}
|
||||
message.success('对账单已提交,共结算 ' + summary.count + ' 笔加氢记录');
|
||||
closeStatementModal();
|
||||
var summary = h2CalcStatementSummary(rows);
|
||||
Modal.confirm({
|
||||
title: '确认提交对账单?',
|
||||
content: '提交后,本批 ' + summary.count + ' 笔记录将在加氢记录、加氢订单(H5)、车辆氢费明细三端同步为「已对账」,且不可再修改,是否确认?',
|
||||
okText: '确认提交',
|
||||
cancelText: '取消',
|
||||
centered: true,
|
||||
onOk: function () {
|
||||
var operateTime = h2OperateTimestamp();
|
||||
var operateDate = h2FormatOperateDateOnly();
|
||||
var statementId = 'stmt-' + Date.now();
|
||||
var recordIds = rows.map(function (r) { return r.id; });
|
||||
var stationRecord = statementModal.record;
|
||||
var balanceAfter = h2NumOrZero(statementModal.balanceAfterSettlement);
|
||||
var newStatement = {
|
||||
id: statementId,
|
||||
stationId: stationRecord.id,
|
||||
stationName: stationRecord.name,
|
||||
reconcileDate: operateTime,
|
||||
reconciler: H2_CURRENT_OPERATOR,
|
||||
startDate: statementModal.startDate,
|
||||
endDate: statementModal.endDate,
|
||||
refuelCount: summary.count,
|
||||
totalKg: summary.totalKg,
|
||||
totalCost: summary.totalCost,
|
||||
balanceAfterSettlement: balanceAfter,
|
||||
receiptDate: statementModal.receiptDate,
|
||||
receiptAmount: receiptAmountNum,
|
||||
invoiceFiles: (statementModal.invoiceFiles || []).slice(),
|
||||
sealFiles: (statementModal.sealFiles || []).slice(),
|
||||
recordIds: recordIds
|
||||
};
|
||||
var patches = [];
|
||||
setLedgerStore(function (prev) {
|
||||
return prev.map(function (r) {
|
||||
if (recordIds.indexOf(r.id) < 0) return r;
|
||||
patches.push({
|
||||
ledgerId: r.id,
|
||||
orderNo: r.orderNo,
|
||||
stationName: r.stationName,
|
||||
plateNo: r.plateNo,
|
||||
hydrogenTime: r.hydrogenTime,
|
||||
reconcileDate: operateTime,
|
||||
receiptDate: operateDate,
|
||||
paymentStatus: H2_PAYMENT_STATUS_PAID,
|
||||
statementRecordId: statementId,
|
||||
reconcileStatus: H2_RECONCILE_RECONCILED,
|
||||
reconciler: H2_CURRENT_OPERATOR,
|
||||
reconciledByName: H2_CURRENT_OPERATOR
|
||||
});
|
||||
return Object.assign({}, r, {
|
||||
statementRecordId: statementId,
|
||||
reconcileStatus: H2_RECONCILE_RECONCILED,
|
||||
reconcileDate: operateTime,
|
||||
receiptDate: operateDate,
|
||||
paymentStatus: H2_PAYMENT_STATUS_PAID,
|
||||
reconciledByName: H2_CURRENT_OPERATOR
|
||||
});
|
||||
});
|
||||
});
|
||||
setStatementRecords(function (prev) { return [newStatement].concat(prev); });
|
||||
setListData(function (prev) {
|
||||
return prev.map(function (item) {
|
||||
if (item.id !== stationRecord.id) return item;
|
||||
return Object.assign({}, item, {
|
||||
prepaidBalance: balanceAfter,
|
||||
updateTime: operateTime
|
||||
});
|
||||
});
|
||||
});
|
||||
h2PushStatementLedgerPatches(patches);
|
||||
if (typeof window !== 'undefined' && window.H2_VEHICLE_LEDGER_API && typeof window.H2_VEHICLE_LEDGER_API.applyPatches === 'function') {
|
||||
window.H2_VEHICLE_LEDGER_API.applyPatches(patches);
|
||||
}
|
||||
message.success('对账单已提交,共结算 ' + summary.count + ' 笔;三端已同步为「已对账」');
|
||||
closeStatementModal();
|
||||
}
|
||||
});
|
||||
}, [statementModal, closeStatementModal]);
|
||||
|
||||
var openStatementHistoryModal = useCallback(function (record) {
|
||||
@@ -8458,9 +8506,9 @@ const Component = function () {
|
||||
React.createElement('strong', null, lastEndDate || '暂无')
|
||||
),
|
||||
!isDraft ? React.createElement('div', { className: 'h2-statement-form-hint' },
|
||||
'数据来源:',
|
||||
'数据来源:仅计入',
|
||||
React.createElement('strong', null, '车辆氢费明细'),
|
||||
' 中标记为「已核对」且尚未对账结算的加氢记录;未核对记录不会进入对账单;明细仅展示成本价与成本总价。'
|
||||
' 中所有「已核对」且尚未对账的加氢记录;未核对记录不会进入对账单。提交后,加氢记录(Web)、加氢订单(H5)、车辆氢费明细三端同步为「已对账」。明细仅展示成本价与成本总价。'
|
||||
) : null
|
||||
),
|
||||
isDraft ? React.createElement('div', { className: 'h2-statement-stats', role: 'group', 'aria-label': '对账统计' },
|
||||
@@ -8556,6 +8604,21 @@ const Component = function () {
|
||||
onChange: function (info) {
|
||||
setStatementModal(function (m) { return Object.assign({}, m, { invoiceFiles: info.fileList || [] }); });
|
||||
}
|
||||
})),
|
||||
formItem('对账单盖章附件', true, React.createElement(ContractFilesUpload, {
|
||||
fileList: statementModal.sealFiles || [],
|
||||
showHint: true,
|
||||
hintText: '支持盖章 PDF、图片(可多选)',
|
||||
accept: '.pdf,image/*',
|
||||
validateFile: h2IsSealAttachmentFile,
|
||||
rejectMessage: '盖章附件仅支持 PDF 或图片文件',
|
||||
uploadLabel: '上传盖章附件',
|
||||
wrapClassName: 'h2-statement-seal-upload',
|
||||
uploadClassName: 'h2-statement-upload-btn-wrap',
|
||||
buttonClassName: 'h2-statement-upload-btn',
|
||||
onChange: function (info) {
|
||||
setStatementModal(function (m) { return Object.assign({}, m, { sealFiles: info.fileList || [] }); });
|
||||
}
|
||||
}))
|
||||
) : null
|
||||
);
|
||||
@@ -8635,6 +8698,25 @@ const Component = function () {
|
||||
if (!stmt) return null;
|
||||
var summary = h2CalcStatementSummary(rows);
|
||||
var invoiceFiles = stmt.invoiceFiles || [];
|
||||
var sealFiles = stmt.sealFiles || [];
|
||||
var renderStmtFileList = function (files, emptyText) {
|
||||
if (!files.length) {
|
||||
return React.createElement('div', { style: { fontSize: 13, color: '#94a3b8', marginTop: 8 } }, emptyText);
|
||||
}
|
||||
return React.createElement('div', { className: 'h2-statement-detail-receipt-file-list' },
|
||||
files.map(function (file) {
|
||||
return React.createElement('div', { key: file.uid || file.name, className: 'h2-statement-detail-receipt-file-item' },
|
||||
React.createElement('span', { className: 'h2-statement-detail-receipt-file-name', title: file.name }, file.name || '未命名附件'),
|
||||
React.createElement(Button, {
|
||||
type: 'link',
|
||||
size: 'small',
|
||||
icon: H2_ICONS.download,
|
||||
onClick: function () { h2DownloadUploadFile(file); }
|
||||
}, '下载')
|
||||
);
|
||||
})
|
||||
);
|
||||
};
|
||||
return React.createElement('div', { className: 'h2-statement-panel' },
|
||||
React.createElement('div', { className: 'h2-statement-station-card' },
|
||||
React.createElement('div', { className: 'h2-statement-station-card__name' }, stmt.stationName || '—'),
|
||||
@@ -8672,21 +8754,11 @@ const Component = function () {
|
||||
),
|
||||
React.createElement('div', { className: 'h2-statement-detail-receipt-files' },
|
||||
React.createElement('div', { className: 'h2-statement-detail-receipt-item__label' }, '发票附件'),
|
||||
invoiceFiles.length
|
||||
? React.createElement('div', { className: 'h2-statement-detail-receipt-file-list' },
|
||||
invoiceFiles.map(function (file) {
|
||||
return React.createElement('div', { key: file.uid || file.name, className: 'h2-statement-detail-receipt-file-item' },
|
||||
React.createElement('span', { className: 'h2-statement-detail-receipt-file-name', title: file.name }, file.name || '未命名附件'),
|
||||
React.createElement(Button, {
|
||||
type: 'link',
|
||||
size: 'small',
|
||||
icon: H2_ICONS.download,
|
||||
onClick: function () { h2DownloadUploadFile(file); }
|
||||
}, '下载')
|
||||
);
|
||||
})
|
||||
)
|
||||
: React.createElement('div', { style: { fontSize: 13, color: '#94a3b8', marginTop: 8 } }, '暂无发票附件')
|
||||
renderStmtFileList(invoiceFiles, '暂无发票附件')
|
||||
),
|
||||
React.createElement('div', { className: 'h2-statement-detail-receipt-files', style: { marginTop: 12 } },
|
||||
React.createElement('div', { className: 'h2-statement-detail-receipt-item__label' }, '对账单盖章附件'),
|
||||
renderStmtFileList(sealFiles, '暂无盖章附件')
|
||||
)
|
||||
),
|
||||
React.createElement('div', { className: 'h2-statement-table-wrap' },
|
||||
|
||||
@@ -93,8 +93,8 @@ const NODE_DEFS = [
|
||||
title: '生成对账单',
|
||||
pageId: 'site-info',
|
||||
color: '#7c3aed',
|
||||
aiPrompt: '选期仅拉取已核对未对账氢费明细、二次确认与结算提交。',
|
||||
annotationText: '账期内仅纳入车辆氢费明细「已核对」记录;未核对不进对账单;提交后标为已对账',
|
||||
aiPrompt: '选期仅拉取已核对未对账氢费明细;结算须上传盖章附件;提交二次确认后三端同步已对账。',
|
||||
annotationText: '仅计入车辆氢费明细全部「已核对」且未对账记录;结算阶段必填对账单盖章附件(PDF/图片);提交确认后 Web/H5/台账三端同步「已对账」',
|
||||
locator: {
|
||||
selectors: ['[data-annotation-id="h2-site-statement-form"]', '.h2-statement-form-wrap'],
|
||||
fingerprint: 'form|statement',
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
|---|---|
|
||||
| 共享 Bridge 不可用 | 列表用本地 fallback;保存/删除提示失败 |
|
||||
| 已核对或已对账点编辑/删除 | 对应警告文案 |
|
||||
| PC 独有 | **导出**含核对/对账列;手工台账上传(站点按角色范围;今日双态提示;右侧预览区确认上传) |
|
||||
| PC 独有 | **导出**含车辆来源/数据来源/核对/对账列;手工台账上传(站点按角色范围;**昨日门禁**禁新增;今日双态提示;右侧预览区确认上传) |
|
||||
|
||||
## 4. 数据流
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
- **车辆氢费明细**:车牌号下方标签(站端=绿 / 羚牛=蓝)
|
||||
- **加氢记录 Web**:车牌号下方同口径标签
|
||||
- 导出 CSV 含「来源」列
|
||||
- 导出 CSV 含「数据来源」列(与「车辆来源」分列)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -27,15 +27,17 @@
|
||||
|
||||
| # | 功能 | 说明 |
|
||||
|---|---|---|
|
||||
| 1 | 列表 / 筛选 | 字段与 H5 一致;默认一行(加氢时间/车牌/站名/对账状态);`splitFilterFields` + **更多筛选/收起**(`vm-filter-expand` / `vm-filter-toggle`)对齐物流业务明细;占位符样式见全局设计规范 §4.1.1 |
|
||||
| 2 | 手工台账 | 与 H5 同逻辑:站点按登录角色范围过滤;今日已传/未传双态提示;右侧「手工台账」预览(空态/全屏翻页/已归档角标)+ 确认前二次提示;月历补传与拦截见 [feature-manual-ledger](../oneos-h5-h2-order/.spec/feature-manual-ledger.md) |
|
||||
| 3 | 新增 / 批量 / 编辑 | 字段:时间、站、车牌、**里程(选填)**、单价、量、总额;已对账只读;同站同车牌同时间拦截;总额偏离单价×量超 0.05 元需确认 |
|
||||
| 1 | 列表 / 筛选 / 详情 | 字段与 H5 一致;默认一行(加氢时间/车牌/站名/对账状态);`splitFilterFields` + **更多筛选/收起**;操作列 **详情**(记录字段 + 车牌/面板原图)在「更多」之前,见 [feature-list.md](./feature-list.md) |
|
||||
| 2 | 手工台账 | 与 H5 同逻辑:站点按登录角色范围过滤;今日已传/未传双态提示;**前一日缺失则今日禁新增**(顶栏固定文案,点新增直达台账并选中昨日);右侧「手工台账」预览 + 确认前二次提示;月历补传见 [feature-manual-ledger](../oneos-h5-h2-order/.spec/feature-manual-ledger.md) |
|
||||
| 3 | 新增 / 批量 / 编辑 | 时间精确至秒;单站默认/多站请选择;车牌旁羚牛/外部车辆标签;总额自动只读;支持批量导入(模板+结果页);顶栏无「批量新增加氢记录」标题;详见 [feature-create.md](./feature-create.md) |
|
||||
| 4 | 删除 | 仅未对账;写共享 Store |
|
||||
| 5 | 导出 | 当前筛选结果 CSV(Web 独有) |
|
||||
| 5 | 导出 | 当前筛选结果 CSV;列含**车辆来源**、**数据来源**(与列表标签一致) |
|
||||
|
||||
### 1.2 列表字段
|
||||
|
||||
加氢时间、加氢站名称、车牌号(附来源标签)、里程(km)、氢气单价(元/kg)、加氢量、加氢总额、核对状态、核对时间、核对人、对账状态、对账时间、对账人;操作含 **变更日志**。
|
||||
加氢时间、加氢站名称、车牌号(附**车辆来源**:羚牛车辆/非羚牛车辆 + **数据来源**:站端上传/羚牛上传)、里程(km)、氢气单价(元/kg)、加氢量、加氢总额、核对状态、核对时间、核对人、对账状态、对账时间、对账人。
|
||||
操作列(`OperationActions`):外显 **详情**(弹窗含字段摘要 + 车牌识别原图 / 加氢机面板原图);**更多** 含编辑、变更日志、删除。
|
||||
非羚牛车辆的核对/对账相关列显示为空,详见 [fleet-vehicle-verify.md](./fleet-vehicle-verify.md)。
|
||||
|
||||
### 1.3 工具栏 Tab
|
||||
|
||||
@@ -54,11 +56,14 @@
|
||||
2. 与 H5 读写同一共享 Store;一端增删改后另一端可见(同会话)
|
||||
3. 筛选默认一行;更多筛选/收起对齐物流业务明细;支持核对状态、核对时间、对账状态、对账时间;占位符样式与全局规范一致
|
||||
4. 已核对或已对账不可编辑、删除;**已对账记录对账时间不为空**(对账单回写);核对由车辆氢费明细「完成核对」触发
|
||||
5. 导出列与列表字段一致(含来源、核对人、对账人)
|
||||
5. 导出列与列表字段一致(含车辆来源、数据来源、核对人、对账人)
|
||||
6. 列表左上角两个 Tab:加氢记录 / 手工台账;悬浮与选中样式符合分段 Tab 规范;无预约加氢入口
|
||||
7. 同站同车牌同加氢时间重复保存被拦截;总额与单价×量偏差 > 0.05 元时出现确认
|
||||
8. 来源标签:站端上报→「站端上传」,台账手增→「羚牛上传」;变更日志含时间/操作人/修改项/原值/新值;核对时间后有核对人、对账时间后有对账人
|
||||
8. 数据来源标签:站端上报→「站端上传」,台账手增→「羚牛上传」;变更日志含时间/操作人/修改项/原值/新值;核对时间后有核对人、对账时间后有对账人
|
||||
9. 每日统计默认收起,第一屏以列表为主
|
||||
10. 车牌附「羚牛车辆 / 非羚牛车辆」;非羚牛车辆核对/对账字段显示为空
|
||||
11. 前一日手工台账缺失时,顶栏提示「手工台账有缺失,作为对账重要依据,请先上传手工台账」;点新增直达手工台账并选中昨日;补传后可新增
|
||||
12. 操作列「详情」在「更多」之前;弹窗可查看加氢记录字段及车牌号、加氢机面板照片(无实拍时用演示占位图)
|
||||
|
||||
---
|
||||
|
||||
@@ -69,3 +74,22 @@
|
||||
| 数据结构与对账 | 站端口径字段;**核对 vs 对账**双状态;PC 多导出 | [record-data-model.md](./record-data-model.md)、[../vehicle-h2-fee-ledger/.spec/verify-reconcile.md](../vehicle-h2-fee-ledger/.spec/verify-reconcile.md) |
|
||||
| 重复键 / 总额偏差 | 业务主键拦截;总额偏差二次确认 | [record-data-model.md](./record-data-model.md) §5–6 |
|
||||
| 来源 / 变更日志 / 操作人 | `recordSource`、行级 `changeLogs`、核对人/对账人 | [record-source-changelog.md](./record-source-changelog.md) |
|
||||
| 羚牛车辆与核对对账可见性 | 系统车辆列表判定;非羚牛字段空展示 | [fleet-vehicle-verify.md](./fleet-vehicle-verify.md) |
|
||||
| 手工台账门禁 | 昨日未传 → 今日禁新增;文案与跳转见规格 | [../oneos-h5-h2-order/.spec/feature-manual-ledger.md](../oneos-h5-h2-order/.spec/feature-manual-ledger.md) |
|
||||
|
||||
---
|
||||
|
||||
## 4. 右侧标注目录
|
||||
|
||||
打开原型右侧「原型标注 / 原型目录」,可阅读:
|
||||
|
||||
| 目录 | 内容 |
|
||||
|---|---|
|
||||
| PRD 全文 | [requirements-prd.md](./requirements-prd.md) |
|
||||
| 需求角色 | [roles.md](./roles.md) |
|
||||
| 用户故事 | [user-stories.md](./user-stories.md) |
|
||||
| 关键逻辑 | [key-logic.md](./key-logic.md) 及数据模型、车辆、来源、台账、对账规格 |
|
||||
| 流程图 | [flows.md](./flows.md)(Mermaid) |
|
||||
| 功能说明 | 列表、新增、台账、导出、每日统计等 |
|
||||
|
||||
由 `scripts/build-annotation-source.mjs` 从 `.spec/*.md` 生成 `annotation-source.json`。
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -4,11 +4,15 @@
|
||||
*/
|
||||
import '../../common/oneosWebLegacy/legacyGlobals';
|
||||
import '../../common/h2VehicleLedgerBridge.js';
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import '../vehicle-management/style.css';
|
||||
import Page2 from './pages/02-加氢记录.jsx';
|
||||
import { OneosWebLegacyShell } from '../../common/oneosWebLegacy/OneosWebLegacyShell';
|
||||
import { AnnotationViewer, type AnnotationSourceDocument } from '@axhub/annotation';
|
||||
import {
|
||||
type AnnotationSourceDocument,
|
||||
type AnnotationViewerOptions,
|
||||
} from '@axhub/annotation';
|
||||
import { PrototypeAnnotationHost } from '../../common/prototype-annotation-host';
|
||||
import annotationSourceDocument from './annotation-source.json';
|
||||
|
||||
const pages = [
|
||||
@@ -16,23 +20,28 @@ const pages = [
|
||||
];
|
||||
|
||||
export default function OneosWebH2Station() {
|
||||
const annotationOptions = useMemo<AnnotationViewerOptions>(
|
||||
() => ({
|
||||
currentPageId: 'h2-record',
|
||||
toolbarEdge: 'right',
|
||||
showToolbar: true,
|
||||
showThemeToggle: true,
|
||||
showColorFilter: true,
|
||||
emptyWhenNoData: false,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<OneosWebLegacyShell
|
||||
moduleTitle="加氢记录"
|
||||
pages={pages}
|
||||
defaultPageId="h2-record"
|
||||
/>
|
||||
<AnnotationViewer
|
||||
source={annotationSourceDocument as unknown as AnnotationSourceDocument}
|
||||
options={{
|
||||
currentPageId: "h2-record",
|
||||
toolbarEdge: 'right',
|
||||
showToolbar: true,
|
||||
showThemeToggle: true,
|
||||
showColorFilter: true,
|
||||
emptyWhenNoData: true,
|
||||
}}
|
||||
moduleTitle="加氢记录"
|
||||
pages={pages}
|
||||
defaultPageId="h2-record"
|
||||
/>
|
||||
<PrototypeAnnotationHost
|
||||
source={annotationSourceDocument as AnnotationSourceDocument}
|
||||
options={annotationOptions}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,22 +0,0 @@
|
||||
/**
|
||||
* @name 工作台
|
||||
* 自 ONE-OS web端 原稿复刻(1 个页面)
|
||||
*/
|
||||
import '../../common/oneosWebLegacy/legacyGlobals';
|
||||
import React from 'react';
|
||||
import Page1 from './pages/01-工作台.jsx';
|
||||
import { OneosWebLegacyShell } from '../../common/oneosWebLegacy/OneosWebLegacyShell';
|
||||
|
||||
const pages = [
|
||||
{ id: 'u5de5u4f5cu53f0', title: '工作台', component: Page1 },
|
||||
];
|
||||
|
||||
export default function OneosWebWorkbench() {
|
||||
return (
|
||||
<OneosWebLegacyShell
|
||||
moduleTitle="工作台"
|
||||
pages={pages}
|
||||
defaultPageId="u5de5u4f5cu53f0"
|
||||
/>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -39,8 +39,8 @@
|
||||
"fingerprint": "pr-list",
|
||||
"path": []
|
||||
},
|
||||
"aiPrompt": "新增、批量导入、导出与收款列表。",
|
||||
"annotationText": "列表含收款时间、汇款账户名、公司主体、银行、金额、备注、关联账单、未认领金额、操作人、操作时间;操作列支持关联账单。",
|
||||
"aiPrompt": "新增、批量导入、导出与收款列表;导入成功演示入账短信通知。",
|
||||
"annotationText": "列表含收款时间、汇款账户名、公司主体、银行、金额、备注、关联账单、未关联金额、操作人、操作时间;操作列支持关联账单。新增/批量导入成功后按账户名通知客户负责人与合同创建人(原型演示)。",
|
||||
"hasMarkdown": false,
|
||||
"color": "#0f766e",
|
||||
"images": [],
|
||||
@@ -104,6 +104,18 @@
|
||||
"id": "pr-doc-overview",
|
||||
"title": "模块总览",
|
||||
"markdown": "# 收款记录\n\n维护客户到账收款,支持关联租赁账单、自营账单、氢费账单。KPI 按收款记录统计:到账总金额 = 收款金额合计;认领总金额 = 已认领金额合计;未关联总金额 = 到账金额 − 已认领金额。"
|
||||
},
|
||||
{
|
||||
"type": "markdown",
|
||||
"id": "pr-doc-prd",
|
||||
"title": "产品需求摘要",
|
||||
"markdownPath": ".spec/requirements-prd.md"
|
||||
},
|
||||
{
|
||||
"type": "markdown",
|
||||
"id": "pr-doc-notify",
|
||||
"title": "入账短信通知",
|
||||
"markdownPath": ".spec/payment-notify.md"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
sumUnclaimedAmount,
|
||||
type PaymentKpiKey,
|
||||
} from './utils/payment';
|
||||
import { notifyPaymentEntrySms, notifyPaymentImportSms } from './utils/notify-recipients';
|
||||
import annotationSourceDocument from './annotation-source.json';
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
@@ -187,6 +188,7 @@ function PaymentRecordsContent() {
|
||||
setRecords((prev) => [row, ...prev]);
|
||||
setAddOpen(false);
|
||||
message.success('收款记录已新增(原型演示)');
|
||||
notifyPaymentEntrySms(row.customerName, message, { source: 'add' });
|
||||
};
|
||||
|
||||
const handleLinkBills = (recordId: string, linkedBills: LinkedBill[]) => {
|
||||
@@ -249,7 +251,13 @@ function PaymentRecordsContent() {
|
||||
type="button"
|
||||
className="vm-btn vm-btn-ghost ldb-toolbar-btn"
|
||||
data-vm-icon="import"
|
||||
onClick={() => message.info('批量导入:请下载模板后上传 Excel(原型演示)')}
|
||||
onClick={() => {
|
||||
const demoCustomers = filtered.slice(0, 3).map((row) => row.customerName);
|
||||
const names = demoCustomers.length > 0
|
||||
? demoCustomers
|
||||
: ['嘉兴港区韵达快递有限公司', '未知客户(演示未匹配)'];
|
||||
notifyPaymentImportSms(names, message);
|
||||
}}
|
||||
>
|
||||
批量导入
|
||||
</button>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
# 物流业务明细 · 产品需求说明(PRD)
|
||||
|
||||
> 参照《2026年业务二部运营台账总表》物流业务明细子表;行内编辑与导入为原型 Toast 演示。
|
||||
> **界面设计规范**与租赁业务明细、租赁合同列表统一,见 `src/prototypes/ledger-shared/DESIGN.md`(`vm-page ldb-page`)。
|
||||
> **界面设计规范**与租赁业务明细、租赁合同列表统一,见 `src/prototypes/ledger-shared/DESIGN.md`(`vm-page ldb-page`)。
|
||||
> **主路径升级**:调度任务办结自动生成明细行;导入降为补录。规则全文见 [ledger-auto-gen.md](./ledger-auto-gen.md)。
|
||||
> 假设:[assumptions.md](../../../resources/self-operated-logistics/assumptions.md)。
|
||||
|
||||
---
|
||||
|
||||
@@ -11,8 +13,9 @@
|
||||
|---|---|
|
||||
| 模块名称 | 台账数据 — 物流业务明细 |
|
||||
| 目标用户 | 业务服务组操作人、主管 |
|
||||
| 核心任务 | 记录出车营收与成本、自动汇总盈亏、导入导出与删改 |
|
||||
| 核心任务 | 承接调度自动出车明细、补录导入、自动汇总盈亏 |
|
||||
| 页面结构 | 筛选区 + KPI 统计 + 工具栏 + 宽表 + 分页 |
|
||||
| 上游 | 自营合同 → 轻量调度任务办结 |
|
||||
|
||||
---
|
||||
|
||||
@@ -73,3 +76,22 @@
|
||||
**总成本** = 氢费 + 人工报销费用 + ETC费用 + 电费 + 薪资 + 车辆费用。
|
||||
|
||||
**盈亏** = 金额 − 总成本;参照 A 股配色:**正数红色、负数绿色**。
|
||||
|
||||
---
|
||||
|
||||
## 6. 调度办结自动生成(摘要)
|
||||
|
||||
| 项 | 说明 |
|
||||
|---|---|
|
||||
| 业务目的 | 办结出车任务后自动落明细,减少手工导入 |
|
||||
| 触发 | `self-operated-dispatch-task` 办结成功(须车辆已交车) |
|
||||
| 粒度 | 一任务一行;备注前缀 `[调度自动·任务号]` |
|
||||
| 成本一期 | 营收侧自动;氢费/ETC 等默认 0,可补录 |
|
||||
| 数据源 | Bridge `ledger-auto` + 种子/导入行合并展示 |
|
||||
| 规格全文 | [ledger-auto-gen.md](./ledger-auto-gen.md) |
|
||||
|
||||
### 验收重点(自动生成)
|
||||
|
||||
- [ ] 调度办结后本页刷新可见新行(同浏览器 localStorage)
|
||||
- [ ] 自动行备注含任务号;金额/盈亏公式正确
|
||||
- [ ] 导入仍可用,不覆盖不同 id 的自动行
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"version": 2,
|
||||
"prototypeName": "self-operated-business-ledger",
|
||||
"pageId": "list",
|
||||
"updatedAt": 1783910257820,
|
||||
"updatedAt": 1784350346385,
|
||||
"nodes": [
|
||||
{
|
||||
"id": "sob-filter",
|
||||
@@ -25,7 +25,7 @@
|
||||
"color": "#2563eb",
|
||||
"images": [],
|
||||
"createdAt": 1740787200000,
|
||||
"updatedAt": 1783910257820
|
||||
"updatedAt": 1784350346385
|
||||
},
|
||||
{
|
||||
"id": "sob-kpi",
|
||||
@@ -45,7 +45,7 @@
|
||||
"color": "#64748b",
|
||||
"images": [],
|
||||
"createdAt": 1740787200000,
|
||||
"updatedAt": 1783910257820
|
||||
"updatedAt": 1784350346385
|
||||
},
|
||||
{
|
||||
"id": "sob-toolbar",
|
||||
@@ -65,7 +65,7 @@
|
||||
"color": "#0f766e",
|
||||
"images": [],
|
||||
"createdAt": 1740787200000,
|
||||
"updatedAt": 1783910257820
|
||||
"updatedAt": 1784350346385
|
||||
},
|
||||
{
|
||||
"id": "sob-table",
|
||||
@@ -85,7 +85,7 @@
|
||||
"color": "#2563eb",
|
||||
"images": [],
|
||||
"createdAt": 1740787200000,
|
||||
"updatedAt": 1783910257820
|
||||
"updatedAt": 1784350346385
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -93,7 +93,7 @@
|
||||
"sob-filter": "## 2. 筛选区\n\n默认展示 4 项,点击「更多筛选」展开。\n\n| 筛选项 | 说明 |\n|---|---|\n| 月份 | YYYY-MM |\n| 出车日期 | 日期区间 |\n| 业务名称 | 精确匹配 |\n| 车牌号码 | 多选 |\n| 车辆承接方 | 我司 / 第三方车队 |\n| 品牌 / 型号 | 级联 |\n| 维护人 | 仅主管可见 |\n\n---",
|
||||
"sob-kpi": "## 3. KPI 统计卡片(6 张)\n\n明细条数、金额合计、总成本合计、氢费总计、ETC费用总计、盈亏合计。\n\n卡片样式对齐租赁业务明细:左侧色条、图标、悬浮说明、盈亏正负着色。\n\n---",
|
||||
"sob-toolbar": "## 4. 列表工具栏\n\n| 按钮 | 行为 |\n|---|---|\n| 批量导入 | 下载模板(23 列手工录入)→ 上传 Excel → 系统车型/金额/总成本/盈亏自动计算 |\n| 导出 | 导出当前筛选结果(原型演示) |\n| 添加行 | 表格底栏新增行(原型演示) |\n\n不支持批量删除与行多选。\n\n### 导入模板列(23 列)\n\n年份、月份、出车日期、业务名称、司机、电话、车牌号码、单价、数量、氢费、ETC费用、薪资、电费、人工报销费用、日社保服务费、日挂车服务费、日挂车费用、日停车费用、日轮胎费用、车辆费用、是否多趟、线路计价、备注。\n\n**必填**:年份、月份、出车日期、业务名称、司机、电话、车牌号码、单价、数量、薪资、是否多趟、线路计价。\n\n**不含**(导入后自动计算/反写):系统车型、金额、总成本、盈亏。\n\n---",
|
||||
"sob-table": "## 5. 列表字段\n\n月份、出车日期、业务名称、司机、电话、车牌号码、系统车型、单价、数量、金额、氢费、ETC费用、薪资、电费、人工报销费用、日社保服务费、日挂车费用、日停车费用、日轮胎费用、车辆费用、总成本、盈亏、是否多趟、线路计价、备注、操作。\n\n**月份展示**:列表合并显示为 `YYYY-MM`;导入模板仍为「年份」「月份」两列,导入后自动合并展示。\n\n**系统车型**:按车牌从车辆主数据反写,展示为「品牌·型号」。\n\n**金额** = 单价 × 数量。\n\n**总成本** = 氢费 + 人工报销费用 + ETC费用 + 电费 + 薪资 + 车辆费用。\n\n**盈亏** = 金额 − 总成本;参照 A 股配色:**正数红色、负数绿色**。"
|
||||
"sob-table": "## 5. 列表字段\n\n月份、出车日期、业务名称、司机、电话、车牌号码、系统车型、单价、数量、金额、氢费、ETC费用、薪资、电费、人工报销费用、日社保服务费、日挂车费用、日停车费用、日轮胎费用、车辆费用、总成本、盈亏、是否多趟、线路计价、备注、操作。\n\n**月份展示**:列表合并显示为 `YYYY-MM`;导入模板仍为「年份」「月份」两列,导入后自动合并展示。\n\n**系统车型**:按车牌从车辆主数据反写,展示为「品牌·型号」。\n\n**金额** = 单价 × 数量。\n\n**总成本** = 氢费 + 人工报销费用 + ETC费用 + 电费 + 薪资 + 车辆费用。\n\n**盈亏** = 金额 − 总成本;参照 A 股配色:**正数红色、负数绿色**。\n\n---"
|
||||
},
|
||||
"assetMap": {},
|
||||
"directory": {
|
||||
@@ -108,7 +108,7 @@
|
||||
"type": "markdown",
|
||||
"id": "sob-doc-overview",
|
||||
"title": "模块总览",
|
||||
"markdown": "## 1. 模块定位\n\n| 项 | 说明 |\n|---|---|\n| 模块名称 | 台账数据 — 物流业务明细 |\n| 目标用户 | 业务服务组操作人、主管 |\n| 核心任务 | 记录出车营收与成本、自动汇总盈亏、导入导出与删改 |\n| 页面结构 | 筛选区 + KPI 统计 + 工具栏 + 宽表 + 分页 |\n\n---"
|
||||
"markdown": "## 1. 模块定位\n\n| 项 | 说明 |\n|---|---|\n| 模块名称 | 台账数据 — 物流业务明细 |\n| 目标用户 | 业务服务组操作人、主管 |\n| 核心任务 | 承接调度自动出车明细、补录导入、自动汇总盈亏 |\n| 页面结构 | 筛选区 + KPI 统计 + 工具栏 + 宽表 + 分页 |\n| 上游 | 自营合同 → 轻量调度任务办结 |\n\n---"
|
||||
},
|
||||
{
|
||||
"type": "link",
|
||||
@@ -121,7 +121,13 @@
|
||||
"type": "markdown",
|
||||
"id": "sob-doc-prd",
|
||||
"title": "PRD 全文",
|
||||
"markdown": "# 物流业务明细 · 产品需求说明(PRD)\n\n> 参照《2026年业务二部运营台账总表》物流业务明细子表;行内编辑与导入为原型 Toast 演示。 \n> **界面设计规范**与租赁业务明细、租赁合同列表统一,见 `src/prototypes/ledger-shared/DESIGN.md`(`vm-page ldb-page`)。\n\n---\n\n## 1. 模块定位\n\n| 项 | 说明 |\n|---|---|\n| 模块名称 | 台账数据 — 物流业务明细 |\n| 目标用户 | 业务服务组操作人、主管 |\n| 核心任务 | 记录出车营收与成本、自动汇总盈亏、导入导出与删改 |\n| 页面结构 | 筛选区 + KPI 统计 + 工具栏 + 宽表 + 分页 |\n\n---\n\n## 2. 筛选区\n\n默认展示 4 项,点击「更多筛选」展开。\n\n| 筛选项 | 说明 |\n|---|---|\n| 月份 | YYYY-MM |\n| 出车日期 | 日期区间 |\n| 业务名称 | 精确匹配 |\n| 车牌号码 | 多选 |\n| 车辆承接方 | 我司 / 第三方车队 |\n| 品牌 / 型号 | 级联 |\n| 维护人 | 仅主管可见 |\n\n---\n\n## 3. KPI 统计卡片(6 张)\n\n明细条数、金额合计、总成本合计、氢费总计、ETC费用总计、盈亏合计。\n\n卡片样式对齐租赁业务明细:左侧色条、图标、悬浮说明、盈亏正负着色。\n\n---\n\n## 4. 列表工具栏\n\n| 按钮 | 行为 |\n|---|---|\n| 批量导入 | 下载模板(23 列手工录入)→ 上传 Excel → 系统车型/金额/总成本/盈亏自动计算 |\n| 导出 | 导出当前筛选结果(原型演示) |\n| 添加行 | 表格底栏新增行(原型演示) |\n\n不支持批量删除与行多选。\n\n### 导入模板列(23 列)\n\n年份、月份、出车日期、业务名称、司机、电话、车牌号码、单价、数量、氢费、ETC费用、薪资、电费、人工报销费用、日社保服务费、日挂车服务费、日挂车费用、日停车费用、日轮胎费用、车辆费用、是否多趟、线路计价、备注。\n\n**必填**:年份、月份、出车日期、业务名称、司机、电话、车牌号码、单价、数量、薪资、是否多趟、线路计价。\n\n**不含**(导入后自动计算/反写):系统车型、金额、总成本、盈亏。\n\n---\n\n## 5. 列表字段\n\n月份、出车日期、业务名称、司机、电话、车牌号码、系统车型、单价、数量、金额、氢费、ETC费用、薪资、电费、人工报销费用、日社保服务费、日挂车费用、日停车费用、日轮胎费用、车辆费用、总成本、盈亏、是否多趟、线路计价、备注、操作。\n\n**月份展示**:列表合并显示为 `YYYY-MM`;导入模板仍为「年份」「月份」两列,导入后自动合并展示。\n\n**系统车型**:按车牌从车辆主数据反写,展示为「品牌·型号」。\n\n**金额** = 单价 × 数量。\n\n**总成本** = 氢费 + 人工报销费用 + ETC费用 + 电费 + 薪资 + 车辆费用。\n\n**盈亏** = 金额 − 总成本;参照 A 股配色:**正数红色、负数绿色**。"
|
||||
"markdown": "# 物流业务明细 · 产品需求说明(PRD)\n\n> 参照《2026年业务二部运营台账总表》物流业务明细子表;行内编辑与导入为原型 Toast 演示。 \n> **界面设计规范**与租赁业务明细、租赁合同列表统一,见 `src/prototypes/ledger-shared/DESIGN.md`(`vm-page ldb-page`)。 \n> **主路径升级**:调度任务办结自动生成明细行;导入降为补录。规则全文见 [ledger-auto-gen.md](./ledger-auto-gen.md)。 \n> 假设:[assumptions.md](../../../resources/self-operated-logistics/assumptions.md)。\n\n---\n\n## 1. 模块定位\n\n| 项 | 说明 |\n|---|---|\n| 模块名称 | 台账数据 — 物流业务明细 |\n| 目标用户 | 业务服务组操作人、主管 |\n| 核心任务 | 承接调度自动出车明细、补录导入、自动汇总盈亏 |\n| 页面结构 | 筛选区 + KPI 统计 + 工具栏 + 宽表 + 分页 |\n| 上游 | 自营合同 → 轻量调度任务办结 |\n\n---\n\n## 2. 筛选区\n\n默认展示 4 项,点击「更多筛选」展开。\n\n| 筛选项 | 说明 |\n|---|---|\n| 月份 | YYYY-MM |\n| 出车日期 | 日期区间 |\n| 业务名称 | 精确匹配 |\n| 车牌号码 | 多选 |\n| 车辆承接方 | 我司 / 第三方车队 |\n| 品牌 / 型号 | 级联 |\n| 维护人 | 仅主管可见 |\n\n---\n\n## 3. KPI 统计卡片(6 张)\n\n明细条数、金额合计、总成本合计、氢费总计、ETC费用总计、盈亏合计。\n\n卡片样式对齐租赁业务明细:左侧色条、图标、悬浮说明、盈亏正负着色。\n\n---\n\n## 4. 列表工具栏\n\n| 按钮 | 行为 |\n|---|---|\n| 批量导入 | 下载模板(23 列手工录入)→ 上传 Excel → 系统车型/金额/总成本/盈亏自动计算 |\n| 导出 | 导出当前筛选结果(原型演示) |\n| 添加行 | 表格底栏新增行(原型演示) |\n\n不支持批量删除与行多选。\n\n### 导入模板列(23 列)\n\n年份、月份、出车日期、业务名称、司机、电话、车牌号码、单价、数量、氢费、ETC费用、薪资、电费、人工报销费用、日社保服务费、日挂车服务费、日挂车费用、日停车费用、日轮胎费用、车辆费用、是否多趟、线路计价、备注。\n\n**必填**:年份、月份、出车日期、业务名称、司机、电话、车牌号码、单价、数量、薪资、是否多趟、线路计价。\n\n**不含**(导入后自动计算/反写):系统车型、金额、总成本、盈亏。\n\n---\n\n## 5. 列表字段\n\n月份、出车日期、业务名称、司机、电话、车牌号码、系统车型、单价、数量、金额、氢费、ETC费用、薪资、电费、人工报销费用、日社保服务费、日挂车费用、日停车费用、日轮胎费用、车辆费用、总成本、盈亏、是否多趟、线路计价、备注、操作。\n\n**月份展示**:列表合并显示为 `YYYY-MM`;导入模板仍为「年份」「月份」两列,导入后自动合并展示。\n\n**系统车型**:按车牌从车辆主数据反写,展示为「品牌·型号」。\n\n**金额** = 单价 × 数量。\n\n**总成本** = 氢费 + 人工报销费用 + ETC费用 + 电费 + 薪资 + 车辆费用。\n\n**盈亏** = 金额 − 总成本;参照 A 股配色:**正数红色、负数绿色**。\n\n---\n\n## 6. 调度办结自动生成(摘要)\n\n| 项 | 说明 |\n|---|---|\n| 业务目的 | 办结出车任务后自动落明细,减少手工导入 |\n| 触发 | `self-operated-dispatch-task` 办结成功(须车辆已交车) |\n| 粒度 | 一任务一行;备注前缀 `[调度自动·任务号]` |\n| 成本一期 | 营收侧自动;氢费/ETC 等默认 0,可补录 |\n| 数据源 | Bridge `ledger-auto` + 种子/导入行合并展示 |\n| 规格全文 | [ledger-auto-gen.md](./ledger-auto-gen.md) |\n\n### 验收重点(自动生成)\n\n- [ ] 调度办结后本页刷新可见新行(同浏览器 localStorage)\n- [ ] 自动行备注含任务号;金额/盈亏公式正确\n- [ ] 导入仍可用,不覆盖不同 id 的自动行"
|
||||
},
|
||||
{
|
||||
"type": "markdown",
|
||||
"id": "sob-doc-auto-gen",
|
||||
"title": "调度自动生成",
|
||||
"markdown": "# 物流业务明细 · 调度办结自动生成\n\n> 实现:`src/common/selfOperatedLogisticsBridge.js` → `completeDispatchTask` / `mergeLedgerWithAutoRows` \n> UI:`self-operated-business-ledger/index.tsx` 合并自动行;触发页在 `self-operated-dispatch-task`\n\n## 业务目的\n\n调度任务办结后自动生成 1 行出车明细,减少人工导入;导入/行编辑保留为补录与纠错。\n\n## 触发条件\n\n| 顺序 | 条件 | 说明 |\n|---|---|---|\n| 1 | 调度任务办结成功 | 见 `dispatch-rules.md` 办结判定 |\n| 2 | 车辆已交车 | 运营态 = self_operated |\n| 3 | 幂等 | 同一 `dispatchTaskId` 覆盖旧自动行 |\n\n## 数据源\n\n| 项 | 说明 |\n|---|---|\n| 自动行存储 | localStorage `oneos.self-operated.ledger-auto.v1` |\n| 列表合并 | `mergeLedgerWithAutoRows(seedOrLocalRows)`:自动行置顶,同 id 去重 |\n| 种子 JSON | `data/rows.json` 仍作历史/演示导入数据 |\n\n原型本地种子 vs **未接真实 API**。\n\n## 用户可见结果\n\n| 结果 | 表现 |\n|---|---|\n| 生成成功 | 调度页 Toast;台账备注前缀 `[调度自动·DD-xxxx]` |\n| 未交车 | 调度页拦截,台账无新行 |\n| 成本一期 | 营收侧(单价/数量/金额/薪资)带出;氢费/ETC 等为 0,可手工补 |\n\n## 与导入关系\n\n| 路径 | 优先级 |\n|---|---|\n| 调度自动生成 | 主路径 |\n| Excel 批量导入 | 补录/历史 |\n| 添加行 | 补录演示 |\n\n取消已办结任务:**禁止**(避免台账孤儿行)。若未来支持作废,须同步删除或标记自动行(本期不做)。\n\n## 公式(与现网一致)\n\n- 金额 = 单价 × 数量 \n- 总成本 = 氢费 + 人工报销 + ETC + 电费 + 薪资 + 车辆费用 \n- 盈亏 = 金额 − 总成本"
|
||||
},
|
||||
{
|
||||
"type": "markdown",
|
||||
@@ -145,7 +151,7 @@
|
||||
"type": "markdown",
|
||||
"id": "sob-doc-sob-table",
|
||||
"title": "物流业务明细宽表",
|
||||
"markdown": "## 5. 列表字段\n\n月份、出车日期、业务名称、司机、电话、车牌号码、系统车型、单价、数量、金额、氢费、ETC费用、薪资、电费、人工报销费用、日社保服务费、日挂车费用、日停车费用、日轮胎费用、车辆费用、总成本、盈亏、是否多趟、线路计价、备注、操作。\n\n**月份展示**:列表合并显示为 `YYYY-MM`;导入模板仍为「年份」「月份」两列,导入后自动合并展示。\n\n**系统车型**:按车牌从车辆主数据反写,展示为「品牌·型号」。\n\n**金额** = 单价 × 数量。\n\n**总成本** = 氢费 + 人工报销费用 + ETC费用 + 电费 + 薪资 + 车辆费用。\n\n**盈亏** = 金额 − 总成本;参照 A 股配色:**正数红色、负数绿色**。"
|
||||
"markdown": "## 5. 列表字段\n\n月份、出车日期、业务名称、司机、电话、车牌号码、系统车型、单价、数量、金额、氢费、ETC费用、薪资、电费、人工报销费用、日社保服务费、日挂车费用、日停车费用、日轮胎费用、车辆费用、总成本、盈亏、是否多趟、线路计价、备注、操作。\n\n**月份展示**:列表合并显示为 `YYYY-MM`;导入模板仍为「年份」「月份」两列,导入后自动合并展示。\n\n**系统车型**:按车牌从车辆主数据反写,展示为「品牌·型号」。\n\n**金额** = 单价 × 数量。\n\n**总成本** = 氢费 + 人工报销费用 + ETC费用 + 电费 + 薪资 + 车辆费用。\n\n**盈亏** = 金额 − 总成本;参照 A 股配色:**正数红色、负数绿色**。\n\n---"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,14 +5,18 @@
|
||||
import '../vehicle-management/style.css';
|
||||
import '../lease-business-ledger/styles/index.css';
|
||||
import './styles/index.css';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { Download, FileUp, Plus } from 'lucide-react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
type AnnotationDirectoryRouteNode,
|
||||
type AnnotationSourceDocument,
|
||||
type AnnotationViewerOptions
|
||||
} from '@axhub/annotation';
|
||||
import { PrototypeAnnotationHost } from '../../common/prototype-annotation-host';
|
||||
import {
|
||||
mergeLedgerWithAutoRows,
|
||||
removeAutoLedgerRow,
|
||||
subscribeSelfOperatedBridge,
|
||||
} from '../../common/selfOperatedLogisticsBridge.js';
|
||||
import seedRows from './data/rows.json';
|
||||
import { FilterPanel } from './components/FilterPanel';
|
||||
import { LedgerKpiRow } from './components/LedgerKpiRow';
|
||||
@@ -36,8 +40,21 @@ import annotationSourceDocument from './annotation-source.json';
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
function loadMergedRecords(): SelfOperatedLedgerRow[] {
|
||||
return mergeLedgerWithAutoRows(seedRows as SelfOperatedLedgerRow[]) as SelfOperatedLedgerRow[];
|
||||
}
|
||||
|
||||
export default function SelfOperatedBusinessLedgerApp() {
|
||||
const [records, setRecords] = useState<SelfOperatedLedgerRow[]>(seedRows as SelfOperatedLedgerRow[]);
|
||||
const [baseRecords, setBaseRecords] = useState<SelfOperatedLedgerRow[]>(
|
||||
() => seedRows as SelfOperatedLedgerRow[],
|
||||
);
|
||||
const [records, setRecords] = useState<SelfOperatedLedgerRow[]>(() => loadMergedRecords());
|
||||
|
||||
useEffect(() => {
|
||||
const sync = () => setRecords(mergeLedgerWithAutoRows(baseRecords) as SelfOperatedLedgerRow[]);
|
||||
sync();
|
||||
return subscribeSelfOperatedBridge(sync);
|
||||
}, [baseRecords]);
|
||||
const [pendingFilters, setPendingFilters] = useState<SelfOperatedFilters>(EMPTY_SELF_FILTERS);
|
||||
const [appliedFilters, setAppliedFilters] = useState<SelfOperatedFilters>(EMPTY_SELF_FILTERS);
|
||||
const [page, setPage] = useState(1);
|
||||
@@ -89,7 +106,7 @@ export default function SelfOperatedBusinessLedgerApp() {
|
||||
}
|
||||
|
||||
if (result.success.length > 0) {
|
||||
setRecords((prev) => [...result.success, ...prev]);
|
||||
setBaseRecords((prev) => [...result.success, ...prev]);
|
||||
setPage(1);
|
||||
}
|
||||
|
||||
@@ -132,6 +149,22 @@ export default function SelfOperatedBusinessLedgerApp() {
|
||||
|
||||
<LedgerKpiRow summary={kpi} />
|
||||
|
||||
<p
|
||||
data-annotation-id="sob-autogen-hint"
|
||||
style={{
|
||||
margin: '0 0 10px',
|
||||
padding: '10px 12px',
|
||||
borderRadius: 8,
|
||||
background: '#f0fdf4',
|
||||
border: '1px solid #bbf7d0',
|
||||
color: '#166534',
|
||||
fontSize: 13,
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
主路径:调度任务办结自动生成明细(备注含「调度自动」);批量导入为补录。可先打开「调度任务」办结种子单 DD-2026-0001 演示。
|
||||
</p>
|
||||
|
||||
<section className="vm-table-section">
|
||||
<div className="vm-table-toolbar ldb-table-toolbar" data-annotation-id="sob-toolbar">
|
||||
<div className="vm-table-actions">
|
||||
@@ -192,7 +225,11 @@ export default function SelfOperatedBusinessLedgerApp() {
|
||||
onCancel={() => setDeleteTarget(null)}
|
||||
onConfirm={() => {
|
||||
if (!deleteTarget) return;
|
||||
setRecords((prev) => prev.filter((row) => row.id !== deleteTarget.id));
|
||||
if (deleteTarget.rowSource === 'auto' || String(deleteTarget.id).startsWith('auto-')) {
|
||||
removeAutoLedgerRow(deleteTarget.id);
|
||||
} else {
|
||||
setBaseRecords((prev) => prev.filter((row) => row.id !== deleteTarget.id));
|
||||
}
|
||||
setDeleteTarget(null);
|
||||
showToast(`已删除 ${deleteTarget.plateNo}(原型)`);
|
||||
}}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { fileURLToPath } from 'node:url';
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const protoRoot = path.resolve(__dirname, '..');
|
||||
const prd = fs.readFileSync(path.join(protoRoot, '.spec/requirements-prd.md'), 'utf8');
|
||||
const autoGen = fs.readFileSync(path.join(protoRoot, '.spec/ledger-auto-gen.md'), 'utf8');
|
||||
|
||||
const NODE_DEFS = [
|
||||
{
|
||||
@@ -112,6 +113,7 @@ const source = {
|
||||
target: 'self',
|
||||
},
|
||||
{ type: 'markdown', id: 'sob-doc-prd', title: 'PRD 全文', markdown: prd.trim() },
|
||||
{ type: 'markdown', id: 'sob-doc-auto-gen', title: '调度自动生成', markdown: autoGen.trim() },
|
||||
...nodes.map((node) => ({
|
||||
type: 'markdown',
|
||||
id: `sob-doc-${node.id}`,
|
||||
|
||||
@@ -29,6 +29,11 @@ export interface SelfOperatedLedgerRow {
|
||||
multiTrip: string;
|
||||
routePricing: string;
|
||||
remark: string;
|
||||
/** seed | import | auto(调度办结) */
|
||||
rowSource?: 'seed' | 'import' | 'auto';
|
||||
dispatchTaskId?: string;
|
||||
dispatchTaskCode?: string;
|
||||
contractCode?: string;
|
||||
}
|
||||
|
||||
export interface SelfOperatedFilters {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
| 产品模块 | 台账数据 → 车辆氢费明细 |
|
||||
| 文档类型 | 产品需求说明 |
|
||||
| 适用读者 | 产品、业务、运营、测试、项目 |
|
||||
| 修订说明 | v2.5:记录来源标签(站端上传/羚牛上传);核对人/对账人;变更日志挂行级字段 |
|
||||
| 修订说明 | v2.6:车牌「羚牛车辆/非羚牛车辆」;非羚牛车辆核对/对账字段空展示 |
|
||||
| 双状态规格 | [verify-reconcile.md](./verify-reconcile.md) |
|
||||
|
||||
---
|
||||
@@ -162,7 +162,9 @@ flowchart LR
|
||||
- **说明:** 合计条含「待保存 / 未对账 / 已对账」等当前列表中的全部可见记录,与导出范围(仅未对账+已对账)不同。
|
||||
|
||||
**列表列顺序(左→右,业务视角):**
|
||||
序号 → 加氢日期 → 加氢时间 → 加氢站名称 → 客户名称 → 车牌号(下来源标签:站端上传 / 羚牛上传)→ 加氢量 → 成本单价 → 成本总价 → 加氢单价 → 加氢总价 → 行驶里程 → 备注 → 业务员 → 承担方式 → 对账日期 → **对账人** → **收票日期** → **加氢站付款状态** → **开票日期** → **客户收款状态** → **开票公司** → 核对状态 → **核对时间** → **核对人** → 对账状态 → 订单编号 → 操作(操作列固定右侧)。
|
||||
序号 → 加氢日期 → 加氢时间 → 加氢站名称 → 客户名称 → 车牌号(附**羚牛车辆/非羚牛车辆** + 来源:站端上传/羚牛上传)→ 加氢量 → 成本单价 → 成本总价 → 加氢单价 → 加氢总价 → 行驶里程 → 备注 → 业务员 → 承担方式 → 对账日期 → **对账人** → **收票日期** → **加氢站付款状态** → **开票日期** → **客户收款状态** → **开票公司** → 核对状态 → **核对时间** → **核对人** → 对账状态 → 订单编号 → 操作(操作列固定右侧)。
|
||||
|
||||
> 非羚牛车辆:核对状态/时间/人、对账状态/日期/人显示为空。判定见 [../oneos-web-h2-station/.spec/fleet-vehicle-verify.md](../oneos-web-h2-station/.spec/fleet-vehicle-verify.md)。
|
||||
|
||||
**系统带出字段(不可手工维护):** 收票日期、加氢站付款状态、开票日期、客户收款状态、开票公司;选择/变更客户等信息后由系统自动刷新,单元格为浅灰底只读样式。
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
|---|---|---|
|
||||
| 1 | 加氢站(H5/Web)新增加氢记录 | 写入共享 Store:`verifyStatus=unverified`,`reconcileStatus=pending`;按站点成本价补 `costUnitPrice`,`costTotal≈单价×量`;同步出现在车辆氢费明细 |
|
||||
| 2 | 能源部在车辆氢费明细确认无误 → **完成核对** | `verifyStatus=verified`,写 `verifiedAt`;站端列表同步「已核对」;**业务员侧不可再改删**(原型规则) |
|
||||
| 3 | 站点信息生成并对账单 **提交** | 仅纳入:**已核对** ∧ **未对账** ∧ 无 `statementRecordId` ∧ 本站 ∧ 账期;提交后 `reconcileStatus=reconciled`,写 `reconcileDate`、`statementRecordId` 等 |
|
||||
| 3 | 站点信息生成并对账单 **提交**(须填收票信息 + **对账单盖章附件**,二次确认) | 仅纳入:**已核对** ∧ **未对账** ∧ 无 `statementRecordId` ∧ 本站 ∧ 账期;提交后三端(台账 / 加氢记录 Web / H5)`reconcileStatus=reconciled`,写 `reconcileDate`、`statementRecordId` 等 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -882,7 +882,9 @@ const Component = function () {
|
||||
'沪AD12345F',
|
||||
'苏EF99887F',
|
||||
'京CN88771F',
|
||||
'川AL55602F'
|
||||
'川AL55602F',
|
||||
/* 非羚牛车辆演示(不在系统车辆列表 / Bridge 车队集合) */
|
||||
'粤Z99999F'
|
||||
];
|
||||
|
||||
/** 原型:按车牌+加氢日期匹配车辆运营态(交还车/库存/异动),联调后接车辆管理 */
|
||||
@@ -2985,6 +2987,28 @@ const Component = function () {
|
||||
return '羚牛上传';
|
||||
}
|
||||
|
||||
function isLingniuVehiclePlate(plateNo) {
|
||||
if (typeof window !== 'undefined' && window.H2VehicleLedgerBridge && typeof window.H2VehicleLedgerBridge.isLingniuVehicle === 'function') {
|
||||
return Boolean(window.H2VehicleLedgerBridge.isLingniuVehicle(plateNo));
|
||||
}
|
||||
var key = String(plateNo || '').trim().toUpperCase().replace(/F$/u, '');
|
||||
if (!key) return false;
|
||||
/* 明确非羚牛演示车牌(未进 Bridge 车队集合) */
|
||||
if (key === '粤Z99999' || key === '浙C77801') return false;
|
||||
return VEHICLE_INFO_TABLE.some(function (p) {
|
||||
return String(p).toUpperCase().replace(/F$/u, '') === key;
|
||||
});
|
||||
}
|
||||
|
||||
function vehicleKindLabel(plateNo) {
|
||||
if (!String(plateNo || '').trim()) return '';
|
||||
return isLingniuVehiclePlate(plateNo) ? '羚牛车辆' : '非羚牛车辆';
|
||||
}
|
||||
|
||||
function showVerifyReconcileFields(row) {
|
||||
return isLingniuVehiclePlate(row && row.plateNo);
|
||||
}
|
||||
|
||||
function recalcRowFields(row) {
|
||||
var mileage = normalizeMileageKm(row.mileageKm);
|
||||
var kg = isEmptyNum(row.hydrogenKg) ? null : roundMoney(row.hydrogenKg);
|
||||
@@ -3211,31 +3235,40 @@ const Component = function () {
|
||||
costTotal: 3168,
|
||||
remark: '成本单价与站点标准价不一致演示'
|
||||
});
|
||||
pushDemoRow('mock-h2-external-plate', {
|
||||
plateNo: '粤Z99999F',
|
||||
hydrogenTime: demoDay && demoDay.subtract ? demoDay.subtract(3, 'day') : demoDay,
|
||||
customerId: '00007',
|
||||
customerName: '上海迅杰物流有限公司',
|
||||
salesperson: '金可鹏',
|
||||
recordSource: 'station',
|
||||
remark: '非羚牛车辆演示:核对/对账字段应显示为空'
|
||||
});
|
||||
return recalcAllRows(rows);
|
||||
}
|
||||
|
||||
var layoutStyle = {
|
||||
padding: '16px 24px 24px',
|
||||
padding: '16px 24px 48px',
|
||||
minHeight: '100vh',
|
||||
background: 'linear-gradient(165deg, #eef4ff 0%, #f5f7fa 42%, #f0f2f5 100%)'
|
||||
background: 'var(--ln-canvas-parchment, #f5f6f6)'
|
||||
};
|
||||
|
||||
var filterLabelStyle = { marginBottom: 6, fontSize: 13, color: 'rgba(0,0,0,0.55)', fontWeight: 500 };
|
||||
var filterLabelStyle = { marginBottom: 6, fontSize: 13, color: 'var(--ln-muted, #71717a)', fontWeight: 500 };
|
||||
var filterItemStyle = { marginBottom: 12 };
|
||||
var filterControlStyle = { width: '100%' };
|
||||
var filterCardStyle = {
|
||||
marginBottom: 20,
|
||||
borderRadius: 16,
|
||||
boxShadow: '0 4px 20px -4px rgba(16,24,40,0.03), 0 0 0 1px rgba(16,24,40,0.06)',
|
||||
border: 'none',
|
||||
background: '#ffffff'
|
||||
marginBottom: 16,
|
||||
borderRadius: 'var(--ln-radius-card, 12px)',
|
||||
boxShadow: '0 1px 2px rgba(15,23,42,0.04), 0 4px 12px rgba(15,23,42,0.03)',
|
||||
border: '1px solid var(--ln-hairline, #e5e7eb)',
|
||||
background: 'var(--ln-surface-card, #ffffff)'
|
||||
};
|
||||
|
||||
var tableCardStyle = {
|
||||
borderRadius: 16,
|
||||
boxShadow: '0 10px 32px -4px rgba(16,24,40,0.06), 0 0 0 1px rgba(16,24,40,0.04)',
|
||||
border: 'none',
|
||||
background: '#ffffff',
|
||||
borderRadius: 'var(--ln-radius-card, 12px)',
|
||||
boxShadow: '0 1px 2px rgba(15,23,42,0.04), 0 4px 12px rgba(15,23,42,0.03)',
|
||||
border: '1px solid var(--ln-hairline, #e5e7eb)',
|
||||
background: 'var(--ln-surface-card, #ffffff)',
|
||||
overflow: 'hidden'
|
||||
};
|
||||
|
||||
@@ -3261,22 +3294,24 @@ const Component = function () {
|
||||
|
||||
// 异常行水印用 background-image,避免 td::after + position 破坏固定列行高同步
|
||||
var ledgerTableStyle =
|
||||
'.h2-ledger-table-wrap{border-radius:12px;overflow:hidden;box-shadow:0 4px 24px -6px rgba(15,23,42,0.05),0 0 0 1px rgba(22,119,255,0.1)}' +
|
||||
'.h2-ledger-table .ant-table-thead>tr>th,.h2-ledger-table .ant-table-thead .ant-table-cell{white-space:nowrap;color:#0f172a!important;font-weight:600!important;font-size:12px!important;' +
|
||||
'background:#e8f4fc!important;border-bottom:1px solid #bae6fd!important;border-inline-end:1px solid #dbeafe!important;padding:0 6px!important;height:36px!important;text-align:center!important}' +
|
||||
'.h2-ledger-table .ant-table-tbody>tr:not(.ant-table-measure-row)>td{padding:4px 6px!important;vertical-align:middle!important;font-size:12px}' +
|
||||
'.h2-seq-cell{line-height:1.35;white-space:nowrap}' +
|
||||
'.h2-ledger-table .ant-table-tbody>tr.h2-row-data:hover>td{background:#f0f9ff!important}' +
|
||||
'.h2-ledger-totals-bar{display:flex;align-items:stretch;gap:0;margin-bottom:10px;border:1px solid #bae6fd;border-radius:10px;overflow:hidden;background:#f8fafc;box-shadow:0 1px 0 rgba(15,23,42,0.04)}' +
|
||||
'.h2-ledger-totals-bar__title{display:flex;align-items:center;justify-content:center;min-width:72px;padding:10px 14px;font-size:14px;font-weight:700;color:#0f172a;background:#e8f4fc;border-right:1px solid #bae6fd}' +
|
||||
'.vh2-page{font-family:Inter,system-ui,-apple-system,sans-serif}' +
|
||||
'.vh2-plate-cell .ant-select{min-width:120px}' +
|
||||
'.h2-ledger-table-wrap{border-radius:var(--ln-radius-card,12px);overflow:hidden;box-shadow:none;border:1px solid var(--ln-hairline,#e5e7eb)}' +
|
||||
'.h2-ledger-table .ant-table-thead>tr>th,.h2-ledger-table .ant-table-thead .ant-table-cell{white-space:nowrap;color:var(--ln-muted,#71717a)!important;font-weight:500!important;font-size:12px!important;' +
|
||||
'background:var(--ln-canvas-soft,#f4f4f5)!important;border-bottom:1px solid var(--ln-hairline,#e5e7eb)!important;border-inline-end:none!important;padding:10px 8px!important;height:auto!important;text-align:center!important}' +
|
||||
'.h2-ledger-table .ant-table-tbody>tr:not(.ant-table-measure-row)>td{padding:8px 8px!important;vertical-align:middle!important;font-size:13px;color:var(--ln-ink,#18181b)}' +
|
||||
'.h2-seq-cell{line-height:1.35;white-space:nowrap;font-variant-numeric:tabular-nums}' +
|
||||
'.h2-ledger-table .ant-table-tbody>tr.h2-row-data:hover>td{background:color-mix(in srgb,var(--ln-primary,#32a06e) 6%,#fff)!important}' +
|
||||
'.h2-ledger-totals-bar{display:flex;align-items:stretch;gap:0;margin-bottom:12px;border:1px solid var(--ln-hairline,#e5e7eb);border-radius:var(--ln-radius-card,12px);overflow:hidden;background:var(--ln-surface-card,#fff);box-shadow:0 1px 2px rgba(15,23,42,0.04)}' +
|
||||
'.h2-ledger-totals-bar__title{display:flex;align-items:center;justify-content:center;min-width:72px;padding:10px 14px;font-size:14px;font-weight:600;color:var(--ln-ink,#18181b);background:var(--ln-canvas-soft,#f4f4f5);border-right:1px solid var(--ln-hairline,#e5e7eb)}' +
|
||||
'.h2-ledger-totals-bar__items{display:flex;flex:1;flex-wrap:wrap}' +
|
||||
'.h2-ledger-totals-bar__item{flex:1;min-width:160px;padding:8px 20px;border-right:1px solid #e2e8f0;display:flex;flex-direction:column;justify-content:center;gap:4px}' +
|
||||
'.h2-ledger-totals-bar__item{flex:1;min-width:160px;padding:8px 20px;border-right:1px solid var(--ln-hairline,#e5e7eb);display:flex;flex-direction:column;justify-content:center;gap:4px}' +
|
||||
'.h2-ledger-totals-bar__item:last-child{border-right:none}' +
|
||||
'.h2-ledger-totals-bar__label{font-size:12px;color:rgba(15,23,42,0.55);font-weight:500;line-height:1.2}' +
|
||||
'.h2-ledger-totals-bar__value{font-size:16px;font-weight:700;color:#0f172a;font-variant-numeric:tabular-nums;line-height:1.3}' +
|
||||
'.h2-cell-readonly{color:#64748b;background:#f8fafc;padding:4px 6px}' +
|
||||
'.h2-cell-auto{color:#475569;background:#f1f5f9;padding:4px 6px}' +
|
||||
'.h2-cell-warn{color:#874d00;background:#fffbe6!important;border:1px solid #ffe58f}' +
|
||||
'.h2-ledger-totals-bar__label{font-size:12px;color:var(--ln-muted,#71717a);font-weight:500;line-height:1.2}' +
|
||||
'.h2-ledger-totals-bar__value{font-size:16px;font-weight:700;color:var(--ln-ink,#18181b);font-variant-numeric:tabular-nums;line-height:1.3}' +
|
||||
'.h2-cell-readonly{color:var(--ln-muted,#71717a);background:var(--ln-canvas-soft,#f4f4f5);padding:4px 6px;border-radius:6px}' +
|
||||
'.h2-cell-auto{color:var(--ln-body,#52525b);background:var(--ln-surface-strong,#f4f4f5);padding:4px 6px;border-radius:6px}' +
|
||||
'.h2-cell-warn{color:#874d00;background:#fffbe6!important;border:1px solid #ffe58f;border-radius:6px}' +
|
||||
'.h2-customer-warn-cell,.h2-cost-warn-cell{display:inline-flex;align-items:center;justify-content:center;gap:4px;max-width:100%}' +
|
||||
'.h2-cost-warn-cell{justify-content:flex-end;gap:4px}' +
|
||||
'.h2-cost-price-type-tag{flex-shrink:0}' +
|
||||
@@ -3322,11 +3357,11 @@ const Component = function () {
|
||||
'.h2-warn-cost-pop .ant-popover-inner-content{padding:10px 12px}' +
|
||||
'.h2-warn-input-wrap .ant-input-number,.h2-warn-input-wrap .ant-input-number-input{background:#fffbe6!important}' +
|
||||
'.h2-ledger-table .ant-picker.h2-datetime-picker{min-width:178px}' +
|
||||
'.h2-row-tier-0>td{background:#f8fafc!important}' +
|
||||
'.h2-row-tier-0>td{background:var(--ln-canvas-soft,#f8fafc)!important}' +
|
||||
'.h2-row-tier-2>td{background:#fffbeb!important}' +
|
||||
'.h2-row-tier-boundary>td{border-top:2px solid #7dd3fc!important}' +
|
||||
'.h2-row-tier-boundary>td{border-top:2px solid color-mix(in srgb,var(--ln-primary,#32a06e) 35%,#fff)!important}' +
|
||||
'.h2-action-icon-btn{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:4px;cursor:pointer;color:rgba(15,23,42,0.55);transition:background .15s,color .15s}' +
|
||||
'.h2-action-icon-btn:hover{background:#f0f9ff;color:#1677ff}' +
|
||||
'.h2-action-icon-btn:hover{background:color-mix(in srgb,var(--ln-primary,#32a06e) 10%,#fff);color:var(--ln-primary,#32a06e)}' +
|
||||
'.h2-action-icon-btn.h2-action-icon-danger:hover{background:#fff1f0;color:#ff4d4f}' +
|
||||
'.h2-action-icon-btn.is-disabled{color:rgba(15,23,42,0.25);cursor:not-allowed;pointer-events:none}' +
|
||||
'.h2-row-more-btn.h2-action-icon-btn:hover{background:#f5f5f5;color:rgba(15,23,42,0.75)}' +
|
||||
@@ -3864,9 +3899,11 @@ const Component = function () {
|
||||
var keySet = {};
|
||||
selectedRowKeys.forEach(function (k) { keySet[k] = true; });
|
||||
var selected = allRows.filter(function (r) { return keySet[r.key]; });
|
||||
var invalid = selected.filter(function (r) { return !isPendingRow(r) || isVerifiedRow(r); });
|
||||
var invalid = selected.filter(function (r) {
|
||||
return !isPendingRow(r) || isVerifiedRow(r) || !showVerifyReconcileFields(r);
|
||||
});
|
||||
if (invalid.length) {
|
||||
message.warning('仅可对「已保存且未核对」的记录完成核对');
|
||||
message.warning('仅可对「已保存且未核对」的羚牛车辆记录完成核对');
|
||||
return;
|
||||
}
|
||||
var verifiedAt = nowDayjs();
|
||||
@@ -4619,14 +4656,21 @@ const Component = function () {
|
||||
title: reqTitle('车牌号'),
|
||||
dataIndex: 'plateNo',
|
||||
key: 'plateNo',
|
||||
width: 132,
|
||||
width: 220,
|
||||
align: 'center',
|
||||
render: function (v, record) {
|
||||
var source = recordSourceLabel(record);
|
||||
var tag = React.createElement(Tag, {
|
||||
var kind = vehicleKindLabel(v);
|
||||
var sourceTag = React.createElement(Tag, {
|
||||
color: source === '站端上传' ? 'green' : 'blue',
|
||||
style: { margin: 0, borderRadius: 4, fontSize: 11, lineHeight: '18px', fontWeight: 600 }
|
||||
}, source);
|
||||
var kindTag = kind
|
||||
? React.createElement(Tag, {
|
||||
color: isLingniuVehiclePlate(v) ? 'green' : 'orange',
|
||||
style: { margin: 0, borderRadius: 4, fontSize: 11, lineHeight: '18px', fontWeight: 600 }
|
||||
}, kind)
|
||||
: null;
|
||||
var plateNode = !canEditRow(record)
|
||||
? readOnlyCell(v)
|
||||
: React.createElement(Select, {
|
||||
@@ -4641,9 +4685,24 @@ const Component = function () {
|
||||
notFoundContent: '车辆不存在',
|
||||
onChange: function (val) { patch(record.key, 'plateNo', val); }
|
||||
});
|
||||
return React.createElement('div', {
|
||||
style: { display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4 }
|
||||
}, plateNode, tag);
|
||||
return React.createElement(
|
||||
'div',
|
||||
{
|
||||
className: 'vh2-plate-cell',
|
||||
style: {
|
||||
display: 'inline-flex',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 4,
|
||||
maxWidth: '100%'
|
||||
}
|
||||
},
|
||||
plateNode,
|
||||
kindTag,
|
||||
sourceTag
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -4827,7 +4886,8 @@ const Component = function () {
|
||||
onHeaderCell: function () {
|
||||
return { 'data-annotation-id': 'vh2-col-reconcile-date' };
|
||||
},
|
||||
render: function (v) {
|
||||
render: function (v, record) {
|
||||
if (!showVerifyReconcileFields(record)) return readOnlyCell('—');
|
||||
if (!v) return readOnlyCell('-');
|
||||
var text = String(v);
|
||||
if (text.indexOf(':') >= 0) return readOnlyCell(text.length > 16 ? text.slice(0, 16) : text);
|
||||
@@ -4840,7 +4900,8 @@ const Component = function () {
|
||||
key: 'reconciledByName',
|
||||
width: 88,
|
||||
align: 'center',
|
||||
render: function (v) {
|
||||
render: function (v, record) {
|
||||
if (!showVerifyReconcileFields(record)) return readOnlyCell('—');
|
||||
return readOnlyCell(v || '—');
|
||||
}
|
||||
},
|
||||
@@ -4901,6 +4962,9 @@ const Component = function () {
|
||||
width: 88,
|
||||
align: 'center',
|
||||
render: function (_, record) {
|
||||
if (!showVerifyReconcileFields(record)) {
|
||||
return React.createElement('span', { style: { color: '#cbd5e1' } }, '—');
|
||||
}
|
||||
var label = rowVerifyLabel(record);
|
||||
if (!label) return React.createElement('span', { style: { color: '#cbd5e1' } }, '—');
|
||||
var color = label === '已核对' ? 'green' : 'orange';
|
||||
@@ -4913,7 +4977,8 @@ const Component = function () {
|
||||
key: 'verifiedAt',
|
||||
width: 148,
|
||||
align: 'center',
|
||||
render: function (v) {
|
||||
render: function (v, record) {
|
||||
if (!showVerifyReconcileFields(record)) return readOnlyCell('—');
|
||||
if (!v) return readOnlyCell('—');
|
||||
return readOnlyCell(formatDateTime(v) || '—');
|
||||
}
|
||||
@@ -4924,7 +4989,8 @@ const Component = function () {
|
||||
key: 'verifiedByName',
|
||||
width: 88,
|
||||
align: 'center',
|
||||
render: function (v) {
|
||||
render: function (v, record) {
|
||||
if (!showVerifyReconcileFields(record)) return readOnlyCell('—');
|
||||
return readOnlyCell(v || '—');
|
||||
}
|
||||
},
|
||||
@@ -4934,6 +5000,9 @@ const Component = function () {
|
||||
width: 88,
|
||||
align: 'center',
|
||||
render: function (_, record) {
|
||||
if (!showVerifyReconcileFields(record)) {
|
||||
return React.createElement('span', { style: { color: '#cbd5e1' } }, '—');
|
||||
}
|
||||
var label = rowStatusLabel(record);
|
||||
if (!label) return React.createElement('span', { style: { color: '#cbd5e1' } }, '—');
|
||||
var color = label === '已对账' ? 'blue' : 'default';
|
||||
@@ -5340,15 +5409,16 @@ const Component = function () {
|
||||
React.createElement('style', null, ledgerTableStyle),
|
||||
React.createElement(
|
||||
'div',
|
||||
{ style: layoutStyle },
|
||||
{ className: 'vm-page ldb-page vh2-page', style: layoutStyle, 'data-annotation-id': 'vh2-page' },
|
||||
React.createElement(
|
||||
'div',
|
||||
{
|
||||
'data-annotation-id': 'vh2-header',
|
||||
className: 'ldb-page-header',
|
||||
style: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
justifyContent: 'flex-end',
|
||||
marginBottom: 12,
|
||||
gap: 16
|
||||
}
|
||||
|
||||
@@ -209,12 +209,12 @@
|
||||
"path": []
|
||||
},
|
||||
"aiPrompt": "列表字段、行内编辑、客户校验与操作列。",
|
||||
"annotationText": "# 列表字段与操作\n\n**核心可编辑字段**(未对账/待保存):加氢时间、站点、车牌、加氢量、成本单价、加氢单价、承担方式等。\n\n**来源标签**(车牌下方):站端主动上报→「站端上传」;本页人工新增→「羚牛上传」。\n\n**系统带出只读**:业务员、开票/付款相关字段;核对时间/核对人(完成核对写入);对账日期/对账人(对账单提交写入)。\n\n## 成本单价列\n\n- 单价前展示 **固定 / 阶梯** 标签(与加氢站价格配置一致)\n- 阶梯价悬浮:当前阶梯量、对应单价\n- 校验图标:正确绿色 ✓ / 异常红色 ⚠\n- **一键替换为标准成本单价**(未对账;创建人或主管),写入变更日志\n\n## 客户名称列\n\n- 与系统登记不一致时整行标红,单元格水印「客户疑似有误」\n- 客户名称后 **警示图标**:悬浮显示异常说明(系统客户、交车时间等)\n- **一键替换为系统客户**(仅「与系统登记客户不一致」场景,未对账数据可操作)\n- 替换写入变更日志\n\n**行操作**(更多菜单):编辑、删除、复制多行、变更日志(时间/操作人/修改项/原值/新值)。\n\n**列表勾选**:仅未对账记录可勾选,用于批量完成核对。\n\n表头支持批量改价(成本单价、加氢单价、承担方式)。",
|
||||
"annotationText": "# 列表字段与操作\n\n**核心可编辑字段**(未对账/待保存):加氢时间、站点、车牌、加氢量、成本单价、加氢单价、承担方式等。\n\n**车辆类型**(车牌下方):系统车辆→「羚牛车辆」,否则→「非羚牛车辆」。\n\n**来源标签**:站端主动上报→「站端上传」;本页人工新增→「羚牛上传」。\n\n**非羚牛车辆**:核对状态/时间/人、对账状态/日期/人显示为空;不可完成核对。\n\n**系统带出只读**:业务员、开票/付款相关字段;核对时间/核对人(完成核对写入);对账日期/对账人(对账单提交写入)。\n\n## 成本单价列\n\n- 单价前展示 **固定 / 阶梯** 标签(与加氢站价格配置一致)\n- 阶梯价悬浮:当前阶梯量、对应单价\n- 校验图标:正确绿色 ✓ / 异常红色 ⚠\n- **一键替换为标准成本单价**(未对账;创建人或主管),写入变更日志\n\n## 客户名称列\n\n- 与系统登记不一致时整行标红,单元格水印「客户疑似有误」\n- 客户名称后 **警示图标**:悬浮显示异常说明(系统客户、交车时间等)\n- **一键替换为系统客户**(仅「与系统登记客户不一致」场景,未对账数据可操作)\n- 替换写入变更日志\n\n**行操作**(更多菜单):编辑、删除、复制多行、变更日志(时间/操作人/修改项/原值/新值)。\n\n**列表勾选**:仅未对账记录可勾选,用于批量完成核对。\n\n表头支持批量改价(成本单价、加氢单价、承担方式)。",
|
||||
"hasMarkdown": true,
|
||||
"color": "#334155",
|
||||
"images": [],
|
||||
"createdAt": 1782806400000,
|
||||
"updatedAt": 1784520000000,
|
||||
"updatedAt": 1784523600000,
|
||||
"controls": []
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/**
|
||||
* @name 车辆氢费明细
|
||||
* 设计规范:ledger-shared/DESIGN.md · vm-page + ldb-page(与维修明细 / 租赁台账对齐)
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
@@ -8,6 +9,8 @@ import {
|
||||
type AnnotationViewerOptions
|
||||
} from '@axhub/annotation';
|
||||
import { PrototypeAnnotationHost } from '../../common/prototype-annotation-host';
|
||||
import '../vehicle-management/style.css';
|
||||
import './styles/index.css';
|
||||
import './globals';
|
||||
import 'antd/dist/reset.css';
|
||||
import H2LedgerPage from './H2LedgerPage.jsx';
|
||||
|
||||
@@ -59,14 +59,10 @@ function FleetModelCard({
|
||||
);
|
||||
}
|
||||
|
||||
function FleetOverviewModal({
|
||||
open,
|
||||
onClose,
|
||||
function FleetShareBody({
|
||||
segmentCounts,
|
||||
onOpenDetail,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
segmentCounts: Record<OnLeaseFleetSegment, number>;
|
||||
onOpenDetail: (item: OnLeaseFleetBucket) => void;
|
||||
}) {
|
||||
@@ -87,6 +83,58 @@ function FleetOverviewModal({
|
||||
? Math.max(...summary.map((item) => item.count))
|
||||
: 1;
|
||||
|
||||
return (
|
||||
<div className="vm-fleet-overview-modal__body">
|
||||
<div className="vm-fleet-overview-modal__brands" role="tablist" aria-label="业务线筛选">
|
||||
{ON_LEASE_FLEET_SEGMENTS.map((segment) => (
|
||||
<button
|
||||
key={segment}
|
||||
type="button"
|
||||
role="tab"
|
||||
className={`vm-fleet-brand-chip${segmentFilter === segment ? ' is-active' : ''}`}
|
||||
onClick={() => setSegmentFilter(segment)}
|
||||
aria-selected={segmentFilter === segment}
|
||||
>
|
||||
{segment}
|
||||
<span className="vm-fleet-brand-chip__count tabular-nums">{segmentCounts[segment]}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{summary.length ? (
|
||||
<div className="vm-fleet-overview-modal__grid" role="list">
|
||||
{summary.map((item) => (
|
||||
<FleetModelCard
|
||||
key={item.key}
|
||||
item={item}
|
||||
totalOnLease={totalOnLease}
|
||||
maxCount={maxCount}
|
||||
onOpenDetail={onOpenDetail}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="vm-fleet-overview-modal__empty">
|
||||
<p>该业务线暂无在租车辆</p>
|
||||
<button type="button" className="vm-btn vm-btn-ghost" data-vm-icon="eye" onClick={() => setSegmentFilter('全部')}>
|
||||
查看全部
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FleetOverviewModal({
|
||||
open,
|
||||
onClose,
|
||||
segmentCounts,
|
||||
onOpenDetail,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
segmentCounts: Record<OnLeaseFleetSegment, number>;
|
||||
onOpenDetail: (item: OnLeaseFleetBucket) => void;
|
||||
}) {
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
@@ -102,51 +150,13 @@ function FleetOverviewModal({
|
||||
<header className="vm-modal-header">
|
||||
<div>
|
||||
<h2 id="vm-fleet-overview-title">在租车型占比</h2>
|
||||
<p>
|
||||
当前筛选共 <span className="tabular-nums">{totalOnLease}</span> 辆在租,展示各品牌型号数量及占当前筛选总数的占比
|
||||
</p>
|
||||
<p>按业务线筛选,展示各品牌型号数量及占当前筛选总数的占比</p>
|
||||
</div>
|
||||
<button type="button" className="vm-modal-close" onClick={onClose} aria-label="关闭">
|
||||
<X size={18} aria-hidden />
|
||||
</button>
|
||||
</header>
|
||||
<div className="vm-fleet-overview-modal__body">
|
||||
<div className="vm-fleet-overview-modal__brands" role="tablist" aria-label="业务线筛选">
|
||||
{ON_LEASE_FLEET_SEGMENTS.map((segment) => (
|
||||
<button
|
||||
key={segment}
|
||||
type="button"
|
||||
role="tab"
|
||||
className={`vm-fleet-brand-chip${segmentFilter === segment ? ' is-active' : ''}`}
|
||||
onClick={() => setSegmentFilter(segment)}
|
||||
aria-selected={segmentFilter === segment}
|
||||
>
|
||||
{segment}
|
||||
<span className="vm-fleet-brand-chip__count tabular-nums">{segmentCounts[segment]}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{summary.length ? (
|
||||
<div className="vm-fleet-overview-modal__grid" role="list">
|
||||
{summary.map((item) => (
|
||||
<FleetModelCard
|
||||
key={item.key}
|
||||
item={item}
|
||||
totalOnLease={totalOnLease}
|
||||
maxCount={maxCount}
|
||||
onOpenDetail={onOpenDetail}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="vm-fleet-overview-modal__empty">
|
||||
<p>该业务线暂无在租车辆</p>
|
||||
<button type="button" className="vm-btn vm-btn-ghost" data-vm-icon="eye" onClick={() => setSegmentFilter('全部')}>
|
||||
查看全部
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<FleetShareBody segmentCounts={segmentCounts} onOpenDetail={onOpenDetail} />
|
||||
<footer className="vm-modal-footer">
|
||||
<button type="button" className="vm-btn vm-btn-primary" onClick={onClose} data-vm-icon="x">关闭</button>
|
||||
</footer>
|
||||
@@ -155,6 +165,114 @@ function FleetOverviewModal({
|
||||
);
|
||||
}
|
||||
|
||||
/** 工作台等场景:直接内联展示在租车型占比仪表(无需点击 KPI 卡打开弹层) */
|
||||
export function OnLeaseFleetSharePanel({ emptyText }: { emptyText?: string }) {
|
||||
const fleetSourceList = useMemo(() => buildOnLeaseFleetSourceList(), []);
|
||||
const segmentCounts = useMemo(
|
||||
() => countOnLeaseFleetBySegment(fleetSourceList),
|
||||
[fleetSourceList],
|
||||
);
|
||||
const [detailItem, setDetailItem] = useState<OnLeaseFleetBucket | null>(null);
|
||||
const [segmentFilter, setSegmentFilter] = useState<OnLeaseFleetSegment>('全部');
|
||||
|
||||
const summary = useMemo(
|
||||
() => buildOnLeaseFleetSummary(fleetSourceList, segmentFilter),
|
||||
[fleetSourceList, segmentFilter],
|
||||
);
|
||||
const totalOnLease = useMemo(
|
||||
() => summary.reduce((sum, item) => sum + item.count, 0),
|
||||
[summary],
|
||||
);
|
||||
|
||||
if (segmentCounts.全部 <= 0) {
|
||||
return (
|
||||
<p className="wb-insights-fleet-share-empty">
|
||||
{emptyText || '暂无在租车辆,无法查看车型占比'}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="vm-fleet-share-panel" data-annotation-id="vm-fleet-share-panel">
|
||||
<div className="vm-fleet-share-panel__head">
|
||||
<h4 className="vm-fleet-share-panel__title">在租车型占比</h4>
|
||||
<p className="vm-fleet-share-panel__sub">
|
||||
当前筛选共 <span className="tabular-nums">{totalOnLease}</span> 辆在租
|
||||
</p>
|
||||
</div>
|
||||
<div className="vm-fleet-overview-modal__brands" role="tablist" aria-label="业务线筛选">
|
||||
{ON_LEASE_FLEET_SEGMENTS.map((segment) => (
|
||||
<button
|
||||
key={segment}
|
||||
type="button"
|
||||
role="tab"
|
||||
className={`vm-fleet-brand-chip${segmentFilter === segment ? ' is-active' : ''}`}
|
||||
onClick={() => setSegmentFilter(segment)}
|
||||
aria-selected={segmentFilter === segment}
|
||||
>
|
||||
{segment}
|
||||
<span className="vm-fleet-brand-chip__count tabular-nums">{segmentCounts[segment]}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{summary.length ? (
|
||||
<div className="vm-fleet-share-gauges" role="list" aria-label="在租车型占比仪表">
|
||||
{summary.map((item) => {
|
||||
const share = totalOnLease ? Math.round((item.count / totalOnLease) * 100) : 0;
|
||||
const r = 34;
|
||||
const c = 2 * Math.PI * r;
|
||||
const dash = (Math.max(4, Math.min(100, share)) / 100) * c;
|
||||
return (
|
||||
<button
|
||||
key={item.key}
|
||||
type="button"
|
||||
className="vm-fleet-share-gauge"
|
||||
role="listitem"
|
||||
onClick={() => setDetailItem(item)}
|
||||
aria-label={`${item.brand} ${item.model},${item.count} 辆,占 ${share}%`}
|
||||
>
|
||||
<span className="vm-fleet-share-gauge__ring" aria-hidden>
|
||||
<svg viewBox="0 0 96 96" className="vm-fleet-share-gauge__svg">
|
||||
<circle className="vm-fleet-share-gauge__track" cx="48" cy="48" r={r} />
|
||||
<circle
|
||||
className="vm-fleet-share-gauge__arc"
|
||||
cx="48"
|
||||
cy="48"
|
||||
r={r}
|
||||
strokeDasharray={`${dash} ${c - dash}`}
|
||||
transform="rotate(-90 48 48)"
|
||||
/>
|
||||
</svg>
|
||||
<span className="vm-fleet-share-gauge__center">
|
||||
<strong className="tabular-nums">{share}%</strong>
|
||||
<span className="tabular-nums">{item.count} 辆</span>
|
||||
</span>
|
||||
</span>
|
||||
<span className="vm-fleet-share-gauge__brand">{item.brand}</span>
|
||||
<span className="vm-fleet-share-gauge__model" title={item.model}>{item.model}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="vm-fleet-overview-modal__empty">
|
||||
<p>该业务线暂无在租车辆</p>
|
||||
<button type="button" className="vm-btn vm-btn-ghost" data-vm-icon="eye" onClick={() => setSegmentFilter('全部')}>
|
||||
查看全部
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<FleetDetailModal
|
||||
open={!!detailItem}
|
||||
item={detailItem}
|
||||
onClose={() => setDetailItem(null)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function FleetDetailModal({
|
||||
open,
|
||||
item,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,11 @@
|
||||
# 车辆氢费明细 — 产品需求说明(PRD)
|
||||
|
||||
> **与站端加氢订单 / Bridge 口径冲突时的权威顺序(2026-07-18)**
|
||||
> 本文为历史业务版说明;其中「保存→未对账→完成对账」等命名与现行「**核对**(能源部)≠ **对账**(站点对账单)」二分不完全同词。
|
||||
> **站端 H5 / Web 加氢记录 / 车辆氢费现行联动**以以下文档为准,勿用本文覆盖:
|
||||
> - [`vehicle-h2-fee-ledger/.spec/verify-reconcile.md`](../../../prototypes/vehicle-h2-fee-ledger/.spec/verify-reconcile.md)
|
||||
> - [`oneos-h5-h2-order/.spec/reconcile-linkage.md`](../../../prototypes/oneos-h5-h2-order/.spec/reconcile-linkage.md)
|
||||
|
||||
| 项目 | 内容 |
|
||||
|------|------|
|
||||
| 文档版本 | v2.1(业务版) |
|
||||
|
||||
Reference in New Issue
Block a user