迭代加氢记录与氢费明细:来源标签、变更日志、核对人/对账人,日统计默认收起;统一筛选规范与导航,下线合同模板原型。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
王冕
2026-07-18 12:30:22 +08:00
parent 7987536ea9
commit c3691be29d
27 changed files with 3328 additions and 4365 deletions

View File

@@ -1,13 +1,7 @@
{ {
"version": 1, "version": 1,
"updatedAt": "2026-07-16T01:04:51.317Z", "updatedAt": "2026-07-18T02:30:01.518Z",
"prototypes": [ "prototypes": [
{
"id": "item-prototypes-oneos-web-approval",
"kind": "item",
"title": "oneos web approval",
"itemKey": "prototypes/oneos-web-approval"
},
{ {
"id": "folder-1783875936186-27raza", "id": "folder-1783875936186-27raza",
"kind": "folder", "kind": "folder",
@@ -19,12 +13,6 @@
"title": "业务管理", "title": "业务管理",
"itemKey": "prototypes/oneos-web-business" "itemKey": "prototypes/oneos-web-business"
}, },
{
"id": "item:prototypes:oneos-web-contract-template",
"kind": "item",
"title": "合同模板管理",
"itemKey": "prototypes/oneos-web-contract-template"
},
{ {
"id": "item:prototypes:oneos-web-data-analysis", "id": "item:prototypes:oneos-web-data-analysis",
"kind": "item", "kind": "item",
@@ -188,7 +176,7 @@
{ {
"id": "item:prototypes:oneos-h5-h2-order", "id": "item:prototypes:oneos-h5-h2-order",
"kind": "item", "kind": "item",
"title": "加氢订单", "title": "加氢记录H5",
"itemKey": "prototypes/oneos-h5-h2-order" "itemKey": "prototypes/oneos-h5-h2-order"
}, },
{ {
@@ -299,6 +287,19 @@
} }
] ]
}, },
{
"id": "folder-1784341576489-spa7ms",
"kind": "folder",
"title": "审批中心",
"children": [
{
"id": "item:prototypes:oneos-web-approval",
"kind": "item",
"title": "审批中心",
"itemKey": "prototypes/oneos-web-approval"
}
]
},
{ {
"id": "item:prototypes:lease-business-line-overview", "id": "item:prototypes:lease-business-line-overview",
"kind": "item", "kind": "item",

View File

@@ -146,25 +146,53 @@ import '../vehicle-management/style.css';
| 元素 | 类名 | 规格 | | 元素 | 类名 | 规格 |
|---|---|---| |---|---|---|
| 容器 | `vm-filter-card` | 白底、12px 圆角、1px 边框、轻阴影 | | 容器 | `vm-filter-card`(兼容 `lc-filter-card` | 白底、12px 圆角、1px 边框、轻阴影 |
| 标题 | `vm-filter-title` | 「筛选条件」1.125rem / 600 | | 标题 | `vm-filter-title` | 「筛选条件」1.125rem / 600 |
| 栅格 | `vm-filter-grid` | 默认 4 列1280px 以下 2 列640px 以下 1 列 | | 栅格 | `vm-filter-grid` | 默认 4 列1280px 以下 2 列640px 以下 1 列 |
| 字段 | `vm-filter-field` | 标签在上、控件在下;标签 12px / muted | | 字段 | `vm-filter-field` | 标签在上、控件在下;标签 12px / muted |
| 操作区 | `vm-filter-actions` | 顶部分割线;右对齐「重置」「查询」 | | 操作区 | `vm-filter-actions` | 顶部分割线;左侧「更多筛选/收起」,右侧「重置」「查询」 |
**日期区间(强制)** **参考实现(强制对齐**[`lease-business-detail/components/FilterPanel.tsx`](../src/prototypes/lease-business-detail/components/FilterPanel.tsx)
#### 4.1.1 筛选提示文本placeholder— 强制统一
筛选卡内所有空态提示输入框、FilterPicker、日期区间、Ant Select必须一致
| 属性 | Token / 值 |
|---|---|
| 字体 | `var(--vm-font)` |
| 字号 | `0.875rem`14px |
| 字重 | `400` |
| 颜色 | `var(--ln-muted-soft)``#a1a1aa` |
| 透明度 | `opacity: 1`(覆盖 Ant Select 默认半透明) |
样式落点:`vehicle-management/style.css``.vm-filter-card` / `.lc-filter-card` 下的 `::placeholder``.ant-select-selection-placeholder`
**禁止**各页单独改占位色、字号或字重。
#### 4.1.2 日期区间(强制)
- 使用 `DateRangeFilterField``vehicle-management/components/DateRangeFilterField`)。 - 使用 `DateRangeFilterField``vehicle-management/components/DateRangeFilterField`)。
- 展示分隔符为中文「**至**」。 - 展示分隔符为中文「**至**」。
- 类名:`vm-date-range-field``vm-filter-picker-control` - 类名:`vm-date-range-field``vm-filter-picker-control`
- 弹层必须 portal + `fixed`(避免滚动容器错位)。
- **禁止**筛选区用 Ant `RangePicker`、双 `<input type="date">` 等替代方案。 - **禁止**筛选区用 Ant `RangePicker`、双 `<input type="date">` 等替代方案。
**折叠筛选** #### 4.1.3 更多筛选 / 收起(强制)
- 超过 4 项时其余放入 `vm-filter-expand`(见 `src/common/vm-filter-panel.ts`)。 超过 4 项时必须折叠;实现与物流业务明细相同:
- 筛选项数组经 `splitFilterFields()` 拆为首行 + 展开区;首行固定最多 4 项,**禁止**首行放 5 项导致第二行孤零零 1 项。
- 展开区项数不得为 4n+1避免展开后末行仅 1 项);由 `splitFilterFields` 自动从首行借调平衡。 | 项 | 规则 |
- 切换按钮:`vm-filter-toggle` + 角标 `vm-filter-toggle-badge` |---|---|
| 拆分 | `splitFilterFields()` / `shouldShowFilterExpand()`[`src/common/vm-filter-panel.ts`](../src/common/vm-filter-panel.ts) |
| 首行 | 最多 4 项;**禁止**首行 5 项导致第二行仅 1 项 |
| 展开区 | `vm-filter-expand` + `is-expanded`;内层 `vm-filter-expand-inner` + 独立 `vm-filter-grid` |
| 文案 | 仅「更多筛选」/「收起」(禁用「展开」) |
| 按钮 | `vm-btn vm-btn-link vm-filter-toggle``data-vm-icon``filter` / `chevron-up` |
| 角标 | 收起且扩展项有值时显示 `vm-filter-toggle-badge`(主色底、白字) |
| 操作区布局 | 切换在左(`margin-right: auto`),重置/查询在右 |
| 别名 | `ldb-filter-expand` / `ldb-filter-toggle` 等同 `vm-filter-*`**新代码优先 `vm-filter-*`** |
新建或改动列表页必须遵循;旧页触及时迁移。
### 4.2 输入框 `.vm-input` ### 4.2 输入框 `.vm-input`
@@ -174,15 +202,16 @@ import '../vehicle-management/style.css';
| 表单页 | min 44px | 1rem移动端友好 | | 表单页 | min 44px | 1rem移动端友好 |
- 边框:`--ln-hairline-strong`focus 时主题色边框。 - 边框:`--ln-hairline-strong`focus 时主题色边框。
- 占位符:`--ln-muted-soft` - 占位符:见 §4.1.1`--ln-muted-soft`0.875rem / 400
### 4.3 下拉选择 `.vm-filter-picker-*` ### 4.3 下拉选择 `.vm-filter-picker-*`
| 元素 | 说明 | | 元素 | 说明 |
|---|---| |---|---|
| `vm-filter-picker-control` | 触发器,与输入框同高 | | `vm-filter-picker-control` | 触发器,与输入框同高32px |
| `vm-filter-picker-popover` | 下拉面板,卡片圆角 + hover 阴影 | | `vm-filter-picker-popover` | 下拉面板,卡片圆角 + hover 阴影 |
| `vm-filter-picker-option.checked` | 主色浅底 + 深绿字 | | `vm-filter-picker-option.checked` | 主色浅底 + 深绿字 |
| 占位符 | 见 §4.1.1与日期区间、Ant Select 同色同字号 |
### 4.4 按钮 `.vm-btn` ### 4.4 按钮 `.vm-btn`

View File

@@ -7,6 +7,9 @@ var H2_RECONCILE_RECONCILED = 'reconciled';
var H2_RECONCILE_PENDING = 'pending'; var H2_RECONCILE_PENDING = 'pending';
var H2_VERIFY_VERIFIED = 'verified'; var H2_VERIFY_VERIFIED = 'verified';
var H2_VERIFY_UNVERIFIED = 'unverified'; var H2_VERIFY_UNVERIFIED = 'unverified';
/** 记录来源:站端上传 | 羚牛上传(车辆氢费明细人工录入) */
var H2_RECORD_SOURCE_STATION = 'station';
var H2_RECORD_SOURCE_LINGNIU = 'lingniu';
var H2_STATION_CODE_MAP = { var H2_STATION_CODE_MAP = {
'中国石油中油高新能源牙谷加油加氢站': 'JX-H2-001', '中国石油中油高新能源牙谷加油加氢站': 'JX-H2-001',
@@ -15,6 +18,14 @@ var H2_STATION_CODE_MAP = {
'苏州工业园区备用站': 'SZ-H2-004' '苏州工业园区备用站': 'SZ-H2-004'
}; };
var H2_STATION_CODE_SET = (function () {
var set = {};
Object.keys(H2_STATION_CODE_MAP).forEach(function (name) {
set[H2_STATION_CODE_MAP[name]] = true;
});
return set;
})();
/** 与车辆氢费明细、加氢站模块对齐的 canonical seed含本站未核对 / 已核对未对账用例) */ /** 与车辆氢费明细、加氢站模块对齐的 canonical seed含本站未核对 / 已核对未对账用例) */
var H2_CANONICAL_LEDGER_SEED = [ var H2_CANONICAL_LEDGER_SEED = [
/* 嘉兴本站 · 列表首页用例:未核对(无核对时间)→ 已核对未对账 → 已核对已对账 */ /* 嘉兴本站 · 列表首页用例:未核对(无核对时间)→ 已核对未对账 → 已核对已对账 */
@@ -33,10 +44,6 @@ var H2_CANONICAL_LEDGER_SEED = [
{ 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 }
]; ];
function h2BridgeCloneRows(rows) {
return (rows || []).map(function (r) { return Object.assign({}, r); });
}
function h2BridgeFormatDateTime(value) { function h2BridgeFormatDateTime(value) {
if (!value) return ''; if (!value) return '';
if (typeof value === 'string') return value; if (typeof value === 'string') return value;
@@ -73,6 +80,127 @@ function h2BridgeMatchStatementPatch(row, patch) {
&& (rowTime === patchTime || String(row.hydrogenTime || '') === patchTime); && (rowTime === patchTime || String(row.hydrogenTime || '') === patchTime);
} }
function h2BridgeInferRecordSource(row) {
if (!row) return H2_RECORD_SOURCE_LINGNIU;
if (row.recordSource === H2_RECORD_SOURCE_STATION || row.recordSource === H2_RECORD_SOURCE_LINGNIU) {
return row.recordSource;
}
var creator = String(row.creatorName || '').trim();
var customer = String(row.customerName || '');
if (creator === '站端账号' || customer.indexOf('·站端上报') >= 0) return H2_RECORD_SOURCE_STATION;
return H2_RECORD_SOURCE_LINGNIU;
}
function h2BridgeNormalizeRow(row) {
if (!row) return row;
var next = Object.assign({}, row);
next.recordSource = h2BridgeInferRecordSource(next);
if (!Array.isArray(next.changeLogs)) next.changeLogs = [];
if (next.verifiedByName == null) next.verifiedByName = '';
if (next.reconciledByName == null) next.reconciledByName = '';
return next;
}
function h2BridgeFormatLogScalar(value) {
if (value == null || value === '') return '—';
if (typeof value === 'number' && isFinite(value)) {
return String(Math.round(value * 100) / 100);
}
return String(value);
}
/** 站端/台账编辑时对比字段,生成变更日志条目(时间倒序由调用方 unshift */
function h2BridgeBuildFieldChangeLogs(prev, next, operatorName) {
if (!prev || !next) return [];
var fields = [
{ key: 'hydrogenTime', label: '加氢时间' },
{ key: 'plateNo', label: '车牌号' },
{ key: 'hydrogenKg', label: '加氢量' },
{ key: 'costUnitPrice', label: '氢气单价' },
{ key: 'costTotal', label: '加氢总额', alt: 'customerAmount' },
{ key: 'mileageKm', label: '里程' }
];
var logs = [];
var now = new Date();
var nowStr = now.getFullYear() + '-' + h2BridgePad2(now.getMonth() + 1) + '-' + h2BridgePad2(now.getDate())
+ ' ' + h2BridgePad2(now.getHours()) + ':' + h2BridgePad2(now.getMinutes()) + ':' + h2BridgePad2(now.getSeconds());
fields.forEach(function (f) {
var before = prev[f.key];
var after = next[f.key];
if (before == null && f.alt) before = prev[f.alt];
if (after == null && f.alt) after = next[f.alt];
var beforeText = h2BridgeFormatLogScalar(before);
var afterText = h2BridgeFormatLogScalar(after);
if (beforeText === afterText) return;
logs.push({
id: 'clog-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8),
at: nowStr,
operatorName: operatorName || next.creatorName || '系统',
userName: operatorName || next.creatorName || '系统',
field: f.key,
fieldLabel: f.label,
oldValue: beforeText,
newValue: afterText,
before: beforeText,
after: afterText
});
});
return logs;
}
function h2BridgeEnrichSeedDefaults(row) {
var next = h2BridgeNormalizeRow(row);
if (next.verifyStatus === H2_VERIFY_VERIFIED && !next.verifiedByName) {
next.verifiedByName = next.creatorName === '站端账号' ? '李业务' : (next.creatorName || '李业务');
}
if (next.reconcileStatus === H2_RECONCILE_RECONCILED && !next.reconciledByName) {
next.reconciledByName = '系统管理员';
}
if (next.key === 'rf-13' && (!next.changeLogs || !next.changeLogs.length)) {
next.changeLogs = [
{
id: 'clog-seed-rf13-1',
at: '2026-07-14 11:20:00',
operatorName: '站端账号',
userName: '站端账号',
field: 'hydrogenKg',
fieldLabel: '加氢量',
oldValue: '13.50',
newValue: '14.00',
before: '13.50',
after: '14.00'
}
];
}
if (next.key === 'rf-1' && (!next.changeLogs || !next.changeLogs.length)) {
next.changeLogs = [
{
id: 'clog-seed-rf1-1',
at: '2026-05-28 11:05:00',
operatorName: '张三',
userName: '张三',
field: 'costUnitPrice',
fieldLabel: '氢气单价',
oldValue: '42.00',
newValue: '42.50',
before: '42.00',
after: '42.50'
}
];
}
return next;
}
function h2BridgeCloneRows(rows) {
return (rows || []).map(function (r) {
var cloned = Object.assign({}, r);
if (Array.isArray(r.changeLogs)) {
cloned.changeLogs = r.changeLogs.map(function (log) { return Object.assign({}, log); });
}
return h2BridgeNormalizeRow(cloned);
});
}
function h2BridgeApplyStatementPatches(rows, patches) { function h2BridgeApplyStatementPatches(rows, patches) {
if (!patches || !patches.length) return h2BridgeCloneRows(rows); if (!patches || !patches.length) return h2BridgeCloneRows(rows);
return (rows || []).map(function (r) { return (rows || []).map(function (r) {
@@ -84,22 +212,24 @@ function h2BridgeApplyStatementPatches(rows, patches) {
break; break;
} }
} }
if (!patch) return r; if (!patch) return h2BridgeNormalizeRow(r);
return Object.assign({}, r, { return h2BridgeNormalizeRow(Object.assign({}, r, {
statementPatched: true, statementPatched: true,
reconcileStatus: H2_RECONCILE_RECONCILED, reconcileStatus: H2_RECONCILE_RECONCILED,
reconcileDate: patch.reconcileDate, reconcileDate: patch.reconcileDate,
receiptDate: patch.receiptDate, receiptDate: patch.receiptDate,
stationPaymentStatus: patch.paymentStatus || 'paid', stationPaymentStatus: patch.paymentStatus || 'paid',
paymentStatus: patch.paymentStatus || 'paid', paymentStatus: patch.paymentStatus || 'paid',
statementRecordId: patch.statementRecordId != null ? patch.statementRecordId : r.statementRecordId statementRecordId: patch.statementRecordId != null ? patch.statementRecordId : r.statementRecordId,
}); reconciledByName: patch.reconciler || patch.reconciledByName || r.reconciledByName || ''
}));
}); });
} }
function h2BridgeBuildInitialRows() { function h2BridgeBuildInitialRows() {
var patches = (typeof window !== 'undefined' && window.H2_STATION_STATEMENT_LEDGER_UPDATES) || []; var patches = (typeof window !== 'undefined' && window.H2_STATION_STATEMENT_LEDGER_UPDATES) || [];
return h2BridgeApplyStatementPatches(h2BridgeCloneRows(H2_CANONICAL_LEDGER_SEED), patches); var seeded = (H2_CANONICAL_LEDGER_SEED || []).map(h2BridgeEnrichSeedDefaults);
return h2BridgeApplyStatementPatches(seeded, patches);
} }
function h2BridgePad2(n) { function h2BridgePad2(n) {
@@ -122,11 +252,15 @@ function h2BridgeManualLedgerKey(stationId, ledgerDate) {
return String(stationId || '').trim() + '|' + String(ledgerDate || '').trim(); return String(stationId || '').trim() + '|' + String(ledgerDate || '').trim();
} }
/** 解析站点编码:支持传入编码或站名(勿把站名原样当 id */
function h2BridgeResolveStationId(stationId, stationName) { function h2BridgeResolveStationId(stationId, stationName) {
var id = String(stationId || '').trim(); var a = String(stationId || '').trim();
if (id) return id; var b = String(stationName || '').trim();
var name = String(stationName || '').trim(); if (a && H2_STATION_CODE_MAP[a]) return H2_STATION_CODE_MAP[a];
return H2_STATION_CODE_MAP[name] || ''; if (b && H2_STATION_CODE_MAP[b]) return H2_STATION_CODE_MAP[b];
if (a && H2_STATION_CODE_SET[a]) return a;
if (b && H2_STATION_CODE_SET[b]) return b;
return a || '';
} }
/** 手工台账种子:昨日已上传;今日默认未上传,便于演示拦截新增 */ /** 手工台账种子:昨日已上传;今日默认未上传,便于演示拦截新增 */
@@ -204,17 +338,34 @@ function h2BridgeCreateStore() {
} }
if (!next.reconcileStatus) next.reconcileStatus = H2_RECONCILE_PENDING; if (!next.reconcileStatus) next.reconcileStatus = H2_RECONCILE_PENDING;
if (!next.verifyStatus) next.verifyStatus = H2_VERIFY_UNVERIFIED; if (!next.verifyStatus) next.verifyStatus = H2_VERIFY_UNVERIFIED;
if (!next.recordSource) next.recordSource = h2BridgeInferRecordSource(next);
var found = false; var found = false;
var saved = null;
state.rows = state.rows.map(function (r) { state.rows = state.rows.map(function (r) {
if ((r.id || r.key) === next.id || (r.id || r.key) === next.key) { if ((r.id || r.key) === next.id || (r.id || r.key) === next.key) {
found = true; found = true;
return Object.assign({}, r, next); var merged = h2BridgeNormalizeRow(Object.assign({}, r, next));
if (!next.recordSource && r.recordSource) merged.recordSource = r.recordSource;
var operator = next.changeLogOperator || next.creatorName || r.creatorName || '系统';
var fieldLogs = h2BridgeBuildFieldChangeLogs(r, merged, operator);
var prevLogs = Array.isArray(r.changeLogs) ? r.changeLogs.slice() : [];
if (Array.isArray(next.changeLogs) && next.replaceChangeLogs) {
merged.changeLogs = next.changeLogs.slice();
} else {
merged.changeLogs = fieldLogs.concat(prevLogs);
}
saved = merged;
return merged;
} }
return r; return r;
}); });
if (!found) state.rows = [next].concat(state.rows); if (!found) {
saved = h2BridgeNormalizeRow(next);
if (!Array.isArray(saved.changeLogs)) saved.changeLogs = [];
state.rows = [saved].concat(state.rows);
}
notify(); notify();
return Object.assign({}, next); return Object.assign({}, saved);
}, },
removeRow: function (id) { removeRow: function (id) {
var target = String(id || ''); var target = String(id || '');
@@ -372,6 +523,9 @@ function h2BridgeMapToHrRecord(row, index, fillerNames) {
var reconcileTime = h2BridgeResolveReconcileTime(row); var reconcileTime = h2BridgeResolveReconcileTime(row);
var isReconciled = row.reconcileStatus === H2_RECONCILE_RECONCILED || Boolean(row.reconcileDate); var isReconciled = row.reconcileStatus === H2_RECONCILE_RECONCILED || Boolean(row.reconcileDate);
var verifiedAt = row.verifiedAt || ''; var verifiedAt = row.verifiedAt || '';
var changeLogs = Array.isArray(row.changeLogs)
? row.changeLogs.map(function (log) { return Object.assign({}, log); })
: [];
return { return {
id: row.id || row.key || ('hr-' + (index + 1)), id: row.id || row.key || ('hr-' + (index + 1)),
hydrogenTime: h2BridgeFormatDateTime(row.hydrogenTime), hydrogenTime: h2BridgeFormatDateTime(row.hydrogenTime),
@@ -388,10 +542,14 @@ function h2BridgeMapToHrRecord(row, index, fillerNames) {
settlementStatus: row.settlementStatus, settlementStatus: row.settlementStatus,
stationName: row.stationName, stationName: row.stationName,
stationCode: row.stationId || H2_STATION_CODE_MAP[row.stationName] || '', stationCode: row.stationId || H2_STATION_CODE_MAP[row.stationName] || '',
recordSource: h2BridgeInferRecordSource(row),
verifyStatus: row.verifyStatus === H2_VERIFY_VERIFIED ? H2_VERIFY_VERIFIED : H2_VERIFY_UNVERIFIED, verifyStatus: row.verifyStatus === H2_VERIFY_VERIFIED ? H2_VERIFY_VERIFIED : H2_VERIFY_UNVERIFIED,
verifiedAt: verifiedAt ? h2BridgeFormatDateTime(verifiedAt) : '', verifiedAt: verifiedAt ? h2BridgeFormatDateTime(verifiedAt) : '',
verifiedByName: row.verifiedByName || '',
reconcileStatus: isReconciled ? H2_RECONCILE_RECONCILED : H2_RECONCILE_PENDING, reconcileStatus: isReconciled ? H2_RECONCILE_RECONCILED : H2_RECONCILE_PENDING,
reconcileTime: isReconciled ? reconcileTime : '' reconcileTime: isReconciled ? reconcileTime : '',
reconciledByName: row.reconciledByName || '',
changeLogs: changeLogs
}; };
} }
@@ -603,6 +761,8 @@ export {
H2_RECONCILE_PENDING, H2_RECONCILE_PENDING,
H2_VERIFY_VERIFIED, H2_VERIFY_VERIFIED,
H2_VERIFY_UNVERIFIED, H2_VERIFY_UNVERIFIED,
H2_RECORD_SOURCE_STATION,
H2_RECORD_SOURCE_LINGNIU,
h2BridgeInit, h2BridgeInit,
h2BridgeGetStore, h2BridgeGetStore,
h2BridgeGetAllRefuelRecords, h2BridgeGetAllRefuelRecords,
@@ -615,5 +775,6 @@ export {
h2BridgeNormalizeHydrogenTime, h2BridgeNormalizeHydrogenTime,
h2BridgeFindDuplicateRow, h2BridgeFindDuplicateRow,
h2BridgeTotalAmountMismatch, h2BridgeTotalAmountMismatch,
h2BridgeLookupStationUnitPrice h2BridgeLookupStationUnitPrice,
h2BridgeInferRecordSource
}; };

View File

@@ -1,13 +1,18 @@
/** /**
* 列表页筛选区全局规则(与 vehicle-management/style.css 中 .vm-filter-grid 4 列布局一致) * 列表页筛选区全局规则(与 vehicle-management/style.css 中 .vm-filter-grid 4 列布局一致)
* *
* 完整视觉与交互规范:`rules/global-design-spec.md` §4.1
* 参考实现:`src/prototypes/lease-business-detail/components/FilterPanel.tsx`
*
* ## 布局约束 * ## 布局约束
* 1. 收起时首行primary最多展示 `VM_FILTER_COLUMNS_PER_ROW`(默认 4占满 1 行。 * 1. 收起时首行primary最多展示 `VM_FILTER_COLUMNS_PER_ROW`(默认 4占满 1 行。
* 2. 展开时:其余项放入 `vm-filter-expand` / `ldb-filter-expand`primary 与 extra 各自使用 4 列栅格。 * 2. 展开时:其余项放入 `vm-filter-expand``ldb-filter-expand` 为同义别名)primary 与 extra 各自使用 4 列栅格。
* 3. 禁止「末行仅 1 项」:若展开区项数为 4n+1从首行尾部借调 1 项到展开区(首行变为 3 项)。 * 3. 禁止「末行仅 1 项」:若展开区项数为 4n+1从首行尾部借调 1 项到展开区(首行变为 3 项)。
* 典型:总项数 5 → 首行 3 + 展开 2总项数 6 → 首行 4 + 展开 2。 * 典型:总项数 5 → 首行 3 + 展开 2总项数 6 → 首行 4 + 展开 2。
* 4. 切换文案仅「更多筛选」/「收起」;按钮类名 `vm-btn vm-btn-link vm-filter-toggle`。
* *
* 实现参考:`payment-records/components/FilterPanel.tsx`、`lease-business-ledger/components/FilterPanel.tsx` * ## 占位符(筛选卡内强制)
* font: `var(--vm-font)`size: 0.875remweight: 400color: `var(--ln-muted-soft)`opacity: 1
*/ */
export const VM_FILTER_COLUMNS_PER_ROW = 4; export const VM_FILTER_COLUMNS_PER_ROW = 4;

View File

@@ -197,6 +197,10 @@
color: var(--ln-muted); color: var(--ln-muted);
} }
/*
* ldb-filter-* 与全局 vm-filter-* 同义(见 rules/global-design-spec.md §4.1.3)。
* 新代码优先使用 vm-filter-body / vm-filter-expand / vm-filter-toggle。
*/
.ldb-filter-expand.is-expanded .ldb-filter-expand-inner { .ldb-filter-expand.is-expanded .ldb-filter-expand-inner {
overflow: visible; overflow: visible;
} }
@@ -251,6 +255,7 @@
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
margin-right: auto;
} }
.ldb-filter-toggle-badge { .ldb-filter-toggle-badge {

View File

@@ -9,13 +9,15 @@
| 项 | 规则 | | 项 | 规则 |
|---|---| |---|---|
| 入口 | H5底部 Tab「手工台账」Web顶栏「手工台账」 | | 入口 | H5底部 Tab「手工台账」Web顶栏「手工台账」 |
| 站点范围 | 仅展示当前登录用户角色对应站点;单站人员仅 1 个、多站可切换、超级管理员可选全部(原型用标注状态切换演示,未接真实登录) |
| 上传形态 | 仅图片(拍照/相册),可多张 | | 上传形态 | 仅图片(拍照/相册),可多张 |
| 维度 | **加氢站 + 自然日(本地日历日)** 一份 | | 维度 | **加氢站 + 自然日(本地日历日)** 一份 |
| 日历 | 月历自本站**首笔加氢记录日**起算:绿=已传、橙=未传;首笔之前无需补传;禁选未来日 | | 日历 | 月历自本站**首笔加氢记录日**起算:绿=已传、橙=未传;首笔之前无需补传;禁选未来日 |
| 补传 | 允许为过往未传日期补传;已传日期仅可**追加**新图 | | 补传 | 允许为过往未传日期补传;已传日期仅可**追加**新图 |
| 归档 | **确认上传后的图片进入已归档状态,不可删除**;未确认的草稿图仍可删除 | | 归档 | **确认上传后的图片进入已归档状态,不可删除**;未确认的草稿图仍可删除 |
| 二次确认 | Web 点「确认上传/补传」先弹窗:「提交后将无法修改,是否确认手工台账照片无误」;确认后才写入 |
| 拦截 | **未上传今日手工台账 → 禁止新增加氢记录**(编辑已有记录不拦;补传历史不替代今日校验) | | 拦截 | **未上传今日手工台账 → 禁止新增加氢记录**(编辑已有记录不拦;补传历史不替代今日校验) |
| 数据 | 共享 `H2VehicleLedgerBridge` 内存 Store未接真实 API | | 数据 | 共享 `H2VehicleLedgerBridge` 内存 Store未接真实 API;站名/编码解析见 Bridge.`h2BridgeResolveStationId` |
## 判定顺序(新增拦截) ## 判定顺序(新增拦截)
@@ -38,9 +40,10 @@
## 用户可见 ## 用户可见
- 今日未上传:橙/黄提示条 + Tab 红点 - 今日状态双反馈Web 放在右侧「当日台账」卡片内未上传「今日尚未上传手工台账YYYY-MM-DD」异常态已上传「今日已上传手工台账YYYY-MM-DD」正常态
- 今日未上传:橙/黄提示 + Tab 红点
- 日历:自首笔加氢日起绿/橙标记;首笔之前无橙点(无需补传);本月未传天数摘要 - 日历:自首笔加氢日起绿/橙标记;首笔之前无橙点(无需补传);本月未传天数摘要
- 选中日:上传区标题为该日期;已归档图显示「已归档」无删除;仅未确认草稿可删;已有台账时主按钮为「确认补传」 - Web 右侧面板标题「手工台账」:空态提示;有图可点进全屏翻页预览并角标「已归档/待提交」;预览区内上传;底部确认前二次弹窗;提交后立即展示已归档缩略图;左右卡片等高
## 代码路径 ## 代码路径

View File

@@ -124,9 +124,6 @@ export function OrderDetail({ order, onBack, onEdit, onDelete }: Props) {
) : ( ) : (
<div className="h5-detail-photo__empty"></div> <div className="h5-detail-photo__empty"></div>
)} )}
{!order.platePhotoUrl && plateSrc ? (
<p className="h5-detail-photo__hint"></p>
) : null}
</div> </div>
<div className="h5-detail-photo"> <div className="h5-detail-photo">
<div className="h5-detail-photo__label"></div> <div className="h5-detail-photo__label"></div>
@@ -135,14 +132,11 @@ export function OrderDetail({ order, onBack, onEdit, onDelete }: Props) {
) : ( ) : (
<div className="h5-detail-photo__empty"></div> <div className="h5-detail-photo__empty"></div>
)} )}
{!order.panelPhotoUrl && panelSrc ? (
<p className="h5-detail-photo__hint"></p>
) : null}
</div> </div>
</div> </div>
<div className="h5-detail-row"> <div className="h5-detail-row h5-detail-row--station">
<span></span> <span></span>
<strong>{order.stationName}</strong> <strong title={order.stationName}>{order.stationName}</strong>
</div> </div>
<div className="h5-detail-row"> <div className="h5-detail-row">
<span></span> <span></span>

View File

@@ -1317,6 +1317,17 @@
color: var(--color-text); color: var(--color-text);
} }
.h5-detail-row--station {
min-width: 0;
}
.h5-detail-row--station strong {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.h5-panel--detail { .h5-panel--detail {
padding-top: 14px; padding-top: 14px;
} }
@@ -1424,13 +1435,6 @@
font-weight: 600; font-weight: 600;
} }
.h5-detail-photo__hint {
margin: 0;
font-size: 11px;
line-height: 1.4;
color: #94a3b8;
}
.h5-loading { .h5-loading {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -159,6 +159,8 @@ export function upsertStationOrder(input: {
platePhotoUrl: String(input.platePhotoUrl || ''), platePhotoUrl: String(input.platePhotoUrl || ''),
panelPhotoUrl: String(input.panelPhotoUrl || ''), panelPhotoUrl: String(input.panelPhotoUrl || ''),
creatorName: '站端账号', creatorName: '站端账号',
recordSource: 'station',
changeLogOperator: '站端账号',
reconcileStatus: 'pending', reconcileStatus: 'pending',
verifyStatus: 'unverified', verifyStatus: 'unverified',
}); });

View File

@@ -113,7 +113,7 @@
{ {
"id": "item:prototypes:oneos-h5-h2-order", "id": "item:prototypes:oneos-h5-h2-order",
"kind": "item", "kind": "item",
"title": "加氢订单", "title": "加氢记录H5",
"itemKey": "prototypes/oneos-h5-h2-order" "itemKey": "prototypes/oneos-h5-h2-order"
}, },
{ {
@@ -224,6 +224,19 @@
} }
] ]
}, },
{
"id": "folder-1784341576489-spa7ms",
"kind": "folder",
"title": "审批中心",
"children": [
{
"id": "item:prototypes:oneos-web-approval",
"kind": "item",
"title": "审批中心",
"itemKey": "prototypes/oneos-web-approval"
}
]
},
{ {
"id": "item:prototypes:lease-business-line-overview", "id": "item:prototypes:lease-business-line-overview",
"kind": "item", "kind": "item",

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"version": 1, "version": 1,
"updatedAt": "2026-07-18T02:26:51.884Z", "updatedAt": "2026-07-18T02:52:26.271Z",
"title": "小羚羚", "title": "小羚羚",
"sectionId": "folder-prototypes-xll-miniapp", "sectionId": "folder-prototypes-xll-miniapp",
"description": "氢能车辆运营移动端原型;菜单与小羚羚「小程序」项目目录同步。", "description": "氢能车辆运营移动端原型;菜单与小羚羚「小程序」项目目录同步。",

View File

@@ -1,23 +0,0 @@
/**
* @name 合同模板管理
* 自 ONE-OS web端 原稿复刻1 个页面)
*/
import '../../common/oneosWebLegacy/legacyGlobals';
import React from 'react';
import '../vehicle-management/style.css';
import Page1 from './pages/01-合同模板管理.jsx';
import { OneosWebLegacyShell } from '../../common/oneosWebLegacy/OneosWebLegacyShell';
const pages = [
{ id: 'u5408u540cu6a21u677fu7ba1u7406', title: '合同模板管理', component: Page1 },
];
export default function OneosWebContractTemplate() {
return (
<OneosWebLegacyShell
moduleTitle="合同模板管理"
pages={pages}
defaultPageId="u5408u540cu6a21u677fu7ba1u7406"
/>
);
}

File diff suppressed because one or more lines are too long

View File

@@ -8,11 +8,12 @@
- 菜单:`.axhub/make/sidebar-tree.json`(加氢站管理分组) - 菜单:`.axhub/make/sidebar-tree.json`(加氢站管理分组)
- 生成时间2026-07-16 09:56 - 生成时间2026-07-16 09:56
- 修复闭环2026-07-16按 Review 建议处理 P1/P2/P3 - 修复闭环2026-07-16按 Review 建议处理 P1/P2/P3
- 产品变更2026-07-18 — **预约加氢已下线**Tab / 受理 / 落账均移除);下文预约相关「已修复」条目仅作历史记录。
- 资料冲突/待确认:**部分仍有**(对账权威入口叙事,见下) - 资料冲突/待确认:**部分仍有**(对账权威入口叙事,见下)
## 总体点评 ## 总体点评
主路径(筛选 → 近 30 日统计 → 列表增删改/导出 → 已对账只读 → 与 H5 共用 Bridge完整可演示。按评审建议已补齐**预约带站名(全站运营)**、**接受预约落账**、**重复键拦截**、**总额偏差确认**、**孤儿页清理与对账入口文档对齐**。剩余待确认项主要为「车辆氢费明细点完成」与「站点对账单提交」两条对账叙事并存,不阻塞本页演示。 主路径(筛选 → 近 30 日统计 → 列表增删改/导出 → 手工台账 → 已对账只读 → 与 H5 共用 Bridge完整可演示。按评审建议已补齐**重复键拦截**、**总额偏差确认**、**孤儿页清理与对账入口文档对齐**。剩余待确认项主要为「车辆氢费明细点完成」与「站点对账单提交」两条对账叙事并存,不阻塞本页演示。
## P0-P3 优先级问题 ## P0-P3 优先级问题
@@ -40,12 +41,11 @@
| 维度 | 结论 | | 维度 | 结论 |
|------|------| |------|------|
| 核心任务 | 已覆盖筛选、统计、CRUD、导出、预约受理并落账、重复键/总额校验 | | 核心任务 | 已覆盖筛选、统计、CRUD、导出、手工台账、重复键/总额校验 |
| 菜单对齐 | 站点信息 / 加氢订单(H5) / 加氢记录 / 数量统计 — 与 sidebar 一致;本包仅承载加氢记录 | | 菜单对齐 | 站点信息 / 加氢订单(H5) / 加氢记录 / 数量统计 — 与 sidebar 一致;本包仅承载加氢记录 |
| 资料冲突/待确认 | 对账权威入口已拍板:见 [../vehicle-h2-fee-ledger/.spec/verify-reconcile.md](../vehicle-h2-fee-ledger/.spec/verify-reconcile.md)(核对 ≠ 对账) | | 资料冲突/待确认 | 对账权威入口已拍板:见 [../vehicle-h2-fee-ledger/.spec/verify-reconcile.md](../vehicle-h2-fee-ledger/.spec/verify-reconcile.md)(核对 ≠ 对账) |
## 业务逻辑连贯性 ## 业务逻辑连贯性
- **流水生命周期**:新增 pending →站点对账提交reconciled → 禁编删。 - **流水生命周期**:新增 pending →站点对账提交reconciled → 禁编删。
- **预约状态机**pending → accepted+ 未对账流水)/ rejected徽标随全站 pending 递减。
- **双端同步**:同会话 Bridge重复键与总额规则 Web/H5 共用。 - **双端同步**:同会话 Bridge重复键与总额规则 Web/H5 共用。

View File

@@ -3,7 +3,7 @@
| 项 | 说明 | | 项 | 说明 |
|---|---| |---|---|
| 代码路径 | UI`pages/02-加氢记录.jsx`Store`src/common/h2VehicleLedgerBridge.js` | | 代码路径 | UI`pages/02-加氢记录.jsx`Store`src/common/h2VehicleLedgerBridge.js` |
| 对齐模块 | H5「加氢订单」`oneos-h5-h2-order`(同一业务、同一 StorePC 多导出 / 预约加氢 | | 对齐模块 | H5「加氢订单」`oneos-h5-h2-order`(同一业务、同一 StorePC 多导出 / 手工台账 |
| 对账入口 | 菜单原型 `oneos-web-h2-station-site`(实现:`oneos-web-h2-station-site/pages/03-站点信息.jsx` | | 对账入口 | 菜单原型 `oneos-web-h2-station-site`(实现:`oneos-web-h2-station-site/pages/03-站点信息.jsx` |
| 数据源 | 原型本地种子 + 内存共享 Store**未接真实 API** | | 数据源 | 原型本地种子 + 内存共享 Store**未接真实 API** |
@@ -24,12 +24,30 @@
| 加氢总额(元) | 映射 `costTotal` / `customerAmount` | | 加氢总额(元) | 映射 `costTotal` / `customerAmount` |
| 核对状态 | `verifyStatus``unverified` / `verified`;能源部「完成核对」写入 | | 核对状态 | `verifyStatus``unverified` / `verified`;能源部「完成核对」写入 |
| 核对时间 | `verifiedAt`;未核对应「—」 | | 核对时间 | `verifiedAt`;未核对应「—」 |
| 核对人 | `verifiedByName`;完成核对手写入操作员姓名 |
| 对账状态 | `pending` / `reconciled`**仅**站点对账单提交后变为已对账 | | 对账状态 | `pending` / `reconciled`**仅**站点对账单提交后变为已对账 |
| 对账时间 | **仅** `reconcileDate`(对账单回写);勿用 `reconciledAt` 判定对账 | | 对账时间 | **仅** `reconcileDate`(对账单回写);勿用 `reconciledAt` 判定对账 |
| 对账人 | `reconciledByName`;对账单提交时的 `reconciler` |
| 记录来源 | `recordSource``station`(站端上传)/ `lingniu`(羚牛上传) |
| 变更日志 | `changeLogs[]`:时间、操作人、修改项、原值、新值 |
写入 Bridge 时默认:`settlementStatus=customer``creatorName=站端账号``verifyStatus=unverified``reconcileStatus=pending`(与 H5 相同)。`mileageKm` 由表单选填写入,未填为 `null` 写入 Bridge 时默认:`settlementStatus=customer``creatorName=站端账号``recordSource=station``verifyStatus=unverified``reconcileStatus=pending`(与 H5 相同)。`mileageKm` 由表单选填写入,未填为 `null`
> 双状态全文:[../vehicle-h2-fee-ledger/.spec/verify-reconcile.md](../vehicle-h2-fee-ledger/.spec/verify-reconcile.md) > 双状态全文:[../vehicle-h2-fee-ledger/.spec/verify-reconcile.md](../vehicle-h2-fee-ledger/.spec/verify-reconcile.md)
> 来源 / 日志 / 操作人:[record-source-changelog.md](./record-source-changelog.md)
### 1.1 列表筛选Web
交互对齐全局规范(`rules/global-design-spec.md` §4.1)与物流业务明细:`splitFilterFields` / `shouldShowFilterExpand` 拆分首行与展开区;默认 **1 行 4 项**`vm-filter-expand` +「更多筛选 / 收起」;占位符统一 `0.875rem` / `--ln-muted-soft`
| 区域 | 条件 | 规则 |
|---|---|---|
| 首行 | 加氢时间 | 按 `hydrogenTime` 日期闭区间 |
| 首行 | 车牌号 / 加氢站 | 精确匹配 |
| 首行 | 对账状态 | 已对账 / 未对账(`hrIsReconciled` |
| 更多 | 核对状态 | 已核对 / 未核对(`hrIsVerified` |
| 更多 | 核对时间 | 按 `verifiedAt` 日期闭区间;无核对时间则不命中 |
| 更多 | 对账时间 | 按 `reconcileTime``reconcileDate` 日期闭区间;无对账时间则不命中 |
--- ---
@@ -47,7 +65,7 @@
|---|---| |---|---|
| 共享 Bridge 不可用 | 列表用本地 fallback保存/删除提示失败 | | 共享 Bridge 不可用 | 列表用本地 fallback保存/删除提示失败 |
| 已核对或已对账点编辑/删除 | 对应警告文案 | | 已核对或已对账点编辑/删除 | 对应警告文案 |
| PC 独有 | **导出**含核对/对账列;**预约加氢**接受后写入未核对流水 | | PC 独有 | **导出**含核对/对账列;手工台账上传(站点按角色范围;今日双态提示;右侧预览区确认上传) |
## 4. 数据流 ## 4. 数据流
@@ -57,11 +75,11 @@ Web / H5 新增加氢记录
→ 车辆氢费明细可见 → 车辆氢费明细可见
车辆氢费明细 · 完成核对 车辆氢费明细 · 完成核对
→ verifyStatus=verified + verifiedAt → verifyStatus=verified + verifiedAt + verifiedByName
→ Web / H5 同步「已核对」 → Web / H5 同步「已核对」
站点信息 · 对账单提交(仅已核对 ∧ 未对账) 站点信息 · 对账单提交(仅已核对 ∧ 未对账)
→ reconcileStatus=reconciled + reconcileDate → reconcileStatus=reconciled + reconcileDate + reconciledByName
→ Web / H5 / 台账同步「已对账」 → Web / H5 / 台账同步「已对账」
``` ```
@@ -84,28 +102,8 @@ Web / H5 新增加氢记录
| 超过容差 | 二次确认「仍要保存」;确认后按用户填写的总额写入(不强制改回) | | 超过容差 | 二次确认「仍要保存」;确认后按用户填写的总额写入(不强制改回) |
| 代码 | Bridge.`totalAmountMismatch`Web Modal / H5 `confirm` | | 代码 | Bridge.`totalAmountMismatch`Web Modal / H5 `confirm` |
## 7. 预约加氢PC 独有 · 全站运营视角) ## 7. 边界
| 字段 | 说明 |
|---|---|
| 加氢站名称 | `stationName`(必有);列表与接受/拒绝确认文案展示 |
| 站点编码 | `stationCode` |
| 车牌 / 预约时间 / 量 / 司机 / 联系方式 | 司机预约信息 |
| 状态 | `pending` / `accepted` / `rejected` |
| 关联流水 | 接受成功后写 `linkedRecordId`(对应加氢记录 id |
| 规则 | 说明 |
|---|---|
| 运营视角 | **全站运营**顶栏「预约加氢」Tab 列出全部站点预约;靠「加氢站名称」列区分归属;列表含处理情况 |
| 徽标 | 统计全部 `pending` 条数,不随主列表站点筛选收缩 |
| 接受落账 | 生成 **未对账** 流水:`hydrogenTime=预约时间``hydrogenKg=预约量``unitPrice=该站近期单价``lookupStationUnitPrice`,缺省 42.5),总额=单价×量;`creatorName=预约受理` |
| 拒绝 | 仅改预约状态,不写流水 |
| 重复键 | 若同站同车牌同预约时间已有流水,**拒绝落账**并提示 |
代码:`HR_INITIAL_APPOINTMENTS``handleAcceptAppointment``pages/02-加氢记录.jsx`)。
## 8. 边界
- Web 不做 OCR录入单价×量可自动带出总额总额仍可手改超容差需确认 - Web 不做 OCR录入单价×量可自动带出总额总额仍可手改超容差需确认
- 预约列表数据本地维护;落账写入共享 Store 后与 H5 / 对账单可见
- 跨页联动依赖同浏览器会话内共享 Store - 跨页联动依赖同浏览器会话内共享 Store
- 本期不提供「预约加氢」Tab / 受理能力

View File

@@ -0,0 +1,95 @@
# 加氢记录 · 来源标注 / 变更日志 / 核对人与对账人
| 项 | 说明 |
|---|---|
| 代码路径 | Bridge`src/common/h2VehicleLedgerBridge.js`Web`pages/02-加氢记录.jsx`;台账:`vehicle-h2-fee-ledger/H2LedgerPage.jsx`H5`oneos-h5-h2-order/utils/orders.ts` |
| 数据源 | 原型本地种子 + 内存共享 Store**未接真实 API** |
---
## 1. 记录来源(`recordSource`
| 值 | 标签文案 | 写入场景 |
|---|---|---|
| `station` | **站端上传** | H5 / Web 站端新增加氢记录;`creatorName=站端账号` 或客户名含 `·站端上报` |
| `lingniu` | **羚牛上传** | 车辆氢费明细人工新增 / 复制 / 导入 |
### 判定优先级
| 优先级 | 条件 | 结果 |
|---|---|---|
| 1 | 行上已有 `recordSource``station` / `lingniu` | 原样使用 |
| 2 | `creatorName === '站端账号'``customerName``·站端上报` | `station` |
| 3 | 其他 | `lingniu` |
### 展示
- **车辆氢费明细**:车牌号下方标签(站端=绿 / 羚牛=蓝)
- **加氢记录 Web**:车牌号下方同口径标签
- 导出 CSV 含「来源」列
---
## 2. 变更日志(`changeLogs[]`
挂在每条加氢记录上,供审计。条目字段:
| 字段 | 说明 |
|---|---|
| `at` | 修改时间 |
| `operatorName` / `userName` | 操作人 |
| `fieldLabel` | 修改项(业务名) |
| `oldValue` / `before` | 原值 |
| `newValue` / `after` | 新值 |
### 写入时机
| 场景 | 规则 |
|---|---|
| 站端编辑Web/H5 upsert | Bridge 对比加氢时间/车牌/量/单价/总额/里程,有差异则追加 |
| 车辆氢费明细字段修改 | `appendChangeLog` 同时写入行内 `changeLogs` 与本地索引 |
| 完成核对 | 记录核对状态变化 |
### 查看
操作列 **更多 → 变更日志**Web 加氢记录 / 车辆氢费明细),弹窗按时间倒序。
### 前置条件
- 无变更时弹窗空态「暂无变更记录」
- 原型演示种子:`rf-13``rf-1` 预置各 1 条日志
---
## 3. 核对人 / 对账人
| 字段 | 含义 | 写入时机 |
|---|---|---|
| `verifiedByName` | 核对人 | 车辆氢费明细「完成核对」→ 当前操作员姓名 |
| `reconciledByName` | 对账人 | 站点信息对账单提交 patches 中的 `reconciler` |
### 展示位置
- **加氢记录 Web**:核对时间后「核对人」;对账时间后「对账人」
- **车辆氢费明细**:核对状态后「核对时间」「核对人」;对账日期后「对账人」
未核/未对时显示「—」。
---
## 4. 每日统计占用(第一屏)
| 项 | 规则 |
|---|---|
| 默认 | **收起**:仅一行标题 + 近 30 天 KPI 胶囊 +「展开明细」 |
| 展开 | 显示 7 日卡片轨与翻页 |
| 目的 | 第一屏优先露出列表行 |
---
## 5. 验收要点
1. 站端新增记录在台账/Web 显示「站端上传」;台账手增显示「羚牛上传」
2. 编辑量/价等后,变更日志可见时间、操作人、修改项、原值、新值
3. 完成核对后「核对人」有值;对账单提交后「对账人」有值
4. 每日统计默认收起,展开后才占日卡片高度

View File

@@ -13,7 +13,7 @@
## 1. 背景与目标 ## 1. 背景与目标
与 H5「加氢订单」为同一业务站端上报加氢流水。Web 供 PC 操作H5 供移动端。数据结构、共享账本与对账规则一致。Web **额外**提供导出、预约加氢处理 与 H5「加氢订单」为同一业务站端上报加氢流水。Web 供 PC 操作H5 供移动端。数据结构、共享账本与对账规则一致。Web **额外**提供导出与手工台账上传
### 1.0 加氢站管理菜单分工 ### 1.0 加氢站管理菜单分工
@@ -21,22 +21,30 @@
|---|---|---| |---|---|---|
| 站点信息 | `oneos-web-h2-station-site` | 站点主数据、对账单生成与提交回写 | | 站点信息 | `oneos-web-h2-station-site` | 站点主数据、对账单生成与提交回写 |
| 加氢订单 | `oneos-h5-h2-order` | 站端手机浏览器录入本站流水 | | 加氢订单 | `oneos-h5-h2-order` | 站端手机浏览器录入本站流水 |
| 加氢记录 | **本原型** | PC 多站流水台账、导出、预约受理 | | 加氢记录 | **本原型** | PC 多站流水台账、导出、手工台账 |
### 1.1 功能清单 ### 1.1 功能清单
| # | 功能 | 说明 | | # | 功能 | 说明 |
|---|---|---| |---|---|---|
| 1 | 列表 / 筛选 | 字段与 H5 一致;可按时间车牌站名对账状态筛选 | | 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) | | 2 | 手工台账 | 与 H5 同逻辑:站点按登录角色范围过滤;今日已传/未传双态提示;右侧「手工台账」预览(空态/全屏翻页/已归档角标)+ 确认前二次提示;月历补传与拦截见 [feature-manual-ledger](../oneos-h5-h2-order/.spec/feature-manual-ledger.md) |
| 3 | 新增 / 批量 / 编辑 | 字段:时间、站、车牌、**里程(选填)**、单价、量、总额;已对账只读;同站同车牌同时间拦截;总额偏离单价×量超 0.05 元需确认 | | 3 | 新增 / 批量 / 编辑 | 字段:时间、站、车牌、**里程(选填)**、单价、量、总额;已对账只读;同站同车牌同时间拦截;总额偏离单价×量超 0.05 元需确认 |
| 4 | 删除 | 仅未对账;写共享 Store | | 4 | 删除 | 仅未对账;写共享 Store |
| 5 | 导出 | 当前筛选结果 CSVWeb 独有) | | 5 | 导出 | 当前筛选结果 CSVWeb 独有) |
| 6 | 预约加氢 | 列表工具栏左上角第三个 Tab列表展示预约信息与处理情况**接受后自动生成未对账加氢记录** |
### 1.2 列表字段 ### 1.2 列表字段
加氢时间、加氢站名称、车牌号、里程km、氢气单价元/kg、加氢量、加氢总额、核对状态、核对时间、对账状态、对账时间。 加氢时间、加氢站名称、车牌号(附来源标签)、里程km、氢气单价元/kg、加氢量、加氢总额、核对状态、核对时间、核对人、对账状态、对账时间、对账人;操作含 **变更日志**
### 1.3 工具栏 Tab
列表工具栏左侧为分段 Tab对齐物流业务明细 `lbd-main-tab`**加氢记录** / **手工台账**
悬浮:浅绿底 + 深色字;选中:主色浅底 + 主色字。本期不提供「预约加氢」。
### 1.4 每日统计(第一屏)
默认**收起**为一行 KPI 摘要 +「展开明细」,避免挤占列表;展开后才显示近 7 日卡片与翻页。规格见 [record-source-changelog.md](./record-source-changelog.md) §4。
--- ---
@@ -44,12 +52,13 @@
1. 列表/表单字段与 H5「加氢订单」一致含选填里程无充装员 / 承担方式录入) 1. 列表/表单字段与 H5「加氢订单」一致含选填里程无充装员 / 承担方式录入)
2. 与 H5 读写同一共享 Store一端增删改后另一端可见同会话 2. 与 H5 读写同一共享 Store一端增删改后另一端可见同会话
3. 已核对或已对账不可编辑、删除;**已对账记录对账时间不为空**(对账单回写);核对由车辆氢费明细「完成核对」触发 3. 筛选默认一行;更多筛选/收起对齐物流业务明细;支持核对状态、核对时间、对账状态、对账时间;占位符样式与全局规范一致
4. 导出列与列表字段一致(含核对/对账) 4. 已核对或已对账不可编辑、删除;**已对账记录对账时间不为空**(对账单回写);核对由车辆氢费明细「完成核对」触发
5. 列表左上角三个 Tab加氢记录 / 手工台账 / 预约加氢;预约 Tab 列表展示预约信息与处理情况 5. 导出列与列表字段一致(含来源、核对人、对账人)
6. 预约徽标为全站待处理条数(全站运营视角,不随列表站点筛选收缩) 6. 列表左上角两个 Tab加氢记录 / 手工台账;悬浮与选中样式符合分段 Tab 规范;无预约加氢入口
7. 接受预约后列表出现对应未对账流水(时间/站/车牌/量与预约一致) 7. 同站同车牌同加氢时间重复保存被拦截;总额与单价×量偏差 > 0.05 元时出现确认
8. 同站同车牌同加氢时间重复保存被拦截;总额与单价×量偏差 > 0.05 元时出现确认 8. 来源标签:站端上报→「站端上传」,台账手增→「羚牛上传」;变更日志含时间/操作人/修改项/原值/新值;核对时间后有核对人、对账时间后有对账人
9. 每日统计默认收起,第一屏以列表为主
--- ---
@@ -57,6 +66,6 @@
| 主题 | 摘要 | 全文 | | 主题 | 摘要 | 全文 |
|---|---|---| |---|---|---|
| 数据结构与对账 | 站端口径字段;**核对 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) | | 数据结构与对账 | 站端口径字段;**核对 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) §56 | | 重复键 / 总额偏差 | 业务主键拦截;总额偏差二次确认 | [record-data-model.md](./record-data-model.md) §56 |
| 预约接受落账 | 接受 → 未对账草稿流水 | [record-data-model.md](./record-data-model.md) §7 | | 来源 / 变更日志 / 操作人 | `recordSource`、行级 `changeLogs`、核对人/对账人 | [record-source-changelog.md](./record-source-changelog.md) |

View File

@@ -4,14 +4,15 @@
- 使用设计依据:`src/prototypes/vm-shared/DESIGN.md` - 使用设计依据:`src/prototypes/vm-shared/DESIGN.md`
- 生成时间2026-07-17 12:55 - 生成时间2026-07-17 12:55
- 修复回写2026-07-17P0P2 已按规范落地) - 修复回写2026-07-17P0P2 已按规范落地)
- 产品变更2026-07-18 — **预约加氢已下线**;工具栏仅「加氢记录 / 手工台账」两 Tab悬浮/选中对齐物流业务明细分段 Tab。下文涉及预约的条目视为历史记录。
## 总体点评 ## 总体点评
加氢记录页是典型中后台列表:筛选、近 30 日 KPI 轮播、 Tab加氢记录 / 手工台账 / 预约加氢)与明细表同屏协作,任务路径清晰。视觉上延续 emerald 主色、白卡片与 tabular-nums 数字排版,和站点信息等 OneOS Web 列表气质一致;列表工具栏左上角放 Tab、右侧放导出/新增,信息架构合理。 加氢记录页是典型中后台列表:筛选、近 30 日 KPI 轮播、 Tab加氢记录 / 手工台账)与明细表同屏协作,任务路径清晰。视觉上延续 emerald 主色、白卡片与 tabular-nums 数字排版,和站点信息等 OneOS Web 列表气质一致;列表工具栏左上角放 Tab、右侧放导出/新增,信息架构合理。
`vm-shared/DESIGN.md` 对照时,偏离曾集中在三条**强制规范**:底部分页、筛选日期区间、操作列。已改为 `TablePagination` / `DateRangeFilterField` / `OperationActions`,数字列去掉右对齐,预约统计卡去掉左侧色条 `vm-shared/DESIGN.md` 对照时,偏离曾集中在三条**强制规范**:底部分页、筛选日期区间、操作列。已改为 `TablePagination` / `DateRangeFilterField` / `OperationActions`,数字列去掉右对齐。
亮点:每日统计卡片带 `aria-label`预约待处理徽标、手工台账拦截提示可点击跳转;新增/编辑页底栏固定、主按钮约 44px。预约 Tab 列表化后比弹窗更易扫读处理情况。 亮点:每日统计卡片带 `aria-label`、手工台账拦截提示可点击跳转;新增/编辑页底栏固定、主按钮约 44px。
## P0-P3 优先级问题 ## P0-P3 优先级问题

View File

@@ -18,7 +18,7 @@
"fingerprint": "filter|h2-record", "fingerprint": "filter|h2-record",
"path": [] "path": []
}, },
"annotationText": "按加氢时间、车牌、加氢站、对账状态筛选;字段口径与 H5「加氢订单」一致。", "annotationText": "默认一行四项splitFilterFields更多筛选/收起用 vm-filter-expand + vm-filter-toggle对齐物流业务明细占位符 0.875rem / --ln-muted-soft 全局统一。",
"hasMarkdown": false, "hasMarkdown": false,
"color": "#2563EB", "color": "#2563EB",
"images": [], "images": [],
@@ -38,7 +38,7 @@
"fingerprint": "toolbar|h2-record", "fingerprint": "toolbar|h2-record",
"path": [] "path": []
}, },
"annotationText": "有待处理预约时显示全站待处理条数。弹窗含加氢站名称(全站运营视角)。接受预约将自动生成一条未对账加氢记录(时间=预约时间、量=预约量);拒绝仅改预约状态。", "annotationText": "左侧分段 Tab加氢记录 / 手工台账(悬浮浅绿底,选中主色;对齐物流业务明细)。右侧导出、新增。",
"hasMarkdown": false, "hasMarkdown": false,
"color": "#059669", "color": "#059669",
"images": [], "images": [],
@@ -58,7 +58,7 @@
"fingerprint": "table|h2-record", "fingerprint": "table|h2-record",
"path": [] "path": []
}, },
"annotationText": "列:加氢时间、站名、车牌、里程、单价、量、总额、核对状态/时间、对账状态/时间。已核对或已对账禁改删;对账仅对账单提交后出现。", "annotationText": "列:加氢时间、站名、车牌(附来源:站端上传/羚牛上传)、里程、单价、量、总额、核对状态/时间/人、对账状态/时间/人。操作更多含变更日志。已核对或已对账禁改删;对账仅对账单提交后出现。",
"hasMarkdown": false, "hasMarkdown": false,
"color": "#FF7D00", "color": "#FF7D00",
"images": [], "images": [],
@@ -79,16 +79,89 @@
"fingerprint": "daily-stats|h2-record", "fingerprint": "daily-stats|h2-record",
"path": [] "path": []
}, },
"annotationText": "近30天含今天自今日前推展示起止日期与加氢总量每日卡片上方「数据条数」下分「总数 / 已核对 / 已对账」,下方为加氢量与加氢总额;随筛选结果实时汇总。", "annotationText": "默认收起为一行 KPI加氢总量 / 总额主题色 / 条数)+「展开明细」,优先保证第一屏列表可见行数;展开后显示 7 日卡片轨与翻页。每日卡以加氢量为主数字、总额为主题色,底部「记录 / 已核对 / 已对账」。随筛选实时汇总。",
"hasMarkdown": false, "hasMarkdown": false,
"color": "#7C3AED", "color": "#7C3AED",
"images": [], "images": [],
"createdAt": 1784013600000, "createdAt": 1784013600000,
"updatedAt": 1784165900000, "updatedAt": 1784345100000,
"controls": []
},
{
"id": "hr-manual-station",
"index": 5,
"title": "加氢站选择(角色范围)",
"pageId": "h2-record",
"locator": {
"selectors": [
"[data-annotation-id=\"hr-manual-station\"]"
],
"fingerprint": "manual-station|h2-record",
"path": []
},
"annotationText": "选项=当前登录用户角色对应站点。单站仅 1 个且不可切换;多站可切换;超级管理员可选全部。原型未接真实登录,用下方状态切换演示。",
"hasMarkdown": false,
"color": "#0F766E",
"images": [],
"createdAt": 1784344200000,
"updatedAt": 1784344200000,
"controls": [
{
"type": "segmented",
"attributeId": "hr_station_scope",
"displayName": "登录角色站点范围",
"info": "演示站端人员可见站点:单站 / 多站 / 超级管理员。",
"initialValue": "single",
"options": [
{ "label": "单站人员", "value": "single" },
{ "label": "多站人员", "value": "multi" },
{ "label": "超级管理员", "value": "super_admin" }
]
}
]
},
{
"id": "hr-manual-today-status",
"index": 6,
"title": "今日台账状态提示",
"pageId": "h2-record",
"locator": {
"selectors": [
"[data-annotation-id=\"hr-manual-today-status\"]"
],
"fingerprint": "manual-today-status|h2-record",
"path": []
},
"annotationText": "放在右侧「当日台账」卡片内。双态异常「今日尚未上传手工台账YYYY-MM-DD」橙底正常「今日已上传手工台账YYYY-MM-DD」绿底。随当前选中站+今日是否已传切换。",
"hasMarkdown": false,
"color": "#D97706",
"images": [],
"createdAt": 1784344200000,
"updatedAt": 1784344200000,
"controls": []
},
{
"id": "hr-manual-upload-panel",
"index": 7,
"title": "手工台账上传面板",
"pageId": "h2-record",
"locator": {
"selectors": [
"[data-annotation-id=\"hr-manual-upload-panel\"]"
],
"fingerprint": "manual-upload-panel|h2-record",
"path": []
},
"annotationText": "标题「手工台账」。中部预览区:空态/缩略图(已归档角标);可点全屏翻页;区内可上传。底部确认前二次提示「提交后将无法修改…」;提交后立即展示已归档图。与左侧月历等高。",
"hasMarkdown": false,
"color": "#2563EB",
"images": [],
"createdAt": 1784344200000,
"updatedAt": 1784344200000,
"controls": [] "controls": []
} }
], ],
"updatedAt": 1784013600000 "updatedAt": 1784344200000
}, },
"markdownMap": {}, "markdownMap": {},
"assetMap": {} "assetMap": {}

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
| 文件 | 说明 | | 文件 | 说明 |
|---|---| |---|---|
| `02-加氢记录.jsx` | 本原型唯一页面PC 加氢流水台账(含导出、预约加氢 | | `02-加氢记录.jsx` | 本原型唯一页面PC 加氢流水台账(含导出、手工台账 |
## 菜单分工(加氢站管理) ## 菜单分工(加氢站管理)

View File

@@ -6,7 +6,7 @@
| 产品模块 | 台账数据 → 车辆氢费明细 | | 产品模块 | 台账数据 → 车辆氢费明细 |
| 文档类型 | 产品需求说明 | | 文档类型 | 产品需求说明 |
| 适用读者 | 产品、业务、运营、测试、项目 | | 适用读者 | 产品、业务、运营、测试、项目 |
| 修订说明 | v2.4核对 vs 对账双状态——「完成核对」≠ 对账结案;对账仅由加氢站对账单提交回写 | | 修订说明 | v2.5记录来源标签站端上传/羚牛上传);核对人/对账人;变更日志挂行级字段 |
| 双状态规格 | [verify-reconcile.md](./verify-reconcile.md) | | 双状态规格 | [verify-reconcile.md](./verify-reconcile.md) |
--- ---
@@ -162,7 +162,7 @@ flowchart LR
- **说明:** 合计条含「待保存 / 未对账 / 已对账」等当前列表中的全部可见记录,与导出范围(仅未对账+已对账)不同。 - **说明:** 合计条含「待保存 / 未对账 / 已对账」等当前列表中的全部可见记录,与导出范围(仅未对账+已对账)不同。
**列表列顺序(左→右,业务视角):** **列表列顺序(左→右,业务视角):**
序号 → 加氢日期 → 加氢时间 → 加氢站名称 → 客户名称 → 车牌号 → 加氢量 → 成本单价 → 成本总价 → 加氢单价 → 加氢总价 → 行驶里程 → 备注 → 业务员 → 承担方式 → 对账日期 → **收票日期****加氢站付款状态****开票日期****客户收款状态****开票公司** → 状态 → 订单编号 → 操作(操作列固定右侧)。 序号 → 加氢日期 → 加氢时间 → 加氢站名称 → 客户名称 → 车牌号(下来源标签:站端上传 / 羚牛上传)→ 加氢量 → 成本单价 → 成本总价 → 加氢单价 → 加氢总价 → 行驶里程 → 备注 → 业务员 → 承担方式 → 对账日期 → **对账人****收票日期****加氢站付款状态****开票日期****客户收款状态****开票公司**核对状态 → **核对时间****核对人** → 对账状态 → 订单编号 → 操作(操作列固定右侧)。
**系统带出字段(不可手工维护):** 收票日期、加氢站付款状态、开票日期、客户收款状态、开票公司;选择/变更客户等信息后由系统自动刷新,单元格为浅灰底只读样式。 **系统带出字段(不可手工维护):** 收票日期、加氢站付款状态、开票日期、客户收款状态、开票公司;选择/变更客户等信息后由系统自动刷新,单元格为浅灰底只读样式。
@@ -202,7 +202,10 @@ flowchart LR
| 备注 | 否 | 自由文本 | | 备注 | 否 | 自由文本 |
| 业务员 | 否 | 选客户后自动带出 | | 业务员 | 否 | 选客户后自动带出 |
| 承担方式 | 是* | 客户承担 / 我司承担 / 客户自行结算 / 其他结算 | | 承担方式 | 是* | 客户承担 / 我司承担 / 客户自行结算 / 其他结算 |
| 对账日期 | 否 | 完成对账等流程带出,只读 | | 对账日期 | 否 | 对账单提交回写,只读 |
| 对账人 | 否 | 对账单提交操作员姓名,只读 |
| 核对时间 | 否 | 「完成核对」写入,只读 |
| 核对人 | 否 | 「完成核对」操作员姓名,只读 |
| 收票日期 | 否 | 由财务/收票模块自动带出,不可编辑 | | 收票日期 | 否 | 由财务/收票模块自动带出,不可编辑 |
| 加氢站付款状态 | 否 | 已付款 / 未付款,由加氢站打款管理带出 | | 加氢站付款状态 | 否 | 已付款 / 未付款,由加氢站打款管理带出 |
| 开票日期 | 否 | 由开票模块自动带出 | | 开票日期 | 否 | 由开票模块自动带出 |
@@ -379,23 +382,30 @@ flowchart LR
--- ---
## 九、变更日志 ## 九、变更日志与来源
### 9.0 记录来源标签
| 来源 | 标签 | 场景 |
|---|---|---|
| 站端主动上报 | **站端上传** | 加氢站 H5/Web 写入共享账本 |
| 台账人工录入 | **羚牛上传** | 本页新增 / 复制 / 导入 |
### 9.1 记录什么 ### 9.1 记录什么
满足审计与纠纷查证,以下变化需留痕: 满足审计与纠纷查证,以下变化需留痕(写入行级 `changeLogs`,并与加氢记录 Web 同源)
- 保存(待保存 → 未对账) - 保存(待保存 → 未对账)
- 完成对账(未对账 → 已对 - 完成核对(未核对 → 已对)
- 各业务字段修改(前后值不同) - 各业务字段修改(前后值不同)
- **成本单价(一键替换)**、**客户名称(一键替换)** - **成本单价(一键替换)**、**客户名称(一键替换)**
- 新增、复制、批量导入产生记录 - 站端编辑加氢量/单价等Bridge 自动追加)
### 9.2 怎么查看 ### 9.2 怎么查看
在操作列点击 **「更多」→ 变更日志**,弹窗展示: 在操作列点击 **「更多」→ 变更日志**,弹窗展示:
- 修改时间、修改人、修改字段、修改前(红)、修改后(绿) - 修改时间、操作人、修改项、原值(红)、新值(绿)
- 按时间倒序排列 - 按时间倒序排列
--- ---

View File

@@ -1782,6 +1782,7 @@ const Component = function () {
key: nextRowKey(), key: nextRowKey(),
createdBy: CURRENT_USER.id, createdBy: CURRENT_USER.id,
creatorName: CURRENT_USER.name, creatorName: CURRENT_USER.name,
recordSource: 'lingniu',
orderNo: '', orderNo: '',
hydrogenTime: parseImportDate(get('加氢时间')) || now, hydrogenTime: parseImportDate(get('加氢时间')) || now,
stationId: station ? station.value : undefined, stationId: station ? station.value : undefined,
@@ -1799,7 +1800,10 @@ const Component = function () {
settlementStatus: SETTLEMENT_LABEL_TO_VALUE[settlementLabel] || 'customer', settlementStatus: SETTLEMENT_LABEL_TO_VALUE[settlementLabel] || 'customer',
remark: get('备注'), remark: get('备注'),
createdAt: now, createdAt: now,
reconcileStatus: null reconcileStatus: null,
changeLogs: [],
verifiedByName: '',
reconciledByName: ''
}; };
return Object.assign({}, draft, resolveLinkedModuleFields(draft)); return Object.assign({}, draft, resolveLinkedModuleFields(draft));
} }
@@ -2598,6 +2602,7 @@ const Component = function () {
key: nextRowKey(), key: nextRowKey(),
createdBy: CURRENT_USER.id, createdBy: CURRENT_USER.id,
creatorName: CURRENT_USER.name, creatorName: CURRENT_USER.name,
recordSource: 'lingniu',
orderNo: '', orderNo: '',
hydrogenTime: cloneRowTimeValue(sourceRow.hydrogenTime), hydrogenTime: cloneRowTimeValue(sourceRow.hydrogenTime),
stationId: sourceRow.stationId, stationId: sourceRow.stationId,
@@ -2624,7 +2629,10 @@ const Component = function () {
verifyStatus: null, verifyStatus: null,
savedAt: null, savedAt: null,
verifiedAt: null, verifiedAt: null,
reconciledAt: null verifiedByName: '',
reconciledAt: null,
reconciledByName: '',
changeLogs: []
}; };
return recalcAllRows([draft])[0]; return recalcAllRows([draft])[0];
} }
@@ -2961,11 +2969,22 @@ const Component = function () {
receiptDate: patch.receiptDate, receiptDate: patch.receiptDate,
receiptAmount: patch.receiptAmount != null ? patch.receiptAmount : r.receiptAmount, receiptAmount: patch.receiptAmount != null ? patch.receiptAmount : r.receiptAmount,
stationPaymentStatus: patch.paymentStatus || 'paid', stationPaymentStatus: patch.paymentStatus || 'paid',
paymentStatus: patch.paymentStatus || 'paid' paymentStatus: patch.paymentStatus || 'paid',
reconciledByName: patch.reconciler || patch.reconciledByName || r.reconciledByName || ''
}); });
}); });
} }
function recordSourceLabel(row) {
if (!row) return '羚牛上传';
if (row.recordSource === 'station') return '站端上传';
if (row.recordSource === 'lingniu') return '羚牛上传';
var creator = String(row.creatorName || '').trim();
var customer = String(row.customerName || '');
if (creator === '站端账号' || customer.indexOf('·站端上报') >= 0) return '站端上传';
return '羚牛上传';
}
function recalcRowFields(row) { function recalcRowFields(row) {
var mileage = normalizeMileageKm(row.mileageKm); var mileage = normalizeMileageKm(row.mileageKm);
var kg = isEmptyNum(row.hydrogenKg) ? null : roundMoney(row.hydrogenKg); var kg = isEmptyNum(row.hydrogenKg) ? null : roundMoney(row.hydrogenKg);
@@ -3017,6 +3036,7 @@ const Component = function () {
key: nextRowKey(), key: nextRowKey(),
createdBy: user.id, createdBy: user.id,
creatorName: user.name, creatorName: user.name,
recordSource: 'lingniu',
orderNo: '', orderNo: '',
hydrogenTime: now, hydrogenTime: now,
stationId: undefined, stationId: undefined,
@@ -3034,7 +3054,10 @@ const Component = function () {
settlementStatus: 'customer', settlementStatus: 'customer',
remark: '', remark: '',
createdAt: now, createdAt: now,
reconcileStatus: null reconcileStatus: null,
changeLogs: [],
verifiedByName: '',
reconciledByName: ''
} }
])[0]; ])[0];
} }
@@ -3080,6 +3103,7 @@ const Component = function () {
key: 'mock-h2-' + (i + 1), key: 'mock-h2-' + (i + 1),
createdBy: creator.id, createdBy: creator.id,
creatorName: creator.name, creatorName: creator.name,
recordSource: i === 1 ? 'station' : 'lingniu',
hydrogenTime: day, hydrogenTime: day,
stationId: station.value, stationId: station.value,
stationName: station.label, stationName: station.label,
@@ -3088,7 +3112,7 @@ const Component = function () {
hydrogenKg: kg, hydrogenKg: kg,
costUnitPrice: costUnit, costUnitPrice: costUnit,
customerId: customerId, customerId: customerId,
customerName: customerName || '', customerName: i === 1 ? ((station.label || '') + '·站端上报') : (customerName || ''),
customerUnitPrice: custUnit, customerUnitPrice: custUnit,
salesperson: salesperson, salesperson: salesperson,
settlementStatus: ['customer', 'internal', 'customer_self', 'other'][i % 4], settlementStatus: ['customer', 'internal', 'customer_self', 'other'][i % 4],
@@ -3103,7 +3127,10 @@ const Component = function () {
verifyStatus: reconcileStatus === RECONCILE_RECONCILED ? VERIFY_VERIFIED : (reconcileStatus === RECONCILE_PENDING ? VERIFY_UNVERIFIED : null), verifyStatus: reconcileStatus === RECONCILE_RECONCILED ? VERIFY_VERIFIED : (reconcileStatus === RECONCILE_PENDING ? VERIFY_UNVERIFIED : null),
savedAt: reconcileStatus !== RECONCILE_DRAFT ? day : null, savedAt: reconcileStatus !== RECONCILE_DRAFT ? day : null,
verifiedAt: reconcileStatus === RECONCILE_RECONCILED ? day : null, verifiedAt: reconcileStatus === RECONCILE_RECONCILED ? day : null,
reconciledAt: reconcileStatus === RECONCILE_RECONCILED ? day : null verifiedByName: reconcileStatus === RECONCILE_RECONCILED ? CURRENT_USER.name : '',
reconciledAt: reconcileStatus === RECONCILE_RECONCILED ? day : null,
reconciledByName: reconcileStatus === RECONCILE_RECONCILED ? '系统管理员' : '',
changeLogs: []
}); });
} }
var demoStation = findStation(HYDROGEN_STATION_LIST[0].value) || HYDROGEN_STATION_LIST[0]; var demoStation = findStation(HYDROGEN_STATION_LIST[0].value) || HYDROGEN_STATION_LIST[0];
@@ -3455,24 +3482,36 @@ const Component = function () {
var beforeText = meta.beforeText != null ? meta.beforeText : formatLogValue(field, before, rowCtx); var beforeText = meta.beforeText != null ? meta.beforeText : formatLogValue(field, before, rowCtx);
var afterText = meta.afterText != null ? meta.afterText : formatLogValue(field, after, rowCtx); var afterText = meta.afterText != null ? meta.afterText : formatLogValue(field, after, rowCtx);
if (!meta.force && beforeText === afterText) return; if (!meta.force && beforeText === afterText) return;
setChangeLogsByKey(function (prev) { var entry = {
var list = (prev[rowKey] || []).slice();
list.unshift({
id: 'clog-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8), id: 'clog-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8),
at: nowDayjs(), at: nowDayjs(),
userId: CURRENT_USER.id, userId: CURRENT_USER.id,
userName: CURRENT_USER.name, userName: CURRENT_USER.name,
operatorName: CURRENT_USER.name,
field: field, field: field,
fieldLabel: fieldLabel, fieldLabel: fieldLabel,
before: beforeText, before: beforeText,
after: afterText, after: afterText,
oldValue: beforeText,
newValue: afterText,
remark: meta.remark || '' remark: meta.remark || ''
}); };
setChangeLogsByKey(function (prev) {
var list = (prev[rowKey] || []).slice();
list.unshift(entry);
var next = Object.assign({}, prev); var next = Object.assign({}, prev);
next[rowKey] = list; next[rowKey] = list;
return next; return next;
}); });
}, []); setAllRows(function (prev) {
return prev.map(function (r) {
if (r.key !== rowKey) return r;
var logs = Array.isArray(r.changeLogs) ? r.changeLogs.slice() : [];
logs.unshift(entry);
return Object.assign({}, r, { changeLogs: logs });
});
});
}, [setAllRows]);
var applySystemCustomerReplace = useCallback(function (rowKey, replaceAction) { var applySystemCustomerReplace = useCallback(function (rowKey, replaceAction) {
if (!rowKey || !replaceAction || !replaceAction.customerId) return; if (!rowKey || !replaceAction || !replaceAction.customerId) return;
@@ -3840,6 +3879,7 @@ const Component = function () {
return Object.assign({}, r, { return Object.assign({}, r, {
verifyStatus: VERIFY_VERIFIED, verifyStatus: VERIFY_VERIFIED,
verifiedAt: verifiedAt, verifiedAt: verifiedAt,
verifiedByName: CURRENT_USER.name,
reconciledAt: verifiedAt reconciledAt: verifiedAt
}); });
} }
@@ -4579,11 +4619,17 @@ const Component = function () {
title: reqTitle('车牌号'), title: reqTitle('车牌号'),
dataIndex: 'plateNo', dataIndex: 'plateNo',
key: 'plateNo', key: 'plateNo',
width: 118, width: 132,
align: 'center', align: 'center',
render: function (v, record) { render: function (v, record) {
if (!canEditRow(record)) return readOnlyCell(v); var source = recordSourceLabel(record);
return React.createElement(Select, { var tag = React.createElement(Tag, {
color: source === '站端上传' ? 'green' : 'blue',
style: { margin: 0, borderRadius: 4, fontSize: 11, lineHeight: '18px', fontWeight: 600 }
}, source);
var plateNode = !canEditRow(record)
? readOnlyCell(v)
: React.createElement(Select, {
style: cellInputStyle, style: cellInputStyle,
showSearch: true, showSearch: true,
allowClear: true, allowClear: true,
@@ -4595,6 +4641,9 @@ const Component = function () {
notFoundContent: '车辆不存在', notFoundContent: '车辆不存在',
onChange: function (val) { patch(record.key, 'plateNo', val); } onChange: function (val) { patch(record.key, 'plateNo', val); }
}); });
return React.createElement('div', {
style: { display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4 }
}, plateNode, tag);
} }
}, },
{ {
@@ -4785,6 +4834,16 @@ const Component = function () {
return readOnlyCell(formatDateTime(v) || formatDate(v) || '-'); return readOnlyCell(formatDateTime(v) || formatDate(v) || '-');
} }
}, },
{
title: '对账人',
dataIndex: 'reconciledByName',
key: 'reconciledByName',
width: 88,
align: 'center',
render: function (v) {
return readOnlyCell(v || '—');
}
},
{ {
title: '收票日期', title: '收票日期',
dataIndex: 'receiptDate', dataIndex: 'receiptDate',
@@ -4848,6 +4907,27 @@ const Component = function () {
return React.createElement(Tag, { color: color, style: { margin: 0 } }, label); return React.createElement(Tag, { color: color, style: { margin: 0 } }, label);
} }
}, },
{
title: '核对时间',
dataIndex: 'verifiedAt',
key: 'verifiedAt',
width: 148,
align: 'center',
render: function (v) {
if (!v) return readOnlyCell('—');
return readOnlyCell(formatDateTime(v) || '—');
}
},
{
title: '核对人',
dataIndex: 'verifiedByName',
key: 'verifiedByName',
width: 88,
align: 'center',
render: function (v) {
return readOnlyCell(v || '—');
}
},
{ {
title: '对账状态', title: '对账状态',
key: 'recordStatus', key: 'recordStatus',
@@ -5149,9 +5229,27 @@ const Component = function () {
var changeLogRows = useMemo( var changeLogRows = useMemo(
function () { function () {
if (!changeLogModal.rowKey) return []; if (!changeLogModal.rowKey) return [];
return changeLogsByKey[changeLogModal.rowKey] || []; var fromLocal = changeLogsByKey[changeLogModal.rowKey] || [];
var row = null;
var i;
for (i = 0; i < allRows.length; i++) {
if (allRows[i].key === changeLogModal.rowKey) {
row = allRows[i];
break;
}
}
var fromRow = (row && Array.isArray(row.changeLogs) ? row.changeLogs : []) || [];
var seen = {};
var merged = [];
fromLocal.concat(fromRow).forEach(function (log) {
var id = log && log.id ? String(log.id) : '';
if (id && seen[id]) return;
if (id) seen[id] = true;
merged.push(log);
});
return merged;
}, },
[changeLogModal.rowKey, changeLogsByKey] [changeLogModal.rowKey, changeLogsByKey, allRows]
); );
var changeLogColumns = useMemo( var changeLogColumns = useMemo(

View File

@@ -209,12 +209,12 @@
"path": [] "path": []
}, },
"aiPrompt": "列表字段、行内编辑、客户校验与操作列。", "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表头支持批量改价成本单价、加氢单价、承担方式。", "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表头支持批量改价成本单价、加氢单价、承担方式。",
"hasMarkdown": true, "hasMarkdown": true,
"color": "#334155", "color": "#334155",
"images": [], "images": [],
"createdAt": 1782806400000, "createdAt": 1782806400000,
"updatedAt": 1782816000000, "updatedAt": 1784520000000,
"controls": [] "controls": []
}, },
{ {

View File

@@ -1,4 +1,5 @@
import React, { useCallback, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { Calendar, X } from 'lucide-react'; import { Calendar, X } from 'lucide-react';
import { DateRangeCalendarPanel } from './DateRangeCalendarPanel'; import { DateRangeCalendarPanel } from './DateRangeCalendarPanel';
import { useDismissOnFocusOutside } from './useDismissOnFocusOutside'; import { useDismissOnFocusOutside } from './useDismissOnFocusOutside';
@@ -15,6 +16,14 @@ interface DateRangeFilterFieldProps {
endPlaceholder?: string; endPlaceholder?: string;
} }
interface PopoverPosition {
top: number;
left: number;
width: number;
}
const POPOVER_MIN_WIDTH = 560;
function formatDisplayText( function formatDisplayText(
startDate: string, startDate: string,
endDate: string, endDate: string,
@@ -36,7 +45,9 @@ export function DateRangeFilterField({
endPlaceholder = '结束日期', endPlaceholder = '结束日期',
}: DateRangeFilterFieldProps) { }: DateRangeFilterFieldProps) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [position, setPosition] = useState<PopoverPosition>({ top: 0, left: 0, width: POPOVER_MIN_WIDTH });
const fieldRef = useRef<HTMLDivElement>(null); const fieldRef = useRef<HTMLDivElement>(null);
const popoverRef = useRef<HTMLDivElement>(null);
const hasValue = Boolean(startDate || endDate); const hasValue = Boolean(startDate || endDate);
const displayText = formatDisplayText(startDate, endDate, startPlaceholder, endPlaceholder); const displayText = formatDisplayText(startDate, endDate, startPlaceholder, endPlaceholder);
const placeholder = `${startPlaceholder}${endPlaceholder}`; const placeholder = `${startPlaceholder}${endPlaceholder}`;
@@ -45,10 +56,41 @@ export function DateRangeFilterField({
setOpen(false); setOpen(false);
}, []); }, []);
useDismissOnFocusOutside(open, fieldRef, dismiss); const updatePosition = useCallback(() => {
const anchor = fieldRef.current;
if (!anchor) return;
const rect = anchor.getBoundingClientRect();
const maxWidth = Math.max(160, window.innerWidth - 16);
const width = Math.min(Math.max(rect.width, POPOVER_MIN_WIDTH), maxWidth);
let left = rect.left;
if (left + width > window.innerWidth - 8) {
left = Math.max(8, window.innerWidth - 8 - width);
}
if (left < 8) left = 8;
let top = rect.bottom + 6;
const estimatedHeight = 360;
if (top + estimatedHeight > window.innerHeight - 8 && rect.top > estimatedHeight + 8) {
top = Math.max(8, rect.top - estimatedHeight - 6);
}
setPosition({ top, left, width });
}, []);
useDismissOnFocusOutside(open, [fieldRef, popoverRef], dismiss);
useEffect(() => {
if (!open) return;
updatePosition();
window.addEventListener('resize', updatePosition);
window.addEventListener('scroll', updatePosition, true);
return () => {
window.removeEventListener('resize', updatePosition);
window.removeEventListener('scroll', updatePosition, true);
};
}, [open, updatePosition]);
const openPicker = () => { const openPicker = () => {
setOpen(true); setOpen(true);
window.requestAnimationFrame(updatePosition);
}; };
const clear = (event: React.MouseEvent) => { const clear = (event: React.MouseEvent) => {
@@ -58,6 +100,31 @@ export function DateRangeFilterField({
setOpen(false); setOpen(false);
}; };
const popover =
open && typeof document !== 'undefined'
? createPortal(
<div
ref={popoverRef}
className="vm-date-range-popover vm-date-range-popover--portal"
role="dialog"
aria-label={ariaLabel}
style={{
top: position.top,
left: position.left,
width: position.width,
}}
>
<DateRangeCalendarPanel
startDate={startDate}
endDate={endDate}
onChange={onChange}
onComplete={dismiss}
/>
</div>,
document.body,
)
: null;
return ( return (
<div className="vm-date-range-field vm-filter-picker-field" ref={fieldRef}> <div className="vm-date-range-field vm-filter-picker-field" ref={fieldRef}>
<div <div
@@ -94,17 +161,7 @@ export function DateRangeFilterField({
) : null} ) : null}
<Calendar size={14} className="vm-date-range-icon" aria-hidden /> <Calendar size={14} className="vm-date-range-icon" aria-hidden />
</div> </div>
{popover}
{open ? (
<div className="vm-date-range-popover" role="dialog" aria-label={ariaLabel}>
<DateRangeCalendarPanel
startDate={startDate}
endDate={endDate}
onChange={onChange}
onComplete={dismiss}
/>
</div>
) : null}
</div> </div>
); );
} }

View File

@@ -211,6 +211,35 @@
margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--ln-hairline); flex-wrap: wrap; margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--ln-hairline); flex-wrap: wrap;
} }
/* —— 筛选提示文本(强制统一,见 rules/global-design-spec.md §4.1.1 —— */
.vm-filter-card .vm-filter-picker-input::placeholder,
.vm-filter-card .vm-input::placeholder,
.vm-filter-card input.vm-input::placeholder,
.vm-filter-card .ant-select-selection-placeholder,
.vm-filter-card .ant-input::placeholder,
.vm-filter-card .ant-input-affix-wrapper input::placeholder,
.lc-filter-card .vm-filter-picker-input::placeholder,
.lc-filter-card .vm-input::placeholder,
.lc-filter-card input.vm-input::placeholder,
.lc-filter-card .ant-select-selection-placeholder,
.lc-filter-card .ant-input::placeholder,
.lc-filter-card .ant-input-affix-wrapper input::placeholder {
font-family: var(--vm-font);
font-size: 0.875rem;
font-weight: 400;
color: var(--ln-muted-soft);
opacity: 1;
line-height: 1.25;
}
.vm-filter-picker-input::placeholder {
font-family: var(--vm-font);
font-size: 0.875rem;
font-weight: 400;
color: var(--ln-muted-soft);
opacity: 1;
}
/* —— 筛选区折叠(全局):超过 1 行4 项)时默认收起,常量见 src/common/vm-filter-panel.ts —— */ /* —— 筛选区折叠(全局):超过 1 行4 项)时默认收起,常量见 src/common/vm-filter-panel.ts —— */
.vm-filter-body { .vm-filter-body {
display: flex; display: flex;
@@ -254,6 +283,7 @@
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
margin-right: auto;
} }
.vm-filter-toggle-badge { .vm-filter-toggle-badge {
@@ -414,10 +444,10 @@
.vm-filter-picker-input { .vm-filter-picker-input {
flex: 1; min-width: 0; height: 100%; flex: 1; min-width: 0; height: 100%;
border: none; background: transparent; padding: 0; border: none; background: transparent; padding: 0;
font-size: 0.875rem; color: var(--ln-ink); font-family: var(--vm-font);
font-size: 0.875rem; font-weight: 400; color: var(--ln-ink);
} }
.vm-filter-picker-input:focus { outline: none; } .vm-filter-picker-input:focus { outline: none; }
.vm-filter-picker-input::placeholder { color: var(--ln-muted-soft); }
.vm-filter-picker-clear { .vm-filter-picker-clear {
display: inline-flex; align-items: center; justify-content: center; display: inline-flex; align-items: center; justify-content: center;
width: 22px; height: 22px; flex-shrink: 0; width: 22px; height: 22px; flex-shrink: 0;
@@ -2254,6 +2284,12 @@ button.vm-btn.vm-btn-back.ant-btn:hover:not(:disabled) {
border-radius: var(--ln-radius-card); border-radius: var(--ln-radius-card);
background: var(--ln-surface-card); background: var(--ln-surface-card);
box-shadow: var(--ln-shadow-hover); box-shadow: var(--ln-shadow-hover);
box-sizing: border-box;
}
.vm-date-range-popover--portal {
position: fixed;
z-index: 1200;
min-width: 0;
} }
.vm-date-range-calendars { .vm-date-range-calendars {
position: relative; position: relative;