迭代加氢记录与氢费明细:来源标签、变更日志、核对人/对账人,日统计默认收起;统一筛选规范与导航,下线合同模板原型。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,13 +1,7 @@
|
||||
{
|
||||
"version": 1,
|
||||
"updatedAt": "2026-07-16T01:04:51.317Z",
|
||||
"updatedAt": "2026-07-18T02:30:01.518Z",
|
||||
"prototypes": [
|
||||
{
|
||||
"id": "item-prototypes-oneos-web-approval",
|
||||
"kind": "item",
|
||||
"title": "oneos web approval",
|
||||
"itemKey": "prototypes/oneos-web-approval"
|
||||
},
|
||||
{
|
||||
"id": "folder-1783875936186-27raza",
|
||||
"kind": "folder",
|
||||
@@ -19,12 +13,6 @@
|
||||
"title": "业务管理",
|
||||
"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",
|
||||
"kind": "item",
|
||||
@@ -188,7 +176,7 @@
|
||||
{
|
||||
"id": "item:prototypes:oneos-h5-h2-order",
|
||||
"kind": "item",
|
||||
"title": "加氢订单",
|
||||
"title": "加氢记录(H5)",
|
||||
"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",
|
||||
"kind": "item",
|
||||
|
||||
@@ -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-grid` | 默认 4 列;1280px 以下 2 列;640px 以下 1 列 |
|
||||
| 字段 | `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`)。
|
||||
- 展示分隔符为中文「**至**」。
|
||||
- 类名:`vm-date-range-field`、`vm-filter-picker-control`。
|
||||
- 弹层必须 portal + `fixed`(避免滚动容器错位)。
|
||||
- **禁止**筛选区用 Ant `RangePicker`、双 `<input type="date">` 等替代方案。
|
||||
|
||||
**折叠筛选**
|
||||
#### 4.1.3 更多筛选 / 收起(强制)
|
||||
|
||||
- 超过 4 项时其余放入 `vm-filter-expand`(见 `src/common/vm-filter-panel.ts`)。
|
||||
- 筛选项数组经 `splitFilterFields()` 拆为首行 + 展开区;首行固定最多 4 项,**禁止**首行放 5 项导致第二行孤零零 1 项。
|
||||
- 展开区项数不得为 4n+1(避免展开后末行仅 1 项);由 `splitFilterFields` 自动从首行借调平衡。
|
||||
- 切换按钮:`vm-filter-toggle` + 角标 `vm-filter-toggle-badge`。
|
||||
超过 4 项时必须折叠;实现与物流业务明细相同:
|
||||
|
||||
| 项 | 规则 |
|
||||
|---|---|
|
||||
| 拆分 | `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`
|
||||
|
||||
@@ -174,15 +202,16 @@ import '../vehicle-management/style.css';
|
||||
| 表单页 | min 44px | 1rem(移动端友好) |
|
||||
|
||||
- 边框:`--ln-hairline-strong`;focus 时主题色边框。
|
||||
- 占位符:`--ln-muted-soft`。
|
||||
- 占位符:见 §4.1.1(`--ln-muted-soft`,0.875rem / 400)。
|
||||
|
||||
### 4.3 下拉选择 `.vm-filter-picker-*`
|
||||
|
||||
| 元素 | 说明 |
|
||||
|---|---|
|
||||
| `vm-filter-picker-control` | 触发器,与输入框同高 |
|
||||
| `vm-filter-picker-control` | 触发器,与输入框同高(32px) |
|
||||
| `vm-filter-picker-popover` | 下拉面板,卡片圆角 + hover 阴影 |
|
||||
| `vm-filter-picker-option.checked` | 主色浅底 + 深绿字 |
|
||||
| 占位符 | 见 §4.1.1;与日期区间、Ant Select 同色同字号 |
|
||||
|
||||
### 4.4 按钮 `.vm-btn`
|
||||
|
||||
|
||||
@@ -7,6 +7,9 @@ var H2_RECONCILE_RECONCILED = 'reconciled';
|
||||
var H2_RECONCILE_PENDING = 'pending';
|
||||
var H2_VERIFY_VERIFIED = 'verified';
|
||||
var H2_VERIFY_UNVERIFIED = 'unverified';
|
||||
/** 记录来源:站端上传 | 羚牛上传(车辆氢费明细人工录入) */
|
||||
var H2_RECORD_SOURCE_STATION = 'station';
|
||||
var H2_RECORD_SOURCE_LINGNIU = 'lingniu';
|
||||
|
||||
var H2_STATION_CODE_MAP = {
|
||||
'中国石油中油高新能源牙谷加油加氢站': 'JX-H2-001',
|
||||
@@ -15,6 +18,14 @@ var H2_STATION_CODE_MAP = {
|
||||
'苏州工业园区备用站': '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(含本站未核对 / 已核对未对账用例) */
|
||||
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 }
|
||||
];
|
||||
|
||||
function h2BridgeCloneRows(rows) {
|
||||
return (rows || []).map(function (r) { return Object.assign({}, r); });
|
||||
}
|
||||
|
||||
function h2BridgeFormatDateTime(value) {
|
||||
if (!value) return '';
|
||||
if (typeof value === 'string') return value;
|
||||
@@ -73,6 +80,127 @@ function h2BridgeMatchStatementPatch(row, patch) {
|
||||
&& (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) {
|
||||
if (!patches || !patches.length) return h2BridgeCloneRows(rows);
|
||||
return (rows || []).map(function (r) {
|
||||
@@ -84,22 +212,24 @@ function h2BridgeApplyStatementPatches(rows, patches) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!patch) return r;
|
||||
return Object.assign({}, r, {
|
||||
if (!patch) return h2BridgeNormalizeRow(r);
|
||||
return h2BridgeNormalizeRow(Object.assign({}, r, {
|
||||
statementPatched: true,
|
||||
reconcileStatus: H2_RECONCILE_RECONCILED,
|
||||
reconcileDate: patch.reconcileDate,
|
||||
receiptDate: patch.receiptDate,
|
||||
stationPaymentStatus: 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() {
|
||||
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) {
|
||||
@@ -122,11 +252,15 @@ function h2BridgeManualLedgerKey(stationId, ledgerDate) {
|
||||
return String(stationId || '').trim() + '|' + String(ledgerDate || '').trim();
|
||||
}
|
||||
|
||||
/** 解析站点编码:支持传入编码或站名(勿把站名原样当 id) */
|
||||
function h2BridgeResolveStationId(stationId, stationName) {
|
||||
var id = String(stationId || '').trim();
|
||||
if (id) return id;
|
||||
var name = String(stationName || '').trim();
|
||||
return H2_STATION_CODE_MAP[name] || '';
|
||||
var a = String(stationId || '').trim();
|
||||
var b = String(stationName || '').trim();
|
||||
if (a && H2_STATION_CODE_MAP[a]) return H2_STATION_CODE_MAP[a];
|
||||
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.verifyStatus) next.verifyStatus = H2_VERIFY_UNVERIFIED;
|
||||
if (!next.recordSource) next.recordSource = h2BridgeInferRecordSource(next);
|
||||
var found = false;
|
||||
var saved = null;
|
||||
state.rows = state.rows.map(function (r) {
|
||||
if ((r.id || r.key) === next.id || (r.id || r.key) === next.key) {
|
||||
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;
|
||||
});
|
||||
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();
|
||||
return Object.assign({}, next);
|
||||
return Object.assign({}, saved);
|
||||
},
|
||||
removeRow: function (id) {
|
||||
var target = String(id || '');
|
||||
@@ -372,6 +523,9 @@ function h2BridgeMapToHrRecord(row, index, fillerNames) {
|
||||
var reconcileTime = h2BridgeResolveReconcileTime(row);
|
||||
var isReconciled = row.reconcileStatus === H2_RECONCILE_RECONCILED || Boolean(row.reconcileDate);
|
||||
var verifiedAt = row.verifiedAt || '';
|
||||
var changeLogs = Array.isArray(row.changeLogs)
|
||||
? row.changeLogs.map(function (log) { return Object.assign({}, log); })
|
||||
: [];
|
||||
return {
|
||||
id: row.id || row.key || ('hr-' + (index + 1)),
|
||||
hydrogenTime: h2BridgeFormatDateTime(row.hydrogenTime),
|
||||
@@ -388,10 +542,14 @@ function h2BridgeMapToHrRecord(row, index, fillerNames) {
|
||||
settlementStatus: row.settlementStatus,
|
||||
stationName: 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,
|
||||
verifiedAt: verifiedAt ? h2BridgeFormatDateTime(verifiedAt) : '',
|
||||
verifiedByName: row.verifiedByName || '',
|
||||
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_VERIFY_VERIFIED,
|
||||
H2_VERIFY_UNVERIFIED,
|
||||
H2_RECORD_SOURCE_STATION,
|
||||
H2_RECORD_SOURCE_LINGNIU,
|
||||
h2BridgeInit,
|
||||
h2BridgeGetStore,
|
||||
h2BridgeGetAllRefuelRecords,
|
||||
@@ -615,5 +775,6 @@ export {
|
||||
h2BridgeNormalizeHydrogenTime,
|
||||
h2BridgeFindDuplicateRow,
|
||||
h2BridgeTotalAmountMismatch,
|
||||
h2BridgeLookupStationUnitPrice
|
||||
h2BridgeLookupStationUnitPrice,
|
||||
h2BridgeInferRecordSource
|
||||
};
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
/**
|
||||
* 列表页筛选区全局规则(与 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 行。
|
||||
* 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 项)。
|
||||
* 典型:总项数 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.875rem;weight: 400;color: `var(--ln-muted-soft)`;opacity: 1
|
||||
*/
|
||||
export const VM_FILTER_COLUMNS_PER_ROW = 4;
|
||||
|
||||
|
||||
@@ -197,6 +197,10 @@
|
||||
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 {
|
||||
overflow: visible;
|
||||
}
|
||||
@@ -251,6 +255,7 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.ldb-filter-toggle-badge {
|
||||
|
||||
@@ -9,13 +9,15 @@
|
||||
| 项 | 规则 |
|
||||
|---|---|
|
||||
| 入口 | 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 右侧面板标题「手工台账」:空态提示;有图可点进全屏翻页预览并角标「已归档/待提交」;预览区内上传;底部确认前二次弹窗;提交后立即展示已归档缩略图;左右卡片等高
|
||||
|
||||
## 代码路径
|
||||
|
||||
|
||||
@@ -124,9 +124,6 @@ export function OrderDetail({ order, onBack, onEdit, onDelete }: Props) {
|
||||
) : (
|
||||
<div className="h5-detail-photo__empty">暂无车牌照片</div>
|
||||
)}
|
||||
{!order.platePhotoUrl && plateSrc ? (
|
||||
<p className="h5-detail-photo__hint">种子演示图;新上报提交后展示实拍水印照</p>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="h5-detail-photo">
|
||||
<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>
|
||||
)}
|
||||
{!order.panelPhotoUrl && panelSrc ? (
|
||||
<p className="h5-detail-photo__hint">种子演示图;新上报提交后展示实拍水印照</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="h5-detail-row">
|
||||
<div className="h5-detail-row h5-detail-row--station">
|
||||
<span>加氢站名称</span>
|
||||
<strong>{order.stationName}</strong>
|
||||
<strong title={order.stationName}>{order.stationName}</strong>
|
||||
</div>
|
||||
<div className="h5-detail-row">
|
||||
<span>车牌号</span>
|
||||
|
||||
@@ -1317,6 +1317,17 @@
|
||||
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 {
|
||||
padding-top: 14px;
|
||||
}
|
||||
@@ -1424,13 +1435,6 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.h5-detail-photo__hint {
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.h5-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -159,6 +159,8 @@ export function upsertStationOrder(input: {
|
||||
platePhotoUrl: String(input.platePhotoUrl || ''),
|
||||
panelPhotoUrl: String(input.panelPhotoUrl || ''),
|
||||
creatorName: '站端账号',
|
||||
recordSource: 'station',
|
||||
changeLogOperator: '站端账号',
|
||||
reconcileStatus: 'pending',
|
||||
verifyStatus: 'unverified',
|
||||
});
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
{
|
||||
"id": "item:prototypes:oneos-h5-h2-order",
|
||||
"kind": "item",
|
||||
"title": "加氢订单",
|
||||
"title": "加氢记录(H5)",
|
||||
"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",
|
||||
"kind": "item",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"version": 1,
|
||||
"updatedAt": "2026-07-18T02:26:51.884Z",
|
||||
"updatedAt": "2026-07-18T02:52:26.271Z",
|
||||
"title": "小羚羚",
|
||||
"sectionId": "folder-prototypes-xll-miniapp",
|
||||
"description": "氢能车辆运营移动端原型;菜单与小羚羚「小程序」项目目录同步。",
|
||||
|
||||
@@ -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
@@ -8,11 +8,12 @@
|
||||
- 菜单:`.axhub/make/sidebar-tree.json`(加氢站管理分组)
|
||||
- 生成时间:2026-07-16 09:56
|
||||
- 修复闭环:2026-07-16(按 Review 建议处理 P1/P2/P3)
|
||||
- 产品变更:2026-07-18 — **预约加氢已下线**(Tab / 受理 / 落账均移除);下文预约相关「已修复」条目仅作历史记录。
|
||||
- 资料冲突/待确认:**部分仍有**(对账权威入口叙事,见下)
|
||||
|
||||
## 总体点评
|
||||
|
||||
主路径(筛选 → 近 30 日统计 → 列表增删改/导出 → 已对账只读 → 与 H5 共用 Bridge)完整可演示。按评审建议已补齐:**预约带站名(全站运营)**、**接受预约落账**、**重复键拦截**、**总额偏差确认**、**孤儿页清理与对账入口文档对齐**。剩余待确认项主要为「车辆氢费明细点完成」与「站点对账单提交」两条对账叙事并存,不阻塞本页演示。
|
||||
主路径(筛选 → 近 30 日统计 → 列表增删改/导出 → 手工台账 → 已对账只读 → 与 H5 共用 Bridge)完整可演示。按评审建议已补齐:**重复键拦截**、**总额偏差确认**、**孤儿页清理与对账入口文档对齐**。剩余待确认项主要为「车辆氢费明细点完成」与「站点对账单提交」两条对账叙事并存,不阻塞本页演示。
|
||||
|
||||
## P0-P3 优先级问题
|
||||
|
||||
@@ -40,12 +41,11 @@
|
||||
|
||||
| 维度 | 结论 |
|
||||
|------|------|
|
||||
| 核心任务 | 已覆盖:筛选、统计、CRUD、导出、预约受理并落账、重复键/总额校验 |
|
||||
| 核心任务 | 已覆盖:筛选、统计、CRUD、导出、手工台账、重复键/总额校验 |
|
||||
| 菜单对齐 | 站点信息 / 加氢订单(H5) / 加氢记录 / 数量统计 — 与 sidebar 一致;本包仅承载加氢记录 |
|
||||
| 资料冲突/待确认 | 对账权威入口已拍板:见 [../vehicle-h2-fee-ledger/.spec/verify-reconcile.md](../vehicle-h2-fee-ledger/.spec/verify-reconcile.md)(核对 ≠ 对账) |
|
||||
|
||||
## 业务逻辑连贯性
|
||||
|
||||
- **流水生命周期**:新增 pending →(站点对账提交)reconciled → 禁编删。
|
||||
- **预约状态机**:pending → accepted(+ 未对账流水)/ rejected;徽标随全站 pending 递减。
|
||||
- **双端同步**:同会话 Bridge;重复键与总额规则 Web/H5 共用。
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
| 项 | 说明 |
|
||||
|---|---|
|
||||
| 代码路径 | UI:`pages/02-加氢记录.jsx`;Store:`src/common/h2VehicleLedgerBridge.js` |
|
||||
| 对齐模块 | H5「加氢订单」`oneos-h5-h2-order`(同一业务、同一 Store;PC 多导出 / 预约加氢) |
|
||||
| 对齐模块 | H5「加氢订单」`oneos-h5-h2-order`(同一业务、同一 Store;PC 多导出 / 手工台账) |
|
||||
| 对账入口 | 菜单原型 `oneos-web-h2-station-site`(实现:`oneos-web-h2-station-site/pages/03-站点信息.jsx`) |
|
||||
| 数据源 | 原型本地种子 + 内存共享 Store(**未接真实 API**) |
|
||||
|
||||
@@ -24,12 +24,30 @@
|
||||
| 加氢总额(元) | 映射 `costTotal` / `customerAmount` |
|
||||
| 核对状态 | `verifyStatus`:`unverified` / `verified`;能源部「完成核对」写入 |
|
||||
| 核对时间 | `verifiedAt`;未核对应「—」 |
|
||||
| 核对人 | `verifiedByName`;完成核对手写入操作员姓名 |
|
||||
| 对账状态 | `pending` / `reconciled`;**仅**站点对账单提交后变为已对账 |
|
||||
| 对账时间 | **仅** `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)
|
||||
> 来源 / 日志 / 操作人:[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;保存/删除提示失败 |
|
||||
| 已核对或已对账点编辑/删除 | 对应警告文案 |
|
||||
| PC 独有 | **导出**含核对/对账列;**预约加氢**接受后写入未核对流水 |
|
||||
| PC 独有 | **导出**含核对/对账列;手工台账上传(站点按角色范围;今日双态提示;右侧预览区确认上传) |
|
||||
|
||||
## 4. 数据流
|
||||
|
||||
@@ -57,11 +75,11 @@ Web / H5 新增加氢记录
|
||||
→ 车辆氢费明细可见
|
||||
|
||||
车辆氢费明细 · 完成核对
|
||||
→ verifyStatus=verified + verifiedAt
|
||||
→ verifyStatus=verified + verifiedAt + verifiedByName
|
||||
→ Web / H5 同步「已核对」
|
||||
|
||||
站点信息 · 对账单提交(仅已核对 ∧ 未对账)
|
||||
→ reconcileStatus=reconciled + reconcileDate
|
||||
→ reconcileStatus=reconciled + reconcileDate + reconciledByName
|
||||
→ Web / H5 / 台账同步「已对账」
|
||||
```
|
||||
|
||||
@@ -84,28 +102,8 @@ Web / H5 新增加氢记录
|
||||
| 超过容差 | 二次确认「仍要保存」;确认后按用户填写的总额写入(不强制改回) |
|
||||
| 代码 | Bridge.`totalAmountMismatch`;Web Modal / H5 `confirm` |
|
||||
|
||||
## 7. 预约加氢(PC 独有 · 全站运营视角)
|
||||
|
||||
| 字段 | 说明 |
|
||||
|---|---|
|
||||
| 加氢站名称 | `stationName`(必有);列表与接受/拒绝确认文案展示 |
|
||||
| 站点编码 | `stationCode` |
|
||||
| 车牌 / 预约时间 / 量 / 司机 / 联系方式 | 司机预约信息 |
|
||||
| 状态 | `pending` / `accepted` / `rejected` |
|
||||
| 关联流水 | 接受成功后写 `linkedRecordId`(对应加氢记录 id) |
|
||||
|
||||
| 规则 | 说明 |
|
||||
|---|---|
|
||||
| 运营视角 | **全站运营**:顶栏「预约加氢」Tab 列出全部站点预约;靠「加氢站名称」列区分归属;列表含处理情况 |
|
||||
| 徽标 | 统计全部 `pending` 条数,不随主列表站点筛选收缩 |
|
||||
| 接受落账 | 生成 **未对账** 流水:`hydrogenTime=预约时间`,`hydrogenKg=预约量`,`unitPrice=该站近期单价`(`lookupStationUnitPrice`,缺省 42.5),总额=单价×量;`creatorName=预约受理` |
|
||||
| 拒绝 | 仅改预约状态,不写流水 |
|
||||
| 重复键 | 若同站同车牌同预约时间已有流水,**拒绝落账**并提示 |
|
||||
|
||||
代码:`HR_INITIAL_APPOINTMENTS`、`handleAcceptAppointment`(`pages/02-加氢记录.jsx`)。
|
||||
|
||||
## 8. 边界
|
||||
## 7. 边界
|
||||
|
||||
- Web 不做 OCR;录入单价×量可自动带出总额,总额仍可手改(超容差需确认)
|
||||
- 预约列表数据本地维护;落账写入共享 Store 后与 H5 / 对账单可见
|
||||
- 跨页联动依赖同浏览器会话内共享 Store
|
||||
- 本期不提供「预约加氢」Tab / 受理能力
|
||||
|
||||
@@ -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. 每日统计默认收起,展开后才占日卡片高度
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
与 H5「加氢订单」为同一业务:站端上报加氢流水。Web 供 PC 操作;H5 供移动端。数据结构、共享账本与对账规则一致。Web **额外**提供导出、预约加氢处理。
|
||||
与 H5「加氢订单」为同一业务:站端上报加氢流水。Web 供 PC 操作;H5 供移动端。数据结构、共享账本与对账规则一致。Web **额外**提供导出与手工台账上传。
|
||||
|
||||
### 1.0 加氢站管理菜单分工
|
||||
|
||||
@@ -21,22 +21,30 @@
|
||||
|---|---|---|
|
||||
| 站点信息 | `oneos-web-h2-station-site` | 站点主数据、对账单生成与提交回写 |
|
||||
| 加氢订单 | `oneos-h5-h2-order` | 站端手机浏览器录入本站流水 |
|
||||
| 加氢记录 | **本原型** | PC 多站流水台账、导出、预约受理 |
|
||||
| 加氢记录 | **本原型** | PC 多站流水台账、导出、手工台账 |
|
||||
|
||||
### 1.1 功能清单
|
||||
|
||||
| # | 功能 | 说明 |
|
||||
|---|---|---|
|
||||
| 1 | 列表 / 筛选 | 字段与 H5 一致;可按时间、车牌、站名、对账状态筛选 |
|
||||
| 2 | 手工台账 | 与 H5 同逻辑:月历标已传/未传、可补传过往日;未上传今日台账不可新增;见 [feature-manual-ledger](../oneos-h5-h2-order/.spec/feature-manual-ledger.md) |
|
||||
| 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 元需确认 |
|
||||
| 4 | 删除 | 仅未对账;写共享 Store |
|
||||
| 5 | 导出 | 当前筛选结果 CSV(Web 独有) |
|
||||
| 6 | 预约加氢 | 列表工具栏左上角第三个 Tab;列表展示预约信息与处理情况;**接受后自动生成未对账加氢记录** |
|
||||
|
||||
### 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「加氢订单」一致(含选填里程;无充装员 / 承担方式录入)
|
||||
2. 与 H5 读写同一共享 Store;一端增删改后另一端可见(同会话)
|
||||
3. 已核对或已对账不可编辑、删除;**已对账记录对账时间不为空**(对账单回写);核对由车辆氢费明细「完成核对」触发
|
||||
4. 导出列与列表字段一致(含核对/对账)
|
||||
5. 列表左上角三个 Tab:加氢记录 / 手工台账 / 预约加氢;预约 Tab 列表展示预约信息与处理情况
|
||||
6. 预约徽标为全站待处理条数(全站运营视角,不随列表站点筛选收缩)
|
||||
7. 接受预约后列表出现对应未对账流水(时间/站/车牌/量与预约一致)
|
||||
8. 同站同车牌同加氢时间重复保存被拦截;总额与单价×量偏差 > 0.05 元时出现确认
|
||||
3. 筛选默认一行;更多筛选/收起对齐物流业务明细;支持核对状态、核对时间、对账状态、对账时间;占位符样式与全局规范一致
|
||||
4. 已核对或已对账不可编辑、删除;**已对账记录对账时间不为空**(对账单回写);核对由车辆氢费明细「完成核对」触发
|
||||
5. 导出列与列表字段一致(含来源、核对人、对账人)
|
||||
6. 列表左上角两个 Tab:加氢记录 / 手工台账;悬浮与选中样式符合分段 Tab 规范;无预约加氢入口
|
||||
7. 同站同车牌同加氢时间重复保存被拦截;总额与单价×量偏差 > 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) §5–6 |
|
||||
| 预约接受落账 | 接受 → 未对账草稿流水 | [record-data-model.md](./record-data-model.md) §7 |
|
||||
| 来源 / 变更日志 / 操作人 | `recordSource`、行级 `changeLogs`、核对人/对账人 | [record-source-changelog.md](./record-source-changelog.md) |
|
||||
|
||||
@@ -4,14 +4,15 @@
|
||||
- 使用设计依据:`src/prototypes/vm-shared/DESIGN.md`
|
||||
- 生成时间:2026-07-17 12:55
|
||||
- 修复回写:2026-07-17(P0–P2 已按规范落地)
|
||||
- 产品变更: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 优先级问题
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"fingerprint": "filter|h2-record",
|
||||
"path": []
|
||||
},
|
||||
"annotationText": "按加氢时间、车牌、加氢站、对账状态筛选;字段口径与 H5「加氢订单」一致。",
|
||||
"annotationText": "默认一行四项(splitFilterFields);更多筛选/收起用 vm-filter-expand + vm-filter-toggle(对齐物流业务明细);占位符 0.875rem / --ln-muted-soft 全局统一。",
|
||||
"hasMarkdown": false,
|
||||
"color": "#2563EB",
|
||||
"images": [],
|
||||
@@ -38,7 +38,7 @@
|
||||
"fingerprint": "toolbar|h2-record",
|
||||
"path": []
|
||||
},
|
||||
"annotationText": "有待处理预约时显示全站待处理条数。弹窗含加氢站名称(全站运营视角)。接受预约将自动生成一条未对账加氢记录(时间=预约时间、量=预约量);拒绝仅改预约状态。",
|
||||
"annotationText": "左侧分段 Tab:加氢记录 / 手工台账(悬浮浅绿底,选中主色;对齐物流业务明细)。右侧导出、新增。",
|
||||
"hasMarkdown": false,
|
||||
"color": "#059669",
|
||||
"images": [],
|
||||
@@ -58,7 +58,7 @@
|
||||
"fingerprint": "table|h2-record",
|
||||
"path": []
|
||||
},
|
||||
"annotationText": "列:加氢时间、站名、车牌、里程、单价、量、总额、核对状态/时间、对账状态/时间。已核对或已对账禁改删;对账仅对账单提交后出现。",
|
||||
"annotationText": "列:加氢时间、站名、车牌(附来源:站端上传/羚牛上传)、里程、单价、量、总额、核对状态/时间/人、对账状态/时间/人。操作更多含变更日志。已核对或已对账禁改删;对账仅对账单提交后出现。",
|
||||
"hasMarkdown": false,
|
||||
"color": "#FF7D00",
|
||||
"images": [],
|
||||
@@ -79,16 +79,89 @@
|
||||
"fingerprint": "daily-stats|h2-record",
|
||||
"path": []
|
||||
},
|
||||
"annotationText": "近30天(含今天,自今日前推):展示起止日期与加氢总量;每日卡片上方「数据条数」下分「总数 / 已核对 / 已对账」,下方为加氢量与加氢总额;随筛选结果实时汇总。",
|
||||
"annotationText": "默认收起为一行 KPI(加氢总量 / 总额主题色 / 条数)+「展开明细」,优先保证第一屏列表可见行数;展开后显示 7 日卡片轨与翻页。每日卡以加氢量为主数字、总额为主题色,底部「记录 / 已核对 / 已对账」。随筛选实时汇总。",
|
||||
"hasMarkdown": false,
|
||||
"color": "#7C3AED",
|
||||
"images": [],
|
||||
"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": []
|
||||
}
|
||||
],
|
||||
"updatedAt": 1784013600000
|
||||
"updatedAt": 1784344200000
|
||||
},
|
||||
"markdownMap": {},
|
||||
"assetMap": {}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
|
||||
| 文件 | 说明 |
|
||||
|---|---|
|
||||
| `02-加氢记录.jsx` | 本原型唯一页面:PC 加氢流水台账(含导出、预约加氢) |
|
||||
| `02-加氢记录.jsx` | 本原型唯一页面:PC 加氢流水台账(含导出、手工台账) |
|
||||
|
||||
## 菜单分工(加氢站管理)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
| 产品模块 | 台账数据 → 车辆氢费明细 |
|
||||
| 文档类型 | 产品需求说明 |
|
||||
| 适用读者 | 产品、业务、运营、测试、项目 |
|
||||
| 修订说明 | v2.4:核对 vs 对账双状态——「完成核对」≠ 对账结案;对账仅由加氢站对账单提交回写 |
|
||||
| 修订说明 | v2.5:记录来源标签(站端上传/羚牛上传);核对人/对账人;变更日志挂行级字段 |
|
||||
| 双状态规格 | [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 记录什么
|
||||
|
||||
满足审计与纠纷查证,以下变化需留痕:
|
||||
满足审计与纠纷查证,以下变化需留痕(写入行级 `changeLogs`,并与加氢记录 Web 同源):
|
||||
|
||||
- 保存(待保存 → 未对账)
|
||||
- 完成对账(未对账 → 已对账)
|
||||
- 完成核对(未核对 → 已核对)
|
||||
- 各业务字段修改(前后值不同)
|
||||
- **成本单价(一键替换)**、**客户名称(一键替换)**
|
||||
- 新增、复制、批量导入产生记录
|
||||
- 站端编辑加氢量/单价等(Bridge 自动追加)
|
||||
|
||||
### 9.2 怎么查看
|
||||
|
||||
在操作列点击 **「更多」→ 变更日志**,弹窗展示:
|
||||
|
||||
- 修改时间、修改人、修改字段、修改前(红)、修改后(绿)
|
||||
- 修改时间、操作人、修改项、原值(红)、新值(绿)
|
||||
- 按时间倒序排列
|
||||
|
||||
---
|
||||
|
||||
@@ -1782,6 +1782,7 @@ const Component = function () {
|
||||
key: nextRowKey(),
|
||||
createdBy: CURRENT_USER.id,
|
||||
creatorName: CURRENT_USER.name,
|
||||
recordSource: 'lingniu',
|
||||
orderNo: '',
|
||||
hydrogenTime: parseImportDate(get('加氢时间')) || now,
|
||||
stationId: station ? station.value : undefined,
|
||||
@@ -1799,7 +1800,10 @@ const Component = function () {
|
||||
settlementStatus: SETTLEMENT_LABEL_TO_VALUE[settlementLabel] || 'customer',
|
||||
remark: get('备注'),
|
||||
createdAt: now,
|
||||
reconcileStatus: null
|
||||
reconcileStatus: null,
|
||||
changeLogs: [],
|
||||
verifiedByName: '',
|
||||
reconciledByName: ''
|
||||
};
|
||||
return Object.assign({}, draft, resolveLinkedModuleFields(draft));
|
||||
}
|
||||
@@ -2598,6 +2602,7 @@ const Component = function () {
|
||||
key: nextRowKey(),
|
||||
createdBy: CURRENT_USER.id,
|
||||
creatorName: CURRENT_USER.name,
|
||||
recordSource: 'lingniu',
|
||||
orderNo: '',
|
||||
hydrogenTime: cloneRowTimeValue(sourceRow.hydrogenTime),
|
||||
stationId: sourceRow.stationId,
|
||||
@@ -2624,7 +2629,10 @@ const Component = function () {
|
||||
verifyStatus: null,
|
||||
savedAt: null,
|
||||
verifiedAt: null,
|
||||
reconciledAt: null
|
||||
verifiedByName: '',
|
||||
reconciledAt: null,
|
||||
reconciledByName: '',
|
||||
changeLogs: []
|
||||
};
|
||||
return recalcAllRows([draft])[0];
|
||||
}
|
||||
@@ -2961,11 +2969,22 @@ const Component = function () {
|
||||
receiptDate: patch.receiptDate,
|
||||
receiptAmount: patch.receiptAmount != null ? patch.receiptAmount : r.receiptAmount,
|
||||
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) {
|
||||
var mileage = normalizeMileageKm(row.mileageKm);
|
||||
var kg = isEmptyNum(row.hydrogenKg) ? null : roundMoney(row.hydrogenKg);
|
||||
@@ -3017,6 +3036,7 @@ const Component = function () {
|
||||
key: nextRowKey(),
|
||||
createdBy: user.id,
|
||||
creatorName: user.name,
|
||||
recordSource: 'lingniu',
|
||||
orderNo: '',
|
||||
hydrogenTime: now,
|
||||
stationId: undefined,
|
||||
@@ -3034,7 +3054,10 @@ const Component = function () {
|
||||
settlementStatus: 'customer',
|
||||
remark: '',
|
||||
createdAt: now,
|
||||
reconcileStatus: null
|
||||
reconcileStatus: null,
|
||||
changeLogs: [],
|
||||
verifiedByName: '',
|
||||
reconciledByName: ''
|
||||
}
|
||||
])[0];
|
||||
}
|
||||
@@ -3080,6 +3103,7 @@ const Component = function () {
|
||||
key: 'mock-h2-' + (i + 1),
|
||||
createdBy: creator.id,
|
||||
creatorName: creator.name,
|
||||
recordSource: i === 1 ? 'station' : 'lingniu',
|
||||
hydrogenTime: day,
|
||||
stationId: station.value,
|
||||
stationName: station.label,
|
||||
@@ -3088,7 +3112,7 @@ const Component = function () {
|
||||
hydrogenKg: kg,
|
||||
costUnitPrice: costUnit,
|
||||
customerId: customerId,
|
||||
customerName: customerName || '',
|
||||
customerName: i === 1 ? ((station.label || '') + '·站端上报') : (customerName || ''),
|
||||
customerUnitPrice: custUnit,
|
||||
salesperson: salesperson,
|
||||
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),
|
||||
savedAt: reconcileStatus !== RECONCILE_DRAFT ? 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];
|
||||
@@ -3455,24 +3482,36 @@ const Component = function () {
|
||||
var beforeText = meta.beforeText != null ? meta.beforeText : formatLogValue(field, before, rowCtx);
|
||||
var afterText = meta.afterText != null ? meta.afterText : formatLogValue(field, after, rowCtx);
|
||||
if (!meta.force && beforeText === afterText) return;
|
||||
setChangeLogsByKey(function (prev) {
|
||||
var list = (prev[rowKey] || []).slice();
|
||||
list.unshift({
|
||||
var entry = {
|
||||
id: 'clog-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8),
|
||||
at: nowDayjs(),
|
||||
userId: CURRENT_USER.id,
|
||||
userName: CURRENT_USER.name,
|
||||
operatorName: CURRENT_USER.name,
|
||||
field: field,
|
||||
fieldLabel: fieldLabel,
|
||||
before: beforeText,
|
||||
after: afterText,
|
||||
oldValue: beforeText,
|
||||
newValue: afterText,
|
||||
remark: meta.remark || ''
|
||||
});
|
||||
};
|
||||
setChangeLogsByKey(function (prev) {
|
||||
var list = (prev[rowKey] || []).slice();
|
||||
list.unshift(entry);
|
||||
var next = Object.assign({}, prev);
|
||||
next[rowKey] = list;
|
||||
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) {
|
||||
if (!rowKey || !replaceAction || !replaceAction.customerId) return;
|
||||
@@ -3840,6 +3879,7 @@ const Component = function () {
|
||||
return Object.assign({}, r, {
|
||||
verifyStatus: VERIFY_VERIFIED,
|
||||
verifiedAt: verifiedAt,
|
||||
verifiedByName: CURRENT_USER.name,
|
||||
reconciledAt: verifiedAt
|
||||
});
|
||||
}
|
||||
@@ -4579,11 +4619,17 @@ const Component = function () {
|
||||
title: reqTitle('车牌号'),
|
||||
dataIndex: 'plateNo',
|
||||
key: 'plateNo',
|
||||
width: 118,
|
||||
width: 132,
|
||||
align: 'center',
|
||||
render: function (v, record) {
|
||||
if (!canEditRow(record)) return readOnlyCell(v);
|
||||
return React.createElement(Select, {
|
||||
var source = recordSourceLabel(record);
|
||||
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,
|
||||
showSearch: true,
|
||||
allowClear: true,
|
||||
@@ -4595,6 +4641,9 @@ 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);
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -4785,6 +4834,16 @@ const Component = function () {
|
||||
return readOnlyCell(formatDateTime(v) || formatDate(v) || '-');
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '对账人',
|
||||
dataIndex: 'reconciledByName',
|
||||
key: 'reconciledByName',
|
||||
width: 88,
|
||||
align: 'center',
|
||||
render: function (v) {
|
||||
return readOnlyCell(v || '—');
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '收票日期',
|
||||
dataIndex: 'receiptDate',
|
||||
@@ -4848,6 +4907,27 @@ const Component = function () {
|
||||
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: '对账状态',
|
||||
key: 'recordStatus',
|
||||
@@ -5149,9 +5229,27 @@ const Component = function () {
|
||||
var changeLogRows = useMemo(
|
||||
function () {
|
||||
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(
|
||||
|
||||
@@ -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表头支持批量改价(成本单价、加氢单价、承担方式)。",
|
||||
"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,
|
||||
"color": "#334155",
|
||||
"images": [],
|
||||
"createdAt": 1782806400000,
|
||||
"updatedAt": 1782816000000,
|
||||
"updatedAt": 1784520000000,
|
||||
"controls": []
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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 { DateRangeCalendarPanel } from './DateRangeCalendarPanel';
|
||||
import { useDismissOnFocusOutside } from './useDismissOnFocusOutside';
|
||||
@@ -15,6 +16,14 @@ interface DateRangeFilterFieldProps {
|
||||
endPlaceholder?: string;
|
||||
}
|
||||
|
||||
interface PopoverPosition {
|
||||
top: number;
|
||||
left: number;
|
||||
width: number;
|
||||
}
|
||||
|
||||
const POPOVER_MIN_WIDTH = 560;
|
||||
|
||||
function formatDisplayText(
|
||||
startDate: string,
|
||||
endDate: string,
|
||||
@@ -36,7 +45,9 @@ export function DateRangeFilterField({
|
||||
endPlaceholder = '结束日期',
|
||||
}: DateRangeFilterFieldProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [position, setPosition] = useState<PopoverPosition>({ top: 0, left: 0, width: POPOVER_MIN_WIDTH });
|
||||
const fieldRef = useRef<HTMLDivElement>(null);
|
||||
const popoverRef = useRef<HTMLDivElement>(null);
|
||||
const hasValue = Boolean(startDate || endDate);
|
||||
const displayText = formatDisplayText(startDate, endDate, startPlaceholder, endPlaceholder);
|
||||
const placeholder = `${startPlaceholder} 至 ${endPlaceholder}`;
|
||||
@@ -45,10 +56,41 @@ export function DateRangeFilterField({
|
||||
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 = () => {
|
||||
setOpen(true);
|
||||
window.requestAnimationFrame(updatePosition);
|
||||
};
|
||||
|
||||
const clear = (event: React.MouseEvent) => {
|
||||
@@ -58,6 +100,31 @@ export function DateRangeFilterField({
|
||||
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 (
|
||||
<div className="vm-date-range-field vm-filter-picker-field" ref={fieldRef}>
|
||||
<div
|
||||
@@ -94,17 +161,7 @@ export function DateRangeFilterField({
|
||||
) : null}
|
||||
<Calendar size={14} className="vm-date-range-icon" aria-hidden />
|
||||
</div>
|
||||
|
||||
{open ? (
|
||||
<div className="vm-date-range-popover" role="dialog" aria-label={ariaLabel}>
|
||||
<DateRangeCalendarPanel
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onChange={onChange}
|
||||
onComplete={dismiss}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{popover}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -211,6 +211,35 @@
|
||||
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 —— */
|
||||
.vm-filter-body {
|
||||
display: flex;
|
||||
@@ -254,6 +283,7 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.vm-filter-toggle-badge {
|
||||
@@ -414,10 +444,10 @@
|
||||
.vm-filter-picker-input {
|
||||
flex: 1; min-width: 0; height: 100%;
|
||||
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::placeholder { color: var(--ln-muted-soft); }
|
||||
.vm-filter-picker-clear {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
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);
|
||||
background: var(--ln-surface-card);
|
||||
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 {
|
||||
position: relative;
|
||||
|
||||
Reference in New Issue
Block a user