迭代加氢订单、站点记录与台账链路,同步审批组件与导航注册,并下线旧工作台入口。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
"server": {
|
||||
"host": "localhost",
|
||||
"allowLAN": true,
|
||||
"lanHost": "192.168.0.102",
|
||||
"enableCommandAPI": false
|
||||
},
|
||||
"projectDefaults": {
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
{
|
||||
"version": 1,
|
||||
"updatedAt": "2026-07-20T05:34:45.927Z",
|
||||
"updatedAt": "2026-07-20T07:33:31.296Z",
|
||||
"prototypes": [
|
||||
{
|
||||
"id": "item:prototypes:oneos-prototype-demo",
|
||||
"kind": "item",
|
||||
"title": "原型演示",
|
||||
"itemKey": "prototypes/oneos-prototype-demo"
|
||||
},
|
||||
{
|
||||
"id": "folder-1783875936186-27raza",
|
||||
"kind": "folder",
|
||||
@@ -69,10 +75,10 @@
|
||||
"title": "OneOS",
|
||||
"children": [
|
||||
{
|
||||
"id": "item:prototypes:oneos-web-workbench",
|
||||
"id": "item:prototypes:oneos-web-workbench-new",
|
||||
"kind": "item",
|
||||
"title": "工作台",
|
||||
"itemKey": "prototypes/oneos-web-workbench"
|
||||
"itemKey": "prototypes/oneos-web-workbench-new"
|
||||
},
|
||||
{
|
||||
"id": "folder-oneos-approval",
|
||||
@@ -141,6 +147,12 @@
|
||||
"title": "租赁合同",
|
||||
"itemKey": "prototypes/lease-contract-management"
|
||||
},
|
||||
{
|
||||
"id": "item:prototypes:self-operated-contract",
|
||||
"kind": "item",
|
||||
"title": "自营合同",
|
||||
"itemKey": "prototypes/self-operated-contract"
|
||||
},
|
||||
{
|
||||
"id": "item:prototypes:insurance-procurement",
|
||||
"kind": "item",
|
||||
@@ -296,6 +308,12 @@
|
||||
"kind": "item",
|
||||
"title": "任务工单",
|
||||
"itemKey": "prototypes/task-work-order"
|
||||
},
|
||||
{
|
||||
"id": "item:prototypes:self-operated-dispatch-task",
|
||||
"kind": "item",
|
||||
"title": "调度任务",
|
||||
"itemKey": "prototypes/self-operated-dispatch-task"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -303,14 +321,7 @@
|
||||
"id": "folder-1784341576489-spa7ms",
|
||||
"kind": "folder",
|
||||
"title": "审批中心",
|
||||
"children": [
|
||||
{
|
||||
"id": "item:prototypes:oneos-web-approval",
|
||||
"kind": "item",
|
||||
"title": "审批中心",
|
||||
"itemKey": "prototypes/oneos-web-approval"
|
||||
}
|
||||
]
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "item:prototypes:lease-business-line-overview",
|
||||
|
||||
53
CLAUDE.md
53
CLAUDE.md
@@ -1,13 +1,20 @@
|
||||
# Agents 工作流程说明
|
||||
## 项目信息
|
||||
|
||||
## 🧭 工作流程
|
||||
- 项目名称:OneOS1.2
|
||||
|
||||
| 步骤 | 说明 | 参考文档 |
|
||||
|------|------|----------|
|
||||
| ① 读取上下文 | 系统规则、用户资料、相关规范、已有原型与资源目录 | — |
|
||||
| ② 产品需求对齐 | 新建原型、明显重构或需求模糊时,先收敛目标用户、核心任务、范围、功能清单、内容来源和验收重点 | `rules/requirements-alignment-guide.md` |
|
||||
| ③ 设计方案对齐 | 产品需求确认后,先让用户从 3-4 个匹配的 `DESIGN.md` 候选中确认设计基底,再把布局、交互、视觉和内容呈现收敛为设计决策 | `rules/requirements-alignment-guide.md` |
|
||||
| ④ 原型开发与验收 | 根据已确认方案实现原型;遇到问题按错误信息定位修复,并完成预览验收 | `rules/prototype-development-guide.md` |
|
||||
# Agent 工作流程
|
||||
|
||||
## 🧭 核心顺序
|
||||
|
||||
```text
|
||||
产品需求确认 -> 设计方案确认 -> 原型实现
|
||||
```
|
||||
|
||||
| 阶段 | 继续前必须确认 | 参考文档 |
|
||||
|------|----------------|----------|
|
||||
| 产品需求 | 目标用户、核心任务、范围、功能清单、内容来源和验收重点 | `rules/requirements-alignment-guide.md` |
|
||||
| 设计方案 | `DESIGN.md` 设计基底、信息架构、交互路径、关键组件取舍和视觉方向 | `rules/requirements-alignment-guide.md` |
|
||||
| 原型实现 | 根据已确认的需求和设计方案实现原型 | `rules/prototype-development-guide.md` |
|
||||
|
||||
## 额外产物
|
||||
|
||||
@@ -16,39 +23,35 @@
|
||||
| vm-page 列表分页 | `src/common/TablePagination.tsx` + `vm-pagination.css` | `src/prototypes/vm-shared/DESIGN.md` |
|
||||
| 主题 | `src/themes/<theme-key>/` | `rules/theme-guide.md` |
|
||||
| 项目资料和文档 | `src/resources/` | `rules/resource-management-guide.md` |
|
||||
| UI Review 结论 | `src/prototypes/<prototype-name>/.spec/ui-review.md` | `rules/ui-review-guide.md` |
|
||||
| 原型 Review 结论 | `src/prototypes/<prototype-name>/.spec/prototype-review.md` | `rules/prototype-review-guide.md` |
|
||||
| 画布 | `src/prototypes/<prototype-name>/canvas.excalidraw`、`canvas-assets/` | 原型画布和画布素材 |
|
||||
| 业务逻辑规格 | `src/prototypes/<prototype-name>/.spec/<topic>.md` | `rules/business-logic-documentation-guide.md` |
|
||||
| 对象存储发布 URL | `.axhub/make/axhub.config.json` → `cloudPublishing.s3` | `rules/cloud-publish-url-guide.md` |
|
||||
| ACP 对话缓存 | `src/prototypes/<prototype-name>/.spec/acp/` | 本地私有运行数据,不提交、不导出、不发布 |
|
||||
| 原型标注布局 | `src/common/prototype-annotation-host.tsx` | `rules/prototype-annotation-layout-guide.md` |
|
||||
|
||||
## ⚠️ 重要原则
|
||||
|
||||
1. **产品需求和设计方案分阶段对齐**
|
||||
- 先确认做什么,再确认怎么表达;读取资料、规格/计划确认和开发验收过程中,发现影响方向的问题都要回到相应阶段继续对齐
|
||||
1. **需求与设计是继续工作的门禁**
|
||||
- 产品需求未确认时,禁止继续找视觉参考、拆页面结构、整理 `DESIGN.md` 候选、写设计方案或开发实现;先用简短摘要让用户确认目标用户、核心任务、范围/功能清单、内容来源和验收重点
|
||||
- 设计方案未确认时,禁止继续写规格/计划或开发实现;先确认设计基底、信息架构、交互路径、关键组件取舍和视觉方向
|
||||
- 发现目标、范围、内容来源、验收重点、信息架构、交互路径、视觉方向或设计基底存在多种合理选择时,立即停在对应门禁补充对齐
|
||||
|
||||
| 阶段 | 需要对齐的情况 |
|
||||
|------|----------------|
|
||||
| 读取资料 | 目标、边界、素材、参考或约束不清 |
|
||||
| 产品需求 | 出现不同目标用户、功能范围、内容来源或验收标准 |
|
||||
| 设计方案 | 出现不同信息架构、交互路径、视觉方向或设计基底 |
|
||||
| 开发验收 | 实现结果、体验取舍或验收标准发生变化 |
|
||||
|
||||
2. **优先创建和维护 task/todo**
|
||||
- 多步骤、高风险、需求对齐、方案确认或跨文件任务,优先用 task/todo 记录当前步骤、状态和下一步
|
||||
- 简单局部修改可以保持轻量,但要清楚说明当前正在处理什么、完成后如何验收
|
||||
3. **设计要判断何时收敛、何时发散**
|
||||
2. **设计要判断何时收敛、何时发散**
|
||||
- AI 应自行判断当前需要收拢需求还是探索解法:需求不清先收敛;需要改善体验或创新表达时再发散。发散是为了帮助用户选择最终方向
|
||||
3. **原型按生产级界面处理**
|
||||
- 本项目中的「原型」默认是可运行、接近正式产品的前端页面,不是黑白灰线框图或低保真草稿;只有用户明确要求时才使用低保真、wireframe、placeholder 等表达
|
||||
4. **不要把截图当唯一真相**
|
||||
- 截图用于视觉参考;有代码、组件、设计系统、业务资料或用户说明时,要结合上下文判断
|
||||
5. **早展示,早反馈**
|
||||
- 产品需求、设计方案或原型应尽早交给用户确认,不要等到全部完成后才暴露方向问题
|
||||
- 涉及页面意图、组件取舍或多方案比稿时,优先用低成本、快速的 Markdown ASCII Wireframe/Diagram 或 Mermaid 展示方案,先对齐需求
|
||||
6. **讲人话,用户不懂技术**
|
||||
- 用用户能理解的方式说明取舍、风险和结果;用户无法执行 CLI 命令,不得省略验收流程
|
||||
- 向用户请求反馈或验收时,提醒用户尽量提供截图、预览链接、页面路径或具体问题位置,便于准确定位和复现
|
||||
7. **复杂业务逻辑必须文档化**
|
||||
- 多步判定、跨模块校验、状态/公式推导、导入规则等,除代码外必须写入 `.spec/*.md` 与 `requirements-prd.md`,并同步标注目录(见 `rules/business-logic-documentation-guide.md`)
|
||||
- 不得默认「先实现、后补文档」;逻辑变更与 PRD/标注更新同一轮交付
|
||||
8. **对象存储发布 URL 与 Make 工具一致**
|
||||
- `{baseUrl}/{prototype-id}/index.html`;禁止擅自改路径后缀(见 `rules/cloud-publish-url-guide.md`)
|
||||
- 发布或写链接时格式为 `{baseUrl}/{prototype-id}/index.html`;禁止加 `prototypes/` 前缀或去掉 `index.html`(见 `rules/cloud-publish-url-guide.md`)
|
||||
|
||||
## 项目结构
|
||||
|
||||
|
||||
736
DESIGN.md
736
DESIGN.md
@@ -1,736 +0,0 @@
|
||||
---
|
||||
version: alpha
|
||||
name: Vercel-design-analysis
|
||||
description: An inspired interpretation of Vercel's design language — a developer-platform brand whose surface is a stark black-and-ink duet on near-white canvas, broken at hero scale by a multi-color mesh gradient (cyan / blue / magenta / amber) that acts as the entire decorative system, paired with a custom geometric sans for headlines and a monospaced caption face for technical labels.
|
||||
|
||||
colors:
|
||||
primary: "#171717"
|
||||
on-primary: "#ffffff"
|
||||
ink: "#171717"
|
||||
body: "#4d4d4d"
|
||||
mute: "#888888"
|
||||
hairline: "#ebebeb"
|
||||
hairline-strong: "#a1a1a1"
|
||||
canvas: "#ffffff"
|
||||
canvas-soft: "#fafafa"
|
||||
canvas-soft-2: "#f5f5f5"
|
||||
link: "#0070f3"
|
||||
link-deep: "#0761d1"
|
||||
link-bg-soft: "#d3e5ff"
|
||||
success: "#0070f3"
|
||||
error: "#ee0000"
|
||||
error-soft: "#f7d4d6"
|
||||
error-deep: "#c50000"
|
||||
warning: "#f5a623"
|
||||
warning-soft: "#ffefcf"
|
||||
warning-deep: "#ab570a"
|
||||
violet: "#7928ca"
|
||||
violet-soft: "#d8ccf1"
|
||||
violet-deep: "#4c2889"
|
||||
cyan: "#50e3c2"
|
||||
cyan-soft: "#aaffec"
|
||||
cyan-deep: "#29bc9b"
|
||||
highlight-pink: "#ff0080"
|
||||
highlight-magenta: "#eb367f"
|
||||
gradient-develop-start: "#007cf0"
|
||||
gradient-develop-end: "#00dfd8"
|
||||
gradient-preview-start: "#7928ca"
|
||||
gradient-preview-end: "#ff0080"
|
||||
gradient-ship-start: "#ff4d4d"
|
||||
gradient-ship-end: "#f9cb28"
|
||||
selection-bg: "#171717"
|
||||
selection-fg: "#f2f2f2"
|
||||
|
||||
typography:
|
||||
display-xl:
|
||||
fontFamily: Geist, Inter, system-ui, -apple-system, sans-serif
|
||||
fontSize: 48px
|
||||
fontWeight: 600
|
||||
lineHeight: 48px
|
||||
letterSpacing: -2.4px
|
||||
display-lg:
|
||||
fontFamily: Geist, Inter, system-ui, -apple-system, sans-serif
|
||||
fontSize: 32px
|
||||
fontWeight: 600
|
||||
lineHeight: 40px
|
||||
letterSpacing: -1.28px
|
||||
display-md:
|
||||
fontFamily: Geist, Inter, system-ui, -apple-system, sans-serif
|
||||
fontSize: 24px
|
||||
fontWeight: 600
|
||||
lineHeight: 32px
|
||||
letterSpacing: -0.96px
|
||||
display-sm:
|
||||
fontFamily: Geist, Inter, system-ui, -apple-system, sans-serif
|
||||
fontSize: 20px
|
||||
fontWeight: 600
|
||||
lineHeight: 28px
|
||||
letterSpacing: -0.6px
|
||||
body-lg:
|
||||
fontFamily: Geist, Inter, system-ui, -apple-system, sans-serif
|
||||
fontSize: 18px
|
||||
fontWeight: 400
|
||||
lineHeight: 28px
|
||||
letterSpacing: 0px
|
||||
body-md:
|
||||
fontFamily: Geist, Inter, system-ui, -apple-system, sans-serif
|
||||
fontSize: 16px
|
||||
fontWeight: 400
|
||||
lineHeight: 24px
|
||||
body-md-strong:
|
||||
fontFamily: Geist, Inter, system-ui, -apple-system, sans-serif
|
||||
fontSize: 16px
|
||||
fontWeight: 500
|
||||
lineHeight: 24px
|
||||
body-sm:
|
||||
fontFamily: Geist, Inter, system-ui, -apple-system, sans-serif
|
||||
fontSize: 14px
|
||||
fontWeight: 400
|
||||
lineHeight: 20px
|
||||
letterSpacing: -0.28px
|
||||
body-sm-strong:
|
||||
fontFamily: Geist, Inter, system-ui, -apple-system, sans-serif
|
||||
fontSize: 14px
|
||||
fontWeight: 500
|
||||
lineHeight: 20px
|
||||
letterSpacing: -0.28px
|
||||
caption:
|
||||
fontFamily: Geist, Inter, system-ui, -apple-system, sans-serif
|
||||
fontSize: 12px
|
||||
fontWeight: 400
|
||||
lineHeight: 16px
|
||||
caption-mono:
|
||||
fontFamily: Geist Mono, ui-monospace, SFMono-Regular, Menlo, Monaco, monospace
|
||||
fontSize: 12px
|
||||
fontWeight: 400
|
||||
lineHeight: 16px
|
||||
code:
|
||||
fontFamily: Geist Mono, ui-monospace, SFMono-Regular, Menlo, Monaco, monospace
|
||||
fontSize: 13px
|
||||
fontWeight: 400
|
||||
lineHeight: 20px
|
||||
button-md:
|
||||
fontFamily: Geist, Inter, system-ui, -apple-system, sans-serif
|
||||
fontSize: 14px
|
||||
fontWeight: 500
|
||||
lineHeight: 20px
|
||||
button-lg:
|
||||
fontFamily: Geist, Inter, system-ui, -apple-system, sans-serif
|
||||
fontSize: 16px
|
||||
fontWeight: 500
|
||||
lineHeight: 24px
|
||||
|
||||
rounded:
|
||||
none: 0px
|
||||
xs: 4px
|
||||
sm: 6px
|
||||
md: 8px
|
||||
lg: 12px
|
||||
xl: 16px
|
||||
pill-sm: 64px
|
||||
pill: 100px
|
||||
full: 9999px
|
||||
|
||||
spacing:
|
||||
xxs: 4px
|
||||
xs: 8px
|
||||
sm: 12px
|
||||
md: 16px
|
||||
lg: 24px
|
||||
xl: 32px
|
||||
2xl: 40px
|
||||
3xl: 48px
|
||||
4xl: 64px
|
||||
5xl: 96px
|
||||
6xl: 128px
|
||||
section: 192px
|
||||
|
||||
components:
|
||||
nav-bar:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.body-sm}"
|
||||
height: 64px
|
||||
padding: "{spacing.sm} {spacing.lg}"
|
||||
nav-link:
|
||||
textColor: "{colors.body}"
|
||||
typography: "{typography.body-sm}"
|
||||
rounded: "{rounded.full}"
|
||||
padding: "{spacing.xs} {spacing.sm}"
|
||||
nav-cta-signup:
|
||||
backgroundColor: "{colors.primary}"
|
||||
textColor: "{colors.on-primary}"
|
||||
typography: "{typography.body-sm-strong}"
|
||||
rounded: "{rounded.sm}"
|
||||
padding: "0px {spacing.xs}"
|
||||
height: 28px
|
||||
nav-cta-login:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.body-sm-strong}"
|
||||
rounded: "{rounded.sm}"
|
||||
padding: "0px {spacing.xs}"
|
||||
height: 28px
|
||||
nav-cta-ask-ai:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.ink}"
|
||||
borderColor: "{colors.hairline}"
|
||||
typography: "{typography.body-sm-strong}"
|
||||
rounded: "{rounded.sm}"
|
||||
padding: "0px {spacing.xs}"
|
||||
height: 28px
|
||||
button-primary:
|
||||
backgroundColor: "{colors.primary}"
|
||||
textColor: "{colors.on-primary}"
|
||||
typography: "{typography.button-lg}"
|
||||
rounded: "{rounded.pill}"
|
||||
padding: "0px {spacing.sm}"
|
||||
button-secondary:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.button-lg}"
|
||||
rounded: "{rounded.pill}"
|
||||
padding: "0px {spacing.sm}"
|
||||
button-primary-sm:
|
||||
backgroundColor: "{colors.primary}"
|
||||
textColor: "{colors.on-primary}"
|
||||
typography: "{typography.button-md}"
|
||||
rounded: "{rounded.pill}"
|
||||
padding: "0px {spacing.xs}"
|
||||
button-secondary-sm:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.button-md}"
|
||||
rounded: "{rounded.pill}"
|
||||
padding: "0px {spacing.xs}"
|
||||
tab-ghost:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.body-sm}"
|
||||
rounded: "{rounded.pill-sm}"
|
||||
padding: "0px {spacing.md}"
|
||||
icon-button-circular:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.ink}"
|
||||
borderColor: "{colors.hairline}"
|
||||
rounded: "{rounded.full}"
|
||||
card-marketing:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.body-md}"
|
||||
rounded: "{rounded.md}"
|
||||
padding: "{spacing.lg}"
|
||||
card-marketing-large:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.body-md}"
|
||||
rounded: "{rounded.lg}"
|
||||
padding: "{spacing.xl}"
|
||||
card-soft:
|
||||
backgroundColor: "{colors.canvas-soft}"
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.body-md}"
|
||||
rounded: "{rounded.md}"
|
||||
padding: "{spacing.lg}"
|
||||
template-card:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.body-md}"
|
||||
rounded: "{rounded.md}"
|
||||
padding: "{spacing.md}"
|
||||
code-editor-mockup:
|
||||
backgroundColor: "{colors.primary}"
|
||||
textColor: "{colors.on-primary}"
|
||||
typography: "{typography.code}"
|
||||
rounded: "{rounded.md}"
|
||||
padding: "{spacing.lg}"
|
||||
form-input:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.ink}"
|
||||
borderColor: "{colors.hairline}"
|
||||
typography: "{typography.body-sm}"
|
||||
rounded: "{rounded.sm}"
|
||||
padding: "0px {spacing.sm}"
|
||||
height: 40px
|
||||
form-input-sm:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.ink}"
|
||||
borderColor: "{colors.hairline}"
|
||||
typography: "{typography.body-sm}"
|
||||
rounded: "{rounded.sm}"
|
||||
padding: "0px {spacing.sm}"
|
||||
height: 32px
|
||||
form-input-lg:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.ink}"
|
||||
borderColor: "{colors.hairline}"
|
||||
typography: "{typography.body-md}"
|
||||
rounded: "{rounded.sm}"
|
||||
padding: "0px {spacing.sm}"
|
||||
height: 48px
|
||||
badge-secondary:
|
||||
backgroundColor: "{colors.canvas-soft}"
|
||||
textColor: "{colors.body}"
|
||||
typography: "{typography.caption}"
|
||||
rounded: "{rounded.full}"
|
||||
padding: "0px {spacing.xs}"
|
||||
pricing-card:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.body-md}"
|
||||
rounded: "{rounded.lg}"
|
||||
padding: "{spacing.xl}"
|
||||
pricing-card-featured:
|
||||
backgroundColor: "{colors.primary}"
|
||||
textColor: "{colors.on-primary}"
|
||||
typography: "{typography.body-md}"
|
||||
rounded: "{rounded.lg}"
|
||||
padding: "{spacing.xl}"
|
||||
logo-strip:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.body}"
|
||||
typography: "{typography.body-sm}"
|
||||
padding: "{spacing.lg} {spacing.xl}"
|
||||
hero-band:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.display-xl}"
|
||||
padding: "{spacing.4xl} {spacing.lg}"
|
||||
feature-mesh-band:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.display-lg}"
|
||||
padding: "{spacing.5xl} {spacing.lg}"
|
||||
showcase-band-light:
|
||||
backgroundColor: "{colors.canvas-soft}"
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.display-lg}"
|
||||
padding: "{spacing.5xl} {spacing.lg}"
|
||||
showcase-band-dark:
|
||||
backgroundColor: "{colors.primary}"
|
||||
textColor: "{colors.on-primary}"
|
||||
typography: "{typography.display-lg}"
|
||||
padding: "{spacing.5xl} {spacing.lg}"
|
||||
footer:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.body}"
|
||||
typography: "{typography.body-sm}"
|
||||
padding: "{spacing.4xl} {spacing.lg}"
|
||||
link-inline:
|
||||
textColor: "{colors.link}"
|
||||
typography: "{typography.body-md}"
|
||||
banner-marketing:
|
||||
backgroundColor: "{colors.canvas-soft}"
|
||||
textColor: "{colors.body}"
|
||||
typography: "{typography.body-sm}"
|
||||
rounded: "{rounded.full}"
|
||||
padding: "{spacing.xs} {spacing.sm}"
|
||||
|
||||
# ─── Examples (illustrative) — auto-derived; resolve any TO_FILL markers below ───
|
||||
ex-pricing-tier:
|
||||
description: "Default tier card. Mirrors pricing-card chrome on canvas-soft surface with a hairline border."
|
||||
backgroundColor: "{colors.canvas-soft}"
|
||||
textColor: "{colors.ink}"
|
||||
borderColor: "{colors.hairline}"
|
||||
rounded: "{rounded.lg}"
|
||||
padding: "{spacing.xl}"
|
||||
ex-pricing-tier-featured:
|
||||
description: "Featured tier — polarity-flipped to ink primary with white text and white CTA."
|
||||
backgroundColor: "{colors.ink}"
|
||||
textColor: "{colors.on-primary}"
|
||||
rounded: "{rounded.lg}"
|
||||
padding: "{spacing.xl}"
|
||||
ex-product-selector:
|
||||
description: "What's Included summary card — repurposed for the brand's GPU / inference / Pro feature tiers."
|
||||
backgroundColor: "{colors.canvas-soft}"
|
||||
rounded: "{rounded.md}"
|
||||
padding: "{spacing.lg}"
|
||||
ex-cart-drawer:
|
||||
description: "Subscription summary — line items per add-on (NOT a literal e-commerce cart)."
|
||||
backgroundColor: "{colors.canvas}"
|
||||
rounded: "{rounded.md}"
|
||||
padding: "{spacing.lg}"
|
||||
item-divider: "{colors.hairline}"
|
||||
ex-app-shell-row:
|
||||
description: "Sidebar nav row. Active state uses brand primary as a left-edge indicator bar."
|
||||
backgroundColor: "{colors.canvas}"
|
||||
activeIndicator: "{colors.primary}"
|
||||
rounded: "{rounded.sm}"
|
||||
padding: "{spacing.xs} {spacing.sm}"
|
||||
ex-data-table-cell:
|
||||
description: "Mirrors the brand's table chrome. Header uses caption-mono uppercase mono; body uses body-sm."
|
||||
headerBackground: "{colors.canvas-soft}"
|
||||
headerTypography: "{typography.caption-mono}"
|
||||
bodyTypography: "{typography.body-sm}"
|
||||
cellPadding: "{spacing.xs} {spacing.sm}"
|
||||
rowBorder: "{colors.hairline}"
|
||||
ex-auth-form-card:
|
||||
description: "Sign-in / sign-up card. Mirrors card-marketing-large chrome with form-input primitives inside."
|
||||
backgroundColor: "{colors.canvas-soft}"
|
||||
rounded: "{rounded.lg}"
|
||||
padding: "{spacing.xl}"
|
||||
ex-modal-card:
|
||||
description: "Modal dialog surface — same chrome as card-marketing-large with Level 5 modal shadow."
|
||||
backgroundColor: "{colors.canvas}"
|
||||
rounded: "{rounded.lg}"
|
||||
padding: "{spacing.xl}"
|
||||
ex-empty-state-card:
|
||||
description: "Empty-state illustration frame. Generous padding on canvas-soft."
|
||||
backgroundColor: "{colors.canvas-soft}"
|
||||
rounded: "{rounded.lg}"
|
||||
padding: "{spacing.3xl}"
|
||||
captionTypography: "{typography.body-md}"
|
||||
ex-toast:
|
||||
description: "Toast notification surface — flat-cornered card-marketing chrome with Level 4 shadow."
|
||||
backgroundColor: "{colors.canvas}"
|
||||
rounded: "{rounded.md}"
|
||||
padding: "{spacing.sm} {spacing.md}"
|
||||
typography: "{typography.body-sm}"
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
Vercel is a developer-platform brand — the page is a deployment dashboard's marketing surface, written for engineers who already know the syntax. It earns that posture with one of the cleanest stark systems on the web: near-white `{colors.canvas-soft}` body background, ink-near-black `{colors.ink}` text, a 200-step gray scale that gives every divider, border, and disabled state its own deliberate step. The only place the brand introduces colour at marketing scale is the multi-stop mesh gradient (`{colors.gradient-develop-start}` → `{colors.gradient-preview-end}` → `{colors.gradient-ship-start}` → cyan / magenta / amber) that floats in atmospheric backdrops, never miniaturised to a swatch. That gradient is the entire decoration system.
|
||||
|
||||
Type is the second decisive voice. The brand's own custom geometric sans (Geist) carries display, body, button — everything narrative — at weight 600 for display, 500 for buttons, 400 for body. A matching monospaced face (Geist Mono) carries technical labels: terminal mockups, code blocks, sometimes filename captions. Headlines are sentence-case with aggressive negative letter-spacing (`-2.4px` at 48 px hero) — the brand never letter-spaces positively, never goes uppercase outside of mono labels.
|
||||
|
||||
Surfaces use a four-step ladder: `{colors.canvas}` (pure white for cards), `{colors.canvas-soft}` 98% (the page body), `{colors.canvas-soft-2}` 95% (occasional inset region), `{colors.primary}` (the deep ink-near-black used as the polarity-flipped band when a section needs the dark mode treatment). Shadows are exceptionally subtle — every elevated card carries a stacked shadow built from `0px 1px 1px #00000005` + `0px 2px 2px #0000000a` + an inset border. Cards never float on heavy drop-shadow; they sit on the page held by hairline + soft glow.
|
||||
|
||||
**Key Characteristics:**
|
||||
- A single black-ink primary CTA `{colors.primary}` carries every conversion target, paired with white-on-white `button-secondary` for the secondary action. The brand uses 100 px pill shape for marketing CTAs and a tight 6 px square shape for in-app nav buttons.
|
||||
- A multi-stop mesh gradient (cyan-blue-magenta-amber) is the only decorative chrome — used at hero scale and inside feature-band atmospheric backdrops. It is the brand.
|
||||
- Every section eyebrow and small label uses the monospace face `{typography.caption-mono}` or `{typography.code}`; everything else is in the geometric sans.
|
||||
- Subtle stacked-shadow elevation — three offsets layered with 4-12 % black opacity — never a single heavy drop-shadow.
|
||||
- A complete 100–1000 gray + blue + red + amber + green + teal + purple + pink colour scale exists as a system token set, but the marketing surface uses only the `100`, `1000`, and `700`-level tones; the rest stay in the design-system tokens for in-product surfaces.
|
||||
- An "Active CPU" pricing rhythm: `pricing-card` lays out 3-up on the pricing page with `pricing-card-featured` (Pro tier) polarity-flipped to `{colors.primary}` against white-card siblings.
|
||||
|
||||
## Colors
|
||||
|
||||
### Brand & Accent
|
||||
- **Ink** (`{colors.primary}` — `#171717`): The single primary CTA color. Black-near-pure ink that carries every Sign Up pill, every footer CTA, the dark-band polarity-flip. Used as text color throughout the page on light surfaces. (Resolved from `--ds-gray-1000`.)
|
||||
- **Cyan** (`{colors.cyan}` — `#50e3c2`): A signature mint-cyan used in the brand gradient and inside Geist-system spotlight tokens. Visible inside the hero gradient stops.
|
||||
- **Highlight Pink** (`{colors.highlight-pink}` — `#ff0080`): The brand's highlight magenta, used as the high-saturation stop in the preview-gradient pair.
|
||||
- **Violet** (`{colors.violet}` — `#7928ca`): The deep purple used as the start of the preview-gradient and inside developer-console highlights.
|
||||
- **Link Blue** (`{colors.link}` — `#0070f3`): The brand's primary link color and the legacy `--geist-success` semantic.
|
||||
|
||||
### Surface
|
||||
- **Canvas** (`{colors.canvas}` — `#ffffff`): The pure-white card / dialog / modal surface.
|
||||
- **Canvas Soft** (`{colors.canvas-soft}` — `#fafafa`): The default page background — 98 % white. Almost every section sits on this tone.
|
||||
- **Canvas Soft 2** (`{colors.canvas-soft-2}` — `#f5f5f5`): A slightly deeper inset surface for "code editor inner background", template-card hover states, and dropdown menus.
|
||||
- **Hairline** (`{colors.hairline}` — `#ebebeb`): 1 px dividers — table rows, card borders, input borders.
|
||||
- **Hairline Strong** (`{colors.hairline-strong}` — `#a1a1a1`): The 500-level gray, used as the slightly-stronger divider on light bands and as the deemphasised text color.
|
||||
|
||||
### Text
|
||||
- **Ink** (`{colors.ink}` — `#171717`): Every heading and body paragraph on light surfaces.
|
||||
- **Body** (`{colors.body}` — `#4d4d4d`): Secondary text — sub-headings, body captions, nav-link inactive text, footer column body.
|
||||
- **Mute** (`{colors.mute}` — `#888888`): Lowest-priority text — placeholder text, fine print, low-key labels.
|
||||
- **On Primary** (`{colors.on-primary}` — `#ffffff`): All text on `{colors.primary}` surfaces.
|
||||
|
||||
### Semantic
|
||||
- **Success / Link** (`{colors.success}` — `#0070f3`): The brand's legacy success indicator doubles as the primary link color. Visible underline-on-hover for inline body links.
|
||||
- **Link Deep** (`{colors.link-deep}` — `#0761d1`): The pressed / visited tone for inline links.
|
||||
- **Link Bg Soft** (`{colors.link-bg-soft}` — `#d3e5ff`): Soft pastel blue fill for "what's new" pill banners and informational badges.
|
||||
- **Error** (`{colors.error}` — `#ee0000`): Validation red for destructive actions and form errors.
|
||||
- **Error Soft** (`{colors.error-soft}` — `#f7d4d6`): Soft pastel red for destructive-state backgrounds.
|
||||
- **Error Deep** (`{colors.error-deep}` — `#c50000`): Pressed / deep destructive state.
|
||||
- **Warning** (`{colors.warning}` — `#f5a623`): Caution / pending status indicator.
|
||||
- **Warning Soft** (`{colors.warning-soft}` — `#ffefcf`) / **Warning Deep** (`{colors.warning-deep}` — `#ab570a`): Background + pressed variants.
|
||||
|
||||
### Brand Gradient
|
||||
The brand's signature decoration is a three-pair gradient stack:
|
||||
- **Develop** (`{colors.gradient-develop-start}` `#007cf0` → `{colors.gradient-develop-end}` `#00dfd8`) — the blue-to-teal pair used to mark the "deploy" / "develop" rhythm.
|
||||
- **Preview** (`{colors.gradient-preview-start}` `#7928ca` → `{colors.gradient-preview-end}` `#ff0080`) — the violet-to-pink pair used for "preview" surfaces.
|
||||
- **Ship** (`{colors.gradient-ship-start}` `#ff4d4d` → `{colors.gradient-ship-end}` `#f9cb28`) — the coral-to-amber pair used for "ship" surfaces.
|
||||
|
||||
The three pairs collapse into a single multi-color mesh gradient when used as the hero atmospheric backdrop. Treat the gradient as one unified object — do not crop down to a single colour, do not reorder the stops, and do not miniaturise. Used at hero scale only.
|
||||
|
||||
## Typography
|
||||
|
||||
### Font Family
|
||||
Two custom faces carry the entire system:
|
||||
|
||||
1. **A custom geometric sans** (extracted as `Geist`) for every display, body, button, link, and label. Weights 400 / 500 / 600 are the working set; the face never appears in 700 or heavier. Display sizes are tracked aggressively negative (`-2.4 px` at 48 px hero, `-1.28 px` at 32 px section); body stays at neutral or slightly-negative tracking.
|
||||
2. **A custom monospaced face** (extracted as `Geist Mono`) for terminal mockups, code blocks, and small mono-caption labels — anything that wants to signal "technical." Weight 400 only at 12 – 13 px. Tracking neutral.
|
||||
|
||||
A condensed display sans (`Space Grotesk`) is loaded as a third face for occasional editorial moments but does not render as the primary face anywhere in the captured surfaces.
|
||||
|
||||
### Hierarchy
|
||||
|
||||
| Token | Size | Weight | Line Height | Letter Spacing | Use |
|
||||
|---|---|---|---|---|---|
|
||||
| `{typography.display-xl}` | 48px | 600 | 48px | -2.4px | Hero headline ("Build and deploy on the AI Cloud."). |
|
||||
| `{typography.display-lg}` | 32px | 600 | 40px | -1.28px | Section headlines ("Your frontend, delivered.", "A compute model for all workloads."). |
|
||||
| `{typography.display-md}` | 24px | 600 | 32px | -0.96px | Card-cluster headlines, pricing-tier names. |
|
||||
| `{typography.display-sm}` | 20px | 600 | 28px | -0.6px | Inline display micro-headings. |
|
||||
| `{typography.body-lg}` | 18px | 400 | 28px | 0 | Lead paragraphs under section headlines. |
|
||||
| `{typography.body-md}` | 16px | 400 | 24px | 0 | Default body paragraph. |
|
||||
| `{typography.body-md-strong}` | 16px | 500 | 24px | 0 | Bolded inline body. |
|
||||
| `{typography.body-sm}` | 14px | 400 | 20px | -0.28px | Secondary body, nav-link text, button-md labels. |
|
||||
| `{typography.body-sm-strong}` | 14px | 500 | 20px | -0.28px | Nav CTA labels, table-row emphasis. |
|
||||
| `{typography.caption}` | 12px | 400 | 16px | 0 | Footer secondary lines, badge labels. |
|
||||
| `{typography.caption-mono}` | 12px | 400 | 16px | 0 | Section eyebrows and label captions that want a technical voice. |
|
||||
| `{typography.code}` | 13px | 400 | 20px | 0 | Inline code, terminal mockups, command snippets. |
|
||||
| `{typography.button-md}` | 14px | 500 | 20px | 0 | Small / nav-scale button labels. |
|
||||
| `{typography.button-lg}` | 16px | 500 | 24px | 0 | Marketing-scale pill button labels. |
|
||||
|
||||
### Principles
|
||||
- **Negative tracking is part of the voice.** Display sizes use aggressive `-2.4` to `-0.6` px tracking. Reverting to default tracking breaks the brand.
|
||||
- **Sentence-case headlines, period-terminated.** Headlines like "Build and deploy on the AI Cloud." end with a deliberate period — that punctuation is part of the brand's voice.
|
||||
- **Mono for the technical layer only.** Section eyebrows, code blocks, terminal mockups. Body paragraphs never set in mono.
|
||||
- **Weight 600 is the display ceiling.** The geometric sans never appears at 700 / 800. The brand reads as a calmer system because of this.
|
||||
|
||||
### Note on Font Substitutes
|
||||
The two primary faces are proprietary (custom-cut for the brand). Open-source substitutes:
|
||||
- **Geometric sans** — *Inter* (400 / 500 / 600) is the closest stylistic match; `font-feature-settings: "ss01", "ss02"` enables the geometric alternates. *Satoshi* is a passable second choice.
|
||||
- **Monospace** — *JetBrains Mono* (400) at 12 – 13 px matches the technical voice. *IBM Plex Mono* is the second-best option.
|
||||
|
||||
## Layout
|
||||
|
||||
### Spacing System
|
||||
- **Base unit**: 4 px. The brand's `--geist-space` token is exactly 4 px and every captured value is a multiple of 4.
|
||||
- **Tokens**: `{spacing.xxs}` 4 px · `{spacing.xs}` 8 px · `{spacing.sm}` 12 px · `{spacing.md}` 16 px · `{spacing.lg}` 24 px · `{spacing.xl}` 32 px · `{spacing.2xl}` 40 px · `{spacing.3xl}` 48 px · `{spacing.4xl}` 64 px · `{spacing.5xl}` 96 px · `{spacing.6xl}` 128 px · `{spacing.section}` 192 px.
|
||||
- **Section padding**: marketing bands use `{spacing.4xl}` to `{spacing.5xl}` top/bottom. Hero bands stretch to `{spacing.section}` to give the mesh gradient room to breathe.
|
||||
- **Card interior padding**: marketing cards sit at `{spacing.lg}` to `{spacing.xl}`; template-grid cards stay tighter at `{spacing.md}` because they sit in a denser grid.
|
||||
- **Inline gap**: button rows, nav rows, and chip rows use `{spacing.sm}` to `{spacing.md}` between siblings. The brand's `--geist-gap` is exactly 24 px.
|
||||
|
||||
### Grid & Container
|
||||
- **Max width**: ~1400 px (`--ds-page-width`); the legacy `--geist-page-width` is 1200 px and still appears on some marketing surfaces. Content centres with horizontal gutters of `{spacing.lg}` 24 px on desktop, `{spacing.md}` 16 px on mobile.
|
||||
- **Column patterns**:
|
||||
- Three-feature row: 3-up at desktop, 1-up at mobile (rows like "Web Apps / Composable Commerce / Multi-tenant Platforms").
|
||||
- Tab pill row: 5-up centred row of `tab-ghost` pills.
|
||||
- Template-grid cluster: 5-up at desktop, scaling to 1-up at mobile.
|
||||
- Pricing tier grid: 3-up at desktop with the middle tier polarity-flipped.
|
||||
- Logo strip: ~5 logos wide, single row.
|
||||
|
||||
### Whitespace Philosophy
|
||||
The mesh gradient does most of the heavy decorative lifting; whitespace separates the bands. Section spacing is generous — `{spacing.4xl}` to `{spacing.5xl}` between bands lets the gradient breathe. Inside a card, the headline/paragraph stack is tight (`{spacing.xs}` 8 px gap), then a wider gap before the CTA cluster. The page reads as engineered — large gaps + tight interior, never the other way around.
|
||||
|
||||
### Responsive Strategy
|
||||
|
||||
#### Breakpoints
|
||||
|
||||
| Name | Width | Key Changes |
|
||||
|---|---|---|
|
||||
| Mobile | < 600px | Hero stacks; nav collapses to hamburger; 3-up feature grids drop to 1-up; tab pill row enables horizontal scroll. |
|
||||
| Tablet | 600–959px | 3-up grids drop to 2-up; nav still horizontal. |
|
||||
| Desktop | 960–1199px | Full 3-up grids; pricing 3-up. |
|
||||
| Wide | 1200–1399px | Container caps at 1400 px content width. |
|
||||
| Ultra-wide | ≥ 1400px | Content stays centred at 1400 px; bands stretch edge-to-edge in colour but content holds the max-width. |
|
||||
|
||||
#### Touch Targets
|
||||
The `button-primary` pill renders at ~32 px tall in nav and ~48 px tall in marketing contexts. Marketing CTAs comfortably meet WCAG AAA at all breakpoints; nav buttons inflate touch area through `{spacing.xs}` padding on mobile to meet the 44 × 44 px floor.
|
||||
|
||||
#### Collapsing Strategy
|
||||
- **Nav**: full link row + Ask AI / Log In / Sign Up pills at desktop. Collapses to logo + hamburger at mobile with the menu opening as a full-overlay.
|
||||
- **Hero**: mesh gradient stays centred; headline + body stack vertically at all breakpoints (the brand doesn't use a split-hero pattern).
|
||||
- **Three-feature row**: 3-up → 2-up → 1-up at the breakpoints above; cards keep their `{rounded.md}` 8 px shape across all viewports.
|
||||
- **Pricing card grid**: 3-up at desktop, vertical stack at mobile with `pricing-card-featured` always sitting in the middle.
|
||||
- **Template grid**: 5-up → 3-up → 2-up → 1-up. Each `template-card` keeps its 16:9 aspect on the image.
|
||||
|
||||
#### Image Behavior
|
||||
- **Mesh gradient**: rendered as inline SVG or canvas-painted gradient; scales fluidly with the hero container; never crops, never tiles.
|
||||
- **Customer logos**: rendered as monochrome SVGs in the logo strip; consistent 24 px height.
|
||||
- **Code editor mockup**: dark `{colors.primary}` rectangle with mono text rendered inside; treated as an image at the layout level.
|
||||
- **Template thumbnails**: 16:9 landscape inside `{rounded.md}` card chrome; lazy-loaded; consistent grayscale palette in the placeholder state.
|
||||
|
||||
## Elevation & Depth
|
||||
|
||||
| Level | Treatment | Use |
|
||||
|---|---|---|
|
||||
| Level 0 — Flat | No shadow, no border. | Full-bleed hero bands and the polarity-flipped dark sections. |
|
||||
| Level 1 — Inset Hairline | `0 0 0 1px #00000014` inset 1 px border. | Default card chrome — the brand's universal "you can see this card" cue. |
|
||||
| Level 2 — Subtle Drop | `0px 1px 1px #00000005, 0px 2px 2px #0000000a` plus inset hairline. | Slightly elevated cards (template-grid, marketing-card). |
|
||||
| Level 3 — Soft Stack | `0px 2px 2px #0000000a, 0px 8px 8px -8px #0000000a` plus inset hairline. | The "medium" elevation — feature-grid cards. |
|
||||
| Level 4 — Float Stack | `0px 2px 2px #0000000a, 0px 8px 16px -4px #0000000a` plus inset hairline. | "Large" elevation — pricing cards, callout panels. |
|
||||
| Level 5 — Modal | `0px 1px 1px #00000005, 0px 8px 16px -4px #0000000a, 0px 24px 32px -8px #0000000f` plus inset hairline. | Modal / dialog surfaces and dropdown menus. |
|
||||
|
||||
The brand uses STACKED shadows — multiple small offsets layered to fake natural light — never a single 8-px-blur generic drop. Inset hairline rings are always added so the card edge stays crisp.
|
||||
|
||||
### Decorative Depth
|
||||
- **Mesh gradient as atmospheric depth**: the hero's multi-stop gradient is the brand's only "atmospheric" effect — applied as a flat 2-D backdrop rather than a 3-D illustration.
|
||||
- **Polarity-flipped dark band as section-depth**: switching the surface from `{colors.canvas-soft}` to `{colors.primary}` (the deep ink) is the brand's chief depth cue between bands.
|
||||
- **Inset-shadow + drop-shadow combo**: the cards' combination of an inset 1 px ring and a multi-stop drop produces a "card sits on the page" effect without ever feeling material-heavy.
|
||||
|
||||
## Shapes
|
||||
|
||||
### Border Radius Scale
|
||||
|
||||
| Token | Value | Use |
|
||||
|---|---|---|
|
||||
| `{rounded.none}` | 0px | Full-bleed hero / footer bands. |
|
||||
| `{rounded.xs}` | 4px | Tightest inline pill — the `nav-cta-signup` 6-px-radius button (mapped to `xs/sm`). |
|
||||
| `{rounded.sm}` | 6px | The brand's `--geist-radius` token — base UI radius for in-app buttons, form inputs, dropdown menus. |
|
||||
| `{rounded.md}` | 8px | The brand's `--geist-marketing-radius` token — feature cards, template cards. |
|
||||
| `{rounded.lg}` | 12px | Slightly larger card chrome (pricing-card variants). |
|
||||
| `{rounded.xl}` | 16px | Largest card chrome — when a card hosts a hero image cap. |
|
||||
| `{rounded.pill-sm}` | 64px | Tab-ghost pills inside the "AI Apps / Web Apps / Ecommerce / Marketing / Platforms" row. |
|
||||
| `{rounded.pill}` | 100px | The marketing CTA pill — `button-primary`, `button-secondary`, "Start Deploying" pill. |
|
||||
| `{rounded.full}` | 9999px | Icon-button circular containers, nav-link ghost pills. |
|
||||
|
||||
### Photography Geometry
|
||||
- **Mesh gradient**: full-bleed 2-D atmospheric backdrop, never cropped to a frame; treated as the page's wallpaper.
|
||||
- **Customer logos**: monochrome SVG, consistent 24 px height in a flex row.
|
||||
- **Code editor mockup**: 16:10 dark rectangle, `{rounded.md}` corners.
|
||||
- **Template thumbnails**: 16:9 landscape inside `{rounded.md}` chrome.
|
||||
- **Showcase imagery**: 2:1 or 16:9 inside `{rounded.lg}` to `{rounded.xl}` chrome with a stacked shadow.
|
||||
|
||||
## Components
|
||||
|
||||
### Buttons
|
||||
|
||||
**`button-primary`** — the canonical 100-px-radius black pill, marketing scale.
|
||||
- Background `{colors.primary}`, text `{colors.on-primary}`, label set in `{typography.button-lg}`, padding `0px {spacing.sm}` 12 px, shape `{rounded.pill}` 100 px. Renders ~48 px tall when paired with the marketing flex layout.
|
||||
|
||||
**`button-secondary`** — the white pill paired with the black primary inside marketing bands.
|
||||
- Background `{colors.canvas}`, text `{colors.ink}`, same typography + padding as `button-primary`, shape `{rounded.pill}`.
|
||||
|
||||
**`button-primary-sm`** — the smaller-scale primary pill used inside nav and pricing-card CTAs.
|
||||
- Background `{colors.primary}`, text `{colors.on-primary}`, label set in `{typography.button-md}` (14 px / 500), shape `{rounded.pill}`.
|
||||
|
||||
**`button-secondary-sm`** — the smaller-scale white pill paired with `button-primary-sm`.
|
||||
- Background `{colors.canvas}`, text `{colors.ink}`, same typography + shape as `button-primary-sm`.
|
||||
|
||||
**`tab-ghost`** — the centred-row tab pill ("AI Apps / Web Apps / Ecommerce / Marketing / Platforms").
|
||||
- Background `{colors.canvas}`, text `{colors.ink}`, label set in `{typography.body-sm}`, padding `0px {spacing.md}`, shape `{rounded.pill-sm}` 64 px.
|
||||
|
||||
**`icon-button-circular`** — the circular icon container (often a "?" or arrow inside).
|
||||
- Background `{colors.canvas}`, dark icon, 1 px solid hairline border, shape `{rounded.full}`.
|
||||
|
||||
**Nav CTAs:**
|
||||
|
||||
**`nav-cta-signup`** — the small black "Sign Up" button in the nav row.
|
||||
- Background `{colors.primary}`, text `{colors.on-primary}`, label `{typography.body-sm-strong}`, padding `0px {spacing.xs}`, height 28 px, shape `{rounded.sm}` 6 px (the brand's `--geist-radius`).
|
||||
|
||||
**`nav-cta-login`** — the white "Log In" button in the nav.
|
||||
- Background `{colors.canvas}`, text `{colors.ink}`, same typography / height / shape as `nav-cta-signup`.
|
||||
|
||||
**`nav-cta-ask-ai`** — the small "Ask AI" button with a faint border.
|
||||
- Background `{colors.canvas}`, text `{colors.ink}`, 1 px solid `{colors.hairline}` border (extracted as `0px solid rgb(235, 235, 235)`), same typography / height / shape.
|
||||
|
||||
### Cards & Containers
|
||||
|
||||
**`card-marketing`** — the canonical marketing feature card (3-up section cards).
|
||||
- Background `{colors.canvas}`, text `{colors.ink}`, padding `{spacing.lg}` 24 px, shape `{rounded.md}` 8 px (the `--geist-marketing-radius`). Carries Level 3 soft-stack shadow.
|
||||
|
||||
**`card-marketing-large`** — the larger marketing card used for "compute model" / "AI Gateway" callouts.
|
||||
- Background `{colors.canvas}`, text `{colors.ink}`, padding `{spacing.xl}`, shape `{rounded.lg}` 12 px. Carries Level 4 float-stack shadow.
|
||||
|
||||
**`card-soft`** — the soft-tinted card used inside cluster groups (lighter than canvas-soft).
|
||||
- Background `{colors.canvas-soft}`, text `{colors.ink}`, padding `{spacing.lg}`, shape `{rounded.md}`.
|
||||
|
||||
**`template-card`** — the deploy-template card in the "Deploy your first app" grid.
|
||||
- Background `{colors.canvas}`, text `{colors.ink}`, padding `{spacing.md}` 16 px, shape `{rounded.md}` 8 px. Hosts a 16:9 thumbnail at the top.
|
||||
|
||||
**`code-editor-mockup`** — the dark code-preview surface inside marketing bands.
|
||||
- Background `{colors.primary}`, text `{colors.on-primary}`, body in `{typography.code}` (13 px / Geist Mono), padding `{spacing.lg}` 24 px, shape `{rounded.md}` 8 px.
|
||||
|
||||
**`pricing-card`** — the default pricing-tier card.
|
||||
- Background `{colors.canvas}`, text `{colors.ink}`, padding `{spacing.xl}` 32 px, shape `{rounded.lg}` 12 px. Inside: tier name in `{typography.display-md}`, price in `{typography.display-xl}`, feature list in `{typography.body-md}` rows, CTA at the bottom.
|
||||
|
||||
**`pricing-card-featured`** — the polarity-flipped "Pro" tier card.
|
||||
- Background `{colors.primary}`, text `{colors.on-primary}`, same shape + padding as `pricing-card`. CTA inverts to `button-secondary-sm` (white pill on black card).
|
||||
|
||||
### Inputs & Forms
|
||||
|
||||
**`form-input`** — the canonical text input.
|
||||
- Background `{colors.canvas}`, text `{colors.ink}`, 1 px solid `{colors.hairline}` border, body in `{typography.body-sm}` (14 px), padding `0px {spacing.sm}`, height 40 px (the brand's `--geist-form-height`), shape `{rounded.sm}` 6 px.
|
||||
|
||||
**`form-input-sm`** — small-height variant (32 px tall) for tight forms.
|
||||
- Same as `form-input` but height 32 px (the `--geist-form-small-height`).
|
||||
|
||||
**`form-input-lg`** — large-height variant (48 px tall) for hero CTAs.
|
||||
- Same as `form-input` but height 48 px (the `--geist-form-large-height`); body in `{typography.body-md}` 16 px.
|
||||
|
||||
### Navigation
|
||||
|
||||
**`nav-bar`** — the sticky top nav.
|
||||
- Background `{colors.canvas}`, text `{colors.ink}`, height 64 px (the brand's `--header-height`), padding `{spacing.sm} {spacing.lg}`. Layout: logo left, link row centre, "Ask AI / Log In / Sign Up" cluster right.
|
||||
|
||||
**`nav-link`** — the centred link row inside `nav-bar`.
|
||||
- Text `{colors.body}`, set in `{typography.body-sm}`, padding `{spacing.xs} {spacing.sm}`, shape `{rounded.full}` (ghost pill — visible only on hover or active, but the radius is documented).
|
||||
|
||||
**`footer`** — the bottom 4-column nav.
|
||||
- Background `{colors.canvas}`, text `{colors.body}`, padding `{spacing.4xl} {spacing.lg}`. Eyebrow column labels in `{typography.caption-mono}` (uppercase mono effect); link rows in `{typography.body-sm}`.
|
||||
|
||||
### Signature Components
|
||||
|
||||
**`hero-band`** — the white hero with the mesh gradient backdrop.
|
||||
- Background `{colors.canvas}` (or `{colors.canvas-soft}` on some surfaces), text `{colors.ink}`, padding `{spacing.4xl} {spacing.lg}`. Inside: a small mono badge above the headline, the headline in `{typography.display-xl}` (sentence-case, period-terminated), a body lead in `{typography.body-lg}`, then a CTA row with `button-primary` + `button-secondary`. The mesh gradient sits behind, scaled to occupy roughly the top half of the band.
|
||||
|
||||
**`feature-mesh-band`** — the secondary section that hosts a mesh-gradient atmospheric backdrop with feature copy on top.
|
||||
- Background `{colors.canvas}`, text `{colors.ink}`, padding `{spacing.5xl} {spacing.lg}`. Section headline in `{typography.display-lg}`; supporting body in `{typography.body-md}`.
|
||||
|
||||
**`showcase-band-light`** — a soft-canvas section ("Deploy your first app in seconds").
|
||||
- Background `{colors.canvas-soft}`, text `{colors.ink}`, padding `{spacing.5xl} {spacing.lg}`.
|
||||
|
||||
**`showcase-band-dark`** — the polarity-flipped dark band ("A compute model for all workloads").
|
||||
- Background `{colors.primary}`, text `{colors.on-primary}`, padding `{spacing.5xl} {spacing.lg}`. Section headline in `{typography.display-lg}` (white on black). Often contains a `code-editor-mockup` flush with the band.
|
||||
|
||||
**`logo-strip`** — the customer-logo wrapping row near the top of the page.
|
||||
- Background `{colors.canvas}`, text `{colors.body}`, padding `{spacing.lg} {spacing.xl}`. Logos rendered as monochrome SVGs at consistent height.
|
||||
|
||||
**`badge-secondary`** — the small inline metadata pill ("New", "Beta", "Live").
|
||||
- Background `{colors.canvas-soft}`, text `{colors.body}`, body in `{typography.caption}`, padding `0px {spacing.xs}`, shape `{rounded.full}`.
|
||||
|
||||
**`banner-marketing`** — the "Introducing X" announcement pill at the top of pages.
|
||||
- Background `{colors.canvas-soft}`, text `{colors.body}`, body in `{typography.body-sm}`, padding `{spacing.xs} {spacing.sm}`, shape `{rounded.full}`.
|
||||
|
||||
**`link-inline`** — body-copy inline links.
|
||||
- Text `{colors.link}` (`#0070f3`), body in `{typography.body-md}`, underlined.
|
||||
|
||||
### Examples (illustrative)
|
||||
|
||||
> Auto-derived kit-mirror demonstration surfaces (`scripts/derive-examples-block.mjs`). Each `ex-*` entry references brand-native primitives so downstream consumers (`/preview-design`, `/generate-kit`) re-skin the same 10 surfaces consistently. `TO_FILL` markers indicate missing primitives — resolve in the LLM judgment pass.
|
||||
|
||||
**`ex-pricing-tier`** — Default Pricing tier card. Re-uses feature-card chrome with brand canvas-soft surface.
|
||||
- Properties: `backgroundColor`, `textColor`, `borderColor`, `rounded`, `padding`
|
||||
|
||||
**`ex-pricing-tier-featured`** — Featured/highlighted tier — polarity-flipped surface (dark fill + light text in light mode, light fill + dark text in dark mode).
|
||||
- Properties: `backgroundColor`, `textColor`, `rounded`, `padding`
|
||||
|
||||
**`ex-product-selector`** — What's Included summary card — re-purposed for SaaS / B2B verticals (NOT a literal product gallery).
|
||||
- Properties: `backgroundColor`, `rounded`, `padding`
|
||||
|
||||
**`ex-cart-drawer`** — Subscription summary — re-purposed for SaaS / B2B (line items per add-on, not literal cart).
|
||||
- Properties: `backgroundColor`, `rounded`, `padding`, `item-divider`
|
||||
|
||||
**`ex-app-shell-row`** — Sidebar nav row inside the App Shell example. Active state uses brand primary as the indicator.
|
||||
- Properties: `backgroundColor`, `activeIndicator`, `rounded`, `padding`
|
||||
|
||||
**`ex-data-table-cell`** — Default data-table th + td chrome. Header uses mono-caps eyebrow typography; body uses body-sm.
|
||||
- Properties: `headerBackground`, `headerTypography`, `bodyTypography`, `cellPadding`, `rowBorder`
|
||||
|
||||
**`ex-auth-form-card`** — Sign-in / sign-up card. Re-uses feature-card chrome with text-input primitives inside.
|
||||
- Properties: `backgroundColor`, `rounded`, `padding`
|
||||
|
||||
**`ex-modal-card`** — Modal dialog surface — same chrome as feature-card with elevated shadow.
|
||||
- Properties: `backgroundColor`, `rounded`, `padding`
|
||||
|
||||
**`ex-empty-state-card`** — Empty-state illustration frame.
|
||||
- Properties: `backgroundColor`, `rounded`, `padding`, `captionTypography`
|
||||
|
||||
**`ex-toast`** — Toast notification surface — feature-card shape + medium shadow.
|
||||
- Properties: `backgroundColor`, `rounded`, `padding`, `typography`
|
||||
|
||||
|
||||
## Do's and Don'ts
|
||||
|
||||
### Do
|
||||
- Reserve `{colors.primary}` (`#171717`) for primary CTAs across the page. Black ink IS the conversion target.
|
||||
- Use `{rounded.pill}` 100 px for every marketing-scale CTA and `{rounded.sm}` 6 px for nav-scale buttons. The two pill scales coexist deliberately.
|
||||
- Set every headline in `{typography.display-*}` weight 600, sentence-case, often period-terminated. Aggressive negative tracking is part of the voice.
|
||||
- Use the brand mesh gradient as atmospheric decoration at hero scale only — never miniaturise it to an icon, never reduce to a single colour.
|
||||
- Layer stacked shadows (multiple small offsets with inset hairline) rather than single heavy drops. The brand's elevation is calmer than Material.
|
||||
- Cycle page surfaces in `{colors.canvas-soft}` → `{colors.canvas}` → `{colors.primary}` polarity-flipped bands; the dark band IS the depth cue.
|
||||
- Set every code block and technical eyebrow in `{typography.code}` / `{typography.caption-mono}`. Mono is the voice of the platform.
|
||||
|
||||
### Don't
|
||||
- Don't introduce a sixth accent colour. The brand operates with ink + gray + the four-pair gradient palette; new accents flatten the voice.
|
||||
- Don't render headlines in all-caps. Sentence-case + negative tracking is non-negotiable.
|
||||
- Don't drop a single heavy drop-shadow on cards. The brand's elevation is built from stacked small offsets + inset hairline rings.
|
||||
- Don't render the brand gradient at icon scale or in a single-colour reduced form. The gradient lives at hero scale only.
|
||||
- Don't promote the geometric sans to weight 700. The brand's display ceiling is 600.
|
||||
- Don't pair the marketing 100-px pill CTA shape with the 6-px nav radius on the same screen — pick a scale and stay there.
|
||||
- Don't set body paragraphs in the mono face. The mono is for code + technical labels only.
|
||||
9
package-lock.json
generated
9
package-lock.json
generated
@@ -9,6 +9,7 @@
|
||||
"version": "0.1.11",
|
||||
"dependencies": {
|
||||
"@axhub/annotation": "^1.0.10",
|
||||
"@axhub/commentary-react": "^1.0.0",
|
||||
"docx-preview": "^0.3.7",
|
||||
"jszip": "^3.10.1",
|
||||
"lucide-react": "^0.562.0",
|
||||
@@ -535,6 +536,14 @@
|
||||
"react-dom": "^18.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@axhub/commentary-react": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@axhub/commentary-react/-/commentary-react-1.0.0.tgz",
|
||||
"integrity": "sha512-2wyrXZxowCr48fo+lLRImd2Vzzvt+IdAT+xXRwq8BIoat0HQXi6vl+MTXvKWQjMS54UVfrGkLdFgAQeEMB7YXQ==",
|
||||
"peerDependencies": {
|
||||
"react": "^18.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@axhub/annotation": "^1.0.10",
|
||||
"@axhub/commentary-react": "^1.0.0",
|
||||
"docx-preview": "^0.3.7",
|
||||
"jszip": "^3.10.1",
|
||||
"lucide-react": "^0.562.0",
|
||||
|
||||
@@ -12,6 +12,12 @@
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/engineering/to-prd/SKILL.md",
|
||||
"computedHash": "5a733e19d825f22e83de52e68b8840c9fd2fa7501282277a122c1aa28a922059"
|
||||
},
|
||||
"user-story-mapping": {
|
||||
"source": "deanpeters/product-manager-skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/user-story-mapping/SKILL.md",
|
||||
"computedHash": "e4ffdd47016435d03ba1bc3bcfae5e13ac3bd7a7e81569a026c11066f5c6c174"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ const MORE_ACTION_ICONS: Record<string, LucideIcon> = {
|
||||
manage: Settings2,
|
||||
preview: Eye,
|
||||
download: Download,
|
||||
failDetail: FileText,
|
||||
};
|
||||
|
||||
function renderMenuItemLabel(item: OperationActionItem) {
|
||||
@@ -95,7 +96,7 @@ function ViewIcon() {
|
||||
}
|
||||
|
||||
function MoreIcon() {
|
||||
return <MoreHorizontal className="vm-op-action__icon" aria-hidden />;
|
||||
return <MoreHorizontal className="vm-op-more-btn__icon" aria-hidden strokeWidth={1.75} />;
|
||||
}
|
||||
|
||||
const PrimaryActionButton = React.forwardRef<
|
||||
@@ -121,18 +122,22 @@ const PrimaryActionButton = React.forwardRef<
|
||||
},
|
||||
ref,
|
||||
) {
|
||||
const isMore = icon === 'more';
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
type="button"
|
||||
className={['vm-op-action', className].filter(Boolean).join(' ')}
|
||||
className={[
|
||||
isMore ? 'vm-op-more-btn' : 'vm-op-action',
|
||||
className,
|
||||
].filter(Boolean).join(' ')}
|
||||
aria-label={ariaLabel}
|
||||
aria-haspopup={ariaHasPopup ? 'menu' : undefined}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
>
|
||||
{icon === 'view' ? <ViewIcon /> : <MoreIcon />}
|
||||
<span className="vm-op-action__label">{label}</span>
|
||||
{isMore ? <MoreIcon /> : <ViewIcon />}
|
||||
{isMore ? null : <span className="vm-op-action__label">{label}</span>}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
@@ -236,9 +241,9 @@ export function OperationActions({
|
||||
<PrimaryActionButton
|
||||
icon="more"
|
||||
label={moreLabel}
|
||||
ariaLabel={moreLabel + '操作'}
|
||||
ariaLabel="更多操作"
|
||||
ariaHasPopup
|
||||
className={moreOpen ? 'vm-op-action--open' : undefined}
|
||||
className={moreOpen ? 'vm-op-more-btn--open' : undefined}
|
||||
/>
|
||||
</Dropdown>
|
||||
) : null}
|
||||
|
||||
@@ -11,6 +11,41 @@ var H2_VERIFY_UNVERIFIED = 'unverified';
|
||||
var H2_RECORD_SOURCE_STATION = 'station';
|
||||
var H2_RECORD_SOURCE_LINGNIU = 'lingniu';
|
||||
|
||||
/**
|
||||
* 原型本地「系统车辆列表」车牌(不含尾缀 F)。命中 → 羚牛车辆,否则 → 非羚牛车辆。
|
||||
* 正式环境应改为车辆管理接口;未接真实 API。
|
||||
*/
|
||||
var H2_FLEET_PLATE_KEYS = [
|
||||
'浙A55666', '浙A77888', '浙A99001', '浙A12345', '浙A67890', '浙A88888', '浙A03561',
|
||||
'浙B23456', '浙B99999', '浙B58888',
|
||||
'沪A88888', '沪BDB9161', '沪ADB9161',
|
||||
'苏E33333',
|
||||
'浙A88H201', '浙F00688', '浙F07588', '浙F06618',
|
||||
'粤AGR8556', '粤AGP5156',
|
||||
/* 车辆氢费明细演示车牌 */
|
||||
'浙AD12345', '浙AH55660', '浙BK33210', '粤BK33210', '沪AD12345', '苏EF99887', '京CN88771', '川AL55602'
|
||||
];
|
||||
|
||||
var H2_FLEET_PLATE_SET = (function () {
|
||||
var set = {};
|
||||
H2_FLEET_PLATE_KEYS.forEach(function (p) {
|
||||
set[String(p).toUpperCase()] = true;
|
||||
});
|
||||
return set;
|
||||
})();
|
||||
|
||||
/** 车牌规范化键:去尾缀 F、大写、去空白 */
|
||||
function h2BridgeNormalizePlateKey(plateNo) {
|
||||
return String(plateNo || '').trim().toUpperCase().replace(/F$/u, '');
|
||||
}
|
||||
|
||||
/** 是否羚牛车辆(系统车辆列表命中) */
|
||||
function h2BridgeIsLingniuVehicle(plateNo) {
|
||||
var key = h2BridgeNormalizePlateKey(plateNo);
|
||||
if (!key) return false;
|
||||
return Boolean(H2_FLEET_PLATE_SET[key]);
|
||||
}
|
||||
|
||||
var H2_STATION_CODE_MAP = {
|
||||
'中国石油中油高新能源牙谷加油加氢站': 'JX-H2-001',
|
||||
'杭州临平加氢站': 'HZ-H2-002',
|
||||
@@ -41,7 +76,9 @@ var H2_CANONICAL_LEDGER_SEED = [
|
||||
{ key: 'rf-7', id: 'rf-7', stationId: 'HZ-H2-002', stationName: '杭州临平加氢站', hydrogenTime: '2026-05-24 11:05:40', plateNo: '浙B58888F', customerName: '杭州临平城配中心', hydrogenKg: 11.8, costUnitPrice: 43.0, costTotal: 507.4, customerUnitPrice: 46, customerAmount: 542.8, settlementStatus: 'customer', mileageKm: 110340, creatorName: '李四', verifyStatus: H2_VERIFY_VERIFIED, verifiedAt: '2026-05-25 10:00:00', reconcileStatus: H2_RECONCILE_RECONCILED, reconcileDate: '2026-05-25 10:00:00' },
|
||||
{ key: 'rf-8', id: 'rf-8', stationId: 'SH-H2-003', stationName: '上海宝山加氢站', hydrogenTime: '2026-04-20 16:45:18', plateNo: '沪A88888F', customerName: '上海羚牛氢运', hydrogenKg: 8.0, costUnitPrice: 44.0, costTotal: 352.0, customerUnitPrice: 47, customerAmount: 376.0, settlementStatus: 'internal', mileageKm: 88420, creatorName: '王静', verifyStatus: H2_VERIFY_VERIFIED, verifiedAt: '2026-04-21 10:00:00', reconcileStatus: H2_RECONCILE_RECONCILED, reconcileDate: '2026-04-21 10:00:00' },
|
||||
{ key: 'rf-9', id: 'rf-9', stationId: 'SH-H2-003', stationName: '上海宝山加氢站', hydrogenTime: '2026-04-08 09:12:55', plateNo: '沪BDB9161F', customerName: '宝山园区试运车队', hydrogenKg: 9.5, costUnitPrice: 44.0, costTotal: 418.0, customerUnitPrice: 47, customerAmount: 446.5, settlementStatus: 'customer_self', mileageKm: 76500, creatorName: '张三', verifyStatus: H2_VERIFY_VERIFIED, verifiedAt: '2026-04-09 10:00:00', reconcileStatus: H2_RECONCILE_PENDING, reconcileDate: null },
|
||||
{ key: 'rf-10', id: 'rf-10', stationId: 'SZ-H2-004', stationName: '苏州工业园区备用站', hydrogenTime: '2026-03-15 10:00:00', plateNo: '苏E33333F', customerName: '苏州试运客户', hydrogenKg: 6.2, costUnitPrice: 41.0, costTotal: 254.2, customerUnitPrice: 44, customerAmount: 272.8, settlementStatus: 'customer', mileageKm: 45210, creatorName: '赵敏', verifyStatus: H2_VERIFY_UNVERIFIED, verifiedAt: null, reconcileStatus: H2_RECONCILE_PENDING }
|
||||
{ key: 'rf-10', id: 'rf-10', stationId: 'SZ-H2-004', stationName: '苏州工业园区备用站', hydrogenTime: '2026-03-15 10:00:00', plateNo: '苏E33333F', customerName: '苏州试运客户', hydrogenKg: 6.2, costUnitPrice: 41.0, costTotal: 254.2, customerUnitPrice: 44, customerAmount: 272.8, settlementStatus: 'customer', mileageKm: 45210, creatorName: '赵敏', verifyStatus: H2_VERIFY_UNVERIFIED, verifiedAt: null, reconcileStatus: H2_RECONCILE_PENDING },
|
||||
/* 非羚牛车辆演示:车牌不在系统车辆列表 → 列表核对/对账字段应显示为空 */
|
||||
{ key: 'rf-ext-1', id: 'rf-ext-1', stationId: 'JX-H2-001', stationName: '中国石油中油高新能源牙谷加油加氢站', hydrogenTime: '2026-07-13 14:22:00', plateNo: '浙C77801F', customerName: '中国石油中油高新能源牙谷加油加氢站·站端上报', hydrogenKg: 8.5, costUnitPrice: 42.5, costTotal: 361.25, customerUnitPrice: 42.5, customerAmount: 361.25, settlementStatus: 'customer', mileageKm: 88000, creatorName: '站端账号', verifyStatus: H2_VERIFY_UNVERIFIED, verifiedAt: null, reconcileStatus: H2_RECONCILE_PENDING }
|
||||
];
|
||||
|
||||
function h2BridgeFormatDateTime(value) {
|
||||
@@ -263,26 +300,28 @@ function h2BridgeResolveStationId(stationId, stationName) {
|
||||
return a || '';
|
||||
}
|
||||
|
||||
/** 手工台账种子:昨日已上传;今日默认未上传,便于演示拦截新增 */
|
||||
/** 手工台账种子:前日已上传;昨日默认未上传,便于演示「前一日缺失则今日禁新增」 */
|
||||
function h2BridgeBuildInitialManualLedgers() {
|
||||
var yesterday = h2BridgeYesterdayDateKey();
|
||||
var d = new Date();
|
||||
d.setDate(d.getDate() - 2);
|
||||
var dayBeforeYesterday = h2BridgeTodayDateKey(d);
|
||||
return [
|
||||
{
|
||||
id: 'ml-seed-yesterday',
|
||||
id: 'ml-seed-day-before-yesterday',
|
||||
stationId: 'JX-H2-001',
|
||||
stationName: '中国石油中油高新能源牙谷加油加氢站',
|
||||
ledgerDate: yesterday,
|
||||
ledgerDate: dayBeforeYesterday,
|
||||
images: [
|
||||
{
|
||||
id: 'ml-img-seed-1',
|
||||
name: '昨日手工台账.jpg',
|
||||
name: '前日手工台账.jpg',
|
||||
dataUrl: '',
|
||||
placeholder: true,
|
||||
archived: true,
|
||||
uploadedAt: yesterday + ' 18:30:00'
|
||||
uploadedAt: dayBeforeYesterday + ' 18:30:00'
|
||||
}
|
||||
],
|
||||
updatedAt: yesterday + ' 18:30:00'
|
||||
updatedAt: dayBeforeYesterday + ' 18:30:00'
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -507,6 +546,9 @@ function h2BridgeMapToStationLedgerRow(row, index) {
|
||||
costTotal: costTotal,
|
||||
settlementStatus: row.settlementStatus,
|
||||
orderNo: h2BridgeBuildRefuelOrderNo(row, index),
|
||||
/* 对账单筛选依赖:须透出核对状态,否则站点侧会把全部行当成未核对 */
|
||||
verifyStatus: row.verifyStatus === H2_VERIFY_VERIFIED ? H2_VERIFY_VERIFIED : H2_VERIFY_UNVERIFIED,
|
||||
verifiedAt: row.verifiedAt || null,
|
||||
reconcileStatus: row.reconcileStatus || (isReconciled ? H2_RECONCILE_RECONCILED : H2_RECONCILE_PENDING),
|
||||
statementRecordId: row.statementRecordId || null,
|
||||
reconcileDate: row.reconcileDate || null,
|
||||
@@ -732,6 +774,7 @@ function h2BridgeInit() {
|
||||
totalAmountMismatch: h2BridgeTotalAmountMismatch,
|
||||
lookupStationUnitPrice: h2BridgeLookupStationUnitPrice,
|
||||
todayDateKey: h2BridgeTodayDateKey,
|
||||
yesterdayDateKey: h2BridgeYesterdayDateKey,
|
||||
getManualLedger: function (stationIdOrName, ledgerDate) {
|
||||
return store.getManualLedger(stationIdOrName, ledgerDate);
|
||||
},
|
||||
@@ -744,9 +787,30 @@ function h2BridgeInit() {
|
||||
upsertManualLedger: function (input) {
|
||||
return store.upsertManualLedger(input);
|
||||
},
|
||||
/** 前一日台账未上传(且昨日≥本站首笔加氢日)→ 今日禁新增 */
|
||||
isManualLedgerGateBlocked: function (stationIdOrName) {
|
||||
var station = String(stationIdOrName || '').trim();
|
||||
if (!station) return false;
|
||||
var todayKey = h2BridgeTodayDateKey();
|
||||
var yesterdayKey = h2BridgeYesterdayDateKey();
|
||||
var rows = store.getRows() || [];
|
||||
var earliest = null;
|
||||
rows.forEach(function (row) {
|
||||
var sid = String(row.stationId || '').trim();
|
||||
var sn = String(row.stationName || '').trim();
|
||||
if (sid !== station && sn !== station) return;
|
||||
var m = String(row.hydrogenTime || '').match(/^(\d{4}-\d{2}-\d{2})/);
|
||||
if (!m) return;
|
||||
if (!earliest || m[1] < earliest) earliest = m[1];
|
||||
});
|
||||
if (!earliest || yesterdayKey > todayKey || yesterdayKey < earliest) return false;
|
||||
return !store.hasManualLedgerForDate(station, yesterdayKey);
|
||||
},
|
||||
upsertRow: function (row) { return store.upsertRow(row); },
|
||||
removeRow: function (id) { return store.removeRow(id); },
|
||||
subscribe: function (fn) { return store.subscribe(fn); }
|
||||
subscribe: function (fn) { return store.subscribe(fn); },
|
||||
isLingniuVehicle: h2BridgeIsLingniuVehicle,
|
||||
normalizePlateKey: h2BridgeNormalizePlateKey
|
||||
};
|
||||
}
|
||||
return store;
|
||||
@@ -776,5 +840,7 @@ export {
|
||||
h2BridgeFindDuplicateRow,
|
||||
h2BridgeTotalAmountMismatch,
|
||||
h2BridgeLookupStationUnitPrice,
|
||||
h2BridgeInferRecordSource
|
||||
h2BridgeInferRecordSource,
|
||||
h2BridgeIsLingniuVehicle,
|
||||
h2BridgeNormalizePlateKey
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ export interface ApprovalCardProps {
|
||||
|
||||
function statusClassName(status: ApprovalCardItem['status']): string {
|
||||
if (status === 'approved') return 'ap-status ap-status--approved';
|
||||
if (status === 'rejected') return 'ap-status ap-status--rejected';
|
||||
if (status === 'rejected' || status === 'terminated') return 'ap-status ap-status--rejected';
|
||||
return 'ap-status ap-status--pending';
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,9 @@ export interface ApprovalDetailPlaceholderProps {
|
||||
|
||||
function statusBadgeClass(status: ApprovalCardItem['status']): string {
|
||||
if (status === 'approved') return 'ap-detail__badge ap-detail__badge--approved';
|
||||
if (status === 'rejected') return 'ap-detail__badge ap-detail__badge--rejected';
|
||||
if (status === 'rejected' || status === 'terminated') {
|
||||
return 'ap-detail__badge ap-detail__badge--rejected';
|
||||
}
|
||||
return 'ap-detail__badge ap-detail__badge--pending';
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,8 @@ const STATUS_LABEL_MAP: Record<ApprovalStatus, string> = {
|
||||
pending: '审批中',
|
||||
processing: '审批中',
|
||||
approved: '已通过',
|
||||
rejected: '已驳回',
|
||||
rejected: '被驳回',
|
||||
terminated: '被终止',
|
||||
};
|
||||
|
||||
export function getApprovalTypeLabel(type: ApprovalTypeKey): string {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type ApprovalStatus = 'pending' | 'processing' | 'approved' | 'rejected';
|
||||
export type ApprovalStatus = 'pending' | 'processing' | 'approved' | 'rejected' | 'terminated';
|
||||
|
||||
export type ApprovalTabKey = 'todo' | 'done' | 'initiated' | 'cc';
|
||||
|
||||
|
||||
@@ -84,6 +84,12 @@
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--ln-primary, #32a06e) 35%, transparent);
|
||||
}
|
||||
|
||||
.vm-op-more-btn--open,
|
||||
.vm-op-more-btn[aria-expanded="true"] {
|
||||
color: var(--ln-ink, #0f172a);
|
||||
background: var(--ln-canvas-soft, #f4f4f5);
|
||||
}
|
||||
|
||||
.vm-op-more-btn__icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
|
||||
@@ -43,7 +43,10 @@ const moment = dayjs;
|
||||
const ANCHOR_TODAY = '2026-06-01';
|
||||
const IPC_STORAGE_KEY = 'oneos_ipc_insurance_v1';
|
||||
const IPC_COMPARE_SHEETS_KEY = 'oneos_ipc_compare_sheets_v1';
|
||||
const IPC_POLICY_RECOGN_TASKS_KEY = 'oneos_ipc_policy_recogn_tasks_v2';
|
||||
const IPC_POLICY_RECOGN_TASKS_KEY = 'oneos_ipc_policy_recogn_tasks_v3';
|
||||
const POLICY_RECOGN_FAIL_REASON_BLUR = 'OCR 识别失败,请检查文件清晰度或重新上传';
|
||||
const POLICY_RECOGN_FAIL_REASON_FORMAT = '文件格式或版式无法解析,请上传清晰 PDF / 图片保单';
|
||||
const POLICY_RECOGN_FAIL_REASON_CORRUPT = '文件损坏或内容为空,请重新导出后上传';
|
||||
const IPC_INSURANCE_HISTORY_EDITS_KEY = 'oneos_ipc_insurance_history_edits_v1';
|
||||
const IPC_EDIT_PLATE_KEY = 'oneos_ipc_edit_plate';
|
||||
const NO_PLATE_LABEL = '暂无车牌';
|
||||
@@ -1084,17 +1087,35 @@ const buildMockOcrResults = (files, mode, insuranceTypeLabel, allInsurance) => (
|
||||
mode
|
||||
);
|
||||
if (files.length >= 2 && idx === files.length - 1) {
|
||||
return {
|
||||
...result,
|
||||
recognSuccess: false,
|
||||
matched: false,
|
||||
matchTip: 'OCR 识别失败,请检查文件清晰度或重新上传',
|
||||
};
|
||||
return markRecognResultFailed(result, POLICY_RECOGN_FAIL_REASON_BLUR);
|
||||
}
|
||||
return result;
|
||||
})
|
||||
);
|
||||
|
||||
/** 识别失败原因:优先 failReason,回退 matchTip */
|
||||
const getPolicyRecognFailReason = (result) => (
|
||||
String(result?.failReason || result?.matchTip || '').trim() || '识别失败,原因未知'
|
||||
);
|
||||
|
||||
const getPolicyRecognFailResults = (results) => (
|
||||
(results || [])
|
||||
.filter((r) => r.recognSuccess === false)
|
||||
.map((r) => ({
|
||||
id: r.id || r.fileUid || r.fileName,
|
||||
fileName: r.fileName || '未命名文件',
|
||||
failReason: getPolicyRecognFailReason(r),
|
||||
}))
|
||||
);
|
||||
|
||||
const markRecognResultFailed = (result, reason) => ({
|
||||
...result,
|
||||
recognSuccess: false,
|
||||
matched: false,
|
||||
matchTip: reason,
|
||||
failReason: reason,
|
||||
});
|
||||
|
||||
const recognResultToPolicyDetail = (result, taskMode = 'policy') => {
|
||||
const mode = resolvePolicyRecognEffectiveMode(taskMode, result);
|
||||
const pd = result.policyDetail || {};
|
||||
@@ -2561,6 +2582,13 @@ const createMockPolicyRecognTasks = () => {
|
||||
];
|
||||
const resultsCompleted1 = buildMockOcrResults(filesCompleted1, 'policy', '交强险', insMap);
|
||||
if (resultsCompleted1[0]) resultsCompleted1[0].confirmed = true;
|
||||
// 末条失败:清晰度问题(buildMockOcrResults 已标记);再给另一张不同失败原因
|
||||
if (resultsCompleted1[1]) {
|
||||
resultsCompleted1[1] = markRecognResultFailed(
|
||||
resultsCompleted1[1],
|
||||
POLICY_RECOGN_FAIL_REASON_FORMAT,
|
||||
);
|
||||
}
|
||||
|
||||
const filesCompleted2 = [
|
||||
{ uid: 'demo-c4', name: '粤B88888_复驶批单.pdf', status: 'done' },
|
||||
@@ -2572,6 +2600,14 @@ const createMockPolicyRecognTasks = () => {
|
||||
resultsCompleted2.forEach((r) => {
|
||||
if (r.recognSuccess !== false) r.confirmed = true;
|
||||
});
|
||||
// 末条已是清晰度失败;确保 failReason 字段齐全
|
||||
const lastResumeFail = resultsCompleted2[resultsCompleted2.length - 1];
|
||||
if (lastResumeFail && lastResumeFail.recognSuccess === false) {
|
||||
resultsCompleted2[resultsCompleted2.length - 1] = markRecognResultFailed(
|
||||
lastResumeFail,
|
||||
POLICY_RECOGN_FAIL_REASON_BLUR,
|
||||
);
|
||||
}
|
||||
|
||||
const filesSuspend = [
|
||||
{ uid: 'demo-s1', name: '沪A06192F_商业险_停保批单.pdf', status: 'done' },
|
||||
@@ -2579,6 +2615,18 @@ const createMockPolicyRecognTasks = () => {
|
||||
];
|
||||
const resultsSuspend = buildMockOcrResults(filesSuspend, 'suspend', '', insMap);
|
||||
|
||||
// 全部失败任务:便于演示「无成功仍可看失败明细」
|
||||
const filesFailOnly = [
|
||||
{ uid: 'demo-f1', name: '损坏文件_保单.pdf', status: 'done' },
|
||||
{ uid: 'demo-f2', name: '非保单截图.jpg', status: 'done' },
|
||||
];
|
||||
const resultsFailOnly = buildMockOcrResults(filesFailOnly, 'policy', '商业险', insMap).map((r, idx) => (
|
||||
markRecognResultFailed(
|
||||
r,
|
||||
idx === 0 ? POLICY_RECOGN_FAIL_REASON_CORRUPT : POLICY_RECOGN_FAIL_REASON_FORMAT,
|
||||
)
|
||||
));
|
||||
|
||||
return [
|
||||
buildPolicyRecognTaskRecord({
|
||||
id: 'TASK-83892906',
|
||||
@@ -2616,6 +2664,18 @@ const createMockPolicyRecognTasks = () => {
|
||||
totalFileCount: filesSuspend.length,
|
||||
recognDoneCount: filesSuspend.length,
|
||||
}),
|
||||
buildPolicyRecognTaskRecord({
|
||||
id: 'TASK-84450001',
|
||||
entry: 'ocr',
|
||||
mode: 'policy',
|
||||
insuranceType: '商业险',
|
||||
results: resultsFailOnly,
|
||||
createdAt: '2026-06-03 11:05:00',
|
||||
completedAt: '2026-06-03 11:06:20',
|
||||
phase: 'results',
|
||||
totalFileCount: filesFailOnly.length,
|
||||
recognDoneCount: filesFailOnly.length,
|
||||
}),
|
||||
buildPolicyRecognTaskRecord({
|
||||
id: 'TASK-84210588',
|
||||
entry: 'ocr',
|
||||
@@ -3317,6 +3377,8 @@ const Component = function () {
|
||||
return stored && stored.length ? stored : createMockPolicyRecognTasks();
|
||||
});
|
||||
const [policyRecognTasksOpen, setPolicyRecognTasksOpen] = useState(false);
|
||||
const [policyRecognFailDetailOpen, setPolicyRecognFailDetailOpen] = useState(false);
|
||||
const [policyRecognFailDetailTask, setPolicyRecognFailDetailTask] = useState(null);
|
||||
const [policyRecognTasksFilters, setPolicyRecognTasksFilters] = useState(() => ({ ...DEFAULT_POLICY_RECOGN_TASK_FILTERS }));
|
||||
const [appliedPolicyRecognTasksFilters, setAppliedPolicyRecognTasksFilters] = useState(() => ({ ...DEFAULT_POLICY_RECOGN_TASK_FILTERS }));
|
||||
const [policyAddOpen, setPolicyAddOpen] = useState(false);
|
||||
@@ -4602,10 +4664,27 @@ const Component = function () {
|
||||
if (preferred) {
|
||||
setPolicyRecognActiveResultId(preferred.id);
|
||||
setPolicyRecognConfirmDraft(buildPolicyRecognConfirmDraft(preferred, task.mode || 'policy'));
|
||||
showPolicyRecognResultPreview(preferred);
|
||||
} else {
|
||||
setPolicyRecognActiveResultId('');
|
||||
setPolicyRecognConfirmDraft({ ...EMPTY_POLICY_DETAIL });
|
||||
}
|
||||
};
|
||||
|
||||
const openPolicyRecognFailDetail = (task) => {
|
||||
const fails = getPolicyRecognFailResults(task?.results);
|
||||
if (!fails.length) {
|
||||
message.info('该任务没有识别失败记录');
|
||||
return;
|
||||
}
|
||||
setPolicyRecognFailDetailTask(task);
|
||||
setPolicyRecognFailDetailOpen(true);
|
||||
};
|
||||
|
||||
const policyRecognFailDetailRows = useMemo(
|
||||
() => getPolicyRecognFailResults(policyRecognFailDetailTask?.results),
|
||||
[policyRecognFailDetailTask],
|
||||
);
|
||||
|
||||
const closePolicyRecogn = () => {
|
||||
const syncedResults = policyRecognPhase === 'results'
|
||||
? persistActiveRecognDraft()
|
||||
@@ -8120,13 +8199,31 @@ const Component = function () {
|
||||
{
|
||||
title: '识别失败数',
|
||||
dataIndex: 'recognFailCount',
|
||||
width: 96,
|
||||
width: 108,
|
||||
align: 'center',
|
||||
render: (val) => (
|
||||
<span style={{ fontVariantNumeric: 'tabular-nums', color: val > 0 ? '#dc2626' : '#64748b', fontWeight: 600 }}>
|
||||
{val ?? 0}
|
||||
render: (val, record) => {
|
||||
const count = val ?? 0;
|
||||
if (count > 0) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="lc-policy-recogn-fail-count-btn"
|
||||
aria-label={`查看 ${count} 条识别失败明细`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openPolicyRecognFailDetail(record);
|
||||
}}
|
||||
>
|
||||
{count}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span style={{ fontVariantNumeric: 'tabular-nums', color: '#64748b', fontWeight: 600 }}>
|
||||
0
|
||||
</span>
|
||||
),
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '识别进度',
|
||||
@@ -8171,31 +8268,112 @@ const Component = function () {
|
||||
fixed: 'right',
|
||||
render: (_, record) => {
|
||||
const recognizing = isPolicyRecognTaskRecognizing(record);
|
||||
const noSuccess = !(record.recognSuccessCount > 0);
|
||||
const disabled = recognizing || noSuccess;
|
||||
const action = (
|
||||
<OperationActions
|
||||
view={{
|
||||
label: '确认识别结果',
|
||||
disabled,
|
||||
onClick: () => openPolicyRecognTaskRecord(record),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
const hasSuccess = (record.recognSuccessCount || 0) > 0;
|
||||
const hasFail = (record.recognFailCount || 0) > 0;
|
||||
if (recognizing) {
|
||||
return (
|
||||
<Tooltip title="请等待识别完成后操作">
|
||||
<span>{action}</span>
|
||||
<span>
|
||||
<OperationActions
|
||||
view={{
|
||||
label: '确认识别结果',
|
||||
disabled: true,
|
||||
onClick: () => {},
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return action;
|
||||
if (!hasSuccess && hasFail) {
|
||||
return (
|
||||
<OperationActions
|
||||
view={{
|
||||
label: '查看失败明细',
|
||||
onClick: () => openPolicyRecognFailDetail(record),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<OperationActions
|
||||
view={{
|
||||
label: '确认识别结果',
|
||||
disabled: !hasSuccess,
|
||||
onClick: () => openPolicyRecognTaskRecord(record),
|
||||
}}
|
||||
more={hasFail ? [{
|
||||
key: 'failDetail',
|
||||
label: '查看失败明细',
|
||||
onClick: () => openPolicyRecognFailDetail(record),
|
||||
}] : undefined}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
className="lc-policy-recogn-fail-detail-modal"
|
||||
open={policyRecognFailDetailOpen}
|
||||
title={policyRecognFailDetailTask
|
||||
? `识别失败明细(${policyRecognFailDetailRows.length})`
|
||||
: '识别失败明细'}
|
||||
width={720}
|
||||
centered
|
||||
footer={(
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
setPolicyRecognFailDetailOpen(false);
|
||||
setPolicyRecognFailDetailTask(null);
|
||||
}}
|
||||
>
|
||||
关闭
|
||||
</Button>
|
||||
)}
|
||||
onCancel={() => {
|
||||
setPolicyRecognFailDetailOpen(false);
|
||||
setPolicyRecognFailDetailTask(null);
|
||||
}}
|
||||
>
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
style={{ marginBottom: 14, borderRadius: 8 }}
|
||||
message="以下文件识别未成功,不会进入确认写入台账;可核对文件后重新发起识别。"
|
||||
/>
|
||||
<Table
|
||||
size="middle"
|
||||
rowKey="id"
|
||||
dataSource={policyRecognFailDetailRows}
|
||||
pagination={policyRecognFailDetailRows.length > 8
|
||||
? { pageSize: 8, showSizeChanger: false }
|
||||
: false}
|
||||
locale={{ emptyText: '暂无失败记录' }}
|
||||
columns={[
|
||||
{
|
||||
title: '文件/保单名',
|
||||
dataIndex: 'fileName',
|
||||
width: 280,
|
||||
ellipsis: true,
|
||||
render: (val) => (
|
||||
<span style={{ fontWeight: 600, color: '#0f172a' }}>{val || '—'}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '失败原因',
|
||||
dataIndex: 'failReason',
|
||||
render: (val) => (
|
||||
<span style={{ color: '#b91c1c', lineHeight: 1.45 }}>{val || '—'}</span>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
className="lc-policy-recogn-modal"
|
||||
title={
|
||||
|
||||
@@ -460,6 +460,46 @@
|
||||
margin-bottom: var(--oneos-space-3);
|
||||
}
|
||||
|
||||
/* 识别任务:失败数可点击入口 */
|
||||
.lc-policy-recogn-fail-count-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 44px;
|
||||
min-height: 44px;
|
||||
padding: 0 8px;
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: #dc2626;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1.2;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
transition: background-color 0.15s ease, color 0.15s ease;
|
||||
}
|
||||
|
||||
.lc-policy-recogn-fail-count-btn:hover,
|
||||
.lc-policy-recogn-fail-count-btn:focus-visible {
|
||||
color: #b91c1c;
|
||||
background: color-mix(in srgb, #ef4444 10%, transparent);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.lc-policy-recogn-fail-count-btn:focus-visible {
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, #ef4444 35%, transparent);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.lc-policy-recogn-fail-count-btn {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
.vm-page.ipc-page .lc-alert-stats-row {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
BookOpen,
|
||||
Building2,
|
||||
Calendar,
|
||||
CalendarClock,
|
||||
Car,
|
||||
CircleDollarSign,
|
||||
ClipboardCheck,
|
||||
@@ -772,34 +773,53 @@ export const SELF_OPERATED_MODULES: LineModule[] = [
|
||||
id: 'self-contract',
|
||||
step: 1,
|
||||
title: '自营合同',
|
||||
subtitle: '关键信息录入 · 上传合同 · 自动交车',
|
||||
subtitle: '关键信息录入 · 业务确认生效 · 可派车',
|
||||
shortLabel: '合同',
|
||||
accent: 'blue',
|
||||
icon: FileSignature,
|
||||
roles: ['业务管理组', '业务服务组'],
|
||||
start: '创建自营合同,记录客户方、车型、车辆数、交车时间与地点等关键信息,并上传合同附件。',
|
||||
start: '创建自营运力服务合同,记录客户方、车型、车辆数、交车时间与地点等关键信息,并上传合同附件。',
|
||||
process: [
|
||||
'录入自营项目与客户方、车型、车辆数量等合同要素。',
|
||||
'填写交车时间、交车地点,上传自营合同扫描件。',
|
||||
'合同提交后自动生成交车任务,推送运维条线处理交付。',
|
||||
'业务确认生效后可创建轻量调度任务,并提示运维交车交付。',
|
||||
],
|
||||
closure: '合同信息与交车任务联动,自营项目从签约起即可进入交付与运营环节。',
|
||||
prototypeHref: '/prototypes/oneos-web-lease-contract',
|
||||
prototypeLabel: '打开车辆租赁合同',
|
||||
closure: '合同界定项目边界,成为调度派车与交车的上游凭证。',
|
||||
prototypeHref: '/prototypes/self-operated-contract',
|
||||
prototypeLabel: '打开自营合同',
|
||||
},
|
||||
{
|
||||
id: 'self-dispatch',
|
||||
step: 2,
|
||||
title: '调度任务',
|
||||
subtitle: '轻量派车 · 交还车校验 · 办结出车',
|
||||
shortLabel: '调度',
|
||||
accent: 'indigo',
|
||||
icon: CalendarClock,
|
||||
roles: ['业务服务组', '调度岗', '运维部'],
|
||||
start: '在生效自营合同下创建出车任务,派车派司机;非 TMS,不做配载与承运商调度。',
|
||||
process: [
|
||||
'选择合同、计划出车日、线路说明、是否多趟与计价口径。',
|
||||
'派车派司机;车辆未交车时先完成运维交车(原型可模拟交车)。',
|
||||
'办结确认实际出车后,自动生成物流业务明细一行。',
|
||||
],
|
||||
closure: '出车事实可追溯到合同与车牌,并驱动台账落账。',
|
||||
prototypeHref: '/prototypes/self-operated-dispatch-task',
|
||||
prototypeLabel: '打开调度任务',
|
||||
},
|
||||
{
|
||||
id: 'self-ledger',
|
||||
step: 2,
|
||||
step: 3,
|
||||
title: '自营台账',
|
||||
subtitle: '导入台账 · 成本利润核算 · 项目盈亏',
|
||||
subtitle: '任务自动生成 · 补录导入 · 盈亏核算',
|
||||
shortLabel: '台账',
|
||||
accent: 'emerald',
|
||||
icon: BookOpen,
|
||||
roles: ['业务服务组', '财务部'],
|
||||
start: '根据导入的自营台账与账单信息,汇总项目侧收入与成本数据。',
|
||||
start: '承接调度办结自动生成的出车明细,并以导入/行编辑补录异常与历史数据。',
|
||||
process: [
|
||||
'导入或维护自营运营台账、账单明细。',
|
||||
'自动计算利润侧数据,以及司机成本、车辆成本侧数据。',
|
||||
'调度办结后自动写入出车营收侧字段,并计算金额/总成本/盈亏。',
|
||||
'氢费、ETC 等成本一期可手工补录;导入模板保留为补录通道。',
|
||||
'汇集至项目盈亏表,支撑项目经营分析。',
|
||||
'演进阶段关联财务「收款记录」,形成业财记录闭环。',
|
||||
],
|
||||
@@ -924,26 +944,26 @@ export const BUSINESS_LINES: BusinessLineConfig[] = [
|
||||
shortLabel: '自营',
|
||||
hero: {
|
||||
title: '自营业务条线',
|
||||
tagline: '自营物流条线合同->账单业财闭环',
|
||||
tagline: '自营合同 → 轻量调度 → 交还车 → 台账自动生成',
|
||||
summary:
|
||||
'两大模块串联自营合同与自营台账:合同驱动交车任务,台账导入后自动核算司机、车辆成本与项目盈亏,逐步对接财务收款形成业财闭环。',
|
||||
'三大模块串联自营合同、轻量调度任务与自营台账:合同界定项目,调度派车办结驱动明细自动生成,运维交还车保障运营态,逐步对接财务收款形成业财闭环。',
|
||||
stats: [
|
||||
{ value: '2', label: '业务模块' },
|
||||
{ value: '3', label: '业务模块' },
|
||||
{ value: '4+', label: '协同部门' },
|
||||
{ value: '1', label: '业财闭环' },
|
||||
{ value: '1', label: '经营闭环' },
|
||||
],
|
||||
},
|
||||
modules: SELF_OPERATED_MODULES,
|
||||
loopTitle: '合同到台账,自营经营可核算',
|
||||
loopTitle: '合同到调度再到台账,自营经营可核算',
|
||||
loopSummary:
|
||||
'自营合同录入并生成交车任务 → 运营数据导入自营台账 → 自动汇总利润与司机、车辆成本 → 汇集项目盈亏表 → 关联收款记录完成业财闭环。',
|
||||
'自营合同生效 → 轻量调度派车(交车后可办结)→ 办结自动生成物流业务明细 → 自动汇总盈亏 → 汇集项目盈亏表 → 演进关联收款记录完成业财闭环。',
|
||||
evolution: {
|
||||
currentLabel: '现阶段 · 人工核算台账',
|
||||
currentLabel: '现阶段 · 合同 + 调度办结落账',
|
||||
currentText:
|
||||
'人工发起自营合同,人工计算车辆成本、司机成本与自营台账,项目盈亏依赖线下表格汇总与人工核对。',
|
||||
'自营合同业务确认生效;轻量调度派车办结(须交车);台账以自动生成为主、导入为辅,盈亏在台账内可算。',
|
||||
targetLabel: '演进方向 · 业财记录闭环',
|
||||
targetText:
|
||||
'仍人工发起合同;司机在线管理、司机成本自动计算;自营台账人工导入后自动汇总,关联财务「收款记录」模块,形成业财记录闭环。',
|
||||
'成本侧氢费/ETC/日成本自动回填;司机在线管理;台账关联财务「收款记录」,形成业财记录闭环;不做完整 TMS。',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
# 加氢订单(H5)— 已确认需求与设计决策
|
||||
|
||||
> **状态:已收敛 / 部分作废(2026-07-18)**
|
||||
> 现行权威规格以 [requirements-prd.md](./requirements-prd.md) 及同目录功能规格为准。
|
||||
> 下文保留为 2026-07-14 对齐快照;下列条目**已被后续决策覆盖,勿再按本文件验收**:
|
||||
>
|
||||
> | 快照原文 | 现行口径 |
|
||||
> |---|---|
|
||||
> | 顶栏演示可切换示例站 | **H5 无切站**,固定登录本站 |
|
||||
> | 列表展示对账状态 / 对账时间 | 列表只展示**核对**;对账见详情 |
|
||||
> | 已对账才只读 | **羚牛**已核对或已对账即锁定;非羚牛不锁 |
|
||||
> | 无底 Tab | 已有底栏 Tab:加氢订单 / 手工台账 |
|
||||
> | 分步 OCR 向导 | 已改为单表单 + OCR 拍摄区 |
|
||||
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 日期 | 2026-07-14 |
|
||||
| 日期 | 2026-07-14(快照);覆盖说明 2026-07-18 |
|
||||
| 原型 ID | `oneos-h5-h2-order` |
|
||||
| 菜单位置 | OneOS → 加氢站管理 → 加氢订单 |
|
||||
| 文档性质 | 决策快照(对齐当时确认,不随实现逐行同步) |
|
||||
| 文档性质 | 历史决策快照(**不随实现同步**;冲突处以 PRD 为准) |
|
||||
|
||||
---
|
||||
|
||||
@@ -14,8 +26,8 @@
|
||||
| 问题 | 用户选择 |
|
||||
|---|---|
|
||||
| 列表数据范围 | **A. 仅本站**(当前演示站点绑定的加氢站) |
|
||||
| 登录态 | **A. 已登录态**;顶栏展示站名,演示可切换示例站 |
|
||||
| 已对账记录 | **A. 不可改**(只读);未对账可编辑 / 删除 |
|
||||
| 登录态 | **A. 已登录态**;顶栏展示站名 ~~演示可切换示例站~~ → **已收敛:无切站** |
|
||||
| 已对账记录 | **A. 不可改**(只读);未对账可编辑 / 删除 → **已收敛:羚牛·已核对或已对账均锁** |
|
||||
| OCR 演示 | **A. 拍照或选图 → 约 1s 模拟识别 → 可校正**(已改为单表单,非三步) |
|
||||
|
||||
### 1.1 目标与范围
|
||||
@@ -61,8 +73,8 @@
|
||||
| 预览壳 | 居中 390×844 手机框;真机浏览器可全宽 |
|
||||
| 首屏 | 列表为主 + 悬浮「+」 |
|
||||
| 卡片优先级 | 车牌、状态、时间、量与金额;单价 / 对账时间次级 |
|
||||
| 无底 Tab | 本模块单入口,不套小羚羚全局 Tab |
|
||||
| 演示切换 | 顶栏站点下拉,仅演示,不代表正式鉴权 |
|
||||
| 无底 Tab | ~~本模块单入口~~ → **已收敛:底栏「加氢订单 / 手工台账」** |
|
||||
| 演示切换 | ~~顶栏站点下拉~~ → **已收敛:H5 固定本站、无切站** |
|
||||
|
||||
### 2.2 信息架构
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## 目标
|
||||
|
||||
查看单笔加氢记录完整字段;在「未核对且未对账」时可编辑或删除。
|
||||
查看单笔加氢记录完整字段;在「未核对且未对账」时可编辑或删除(**锁定仅适用于羚牛车辆**)。
|
||||
|
||||
## 详情展示
|
||||
|
||||
@@ -11,15 +11,18 @@
|
||||
- **识别原图**:车牌照片、加氢机面板照片(均为拍摄时叠加时间/地点水印后的原图;种子数据无实拍时用演示水印图)
|
||||
- 明细行:站点、车牌、里程、单价、核对/对账状态与时间
|
||||
|
||||
> 非羚牛车辆:核对/对账字段展示「—」,不展示核对待办语义(见 [fleet-plate-tag.md](./fleet-plate-tag.md))。
|
||||
|
||||
## 锁定规则
|
||||
|
||||
| 条件 | 结果 |
|
||||
|------|------|
|
||||
| 已对账 | 不可编辑/删除;提示已纳入站点对账单 |
|
||||
| 已核对(未对账) | 不可编辑/删除;提示能源部已核对 |
|
||||
| 未核对且未对账 | 可编辑、可删除 |
|
||||
| 优先级 | 条件 | 结果 |
|
||||
|--------|------|------|
|
||||
| 1 | **非羚牛车辆** | **不锁定**(不以核对/对账状态禁改删) |
|
||||
| 2 | 羚牛 ∧ 已对账 | 不可编辑/删除;提示已纳入站点对账单 |
|
||||
| 3 | 羚牛 ∧ 已核对(未对账) | 不可编辑/删除;提示能源部已核对 |
|
||||
| 4 | 羚牛 ∧ 未核对且未对账 | 可编辑、可删除 |
|
||||
|
||||
判定函数:`isOrderLocked`(核对或对账任一成立即锁)。
|
||||
判定函数:`isOrderLocked`(先判羚牛;再核「核对或对账任一成立即锁」)。
|
||||
|
||||
## 编辑
|
||||
|
||||
@@ -27,11 +30,12 @@
|
||||
|
||||
## 删除
|
||||
|
||||
二次确认后从 Bridge 移除该行。
|
||||
二次确认文案:「确认删除车牌 … 的未锁定记录?」;确认后从 Bridge 移除该行。
|
||||
|
||||
## 验收
|
||||
|
||||
1. 未核对未对账:底部有删除/编辑
|
||||
2. 已核对或已对账:无操作栏,有锁定说明
|
||||
3. 详情含对账信息;列表卡片不含对账
|
||||
4. 详情展示车牌与面板带水印原图(新上报提交后为实拍;种子行为演示图)
|
||||
1. 羚牛 ∧ 未核对未对账:底部有删除/编辑
|
||||
2. 羚牛 ∧ 已核对或已对账:无操作栏,有锁定说明
|
||||
3. 非羚牛:可改删(不因核对/对账锁定);核对/对账展示「—」
|
||||
4. 详情含对账信息;列表卡片不含对账
|
||||
5. 详情展示车牌与面板带水印原图(新上报提交后为实拍;种子行为演示图)
|
||||
|
||||
@@ -9,14 +9,14 @@
|
||||
| 项 | 规则 |
|
||||
|---|---|
|
||||
| 入口 | H5:底部 Tab「手工台账」;Web:顶栏「手工台账」 |
|
||||
| 站点范围 | 仅展示当前登录用户角色对应站点;单站人员仅 1 个、多站可切换、超级管理员可选全部(原型用标注状态切换演示,未接真实登录) |
|
||||
| 站点范围 | **H5 固定本站**(无切站);Web 可按角色多站/超管切换(标注状态演示,未接真实登录) |
|
||||
| 上传形态 | 仅图片(拍照/相册),可多张 |
|
||||
| 维度 | **加氢站 + 自然日(本地日历日)** 一份 |
|
||||
| 日历 | 月历自本站**首笔加氢记录日**起算:绿=已传、橙=未传;首笔之前无需补传;禁选未来日 |
|
||||
| 补传 | 允许为过往未传日期补传;已传日期仅可**追加**新图 |
|
||||
| 归档 | **确认上传后的图片进入已归档状态,不可删除**;未确认的草稿图仍可删除 |
|
||||
| 二次确认 | Web 点「确认上传/补传」先弹窗:「提交后将无法修改,是否确认手工台账照片无误」;确认后才写入 |
|
||||
| 拦截 | **未上传今日手工台账 → 禁止新增加氢记录**(编辑已有记录不拦;补传历史不替代今日校验) |
|
||||
| 二次确认 | H5 / Web 确认上传前弹窗:「提交后将无法修改,是否确认手工台账照片无误」;确认后才写入 |
|
||||
| 拦截 | **前一日手工台账未上传 → 今日禁止新增加氢记录**(编辑已有记录不拦;补传更早历史日不替代昨日校验) |
|
||||
| 数据 | 共享 `H2VehicleLedgerBridge` 内存 Store(未接真实 API);站名/编码解析见 Bridge.`h2BridgeResolveStationId` |
|
||||
|
||||
## 判定顺序(新增拦截)
|
||||
@@ -24,8 +24,9 @@
|
||||
| 优先级 | 条件 | 结果 |
|
||||
|--------|------|------|
|
||||
| 1 | 操作=编辑已有记录 | 不校验手工台账 |
|
||||
| 2 | 操作=新增,且目标站今日已有 ≥1 张台账图 | 允许进入新增 |
|
||||
| 3 | 操作=新增,今日未上传 | 提示并跳转手工台账 Tab |
|
||||
| 2 | 操作=新增,且昨日早于本站首笔加氢日(或不要求昨日台账) | 允许进入新增 |
|
||||
| 3 | 操作=新增,且目标站昨日已有 ≥1 张台账图 | 允许进入新增 |
|
||||
| 4 | 操作=新增,昨日未上传 | 顶部/Toast 提示「手工台账有缺失,作为对账重要依据,请先上传手工台账」,并**直接跳转**手工台账页且选中昨日 |
|
||||
|
||||
## 日历与补传
|
||||
|
||||
@@ -36,26 +37,27 @@
|
||||
| 未传计数 | 本月内 `max(月初, 首笔日)`~min(月末, 今日) 中未上传的天数 |
|
||||
| 点选 | 仅 `dateKey ≤ 今日`;选中后右侧/下方展示该日草稿图与上传按钮 |
|
||||
| 归档不可删 | Store 已有图强制保留;`upsertManualLedger` 仅追加新图,忽略删除已有图的请求 |
|
||||
| 与拦截关系 | 仅 **今日** 影响「禁止新增」;历史补传解决核对缺口,不放宽今日门禁 |
|
||||
| 与拦截关系 | 仅 **昨日** 影响「禁止新增」;历史补传解决核对缺口;今日台账仍可按日常上传,但不作为新增门禁 |
|
||||
|
||||
## 用户可见
|
||||
|
||||
- 今日状态双反馈(Web 放在右侧「当日台账」卡片内):未上传「今日尚未上传手工台账(YYYY-MM-DD)」异常态;已上传「今日已上传手工台账(YYYY-MM-DD)」正常态
|
||||
- 今日未上传:橙/黄提示 + Tab 红点
|
||||
- 今日状态双反馈(H5 台账页顶部卡片 / Web 右侧「当日台账」):未上传「今日尚未上传…」异常态;已上传「今日已上传…」正常态。**日常上传反馈 ≠ 新增门禁**;H5 文案须写明「今日不作为新增门禁,仅要求前一日已上传」
|
||||
- 昨日缺失(H5):**列表顶栏可点橙条** + 底栏 FAB 旁提示 + Tab 红点;台账页同样展示橙条并可选中昨日;文案固定为「手工台账有缺失,作为对账重要依据,请先上传手工台账」
|
||||
- 日历:自首笔加氢日起绿/橙标记;首笔之前无橙点(无需补传);本月未传天数摘要
|
||||
- Web 右侧面板标题「手工台账」:空态提示;有图可点进全屏翻页预览并角标「已归档/待提交」;预览区内上传;底部确认前二次弹窗;提交后立即展示已归档缩略图;左右卡片等高
|
||||
|
||||
## 代码路径
|
||||
|
||||
- Bridge:`src/common/h2VehicleLedgerBridge.js`(`hasManualLedgerForDate` / `upsertManualLedger` / `listManualLedgersByStation`)
|
||||
- H5:`oneos-h5-h2-order` · `ManualLedgerPanel` + `utils/manual-ledger.ts`(`buildManualMonthCells` / `getFirstHydrogenDateKey`)
|
||||
- Web:`oneos-web-h2-station/pages/02-加氢记录.jsx`(手工台账 Tab 月历)
|
||||
- Bridge:`src/common/h2VehicleLedgerBridge.js`(`hasManualLedgerForDate` / `isManualLedgerGateBlocked` / `upsertManualLedger` / `listManualLedgersByStation`)
|
||||
- H5:`oneos-h5-h2-order` · `ManualLedgerPanel` + `utils/manual-ledger.ts`(`isManualLedgerGateBlocked` / `buildManualMonthCells` / `getFirstHydrogenDateKey`)
|
||||
- Web:`oneos-web-h2-station/pages/02-加氢记录.jsx`(`hrIsManualLedgerGateBlocked` / 跳转选中昨日)
|
||||
|
||||
## 验收
|
||||
|
||||
1. 今日未上传时,H5/Web 点「新增」均被拦截并引导上传
|
||||
2. 上传至少一张图并确认后,可新增电子记录
|
||||
1. 昨日未上传时,H5/Web 列表顶栏提示固定文案;点「新增」被拦截并直接进入手工台账(选中昨日)
|
||||
2. 补传昨日台账并确认后,可新增加氢记录
|
||||
3. 日历自首笔加氢日起区分已传/未传;首笔之前不标未传、不计入补传;可点选范围内未传日补传
|
||||
4. 补传历史日后,该日日历变为已传;今日仍未传时新增仍被拦截
|
||||
4. 仅补传更早历史日、昨日仍未传时,新增仍被拦截
|
||||
5. 确认上传后的图显示已归档且不可删除;仅可追加补传
|
||||
6. H5 与 Web 共用同一 Store,一端上传另一端可见(同会话)
|
||||
7. Web 导出 CSV 列含「车辆来源」(羚牛车辆/非羚牛车辆)、「数据来源」(站端上传/羚牛上传),与列表标签口径一致
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
原型本地判定,未接真实车辆管理 API。
|
||||
|
||||
全文(含核对/对账字段空展示规则,跨 Web / 台账 / H5):
|
||||
[../../oneos-web-h2-station/.spec/fleet-vehicle-verify.md](../../oneos-web-h2-station/.spec/fleet-vehicle-verify.md)
|
||||
|
||||
## 判定顺序
|
||||
|
||||
| 优先级 | 条件 | 结果 |
|
||||
@@ -17,14 +20,14 @@
|
||||
|
||||
## 数据源
|
||||
|
||||
- `utils/fleet-plates.ts` 内本地种子车牌集合(含加氢 Bridge 演示车牌与部分车辆管理样例)。
|
||||
- 正式环境应改为查询车辆管理 / 系统车辆列表接口。
|
||||
- 优先 `H2VehicleLedgerBridge.isLingniuVehicle`;本地回退见 `utils/fleet-plates.ts`(与 Bridge `H2_FLEET_PLATE_KEYS` 同步)。
|
||||
|
||||
## 用户可见结果
|
||||
|
||||
- 绿色标签「羚牛车辆」或橙色标签「非羚牛车辆」。
|
||||
- **非羚牛车辆**:列表/详情中核对状态、核对时间、对账状态、对账时间显示「—」,不展示核对待办语义。
|
||||
- 不拦截提交(仅提示归属)。
|
||||
|
||||
## 代码路径
|
||||
|
||||
- `getVehicleTag` / `isLingniuVehicle` → `CreateWizard` 车牌行右侧标签。
|
||||
- `getVehicleTag` / `isLingniuVehicle` → `CreateWizard`、`OrderCard`、`OrderDetail`
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|---|---|
|
||||
| 代码路径 | Store:`src/common/h2VehicleLedgerBridge.js`;对账单:`oneos-web-h2-station-site`;PC 加氢记录:`oneos-web-h2-station`;车辆氢费明细:`vehicle-h2-fee-ledger` |
|
||||
| 规格全文 | [../vehicle-h2-fee-ledger/.spec/verify-reconcile.md](../vehicle-h2-fee-ledger/.spec/verify-reconcile.md) |
|
||||
| 车辆标签 | [fleet-plate-tag.md](./fleet-plate-tag.md) |
|
||||
| 数据源 | 原型本地种子 + 内存共享 Store(**未接真实 API**) |
|
||||
|
||||
---
|
||||
@@ -17,6 +18,8 @@
|
||||
|
||||
> `reconciledAt` 仅作完成核对时的兼容写入,**不得**再用于判定「已对账」。
|
||||
|
||||
---
|
||||
|
||||
## 2. 数据流
|
||||
|
||||
```text
|
||||
@@ -26,23 +29,29 @@
|
||||
|
||||
能源部 · 完成核对
|
||||
→ verifyStatus=verified + verifiedAt
|
||||
→ 站端同步「已核对」,禁改删
|
||||
→ 站端同步「已核对」;羚牛车辆禁改删
|
||||
|
||||
站点信息 · 对账单提交(仅已核对 ∧ 未对账)
|
||||
→ reconcileStatus=reconciled + reconcileDate + statementRecordId
|
||||
→ 站端 / 台账同步「已对账」
|
||||
→ 站端 / 台账同步「已对账」;羚牛车辆禁改删
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 用户可见结果(H5)
|
||||
|
||||
| 结果 | 行为 |
|
||||
|---|---|
|
||||
| 未核对 | 可编辑、删除 |
|
||||
| 已核对未对账 | 只读;提示能源部已核对 |
|
||||
| 已对账 | 只读;提示已纳入对账单 |
|
||||
| 非羚牛车辆 | 核对/对账展示「—」;**不锁定**改删 |
|
||||
| 羚牛 · 未核对 | 可编辑、删除 |
|
||||
| 羚牛 · 已核对未对账 | 只读;提示能源部已核对 |
|
||||
| 羚牛 · 已对账 | 只读;提示已纳入对账单 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 边界
|
||||
|
||||
- 站端上报默认未核对、未对账
|
||||
- **站端改删锁定仅适用于羚牛车辆**(与 Web / 台账车辆标签口径一致)
|
||||
- 对账时间只认 `reconcileDate`(对账单回写)
|
||||
- 跨页联动依赖同浏览器会话共享 Store
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 文档版本 | v1.5 |
|
||||
| 文档版本 | v1.7 |
|
||||
| 模块名称 | 加氢站管理 → 加氢订单 |
|
||||
| 所属系统 | ONE-OS(加氢站手机浏览器) |
|
||||
| 交互原型 | `/prototypes/oneos-h5-h2-order` |
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
采购创建加氢站并绑定供应商、开放系统账号后,加氢站人员通过手机浏览器登录 OneOS,在「加氢订单」模块上报加氢记录。Web「加氢记录」为同一业务的 PC 入口(共享 Bridge 数据;PC 另有导出与预约加氢)。
|
||||
|
||||
每日须上传**手工台账**照片,作为电子记录的核对依据;未上传当日手工台账时不可新增加氢记录。手工台账页提供月历,标出已传/未传日期,支持补传过往缺口。
|
||||
每日须上传**手工台账**照片,作为电子记录的核对依据;**前一日**手工台账未上传时,今日不可新增加氢记录。手工台账页提供月历,标出已传/未传日期,支持补传过往缺口。
|
||||
|
||||
### 1.1 目标用户
|
||||
|
||||
@@ -46,14 +46,15 @@
|
||||
|
||||
1. 菜单位于 OneOS → 加氢站管理 → 加氢订单
|
||||
2. 默认已登录本站;列表仅本站(无切站)
|
||||
3. 底栏 Tab:加氢订单 / 手工台账;月历可区分已传/未传并补传过往日;未上传今日手工台账不可新增;上传后可新增;时间默认点击新增时刻(到分)
|
||||
4. 已核对或已对账不可编辑/删除;未核对未对账可
|
||||
5. 能源部完成核对后同步「已核对」;站点对账单提交后同步「已对账」+ 对账时间
|
||||
3. 底栏 Tab:加氢订单 / 手工台账;月历可区分已传/未传并补传过往日;前一日未上传时提示「手工台账有缺失,作为对账重要依据,请先上传手工台账」且点新增直达台账页;补传昨日后可新增;时间默认点击新增时刻(到分)
|
||||
4. **羚牛车辆**已核对或已对账不可编辑/删除;非羚牛不锁定;未核对未对账(羚牛)可改删
|
||||
5. 能源部完成核对后同步「已核对」;站点对账单提交后同步「已对账」+ 对账时间(锁定仅羚牛)
|
||||
6. 同站同车牌同加氢时间重复保存被拦截
|
||||
7. Web「加氢记录」与 H5「加氢订单」手工台账逻辑一致(同 Store)
|
||||
8. 车牌/面板拍摄预览含水印(时间、地点=本站名称);里程可从车机获取(Mock)
|
||||
9. 面板 OCR 可展示站点信息维护的加氢机品牌型号线索
|
||||
10. 详情页展示车牌与面板带水印原图
|
||||
11. 昨日台账缺失时:列表顶栏橙条 + 底栏提示 + Tab 红点;台账页不把「今日未传」写成新增门禁
|
||||
|
||||
---
|
||||
|
||||
@@ -61,11 +62,11 @@
|
||||
|
||||
| 主题 | 摘要 | 全文 |
|
||||
|---|---|---|
|
||||
| 对账 / 核对 | 核对≠对账;触发方不同 | [reconcile-linkage.md](./reconcile-linkage.md)、[车辆氢费 verify-reconcile](../vehicle-h2-fee-ledger/.spec/verify-reconcile.md) |
|
||||
| 对账 / 核对 | 核对≠对账;**锁定仅羚牛** | [reconcile-linkage.md](./reconcile-linkage.md)、[车辆氢费 verify-reconcile](../vehicle-h2-fee-ledger/.spec/verify-reconcile.md) |
|
||||
| 重复键 | 与 Web 共用 Bridge | [Web 数据模型](../oneos-web-h2-station/.spec/record-data-model.md) |
|
||||
| 羚牛车辆标签 | 本地系统车辆集合判定 | [fleet-plate-tag.md](./fleet-plate-tag.md) |
|
||||
| 总额 | 单价×加氢量自动算,界面不展示公式 | [feature-create.md](./feature-create.md) |
|
||||
| 手工台账 | 按站+自然日上传图片;月历标已传/未传可补传;未上传今日禁新增 | [feature-manual-ledger.md](./feature-manual-ledger.md) |
|
||||
| 手工台账 | 按站+自然日上传图片;月历标已传/未传可补传;**昨日未传则今日禁新增** | [feature-manual-ledger.md](./feature-manual-ledger.md) |
|
||||
|
||||
---
|
||||
|
||||
@@ -77,4 +78,16 @@
|
||||
| `create` | 时间选择、站名、车牌键盘、面板 OCR、金额字段 |
|
||||
| `detail` | 详情字段、锁定说明、编辑/删除 |
|
||||
|
||||
右侧工具栏「原型目录」结构:
|
||||
|
||||
| 目录 | 内容 |
|
||||
|---|---|
|
||||
| PRD 全文 | requirements-prd.md |
|
||||
| 需求角色 | roles.md |
|
||||
| 用户故事 | user-stories.md |
|
||||
| 关键逻辑 | key-logic + 台账门禁 / 对账 / 车辆标签 |
|
||||
| 流程图 | flows.md(Mermaid) |
|
||||
| 功能说明 | 列表 / 新增 / 台账 / 详情 / 车辆 / 对账 |
|
||||
| 按页面 | list / create / detail 要点 |
|
||||
|
||||
标注数据:`annotation-source.json`(由 `scripts/build-annotation-source.mjs` 从本目录 Markdown 生成)。
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
"version": 2,
|
||||
"prototypeName": "oneos-h5-h2-order",
|
||||
"pageId": "list",
|
||||
"updatedAt": 1784280448632,
|
||||
"updatedAt": 1784378211658,
|
||||
"nodes": [
|
||||
{
|
||||
"images": [],
|
||||
"controls": [],
|
||||
"createdAt": 1784280448632,
|
||||
"updatedAt": 1784280448632,
|
||||
"createdAt": 1784378211658,
|
||||
"updatedAt": 1784378211658,
|
||||
"hasMarkdown": true,
|
||||
"annotationText": "",
|
||||
"id": "h5-hero",
|
||||
@@ -31,8 +31,8 @@
|
||||
{
|
||||
"images": [],
|
||||
"controls": [],
|
||||
"createdAt": 1784280448632,
|
||||
"updatedAt": 1784280448632,
|
||||
"createdAt": 1784378211658,
|
||||
"updatedAt": 1784378211658,
|
||||
"hasMarkdown": true,
|
||||
"annotationText": "",
|
||||
"id": "h5-summary",
|
||||
@@ -52,8 +52,8 @@
|
||||
{
|
||||
"images": [],
|
||||
"controls": [],
|
||||
"createdAt": 1784280448632,
|
||||
"updatedAt": 1784280448632,
|
||||
"createdAt": 1784378211658,
|
||||
"updatedAt": 1784378211658,
|
||||
"hasMarkdown": true,
|
||||
"annotationText": "",
|
||||
"id": "h5-card",
|
||||
@@ -73,8 +73,8 @@
|
||||
{
|
||||
"images": [],
|
||||
"controls": [],
|
||||
"createdAt": 1784280448632,
|
||||
"updatedAt": 1784280448632,
|
||||
"createdAt": 1784378211658,
|
||||
"updatedAt": 1784378211658,
|
||||
"hasMarkdown": true,
|
||||
"annotationText": "",
|
||||
"id": "h5-fab",
|
||||
@@ -94,8 +94,8 @@
|
||||
{
|
||||
"images": [],
|
||||
"controls": [],
|
||||
"createdAt": 1784280448632,
|
||||
"updatedAt": 1784280448632,
|
||||
"createdAt": 1784378211658,
|
||||
"updatedAt": 1784378211658,
|
||||
"hasMarkdown": true,
|
||||
"annotationText": "",
|
||||
"id": "h5-time",
|
||||
@@ -115,8 +115,8 @@
|
||||
{
|
||||
"images": [],
|
||||
"controls": [],
|
||||
"createdAt": 1784280448632,
|
||||
"updatedAt": 1784280448632,
|
||||
"createdAt": 1784378211658,
|
||||
"updatedAt": 1784378211658,
|
||||
"hasMarkdown": true,
|
||||
"annotationText": "",
|
||||
"id": "h5-station",
|
||||
@@ -136,8 +136,8 @@
|
||||
{
|
||||
"images": [],
|
||||
"controls": [],
|
||||
"createdAt": 1784280448632,
|
||||
"updatedAt": 1784280448632,
|
||||
"createdAt": 1784378211658,
|
||||
"updatedAt": 1784378211658,
|
||||
"hasMarkdown": true,
|
||||
"annotationText": "",
|
||||
"id": "h5-plate",
|
||||
@@ -157,8 +157,8 @@
|
||||
{
|
||||
"images": [],
|
||||
"controls": [],
|
||||
"createdAt": 1784280448632,
|
||||
"updatedAt": 1784280448632,
|
||||
"createdAt": 1784378211658,
|
||||
"updatedAt": 1784378211658,
|
||||
"hasMarkdown": true,
|
||||
"annotationText": "",
|
||||
"id": "h5-mileage",
|
||||
@@ -178,8 +178,8 @@
|
||||
{
|
||||
"images": [],
|
||||
"controls": [],
|
||||
"createdAt": 1784280448632,
|
||||
"updatedAt": 1784280448632,
|
||||
"createdAt": 1784378211658,
|
||||
"updatedAt": 1784378211658,
|
||||
"hasMarkdown": true,
|
||||
"annotationText": "",
|
||||
"id": "h5-dispenser-brands",
|
||||
@@ -199,8 +199,8 @@
|
||||
{
|
||||
"images": [],
|
||||
"controls": [],
|
||||
"createdAt": 1784280448632,
|
||||
"updatedAt": 1784280448632,
|
||||
"createdAt": 1784378211658,
|
||||
"updatedAt": 1784378211658,
|
||||
"hasMarkdown": true,
|
||||
"annotationText": "",
|
||||
"id": "h5-price",
|
||||
@@ -220,8 +220,8 @@
|
||||
{
|
||||
"images": [],
|
||||
"controls": [],
|
||||
"createdAt": 1784280448632,
|
||||
"updatedAt": 1784280448632,
|
||||
"createdAt": 1784378211658,
|
||||
"updatedAt": 1784378211658,
|
||||
"hasMarkdown": true,
|
||||
"annotationText": "",
|
||||
"id": "h5-kg",
|
||||
@@ -241,8 +241,8 @@
|
||||
{
|
||||
"images": [],
|
||||
"controls": [],
|
||||
"createdAt": 1784280448632,
|
||||
"updatedAt": 1784280448632,
|
||||
"createdAt": 1784378211658,
|
||||
"updatedAt": 1784378211658,
|
||||
"hasMarkdown": true,
|
||||
"annotationText": "",
|
||||
"id": "h5-total-label",
|
||||
@@ -262,8 +262,8 @@
|
||||
{
|
||||
"images": [],
|
||||
"controls": [],
|
||||
"createdAt": 1784280448632,
|
||||
"updatedAt": 1784280448632,
|
||||
"createdAt": 1784378211658,
|
||||
"updatedAt": 1784378211658,
|
||||
"hasMarkdown": true,
|
||||
"annotationText": "",
|
||||
"id": "h5-detail",
|
||||
@@ -283,8 +283,8 @@
|
||||
{
|
||||
"images": [],
|
||||
"controls": [],
|
||||
"createdAt": 1784280448632,
|
||||
"updatedAt": 1784280448632,
|
||||
"createdAt": 1784378211658,
|
||||
"updatedAt": 1784378211658,
|
||||
"hasMarkdown": true,
|
||||
"annotationText": "",
|
||||
"id": "h5-detail-photos",
|
||||
@@ -304,8 +304,8 @@
|
||||
{
|
||||
"images": [],
|
||||
"controls": [],
|
||||
"createdAt": 1784280448632,
|
||||
"updatedAt": 1784280448632,
|
||||
"createdAt": 1784378211658,
|
||||
"updatedAt": 1784378211658,
|
||||
"hasMarkdown": true,
|
||||
"annotationText": "",
|
||||
"id": "h5-manual",
|
||||
@@ -325,8 +325,8 @@
|
||||
{
|
||||
"images": [],
|
||||
"controls": [],
|
||||
"createdAt": 1784280448632,
|
||||
"updatedAt": 1784280448632,
|
||||
"createdAt": 1784378211658,
|
||||
"updatedAt": 1784378211658,
|
||||
"hasMarkdown": true,
|
||||
"annotationText": "",
|
||||
"id": "h5-manual-calendar",
|
||||
@@ -352,16 +352,16 @@
|
||||
"h5-fab": "## 底栏新增\n\n固定在列表底部,不遮挡卡片滚动区。点击进入新增表单,加氢时间默认=点击时刻。\n\n详见功能说明「新增加氢记录」。",
|
||||
"h5-time": "## 加氢时间(必填)\n\n默认=点击「新增」时的当前时间;点击后底部弹层选择日期 + 时 + 分。",
|
||||
"h5-station": "## 加氢站名称\n\n只读。默认显示当前登录账号对应加氢站名称,不可切换。",
|
||||
"h5-plate": "# 车牌「羚牛车辆 / 非羚牛车辆」标签\n\n原型本地判定,未接真实车辆管理 API。\n\n## 判定顺序\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 车牌为空 | 不展示标签 |\n| 2 | 规范化后车牌 ∈ 本地系统车辆集合 | **羚牛车辆** |\n| 3 | 有车牌但不在集合中 | **非羚牛车辆** |\n\n## 前置条件\n\n- 识别(OCR)或键盘输入得到车牌后才展示。\n- 比较前去掉尾缀 `F`,统一大写。\n\n## 数据源\n\n- `utils/fleet-plates.ts` 内本地种子车牌集合(含加氢 Bridge 演示车牌与部分车辆管理样例)。\n- 正式环境应改为查询车辆管理 / 系统车辆列表接口。\n\n## 用户可见结果\n\n- 绿色标签「羚牛车辆」或橙色标签「非羚牛车辆」。\n- 不拦截提交(仅提示归属)。\n\n## 代码路径\n\n- `getVehicleTag` / `isLingniuVehicle` → `CreateWizard` 车牌行右侧标签。\n",
|
||||
"h5-plate": "# 车牌「羚牛车辆 / 非羚牛车辆」标签\n\n原型本地判定,未接真实车辆管理 API。\n\n全文(含核对/对账字段空展示规则,跨 Web / 台账 / H5): \n[../../oneos-web-h2-station/.spec/fleet-vehicle-verify.md](../../oneos-web-h2-station/.spec/fleet-vehicle-verify.md)\n\n## 判定顺序\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 车牌为空 | 不展示标签 |\n| 2 | 规范化后车牌 ∈ 本地系统车辆集合 | **羚牛车辆** |\n| 3 | 有车牌但不在集合中 | **非羚牛车辆** |\n\n## 前置条件\n\n- 识别(OCR)或键盘输入得到车牌后才展示。\n- 比较前去掉尾缀 `F`,统一大写。\n\n## 数据源\n\n- 优先 `H2VehicleLedgerBridge.isLingniuVehicle`;本地回退见 `utils/fleet-plates.ts`(与 Bridge `H2_FLEET_PLATE_KEYS` 同步)。\n\n## 用户可见结果\n\n- 绿色标签「羚牛车辆」或橙色标签「非羚牛车辆」。\n- **非羚牛车辆**:列表/详情中核对状态、核对时间、对账状态、对账时间显示「—」,不展示核对待办语义。\n- 不拦截提交(仅提示归属)。\n\n## 代码路径\n\n- `getVehicleTag` / `isLingniuVehicle` → `CreateWizard`、`OrderCard`、`OrderDetail`\n",
|
||||
"h5-mileage": "## 里程(km)\n\n选填。支持「从车机获取」(原型 Mock);识别车牌后可自动回填。正式环境对接车机 / T-Box。\n\n详见 [新增加氢记录](feature-create)。",
|
||||
"h5-dispenser-brands": "## 加氢机品牌参考\n\n展示站点信息「加氢机品牌管理」维护的品牌·型号,供面板 OCR 模型判断参考。数据源:`h2DispenserBrandStore`。",
|
||||
"h5-price": "## 氢气单价(必填)\n\n可 OCR 识别后校正;变更后自动重算加氢总额(单价×加氢量)。",
|
||||
"h5-kg": "## 加氢量(必填)\n\n可 OCR 识别后校正;变更后自动重算加氢总额。",
|
||||
"h5-total-label": "## 加氢总额(元)\n\n只读展示;由单价×加氢量自动计算(两位小数)。界面标签不展示公式文案。",
|
||||
"h5-detail": "# 功能:详情、编辑与删除\n\n## 目标\n\n查看单笔加氢记录完整字段;在「未核对且未对账」时可编辑或删除。\n\n## 详情展示\n\n- 顶部:车牌、加氢时间、核对标签、对账标签 \n- 指标条:加氢总额、加氢量 \n- **识别原图**:车牌照片、加氢机面板照片(均为拍摄时叠加时间/地点水印后的原图;种子数据无实拍时用演示水印图) \n- 明细行:站点、车牌、里程、单价、核对/对账状态与时间 \n\n## 锁定规则\n\n| 条件 | 结果 |\n|------|------|\n| 已对账 | 不可编辑/删除;提示已纳入站点对账单 |\n| 已核对(未对账) | 不可编辑/删除;提示能源部已核对 |\n| 未核对且未对账 | 可编辑、可删除 |\n\n判定函数:`isOrderLocked`(核对或对账任一成立即锁)。\n\n## 编辑\n\n进入与「新增」同一套表单,预填当前值(含已存水印照片);提交走更新逻辑(仍校验重复键)。\n\n## 删除\n\n二次确认后从 Bridge 移除该行。\n\n## 验收\n\n1. 未核对未对账:底部有删除/编辑 \n2. 已核对或已对账:无操作栏,有锁定说明 \n3. 详情含对账信息;列表卡片不含对账 \n4. 详情展示车牌与面板带水印原图(新上报提交后为实拍;种子行为演示图) \n",
|
||||
"h5-detail": "# 功能:详情、编辑与删除\n\n## 目标\n\n查看单笔加氢记录完整字段;在「未核对且未对账」时可编辑或删除(**锁定仅适用于羚牛车辆**)。\n\n## 详情展示\n\n- 顶部:车牌、加氢时间、核对标签、对账标签 \n- 指标条:加氢总额、加氢量 \n- **识别原图**:车牌照片、加氢机面板照片(均为拍摄时叠加时间/地点水印后的原图;种子数据无实拍时用演示水印图) \n- 明细行:站点、车牌、里程、单价、核对/对账状态与时间 \n\n> 非羚牛车辆:核对/对账字段展示「—」,不展示核对待办语义(见 [fleet-plate-tag.md](./fleet-plate-tag.md))。\n\n## 锁定规则\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | **非羚牛车辆** | **不锁定**(不以核对/对账状态禁改删) |\n| 2 | 羚牛 ∧ 已对账 | 不可编辑/删除;提示已纳入站点对账单 |\n| 3 | 羚牛 ∧ 已核对(未对账) | 不可编辑/删除;提示能源部已核对 |\n| 4 | 羚牛 ∧ 未核对且未对账 | 可编辑、可删除 |\n\n判定函数:`isOrderLocked`(先判羚牛;再核「核对或对账任一成立即锁」)。\n\n## 编辑\n\n进入与「新增」同一套表单,预填当前值(含已存水印照片);提交走更新逻辑(仍校验重复键)。\n\n## 删除\n\n二次确认文案:「确认删除车牌 … 的未锁定记录?」;确认后从 Bridge 移除该行。\n\n## 验收\n\n1. 羚牛 ∧ 未核对未对账:底部有删除/编辑 \n2. 羚牛 ∧ 已核对或已对账:无操作栏,有锁定说明 \n3. 非羚牛:可改删(不因核对/对账锁定);核对/对账展示「—」 \n4. 详情含对账信息;列表卡片不含对账 \n5. 详情展示车牌与面板带水印原图(新上报提交后为实拍;种子行为演示图) \n",
|
||||
"h5-detail-photos": "## 识别原图(含水印)\n\n详情展示车牌识别、加氢机面板的原始照片;照片在拍摄/选图时已叠加时间与地点水印并随记录保存。种子数据无实拍时用演示水印图。\n\n详见 [详情编辑删除](feature-detail)。",
|
||||
"h5-manual": "# 功能:手工台账上传\n\n## 目标\n\n站端每日上传纸质**手工台账照片**,作为系统电子加氢记录的核对依据。多日未操作时,可通过日历查看缺口并补传过往日期。\n\n## 产品口径\n\n| 项 | 规则 |\n|---|---|\n| 入口 | H5:底部 Tab「手工台账」;Web:顶栏「手工台账」 |\n| 上传形态 | 仅图片(拍照/相册),可多张 |\n| 维度 | **加氢站 + 自然日(本地日历日)** 一份 |\n| 日历 | 月历自本站**首笔加氢记录日**起算:绿=已传、橙=未传;首笔之前无需补传;禁选未来日 |\n| 补传 | 允许为过往未传日期补传;已传日期仅可**追加**新图 |\n| 归档 | **确认上传后的图片进入已归档状态,不可删除**;未确认的草稿图仍可删除 |\n| 拦截 | **未上传今日手工台账 → 禁止新增加氢记录**(编辑已有记录不拦;补传历史不替代今日校验) |\n| 数据 | 共享 `H2VehicleLedgerBridge` 内存 Store(未接真实 API) |\n\n## 判定顺序(新增拦截)\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 操作=编辑已有记录 | 不校验手工台账 |\n| 2 | 操作=新增,且目标站今日已有 ≥1 张台账图 | 允许进入新增 |\n| 3 | 操作=新增,今日未上传 | 提示并跳转手工台账 Tab |\n\n## 日历与补传\n\n| 规则 | 说明 |\n|------|------|\n| 数据源 | `listManualLedgersByStation`;当日有 ≥1 张图视为已上传 |\n| 业务起点 | 取本站 Bridge 加氢记录中最早 `hydrogenTime` 自然日;无记录则历史日均不要求补传 |\n| 未传计数 | 本月内 `max(月初, 首笔日)`~min(月末, 今日) 中未上传的天数 |\n| 点选 | 仅 `dateKey ≤ 今日`;选中后右侧/下方展示该日草稿图与上传按钮 |\n| 归档不可删 | Store 已有图强制保留;`upsertManualLedger` 仅追加新图,忽略删除已有图的请求 |\n| 与拦截关系 | 仅 **今日** 影响「禁止新增」;历史补传解决核对缺口,不放宽今日门禁 |\n\n## 用户可见\n\n- 今日未上传:橙/黄提示条 + Tab 红点 \n- 日历:自首笔加氢日起绿/橙标记;首笔之前无橙点(无需补传);本月未传天数摘要 \n- 选中日:上传区标题为该日期;已归档图显示「已归档」无删除;仅未确认草稿可删;已有台账时主按钮为「确认补传」\n\n## 代码路径\n\n- Bridge:`src/common/h2VehicleLedgerBridge.js`(`hasManualLedgerForDate` / `upsertManualLedger` / `listManualLedgersByStation`) \n- H5:`oneos-h5-h2-order` · `ManualLedgerPanel` + `utils/manual-ledger.ts`(`buildManualMonthCells` / `getFirstHydrogenDateKey`) \n- Web:`oneos-web-h2-station/pages/02-加氢记录.jsx`(手工台账 Tab 月历) \n\n## 验收\n\n1. 今日未上传时,H5/Web 点「新增」均被拦截并引导上传 \n2. 上传至少一张图并确认后,可新增电子记录 \n3. 日历自首笔加氢日起区分已传/未传;首笔之前不标未传、不计入补传;可点选范围内未传日补传 \n4. 补传历史日后,该日日历变为已传;今日仍未传时新增仍被拦截 \n5. 确认上传后的图显示已归档且不可删除;仅可追加补传 \n6. H5 与 Web 共用同一 Store,一端上传另一端可见(同会话) \n",
|
||||
"h5-manual-calendar": "## 台账日历\n\n自本站首笔加氢记录日起:绿=已上传,橙=未上传;首笔之前无需补传。禁选未来日。点选范围内未传日可补传。仅「今日」是否上传影响新增加氢记录。\n\n详见 [手工台账上传](feature-manual-ledger)。"
|
||||
"h5-manual": "# 功能:手工台账上传\n\n## 目标\n\n站端每日上传纸质**手工台账照片**,作为系统电子加氢记录的核对依据。多日未操作时,可通过日历查看缺口并补传过往日期。\n\n## 产品口径\n\n| 项 | 规则 |\n|---|---|\n| 入口 | H5:底部 Tab「手工台账」;Web:顶栏「手工台账」 |\n| 站点范围 | **H5 固定本站**(无切站);Web 可按角色多站/超管切换(标注状态演示,未接真实登录) |\n| 上传形态 | 仅图片(拍照/相册),可多张 |\n| 维度 | **加氢站 + 自然日(本地日历日)** 一份 |\n| 日历 | 月历自本站**首笔加氢记录日**起算:绿=已传、橙=未传;首笔之前无需补传;禁选未来日 |\n| 补传 | 允许为过往未传日期补传;已传日期仅可**追加**新图 |\n| 归档 | **确认上传后的图片进入已归档状态,不可删除**;未确认的草稿图仍可删除 |\n| 二次确认 | H5 / Web 确认上传前弹窗:「提交后将无法修改,是否确认手工台账照片无误」;确认后才写入 |\n| 拦截 | **前一日手工台账未上传 → 今日禁止新增加氢记录**(编辑已有记录不拦;补传更早历史日不替代昨日校验) |\n| 数据 | 共享 `H2VehicleLedgerBridge` 内存 Store(未接真实 API);站名/编码解析见 Bridge.`h2BridgeResolveStationId` |\n\n## 判定顺序(新增拦截)\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 操作=编辑已有记录 | 不校验手工台账 |\n| 2 | 操作=新增,且昨日早于本站首笔加氢日(或不要求昨日台账) | 允许进入新增 |\n| 3 | 操作=新增,且目标站昨日已有 ≥1 张台账图 | 允许进入新增 |\n| 4 | 操作=新增,昨日未上传 | 顶部/Toast 提示「手工台账有缺失,作为对账重要依据,请先上传手工台账」,并**直接跳转**手工台账页且选中昨日 |\n\n## 日历与补传\n\n| 规则 | 说明 |\n|------|------|\n| 数据源 | `listManualLedgersByStation`;当日有 ≥1 张图视为已上传 |\n| 业务起点 | 取本站 Bridge 加氢记录中最早 `hydrogenTime` 自然日;无记录则历史日均不要求补传 |\n| 未传计数 | 本月内 `max(月初, 首笔日)`~min(月末, 今日) 中未上传的天数 |\n| 点选 | 仅 `dateKey ≤ 今日`;选中后右侧/下方展示该日草稿图与上传按钮 |\n| 归档不可删 | Store 已有图强制保留;`upsertManualLedger` 仅追加新图,忽略删除已有图的请求 |\n| 与拦截关系 | 仅 **昨日** 影响「禁止新增」;历史补传解决核对缺口;今日台账仍可按日常上传,但不作为新增门禁 |\n\n## 用户可见\n\n- 今日状态双反馈(H5 台账页顶部卡片 / Web 右侧「当日台账」):未上传「今日尚未上传…」异常态;已上传「今日已上传…」正常态。**日常上传反馈 ≠ 新增门禁**;H5 文案须写明「今日不作为新增门禁,仅要求前一日已上传」 \n- 昨日缺失(H5):**列表顶栏可点橙条** + 底栏 FAB 旁提示 + Tab 红点;台账页同样展示橙条并可选中昨日;文案固定为「手工台账有缺失,作为对账重要依据,请先上传手工台账」 \n- 日历:自首笔加氢日起绿/橙标记;首笔之前无橙点(无需补传);本月未传天数摘要 \n- Web 右侧面板标题「手工台账」:空态提示;有图可点进全屏翻页预览并角标「已归档/待提交」;预览区内上传;底部确认前二次弹窗;提交后立即展示已归档缩略图;左右卡片等高\n\n## 代码路径\n\n- Bridge:`src/common/h2VehicleLedgerBridge.js`(`hasManualLedgerForDate` / `isManualLedgerGateBlocked` / `upsertManualLedger` / `listManualLedgersByStation`) \n- H5:`oneos-h5-h2-order` · `ManualLedgerPanel` + `utils/manual-ledger.ts`(`isManualLedgerGateBlocked` / `buildManualMonthCells` / `getFirstHydrogenDateKey`) \n- Web:`oneos-web-h2-station/pages/02-加氢记录.jsx`(`hrIsManualLedgerGateBlocked` / 跳转选中昨日) \n\n## 验收\n\n1. 昨日未上传时,H5/Web 列表顶栏提示固定文案;点「新增」被拦截并直接进入手工台账(选中昨日) \n2. 补传昨日台账并确认后,可新增加氢记录 \n3. 日历自首笔加氢日起区分已传/未传;首笔之前不标未传、不计入补传;可点选范围内未传日补传 \n4. 仅补传更早历史日、昨日仍未传时,新增仍被拦截 \n5. 确认上传后的图显示已归档且不可删除;仅可追加补传 \n6. H5 与 Web 共用同一 Store,一端上传另一端可见(同会话) \n7. Web 导出 CSV 列含「车辆来源」(羚牛车辆/非羚牛车辆)、「数据来源」(站端上传/羚牛上传),与列表标签口径一致 \n",
|
||||
"h5-manual-calendar": "## 台账日历\n\n自本站首笔加氢记录日起:绿=已上传,橙=未上传;首笔之前无需补传。禁选未来日。点选范围内未传日可补传。**前一日**未上传时禁止新增加氢记录(今日台账状态不作为门禁)。\n\n详见 [手工台账上传](feature-manual-ledger)。"
|
||||
},
|
||||
"assetMap": {},
|
||||
"directory": {
|
||||
@@ -376,9 +376,90 @@
|
||||
"type": "markdown",
|
||||
"id": "h5-doc-prd",
|
||||
"title": "PRD 全文",
|
||||
"markdown": "# 加氢订单(H5)— 产品需求文档(PRD)\n\n| 项 | 内容 |\n|---|---|\n| 文档版本 | v1.5 |\n| 模块名称 | 加氢站管理 → 加氢订单 |\n| 所属系统 | ONE-OS(加氢站手机浏览器) |\n| 交互原型 | `/prototypes/oneos-h5-h2-order` |\n| 设计基底 | 小羚羚 `xll-miniapp/DESIGN.md` |\n| 关联模块 | [站点信息](/prototypes/oneos-web-h2-station-site)、[加氢记录 Web](/prototypes/oneos-web-h2-station)、[车辆氢费明细](/prototypes/vehicle-h2-fee-ledger) |\n\n---\n\n## 1. 背景与目标\n\n采购创建加氢站并绑定供应商、开放系统账号后,加氢站人员通过手机浏览器登录 OneOS,在「加氢订单」模块上报加氢记录。Web「加氢记录」为同一业务的 PC 入口(共享 Bridge 数据;PC 另有导出与预约加氢)。\n\n每日须上传**手工台账**照片,作为电子记录的核对依据;未上传当日手工台账时不可新增加氢记录。手工台账页提供月历,标出已传/未传日期,支持补传过往缺口。\n\n### 1.1 目标用户\n\n加氢站操作人员(户外 / 站场手机使用)。\n\n### 1.2 功能清单\n\n| # | 功能 | 说明文档 |\n|---|---|---|\n| 1 | 本站列表与 KPI | [feature-list.md](./feature-list.md) |\n| 2 | 新增加氢记录 | [feature-create.md](./feature-create.md) |\n| 3 | 详情 / 编辑 / 删除 | [feature-detail.md](./feature-detail.md) |\n| 4 | **手工台账上传** | [feature-manual-ledger.md](./feature-manual-ledger.md) |\n| 5 | 羚牛车辆标签 | [fleet-plate-tag.md](./fleet-plate-tag.md) |\n| 6 | 核对与对账联动 | [reconcile-linkage.md](./reconcile-linkage.md) |\n\n### 1.3 列表字段\n\n加氢时间、车牌号、氢气单价、加氢量、加氢总额、核对状态;已核对时展示核对时间。里程在新增/详情展示(选填),列表不展示。列表不展示对账状态。\n\n### 1.4 不做\n\n真登录、真 OCR SDK、PLC、原生 App、后端联调。\n\n---\n\n## 2. 验收项\n\n1. 菜单位于 OneOS → 加氢站管理 → 加氢订单 \n2. 默认已登录本站;列表仅本站(无切站) \n3. 底栏 Tab:加氢订单 / 手工台账;月历可区分已传/未传并补传过往日;未上传今日手工台账不可新增;上传后可新增;时间默认点击新增时刻(到分) \n4. 已核对或已对账不可编辑/删除;未核对未对账可 \n5. 能源部完成核对后同步「已核对」;站点对账单提交后同步「已对账」+ 对账时间 \n6. 同站同车牌同加氢时间重复保存被拦截 \n7. Web「加氢记录」与 H5「加氢订单」手工台账逻辑一致(同 Store) \n8. 车牌/面板拍摄预览含水印(时间、地点=本站名称);里程可从车机获取(Mock) \n9. 面板 OCR 可展示站点信息维护的加氢机品牌型号线索 \n10. 详情页展示车牌与面板带水印原图 \n\n---\n\n## 3. 复杂逻辑摘要\n\n| 主题 | 摘要 | 全文 |\n|---|---|---|\n| 对账 / 核对 | 核对≠对账;触发方不同 | [reconcile-linkage.md](./reconcile-linkage.md)、[车辆氢费 verify-reconcile](../vehicle-h2-fee-ledger/.spec/verify-reconcile.md) |\n| 重复键 | 与 Web 共用 Bridge | [Web 数据模型](../oneos-web-h2-station/.spec/record-data-model.md) |\n| 羚牛车辆标签 | 本地系统车辆集合判定 | [fleet-plate-tag.md](./fleet-plate-tag.md) |\n| 总额 | 单价×加氢量自动算,界面不展示公式 | [feature-create.md](./feature-create.md) |\n| 手工台账 | 按站+自然日上传图片;月历标已传/未传可补传;未上传今日禁新增 | [feature-manual-ledger.md](./feature-manual-ledger.md) |\n\n---\n\n## 4. 页面与标注对照\n\n| 页面 pageId | 主要能力 |\n|---|---|\n| `list` | 站点头、KPI、订单卡片、底栏新增 |\n| `create` | 时间选择、站名、车牌键盘、面板 OCR、金额字段 |\n| `detail` | 详情字段、锁定说明、编辑/删除 |\n\n标注数据:`annotation-source.json`(由 `scripts/build-annotation-source.mjs` 从本目录 Markdown 生成)。\n",
|
||||
"markdown": "# 加氢订单(H5)— 产品需求文档(PRD)\n\n| 项 | 内容 |\n|---|---|\n| 文档版本 | v1.7 |\n| 模块名称 | 加氢站管理 → 加氢订单 |\n| 所属系统 | ONE-OS(加氢站手机浏览器) |\n| 交互原型 | `/prototypes/oneos-h5-h2-order` |\n| 设计基底 | 小羚羚 `xll-miniapp/DESIGN.md` |\n| 关联模块 | [站点信息](/prototypes/oneos-web-h2-station-site)、[加氢记录 Web](/prototypes/oneos-web-h2-station)、[车辆氢费明细](/prototypes/vehicle-h2-fee-ledger) |\n\n---\n\n## 1. 背景与目标\n\n采购创建加氢站并绑定供应商、开放系统账号后,加氢站人员通过手机浏览器登录 OneOS,在「加氢订单」模块上报加氢记录。Web「加氢记录」为同一业务的 PC 入口(共享 Bridge 数据;PC 另有导出与预约加氢)。\n\n每日须上传**手工台账**照片,作为电子记录的核对依据;**前一日**手工台账未上传时,今日不可新增加氢记录。手工台账页提供月历,标出已传/未传日期,支持补传过往缺口。\n\n### 1.1 目标用户\n\n加氢站操作人员(户外 / 站场手机使用)。\n\n### 1.2 功能清单\n\n| # | 功能 | 说明文档 |\n|---|---|---|\n| 1 | 本站列表与 KPI | [feature-list.md](./feature-list.md) |\n| 2 | 新增加氢记录 | [feature-create.md](./feature-create.md) |\n| 3 | 详情 / 编辑 / 删除 | [feature-detail.md](./feature-detail.md) |\n| 4 | **手工台账上传** | [feature-manual-ledger.md](./feature-manual-ledger.md) |\n| 5 | 羚牛车辆标签 | [fleet-plate-tag.md](./fleet-plate-tag.md) |\n| 6 | 核对与对账联动 | [reconcile-linkage.md](./reconcile-linkage.md) |\n\n### 1.3 列表字段\n\n加氢时间、车牌号、氢气单价、加氢量、加氢总额、核对状态;已核对时展示核对时间。里程在新增/详情展示(选填),列表不展示。列表不展示对账状态。\n\n### 1.4 不做\n\n真登录、真 OCR SDK、PLC、原生 App、后端联调。\n\n---\n\n## 2. 验收项\n\n1. 菜单位于 OneOS → 加氢站管理 → 加氢订单 \n2. 默认已登录本站;列表仅本站(无切站) \n3. 底栏 Tab:加氢订单 / 手工台账;月历可区分已传/未传并补传过往日;前一日未上传时提示「手工台账有缺失,作为对账重要依据,请先上传手工台账」且点新增直达台账页;补传昨日后可新增;时间默认点击新增时刻(到分) \n4. **羚牛车辆**已核对或已对账不可编辑/删除;非羚牛不锁定;未核对未对账(羚牛)可改删 \n5. 能源部完成核对后同步「已核对」;站点对账单提交后同步「已对账」+ 对账时间(锁定仅羚牛) \n6. 同站同车牌同加氢时间重复保存被拦截 \n7. Web「加氢记录」与 H5「加氢订单」手工台账逻辑一致(同 Store) \n8. 车牌/面板拍摄预览含水印(时间、地点=本站名称);里程可从车机获取(Mock) \n9. 面板 OCR 可展示站点信息维护的加氢机品牌型号线索 \n10. 详情页展示车牌与面板带水印原图 \n11. 昨日台账缺失时:列表顶栏橙条 + 底栏提示 + Tab 红点;台账页不把「今日未传」写成新增门禁 \n\n---\n\n## 3. 复杂逻辑摘要\n\n| 主题 | 摘要 | 全文 |\n|---|---|---|\n| 对账 / 核对 | 核对≠对账;**锁定仅羚牛** | [reconcile-linkage.md](./reconcile-linkage.md)、[车辆氢费 verify-reconcile](../vehicle-h2-fee-ledger/.spec/verify-reconcile.md) |\n| 重复键 | 与 Web 共用 Bridge | [Web 数据模型](../oneos-web-h2-station/.spec/record-data-model.md) |\n| 羚牛车辆标签 | 本地系统车辆集合判定 | [fleet-plate-tag.md](./fleet-plate-tag.md) |\n| 总额 | 单价×加氢量自动算,界面不展示公式 | [feature-create.md](./feature-create.md) |\n| 手工台账 | 按站+自然日上传图片;月历标已传/未传可补传;**昨日未传则今日禁新增** | [feature-manual-ledger.md](./feature-manual-ledger.md) |\n\n---\n\n## 4. 页面与标注对照\n\n| 页面 pageId | 主要能力 |\n|---|---|\n| `list` | 站点头、KPI、订单卡片、底栏新增 |\n| `create` | 时间选择、站名、车牌键盘、面板 OCR、金额字段 |\n| `detail` | 详情字段、锁定说明、编辑/删除 |\n\n右侧工具栏「原型目录」结构:\n\n| 目录 | 内容 |\n|---|---|\n| PRD 全文 | requirements-prd.md |\n| 需求角色 | roles.md |\n| 用户故事 | user-stories.md |\n| 关键逻辑 | key-logic + 台账门禁 / 对账 / 车辆标签 |\n| 流程图 | flows.md(Mermaid) |\n| 功能说明 | 列表 / 新增 / 台账 / 详情 / 车辆 / 对账 |\n| 按页面 | list / create / detail 要点 |\n\n标注数据:`annotation-source.json`(由 `scripts/build-annotation-source.mjs` 从本目录 Markdown 生成)。\n",
|
||||
"markdownPath": ".spec/requirements-prd.md"
|
||||
},
|
||||
{
|
||||
"type": "folder",
|
||||
"id": "h5-doc-roles",
|
||||
"title": "需求角色",
|
||||
"defaultExpanded": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "markdown",
|
||||
"id": "h5-doc-roles-md",
|
||||
"title": "角色说明",
|
||||
"markdown": "# 需求角色\n\n| 项 | 说明 |\n|---|---|\n| 适用原型 | 加氢订单(H5)`oneos-h5-h2-order` |\n| 关联 Web | 加氢记录 `oneos-web-h2-station`(同一业务、不同端) |\n| 数据 | 原型本地种子 + 共享 Bridge(未接真实登录 / API) |\n\n---\n\n## 1. 角色一览\n\n| 角色 | 端 | 核心诉求 | 本原型是否为主用户 |\n|---|---|---|---|\n| **加氢站操作人员** | H5(手机浏览器) | 在站场快速上报本站加氢流水;补传手工台账照片 | **是** |\n| **加氢站站长 / 多站管理员** | Web 为主,H5 可看本站 | 多站台账、导出、手工台账补传与督导 | 间接(H5 固定单站演示) |\n| **能源部核对人员** | 车辆氢费明细 | 完成核对,回写「已核对」 | 否(触发方在台账) |\n| **站点对账人员** | 站点信息 · 对账单 | 提交对账单,回写「已对账」 | 否(触发方在站点信息) |\n| **超级管理员** | Web | 查看全部站点流水与台账 | 否(Web 标注状态演示) |\n\n---\n\n## 2. 本原型主角色:加氢站操作人员\n\n| 项 | 说明 |\n|---|---|\n| 场景 | 户外 / 站场,手机竖屏 |\n| 权限边界 | 仅本站数据;不可切站(原型固定演示站) |\n| 日常动作 | 看列表 KPI → 上传昨日手工台账(若缺失)→ 新增加氢记录 → 查看/编辑未锁定记录 |\n| 约束 | 前一日手工台账未上传则今日不可新增;已核对或已对账不可改删 |\n\n---\n\n## 3. 角色边界(本期不做)\n\n- 真登录与组织权限中心对接 \n- 能源部 / 对账人员在 H5 内操作核对或对账 \n- 跨站汇总与导出(属 Web「加氢记录」) \n",
|
||||
"markdownPath": ".spec/roles.md"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "folder",
|
||||
"id": "h5-doc-stories",
|
||||
"title": "用户故事",
|
||||
"defaultExpanded": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "markdown",
|
||||
"id": "h5-doc-stories-md",
|
||||
"title": "用户故事",
|
||||
"markdown": "# 用户故事\n\n> 以加氢站操作人员为主;验收口径见 [requirements-prd.md](./requirements-prd.md)。\n\n---\n\n## US-01 查看本站加氢订单\n\n**作为** 加氢站操作人员, \n**我希望** 打开加氢订单即看到本站列表与 KPI, \n**以便** 快速了解今日/近期上报情况。\n\n**验收要点**\n\n- 默认已登录本站;无切站 \n- KPI:订单数 / 加氢量 / 加氢金额随列表汇总 \n- 卡片展示加氢时间、车牌、量、额、核对状态等 \n\n---\n\n## US-02 补传缺失的手工台账后再新增\n\n**作为** 加氢站操作人员, \n**我希望** 在前一日手工台账未上传时被明确拦截,并直达台账页补传, \n**以便** 保证纸质台账作为对账依据不缺失。\n\n**验收要点**\n\n- 顶栏橙条 + 底栏提示:「手工台账有缺失,作为对账重要依据,请先上传手工台账」 \n- 点「新增」或顶栏橙条跳转手工台账并选中昨日 \n- 补传昨日确认后可新增 \n\n---\n\n## US-03 新增加氢记录\n\n**作为** 加氢站操作人员, \n**我希望** 用车牌键盘 + 面板拍照/OCR 快速填单, \n**以便** 在站场少打字完成上报。\n\n**验收要点**\n\n- 加氢时间默认=点击新增时刻(到分) \n- 站名只读为本站 \n- 总额 = 单价 × 加氢量(界面不展示公式) \n- 同站同车牌同时间重复保存拦截 \n\n---\n\n## US-04 查看详情并编辑/删除未锁定记录\n\n**作为** 加氢站操作人员, \n**我希望** 打开详情查看字段与识别原图,并对未核对未对账记录改删, \n**以便** 纠正录入错误。\n\n**验收要点**\n\n- 羚牛车辆已核对或已对账:不可编辑/删除,有锁定说明;非羚牛不锁定 \n- 详情展示车牌/面板含水印原图 \n\n---\n\n## US-05 按日历管理手工台账\n\n**作为** 加氢站操作人员, \n**我希望** 在月历上看已传/未传并补传过往日, \n**以便** 多日未操作后仍能补齐缺口。\n\n**验收要点**\n\n- 自首笔加氢日起绿/橙标记;首笔前不要求 \n- 禁选未来日;已归档图不可删,仅可追加 \n\n---\n\n## US-06 识别羚牛车辆\n\n**作为** 加氢站操作人员, \n**我希望** 在列表/详情看到是否羚牛车辆, \n**以便** 理解后续核对对账是否适用该车。\n\n**验收要点**\n\n- 标签:羚牛车辆 / 非羚牛车辆(本地车辆集合判定) \n",
|
||||
"markdownPath": ".spec/user-stories.md"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "folder",
|
||||
"id": "h5-doc-logic",
|
||||
"title": "关键逻辑",
|
||||
"defaultExpanded": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "markdown",
|
||||
"id": "h5-doc-logic-index",
|
||||
"title": "逻辑索引",
|
||||
"markdown": "# 关键逻辑索引\n\n本页汇总加氢订单(H5)复杂判定入口;细则见各规格全文。原型数据均为本地种子 / 内存 Store,**未接真实 API**。\n\n---\n\n## 1. 判定主题一览\n\n| 主题 | 一句话 | 全文 |\n|---|---|---|\n| 手工台账门禁 | **昨日**未传 → 今日禁新增;固定提示文案并直达台账 | [feature-manual-ledger.md](./feature-manual-ledger.md) |\n| 核对 ≠ 对账 | 能源部核对 / 站点对账单提交,触发方不同 | [reconcile-linkage.md](./reconcile-linkage.md) |\n| 记录锁定 | **仅羚牛**:已核对或已对账 → 站端不可改删;非羚牛不锁 | [feature-detail.md](./feature-detail.md)、[reconcile-linkage.md](./reconcile-linkage.md)、[fleet-plate-tag.md](./fleet-plate-tag.md) |\n| 重复键 | 同站 + 同车牌 + 同加氢时间 | Bridge / [Web record-data-model](../oneos-web-h2-station/.spec/record-data-model.md) |\n| 总额计算 | 总额 = 单价 × 量(两位小数) | [feature-create.md](./feature-create.md) |\n| 羚牛车辆 | 本地系统车牌集合 | [fleet-plate-tag.md](./fleet-plate-tag.md) |\n\n---\n\n## 2. 手工台账门禁(摘要)\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 编辑已有记录 | 不校验 |\n| 2 | 昨日早于本站首笔加氢日 | 不要求昨日台账,可新增 |\n| 3 | 昨日已有 ≥1 张台账图 | 可新增 |\n| 4 | 昨日未上传 | 提示并跳转手工台账(选中昨日) |\n\n用户可见文案:`手工台账有缺失,作为对账重要依据,请先上传手工台账`\n\n---\n\n## 3. 核对 / 对账(摘要)\n\n| 状态 | 谁触发 | 站端影响 |\n|---|---|---|\n| 已核对 | 车辆氢费明细「完成核对」 | **羚牛**不可改删 |\n| 已对账 | 站点信息对账单提交 | **羚牛**不可改删;回写对账时间 |\n| 非羚牛 | — | 不参与核对/对账展示与锁定 |\n\nH5 列表主要展示核对状态(仅羚牛);对账细节见联动规格。\n\n---\n\n## 4. 代码路径\n\n| 能力 | 路径 |\n|---|---|\n| Bridge | `src/common/h2VehicleLedgerBridge.js` |\n| 门禁工具 | `utils/manual-ledger.ts` → `isManualLedgerGateBlocked` |\n| 订单 CRUD | `utils/orders.ts` |\n| 页面 | `index.tsx` / `ManualLedgerPanel` / `CreateWizard` |\n",
|
||||
"markdownPath": ".spec/key-logic.md"
|
||||
},
|
||||
{
|
||||
"type": "markdown",
|
||||
"id": "h5-doc-logic-manual",
|
||||
"title": "手工台账门禁",
|
||||
"markdown": "# 功能:手工台账上传\n\n## 目标\n\n站端每日上传纸质**手工台账照片**,作为系统电子加氢记录的核对依据。多日未操作时,可通过日历查看缺口并补传过往日期。\n\n## 产品口径\n\n| 项 | 规则 |\n|---|---|\n| 入口 | H5:底部 Tab「手工台账」;Web:顶栏「手工台账」 |\n| 站点范围 | **H5 固定本站**(无切站);Web 可按角色多站/超管切换(标注状态演示,未接真实登录) |\n| 上传形态 | 仅图片(拍照/相册),可多张 |\n| 维度 | **加氢站 + 自然日(本地日历日)** 一份 |\n| 日历 | 月历自本站**首笔加氢记录日**起算:绿=已传、橙=未传;首笔之前无需补传;禁选未来日 |\n| 补传 | 允许为过往未传日期补传;已传日期仅可**追加**新图 |\n| 归档 | **确认上传后的图片进入已归档状态,不可删除**;未确认的草稿图仍可删除 |\n| 二次确认 | H5 / Web 确认上传前弹窗:「提交后将无法修改,是否确认手工台账照片无误」;确认后才写入 |\n| 拦截 | **前一日手工台账未上传 → 今日禁止新增加氢记录**(编辑已有记录不拦;补传更早历史日不替代昨日校验) |\n| 数据 | 共享 `H2VehicleLedgerBridge` 内存 Store(未接真实 API);站名/编码解析见 Bridge.`h2BridgeResolveStationId` |\n\n## 判定顺序(新增拦截)\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 操作=编辑已有记录 | 不校验手工台账 |\n| 2 | 操作=新增,且昨日早于本站首笔加氢日(或不要求昨日台账) | 允许进入新增 |\n| 3 | 操作=新增,且目标站昨日已有 ≥1 张台账图 | 允许进入新增 |\n| 4 | 操作=新增,昨日未上传 | 顶部/Toast 提示「手工台账有缺失,作为对账重要依据,请先上传手工台账」,并**直接跳转**手工台账页且选中昨日 |\n\n## 日历与补传\n\n| 规则 | 说明 |\n|------|------|\n| 数据源 | `listManualLedgersByStation`;当日有 ≥1 张图视为已上传 |\n| 业务起点 | 取本站 Bridge 加氢记录中最早 `hydrogenTime` 自然日;无记录则历史日均不要求补传 |\n| 未传计数 | 本月内 `max(月初, 首笔日)`~min(月末, 今日) 中未上传的天数 |\n| 点选 | 仅 `dateKey ≤ 今日`;选中后右侧/下方展示该日草稿图与上传按钮 |\n| 归档不可删 | Store 已有图强制保留;`upsertManualLedger` 仅追加新图,忽略删除已有图的请求 |\n| 与拦截关系 | 仅 **昨日** 影响「禁止新增」;历史补传解决核对缺口;今日台账仍可按日常上传,但不作为新增门禁 |\n\n## 用户可见\n\n- 今日状态双反馈(H5 台账页顶部卡片 / Web 右侧「当日台账」):未上传「今日尚未上传…」异常态;已上传「今日已上传…」正常态。**日常上传反馈 ≠ 新增门禁**;H5 文案须写明「今日不作为新增门禁,仅要求前一日已上传」 \n- 昨日缺失(H5):**列表顶栏可点橙条** + 底栏 FAB 旁提示 + Tab 红点;台账页同样展示橙条并可选中昨日;文案固定为「手工台账有缺失,作为对账重要依据,请先上传手工台账」 \n- 日历:自首笔加氢日起绿/橙标记;首笔之前无橙点(无需补传);本月未传天数摘要 \n- Web 右侧面板标题「手工台账」:空态提示;有图可点进全屏翻页预览并角标「已归档/待提交」;预览区内上传;底部确认前二次弹窗;提交后立即展示已归档缩略图;左右卡片等高\n\n## 代码路径\n\n- Bridge:`src/common/h2VehicleLedgerBridge.js`(`hasManualLedgerForDate` / `isManualLedgerGateBlocked` / `upsertManualLedger` / `listManualLedgersByStation`) \n- H5:`oneos-h5-h2-order` · `ManualLedgerPanel` + `utils/manual-ledger.ts`(`isManualLedgerGateBlocked` / `buildManualMonthCells` / `getFirstHydrogenDateKey`) \n- Web:`oneos-web-h2-station/pages/02-加氢记录.jsx`(`hrIsManualLedgerGateBlocked` / 跳转选中昨日) \n\n## 验收\n\n1. 昨日未上传时,H5/Web 列表顶栏提示固定文案;点「新增」被拦截并直接进入手工台账(选中昨日) \n2. 补传昨日台账并确认后,可新增加氢记录 \n3. 日历自首笔加氢日起区分已传/未传;首笔之前不标未传、不计入补传;可点选范围内未传日补传 \n4. 仅补传更早历史日、昨日仍未传时,新增仍被拦截 \n5. 确认上传后的图显示已归档且不可删除;仅可追加补传 \n6. H5 与 Web 共用同一 Store,一端上传另一端可见(同会话) \n7. Web 导出 CSV 列含「车辆来源」(羚牛车辆/非羚牛车辆)、「数据来源」(站端上传/羚牛上传),与列表标签口径一致 \n",
|
||||
"markdownPath": ".spec/feature-manual-ledger.md"
|
||||
},
|
||||
{
|
||||
"type": "markdown",
|
||||
"id": "h5-doc-logic-reconcile",
|
||||
"title": "核对与对账联动",
|
||||
"markdown": "# 加氢订单(H5)· 对账 / 核对联动规则\n\n| 项 | 说明 |\n|---|---|\n| 代码路径 | Store:`src/common/h2VehicleLedgerBridge.js`;对账单:`oneos-web-h2-station-site`;PC 加氢记录:`oneos-web-h2-station`;车辆氢费明细:`vehicle-h2-fee-ledger` |\n| 规格全文 | [../vehicle-h2-fee-ledger/.spec/verify-reconcile.md](../vehicle-h2-fee-ledger/.spec/verify-reconcile.md) |\n| 车辆标签 | [fleet-plate-tag.md](./fleet-plate-tag.md) |\n| 数据源 | 原型本地种子 + 内存共享 Store(**未接真实 API**) |\n\n---\n\n## 1. 两套状态(判定优先级)\n\n| 优先级 | 状态 | 字段 | 谁触发 | 用户可见 |\n|---|---|---|---|---|\n| 1 | **核对** | `verifyStatus` / `verifiedAt` | 车辆氢费明细 · **完成核对** | 未核对 / 已核对 + 核对时间 |\n| 2 | **对账** | `reconcileStatus` / `reconcileDate` | 站点信息 · **对账单提交** | 未对账 / 已对账 + 对账时间 |\n\n> `reconciledAt` 仅作完成核对时的兼容写入,**不得**再用于判定「已对账」。\n\n---\n\n## 2. 数据流\n\n```text\n站端 H5 / Web 新增加氢记录\n → Bridge.upsertRow(verify=unverified,reconcile=pending;成本单价/量/总额)\n → 车辆氢费明细可见\n\n能源部 · 完成核对\n → verifyStatus=verified + verifiedAt\n → 站端同步「已核对」;羚牛车辆禁改删\n\n站点信息 · 对账单提交(仅已核对 ∧ 未对账)\n → reconcileStatus=reconciled + reconcileDate + statementRecordId\n → 站端 / 台账同步「已对账」;羚牛车辆禁改删\n```\n\n---\n\n## 3. 用户可见结果(H5)\n\n| 结果 | 行为 |\n|---|---|\n| 非羚牛车辆 | 核对/对账展示「—」;**不锁定**改删 |\n| 羚牛 · 未核对 | 可编辑、删除 |\n| 羚牛 · 已核对未对账 | 只读;提示能源部已核对 |\n| 羚牛 · 已对账 | 只读;提示已纳入对账单 |\n\n---\n\n## 4. 边界\n\n- 站端上报默认未核对、未对账\n- **站端改删锁定仅适用于羚牛车辆**(与 Web / 台账车辆标签口径一致)\n- 对账时间只认 `reconcileDate`(对账单回写)\n- 跨页联动依赖同浏览器会话共享 Store\n",
|
||||
"markdownPath": ".spec/reconcile-linkage.md"
|
||||
},
|
||||
{
|
||||
"type": "markdown",
|
||||
"id": "h5-doc-logic-fleet",
|
||||
"title": "羚牛车辆标签",
|
||||
"markdown": "# 车牌「羚牛车辆 / 非羚牛车辆」标签\n\n原型本地判定,未接真实车辆管理 API。\n\n全文(含核对/对账字段空展示规则,跨 Web / 台账 / H5): \n[../../oneos-web-h2-station/.spec/fleet-vehicle-verify.md](../../oneos-web-h2-station/.spec/fleet-vehicle-verify.md)\n\n## 判定顺序\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 车牌为空 | 不展示标签 |\n| 2 | 规范化后车牌 ∈ 本地系统车辆集合 | **羚牛车辆** |\n| 3 | 有车牌但不在集合中 | **非羚牛车辆** |\n\n## 前置条件\n\n- 识别(OCR)或键盘输入得到车牌后才展示。\n- 比较前去掉尾缀 `F`,统一大写。\n\n## 数据源\n\n- 优先 `H2VehicleLedgerBridge.isLingniuVehicle`;本地回退见 `utils/fleet-plates.ts`(与 Bridge `H2_FLEET_PLATE_KEYS` 同步)。\n\n## 用户可见结果\n\n- 绿色标签「羚牛车辆」或橙色标签「非羚牛车辆」。\n- **非羚牛车辆**:列表/详情中核对状态、核对时间、对账状态、对账时间显示「—」,不展示核对待办语义。\n- 不拦截提交(仅提示归属)。\n\n## 代码路径\n\n- `getVehicleTag` / `isLingniuVehicle` → `CreateWizard`、`OrderCard`、`OrderDetail`\n",
|
||||
"markdownPath": ".spec/fleet-plate-tag.md"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "folder",
|
||||
"id": "h5-doc-flows",
|
||||
"title": "流程图",
|
||||
"defaultExpanded": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "markdown",
|
||||
"id": "h5-doc-flows-md",
|
||||
"title": "操作流程图",
|
||||
"markdown": "# 流程图\n\n> 标注面板支持 Mermaid 渲染。下列流程与当前原型行为一致(本地 Store,未接真实 API)。\n\n---\n\n## 1. 主路径:从打开到上报\n\n```mermaid\nflowchart TD\n A[打开加氢订单 H5] --> B[查看本站列表与 KPI]\n B --> C{前一日手工台账已上传?}\n C -->|否| D[提示:手工台账有缺失…]\n D --> E[进入手工台账 · 选中昨日]\n E --> F[拍照/相册上传并确认]\n F --> C\n C -->|是| G[点新增]\n G --> H[填写时间/车牌/面板/量价]\n H --> I{同站同车牌同时间已存在?}\n I -->|是| J[拦截提示]\n J --> H\n I -->|否| K[保存写入 Bridge]\n K --> B\n```\n\n---\n\n## 2. 手工台账补传\n\n```mermaid\nflowchart LR\n A[手工台账 Tab] --> B[月历:绿已传 / 橙未传]\n B --> C[点选 ≤今日 且 ≥首笔日]\n C --> D[上传图片]\n D --> E[二次确认]\n E --> F[归档不可删 · 可追加]\n```\n\n---\n\n## 3. 编辑 / 删除与锁定\n\n```mermaid\nflowchart TD\n A[打开详情] --> L{羚牛车辆?}\n L -->|否| D[可编辑 / 可删除]\n L -->|是| B{已核对或已对账?}\n B -->|是| C[只读 · 禁改删]\n B -->|否| D\n D --> E[写回 Bridge]\n```\n\n---\n\n## 4. 与 Web / 台账联动(概念)\n\n```mermaid\nsequenceDiagram\n participant H5 as 加氢订单 H5\n participant BR as Bridge Store\n participant WEB as 加氢记录 Web\n participant LED as 车辆氢费明细\n participant SITE as 站点对账单\n H5->>BR: 新增/改删流水 · 上传台账\n WEB->>BR: 同 Store 读写 · 导出\n LED->>BR: 完成核对 → 已核对\n SITE->>BR: 提交对账单 → 已对账\n```\n",
|
||||
"markdownPath": ".spec/flows.md"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "folder",
|
||||
"id": "h5-doc-features",
|
||||
@@ -403,28 +484,28 @@
|
||||
"type": "markdown",
|
||||
"id": "h5-doc-manual",
|
||||
"title": "手工台账上传",
|
||||
"markdown": "# 功能:手工台账上传\n\n## 目标\n\n站端每日上传纸质**手工台账照片**,作为系统电子加氢记录的核对依据。多日未操作时,可通过日历查看缺口并补传过往日期。\n\n## 产品口径\n\n| 项 | 规则 |\n|---|---|\n| 入口 | H5:底部 Tab「手工台账」;Web:顶栏「手工台账」 |\n| 上传形态 | 仅图片(拍照/相册),可多张 |\n| 维度 | **加氢站 + 自然日(本地日历日)** 一份 |\n| 日历 | 月历自本站**首笔加氢记录日**起算:绿=已传、橙=未传;首笔之前无需补传;禁选未来日 |\n| 补传 | 允许为过往未传日期补传;已传日期仅可**追加**新图 |\n| 归档 | **确认上传后的图片进入已归档状态,不可删除**;未确认的草稿图仍可删除 |\n| 拦截 | **未上传今日手工台账 → 禁止新增加氢记录**(编辑已有记录不拦;补传历史不替代今日校验) |\n| 数据 | 共享 `H2VehicleLedgerBridge` 内存 Store(未接真实 API) |\n\n## 判定顺序(新增拦截)\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 操作=编辑已有记录 | 不校验手工台账 |\n| 2 | 操作=新增,且目标站今日已有 ≥1 张台账图 | 允许进入新增 |\n| 3 | 操作=新增,今日未上传 | 提示并跳转手工台账 Tab |\n\n## 日历与补传\n\n| 规则 | 说明 |\n|------|------|\n| 数据源 | `listManualLedgersByStation`;当日有 ≥1 张图视为已上传 |\n| 业务起点 | 取本站 Bridge 加氢记录中最早 `hydrogenTime` 自然日;无记录则历史日均不要求补传 |\n| 未传计数 | 本月内 `max(月初, 首笔日)`~min(月末, 今日) 中未上传的天数 |\n| 点选 | 仅 `dateKey ≤ 今日`;选中后右侧/下方展示该日草稿图与上传按钮 |\n| 归档不可删 | Store 已有图强制保留;`upsertManualLedger` 仅追加新图,忽略删除已有图的请求 |\n| 与拦截关系 | 仅 **今日** 影响「禁止新增」;历史补传解决核对缺口,不放宽今日门禁 |\n\n## 用户可见\n\n- 今日未上传:橙/黄提示条 + Tab 红点 \n- 日历:自首笔加氢日起绿/橙标记;首笔之前无橙点(无需补传);本月未传天数摘要 \n- 选中日:上传区标题为该日期;已归档图显示「已归档」无删除;仅未确认草稿可删;已有台账时主按钮为「确认补传」\n\n## 代码路径\n\n- Bridge:`src/common/h2VehicleLedgerBridge.js`(`hasManualLedgerForDate` / `upsertManualLedger` / `listManualLedgersByStation`) \n- H5:`oneos-h5-h2-order` · `ManualLedgerPanel` + `utils/manual-ledger.ts`(`buildManualMonthCells` / `getFirstHydrogenDateKey`) \n- Web:`oneos-web-h2-station/pages/02-加氢记录.jsx`(手工台账 Tab 月历) \n\n## 验收\n\n1. 今日未上传时,H5/Web 点「新增」均被拦截并引导上传 \n2. 上传至少一张图并确认后,可新增电子记录 \n3. 日历自首笔加氢日起区分已传/未传;首笔之前不标未传、不计入补传;可点选范围内未传日补传 \n4. 补传历史日后,该日日历变为已传;今日仍未传时新增仍被拦截 \n5. 确认上传后的图显示已归档且不可删除;仅可追加补传 \n6. H5 与 Web 共用同一 Store,一端上传另一端可见(同会话) \n",
|
||||
"markdown": "# 功能:手工台账上传\n\n## 目标\n\n站端每日上传纸质**手工台账照片**,作为系统电子加氢记录的核对依据。多日未操作时,可通过日历查看缺口并补传过往日期。\n\n## 产品口径\n\n| 项 | 规则 |\n|---|---|\n| 入口 | H5:底部 Tab「手工台账」;Web:顶栏「手工台账」 |\n| 站点范围 | **H5 固定本站**(无切站);Web 可按角色多站/超管切换(标注状态演示,未接真实登录) |\n| 上传形态 | 仅图片(拍照/相册),可多张 |\n| 维度 | **加氢站 + 自然日(本地日历日)** 一份 |\n| 日历 | 月历自本站**首笔加氢记录日**起算:绿=已传、橙=未传;首笔之前无需补传;禁选未来日 |\n| 补传 | 允许为过往未传日期补传;已传日期仅可**追加**新图 |\n| 归档 | **确认上传后的图片进入已归档状态,不可删除**;未确认的草稿图仍可删除 |\n| 二次确认 | H5 / Web 确认上传前弹窗:「提交后将无法修改,是否确认手工台账照片无误」;确认后才写入 |\n| 拦截 | **前一日手工台账未上传 → 今日禁止新增加氢记录**(编辑已有记录不拦;补传更早历史日不替代昨日校验) |\n| 数据 | 共享 `H2VehicleLedgerBridge` 内存 Store(未接真实 API);站名/编码解析见 Bridge.`h2BridgeResolveStationId` |\n\n## 判定顺序(新增拦截)\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 操作=编辑已有记录 | 不校验手工台账 |\n| 2 | 操作=新增,且昨日早于本站首笔加氢日(或不要求昨日台账) | 允许进入新增 |\n| 3 | 操作=新增,且目标站昨日已有 ≥1 张台账图 | 允许进入新增 |\n| 4 | 操作=新增,昨日未上传 | 顶部/Toast 提示「手工台账有缺失,作为对账重要依据,请先上传手工台账」,并**直接跳转**手工台账页且选中昨日 |\n\n## 日历与补传\n\n| 规则 | 说明 |\n|------|------|\n| 数据源 | `listManualLedgersByStation`;当日有 ≥1 张图视为已上传 |\n| 业务起点 | 取本站 Bridge 加氢记录中最早 `hydrogenTime` 自然日;无记录则历史日均不要求补传 |\n| 未传计数 | 本月内 `max(月初, 首笔日)`~min(月末, 今日) 中未上传的天数 |\n| 点选 | 仅 `dateKey ≤ 今日`;选中后右侧/下方展示该日草稿图与上传按钮 |\n| 归档不可删 | Store 已有图强制保留;`upsertManualLedger` 仅追加新图,忽略删除已有图的请求 |\n| 与拦截关系 | 仅 **昨日** 影响「禁止新增」;历史补传解决核对缺口;今日台账仍可按日常上传,但不作为新增门禁 |\n\n## 用户可见\n\n- 今日状态双反馈(H5 台账页顶部卡片 / Web 右侧「当日台账」):未上传「今日尚未上传…」异常态;已上传「今日已上传…」正常态。**日常上传反馈 ≠ 新增门禁**;H5 文案须写明「今日不作为新增门禁,仅要求前一日已上传」 \n- 昨日缺失(H5):**列表顶栏可点橙条** + 底栏 FAB 旁提示 + Tab 红点;台账页同样展示橙条并可选中昨日;文案固定为「手工台账有缺失,作为对账重要依据,请先上传手工台账」 \n- 日历:自首笔加氢日起绿/橙标记;首笔之前无橙点(无需补传);本月未传天数摘要 \n- Web 右侧面板标题「手工台账」:空态提示;有图可点进全屏翻页预览并角标「已归档/待提交」;预览区内上传;底部确认前二次弹窗;提交后立即展示已归档缩略图;左右卡片等高\n\n## 代码路径\n\n- Bridge:`src/common/h2VehicleLedgerBridge.js`(`hasManualLedgerForDate` / `isManualLedgerGateBlocked` / `upsertManualLedger` / `listManualLedgersByStation`) \n- H5:`oneos-h5-h2-order` · `ManualLedgerPanel` + `utils/manual-ledger.ts`(`isManualLedgerGateBlocked` / `buildManualMonthCells` / `getFirstHydrogenDateKey`) \n- Web:`oneos-web-h2-station/pages/02-加氢记录.jsx`(`hrIsManualLedgerGateBlocked` / 跳转选中昨日) \n\n## 验收\n\n1. 昨日未上传时,H5/Web 列表顶栏提示固定文案;点「新增」被拦截并直接进入手工台账(选中昨日) \n2. 补传昨日台账并确认后,可新增加氢记录 \n3. 日历自首笔加氢日起区分已传/未传;首笔之前不标未传、不计入补传;可点选范围内未传日补传 \n4. 仅补传更早历史日、昨日仍未传时,新增仍被拦截 \n5. 确认上传后的图显示已归档且不可删除;仅可追加补传 \n6. H5 与 Web 共用同一 Store,一端上传另一端可见(同会话) \n7. Web 导出 CSV 列含「车辆来源」(羚牛车辆/非羚牛车辆)、「数据来源」(站端上传/羚牛上传),与列表标签口径一致 \n",
|
||||
"markdownPath": ".spec/feature-manual-ledger.md"
|
||||
},
|
||||
{
|
||||
"type": "markdown",
|
||||
"id": "h5-doc-detail",
|
||||
"title": "详情编辑删除",
|
||||
"markdown": "# 功能:详情、编辑与删除\n\n## 目标\n\n查看单笔加氢记录完整字段;在「未核对且未对账」时可编辑或删除。\n\n## 详情展示\n\n- 顶部:车牌、加氢时间、核对标签、对账标签 \n- 指标条:加氢总额、加氢量 \n- **识别原图**:车牌照片、加氢机面板照片(均为拍摄时叠加时间/地点水印后的原图;种子数据无实拍时用演示水印图) \n- 明细行:站点、车牌、里程、单价、核对/对账状态与时间 \n\n## 锁定规则\n\n| 条件 | 结果 |\n|------|------|\n| 已对账 | 不可编辑/删除;提示已纳入站点对账单 |\n| 已核对(未对账) | 不可编辑/删除;提示能源部已核对 |\n| 未核对且未对账 | 可编辑、可删除 |\n\n判定函数:`isOrderLocked`(核对或对账任一成立即锁)。\n\n## 编辑\n\n进入与「新增」同一套表单,预填当前值(含已存水印照片);提交走更新逻辑(仍校验重复键)。\n\n## 删除\n\n二次确认后从 Bridge 移除该行。\n\n## 验收\n\n1. 未核对未对账:底部有删除/编辑 \n2. 已核对或已对账:无操作栏,有锁定说明 \n3. 详情含对账信息;列表卡片不含对账 \n4. 详情展示车牌与面板带水印原图(新上报提交后为实拍;种子行为演示图) \n",
|
||||
"markdown": "# 功能:详情、编辑与删除\n\n## 目标\n\n查看单笔加氢记录完整字段;在「未核对且未对账」时可编辑或删除(**锁定仅适用于羚牛车辆**)。\n\n## 详情展示\n\n- 顶部:车牌、加氢时间、核对标签、对账标签 \n- 指标条:加氢总额、加氢量 \n- **识别原图**:车牌照片、加氢机面板照片(均为拍摄时叠加时间/地点水印后的原图;种子数据无实拍时用演示水印图) \n- 明细行:站点、车牌、里程、单价、核对/对账状态与时间 \n\n> 非羚牛车辆:核对/对账字段展示「—」,不展示核对待办语义(见 [fleet-plate-tag.md](./fleet-plate-tag.md))。\n\n## 锁定规则\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | **非羚牛车辆** | **不锁定**(不以核对/对账状态禁改删) |\n| 2 | 羚牛 ∧ 已对账 | 不可编辑/删除;提示已纳入站点对账单 |\n| 3 | 羚牛 ∧ 已核对(未对账) | 不可编辑/删除;提示能源部已核对 |\n| 4 | 羚牛 ∧ 未核对且未对账 | 可编辑、可删除 |\n\n判定函数:`isOrderLocked`(先判羚牛;再核「核对或对账任一成立即锁」)。\n\n## 编辑\n\n进入与「新增」同一套表单,预填当前值(含已存水印照片);提交走更新逻辑(仍校验重复键)。\n\n## 删除\n\n二次确认文案:「确认删除车牌 … 的未锁定记录?」;确认后从 Bridge 移除该行。\n\n## 验收\n\n1. 羚牛 ∧ 未核对未对账:底部有删除/编辑 \n2. 羚牛 ∧ 已核对或已对账:无操作栏,有锁定说明 \n3. 非羚牛:可改删(不因核对/对账锁定);核对/对账展示「—」 \n4. 详情含对账信息;列表卡片不含对账 \n5. 详情展示车牌与面板带水印原图(新上报提交后为实拍;种子行为演示图) \n",
|
||||
"markdownPath": ".spec/feature-detail.md"
|
||||
},
|
||||
{
|
||||
"type": "markdown",
|
||||
"id": "h5-doc-fleet",
|
||||
"title": "羚牛车辆标签",
|
||||
"markdown": "# 车牌「羚牛车辆 / 非羚牛车辆」标签\n\n原型本地判定,未接真实车辆管理 API。\n\n## 判定顺序\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 车牌为空 | 不展示标签 |\n| 2 | 规范化后车牌 ∈ 本地系统车辆集合 | **羚牛车辆** |\n| 3 | 有车牌但不在集合中 | **非羚牛车辆** |\n\n## 前置条件\n\n- 识别(OCR)或键盘输入得到车牌后才展示。\n- 比较前去掉尾缀 `F`,统一大写。\n\n## 数据源\n\n- `utils/fleet-plates.ts` 内本地种子车牌集合(含加氢 Bridge 演示车牌与部分车辆管理样例)。\n- 正式环境应改为查询车辆管理 / 系统车辆列表接口。\n\n## 用户可见结果\n\n- 绿色标签「羚牛车辆」或橙色标签「非羚牛车辆」。\n- 不拦截提交(仅提示归属)。\n\n## 代码路径\n\n- `getVehicleTag` / `isLingniuVehicle` → `CreateWizard` 车牌行右侧标签。\n",
|
||||
"markdown": "# 车牌「羚牛车辆 / 非羚牛车辆」标签\n\n原型本地判定,未接真实车辆管理 API。\n\n全文(含核对/对账字段空展示规则,跨 Web / 台账 / H5): \n[../../oneos-web-h2-station/.spec/fleet-vehicle-verify.md](../../oneos-web-h2-station/.spec/fleet-vehicle-verify.md)\n\n## 判定顺序\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 车牌为空 | 不展示标签 |\n| 2 | 规范化后车牌 ∈ 本地系统车辆集合 | **羚牛车辆** |\n| 3 | 有车牌但不在集合中 | **非羚牛车辆** |\n\n## 前置条件\n\n- 识别(OCR)或键盘输入得到车牌后才展示。\n- 比较前去掉尾缀 `F`,统一大写。\n\n## 数据源\n\n- 优先 `H2VehicleLedgerBridge.isLingniuVehicle`;本地回退见 `utils/fleet-plates.ts`(与 Bridge `H2_FLEET_PLATE_KEYS` 同步)。\n\n## 用户可见结果\n\n- 绿色标签「羚牛车辆」或橙色标签「非羚牛车辆」。\n- **非羚牛车辆**:列表/详情中核对状态、核对时间、对账状态、对账时间显示「—」,不展示核对待办语义。\n- 不拦截提交(仅提示归属)。\n\n## 代码路径\n\n- `getVehicleTag` / `isLingniuVehicle` → `CreateWizard`、`OrderCard`、`OrderDetail`\n",
|
||||
"markdownPath": ".spec/fleet-plate-tag.md"
|
||||
},
|
||||
{
|
||||
"type": "markdown",
|
||||
"id": "h5-doc-reconcile",
|
||||
"title": "核对与对账联动",
|
||||
"markdown": "# 加氢订单(H5)· 对账 / 核对联动规则\n\n| 项 | 说明 |\n|---|---|\n| 代码路径 | Store:`src/common/h2VehicleLedgerBridge.js`;对账单:`oneos-web-h2-station-site`;PC 加氢记录:`oneos-web-h2-station`;车辆氢费明细:`vehicle-h2-fee-ledger` |\n| 规格全文 | [../vehicle-h2-fee-ledger/.spec/verify-reconcile.md](../vehicle-h2-fee-ledger/.spec/verify-reconcile.md) |\n| 数据源 | 原型本地种子 + 内存共享 Store(**未接真实 API**) |\n\n---\n\n## 1. 两套状态(判定优先级)\n\n| 优先级 | 状态 | 字段 | 谁触发 | 用户可见 |\n|---|---|---|---|---|\n| 1 | **核对** | `verifyStatus` / `verifiedAt` | 车辆氢费明细 · **完成核对** | 未核对 / 已核对 + 核对时间 |\n| 2 | **对账** | `reconcileStatus` / `reconcileDate` | 站点信息 · **对账单提交** | 未对账 / 已对账 + 对账时间 |\n\n> `reconciledAt` 仅作完成核对时的兼容写入,**不得**再用于判定「已对账」。\n\n## 2. 数据流\n\n```text\n站端 H5 / Web 新增加氢记录\n → Bridge.upsertRow(verify=unverified,reconcile=pending;成本单价/量/总额)\n → 车辆氢费明细可见\n\n能源部 · 完成核对\n → verifyStatus=verified + verifiedAt\n → 站端同步「已核对」,禁改删\n\n站点信息 · 对账单提交(仅已核对 ∧ 未对账)\n → reconcileStatus=reconciled + reconcileDate + statementRecordId\n → 站端 / 台账同步「已对账」\n```\n\n## 3. 用户可见结果(H5)\n\n| 结果 | 行为 |\n|---|---|\n| 未核对 | 可编辑、删除 |\n| 已核对未对账 | 只读;提示能源部已核对 |\n| 已对账 | 只读;提示已纳入对账单 |\n\n## 4. 边界\n\n- 站端上报默认未核对、未对账\n- 对账时间只认 `reconcileDate`(对账单回写)\n- 跨页联动依赖同浏览器会话共享 Store\n",
|
||||
"markdown": "# 加氢订单(H5)· 对账 / 核对联动规则\n\n| 项 | 说明 |\n|---|---|\n| 代码路径 | Store:`src/common/h2VehicleLedgerBridge.js`;对账单:`oneos-web-h2-station-site`;PC 加氢记录:`oneos-web-h2-station`;车辆氢费明细:`vehicle-h2-fee-ledger` |\n| 规格全文 | [../vehicle-h2-fee-ledger/.spec/verify-reconcile.md](../vehicle-h2-fee-ledger/.spec/verify-reconcile.md) |\n| 车辆标签 | [fleet-plate-tag.md](./fleet-plate-tag.md) |\n| 数据源 | 原型本地种子 + 内存共享 Store(**未接真实 API**) |\n\n---\n\n## 1. 两套状态(判定优先级)\n\n| 优先级 | 状态 | 字段 | 谁触发 | 用户可见 |\n|---|---|---|---|---|\n| 1 | **核对** | `verifyStatus` / `verifiedAt` | 车辆氢费明细 · **完成核对** | 未核对 / 已核对 + 核对时间 |\n| 2 | **对账** | `reconcileStatus` / `reconcileDate` | 站点信息 · **对账单提交** | 未对账 / 已对账 + 对账时间 |\n\n> `reconciledAt` 仅作完成核对时的兼容写入,**不得**再用于判定「已对账」。\n\n---\n\n## 2. 数据流\n\n```text\n站端 H5 / Web 新增加氢记录\n → Bridge.upsertRow(verify=unverified,reconcile=pending;成本单价/量/总额)\n → 车辆氢费明细可见\n\n能源部 · 完成核对\n → verifyStatus=verified + verifiedAt\n → 站端同步「已核对」;羚牛车辆禁改删\n\n站点信息 · 对账单提交(仅已核对 ∧ 未对账)\n → reconcileStatus=reconciled + reconcileDate + statementRecordId\n → 站端 / 台账同步「已对账」;羚牛车辆禁改删\n```\n\n---\n\n## 3. 用户可见结果(H5)\n\n| 结果 | 行为 |\n|---|---|\n| 非羚牛车辆 | 核对/对账展示「—」;**不锁定**改删 |\n| 羚牛 · 未核对 | 可编辑、删除 |\n| 羚牛 · 已核对未对账 | 只读;提示能源部已核对 |\n| 羚牛 · 已对账 | 只读;提示已纳入对账单 |\n\n---\n\n## 4. 边界\n\n- 站端上报默认未核对、未对账\n- **站端改删锁定仅适用于羚牛车辆**(与 Web / 台账车辆标签口径一致)\n- 对账时间只认 `reconcileDate`(对账单回写)\n- 跨页联动依赖同浏览器会话共享 Store\n",
|
||||
"markdownPath": ".spec/reconcile-linkage.md"
|
||||
}
|
||||
]
|
||||
@@ -433,7 +514,7 @@
|
||||
"type": "folder",
|
||||
"id": "h5-doc-pages",
|
||||
"title": "按页面",
|
||||
"defaultExpanded": true,
|
||||
"defaultExpanded": false,
|
||||
"children": [
|
||||
{
|
||||
"type": "folder",
|
||||
@@ -500,7 +581,7 @@
|
||||
"type": "markdown",
|
||||
"id": "h5-page-detail-md",
|
||||
"title": "详情页要点",
|
||||
"markdown": "# 功能:详情、编辑与删除\n\n## 目标\n\n查看单笔加氢记录完整字段;在「未核对且未对账」时可编辑或删除。\n\n## 详情展示\n\n- 顶部:车牌、加氢时间、核对标签、对账标签 \n- 指标条:加氢总额、加氢量 \n- **识别原图**:车牌照片、加氢机面板照片(均为拍摄时叠加时间/地点水印后的原图;种子数据无实拍时用演示水印图) \n- 明细行:站点、车牌、里程、单价、核对/对账状态与时间 \n\n## 锁定规则\n\n| 条件 | 结果 |\n|------|------|\n| 已对账 | 不可编辑/删除;提示已纳入站点对账单 |\n| 已核对(未对账) | 不可编辑/删除;提示能源部已核对 |\n| 未核对且未对账 | 可编辑、可删除 |\n\n判定函数:`isOrderLocked`(核对或对账任一成立即锁)。\n\n## 编辑\n\n进入与「新增」同一套表单,预填当前值(含已存水印照片);提交走更新逻辑(仍校验重复键)。\n\n## 删除\n\n二次确认后从 Bridge 移除该行。\n\n## 验收\n\n1. 未核对未对账:底部有删除/编辑 \n2. 已核对或已对账:无操作栏,有锁定说明 \n3. 详情含对账信息;列表卡片不含对账 \n4. 详情展示车牌与面板带水印原图(新上报提交后为实拍;种子行为演示图) \n"
|
||||
"markdown": "# 功能:详情、编辑与删除\n\n## 目标\n\n查看单笔加氢记录完整字段;在「未核对且未对账」时可编辑或删除(**锁定仅适用于羚牛车辆**)。\n\n## 详情展示\n\n- 顶部:车牌、加氢时间、核对标签、对账标签 \n- 指标条:加氢总额、加氢量 \n- **识别原图**:车牌照片、加氢机面板照片(均为拍摄时叠加时间/地点水印后的原图;种子数据无实拍时用演示水印图) \n- 明细行:站点、车牌、里程、单价、核对/对账状态与时间 \n\n> 非羚牛车辆:核对/对账字段展示「—」,不展示核对待办语义(见 [fleet-plate-tag.md](./fleet-plate-tag.md))。\n\n## 锁定规则\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | **非羚牛车辆** | **不锁定**(不以核对/对账状态禁改删) |\n| 2 | 羚牛 ∧ 已对账 | 不可编辑/删除;提示已纳入站点对账单 |\n| 3 | 羚牛 ∧ 已核对(未对账) | 不可编辑/删除;提示能源部已核对 |\n| 4 | 羚牛 ∧ 未核对且未对账 | 可编辑、可删除 |\n\n判定函数:`isOrderLocked`(先判羚牛;再核「核对或对账任一成立即锁」)。\n\n## 编辑\n\n进入与「新增」同一套表单,预填当前值(含已存水印照片);提交走更新逻辑(仍校验重复键)。\n\n## 删除\n\n二次确认文案:「确认删除车牌 … 的未锁定记录?」;确认后从 Bridge 移除该行。\n\n## 验收\n\n1. 羚牛 ∧ 未核对未对账:底部有删除/编辑 \n2. 羚牛 ∧ 已核对或已对账:无操作栏,有锁定说明 \n3. 非羚牛:可改删(不因核对/对账锁定);核对/对账展示「—」 \n4. 详情含对账信息;列表卡片不含对账 \n5. 详情展示车牌与面板带水印原图(新上报提交后为实拍;种子行为演示图) \n"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -7,9 +7,11 @@ import {
|
||||
getFirstHydrogenDateKey,
|
||||
getManualLedger,
|
||||
isArchivedManualImage,
|
||||
isManualLedgerGateBlocked,
|
||||
listManualLedgersByStation,
|
||||
todayDateKey,
|
||||
upsertManualLedger,
|
||||
yesterdayDateKey,
|
||||
type ManualLedgerImage,
|
||||
} from '../utils/manual-ledger';
|
||||
import { IconCamera, IconCheck } from './Icons';
|
||||
@@ -19,21 +21,31 @@ type Props = {
|
||||
tick: number;
|
||||
onUploaded: () => void;
|
||||
showToast: (msg: string) => void;
|
||||
/** 打开面板时优先选中该日(如昨日缺口) */
|
||||
focusDate?: string;
|
||||
};
|
||||
|
||||
const WEEK_LABELS = ['日', '一', '二', '三', '四', '五', '六'];
|
||||
|
||||
export function ManualLedgerPanel({ station, tick, onUploaded, showToast }: Props) {
|
||||
export function ManualLedgerPanel({ station, tick, onUploaded, showToast, focusDate }: Props) {
|
||||
const today = todayDateKey();
|
||||
const fileRef = useRef<HTMLInputElement>(null);
|
||||
const [draftImages, setDraftImages] = useState<ManualLedgerImage[]>([]);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [selectedDate, setSelectedDate] = useState(today);
|
||||
const initialDate = focusDate && focusDate <= today ? focusDate : today;
|
||||
const [selectedDate, setSelectedDate] = useState(initialDate);
|
||||
const [cursorYm, setCursorYm] = useState(() => {
|
||||
const [y, m] = today.split('-').map(Number);
|
||||
const [y, m] = initialDate.split('-').map(Number);
|
||||
return { y, m };
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!focusDate || focusDate > today) return;
|
||||
setSelectedDate(focusDate);
|
||||
const [y, m] = focusDate.split('-').map(Number);
|
||||
setCursorYm({ y, m });
|
||||
}, [focusDate, today]);
|
||||
|
||||
const stationKey = station.stationId || station.stationName;
|
||||
|
||||
const allEntries = useMemo(
|
||||
@@ -127,7 +139,7 @@ export function ManualLedgerPanel({ station, tick, onUploaded, showToast }: Prop
|
||||
showToast('请至少上传一张手工台账照片');
|
||||
return;
|
||||
}
|
||||
const ok = window.confirm('确认上传后该记录将归档无法修改,是否确认无误');
|
||||
const ok = window.confirm('提交后将无法修改,是否确认手工台账照片无误');
|
||||
if (!ok) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
@@ -174,8 +186,24 @@ export function ManualLedgerPanel({ station, tick, onUploaded, showToast }: Prop
|
||||
setSelectedDate(dateKey);
|
||||
}
|
||||
|
||||
const yesterday = yesterdayDateKey();
|
||||
const gateBlocked = isManualLedgerGateBlocked(stationKey);
|
||||
|
||||
return (
|
||||
<div className="h5-manual" data-annotation-id="h5-manual">
|
||||
{gateBlocked ? (
|
||||
<button
|
||||
type="button"
|
||||
className="h5-banner-warn"
|
||||
onClick={() => {
|
||||
setSelectedDate(yesterday);
|
||||
const [y, m] = yesterday.split('-').map(Number);
|
||||
setCursorYm({ y, m });
|
||||
}}
|
||||
>
|
||||
手工台账有缺失,作为对账重要依据,请先上传手工台账(选中昨日 {yesterday})
|
||||
</button>
|
||||
) : null}
|
||||
<div
|
||||
className={`h5-manual-status${
|
||||
uploadedSet.has(today) ? ' h5-manual-status--ok' : ''
|
||||
@@ -186,7 +214,9 @@ export function ManualLedgerPanel({ station, tick, onUploaded, showToast }: Prop
|
||||
<strong>{uploadedSet.has(today) ? '今日已上传' : '今日尚未上传'}</strong>
|
||||
<p>
|
||||
{today} · {station.stationName}
|
||||
{!uploadedSet.has(today) ? '。上传今日台账后才可新增加氢记录。' : ''}
|
||||
{!uploadedSet.has(today)
|
||||
? '。今日台账可按日常上传,不作为新增门禁;新增加氢记录仅要求前一日已上传。'
|
||||
: ''}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import type { H5OrderRecord } from '../types';
|
||||
import { formatDateTimeMinute, formatKg, formatMoney, formatPlateDisplay } from '../utils/format';
|
||||
import { getVehicleTag } from '../utils/fleet-plates';
|
||||
import { IconCheck, IconClock } from './Icons';
|
||||
|
||||
type Props = {
|
||||
@@ -10,22 +11,34 @@ type Props = {
|
||||
};
|
||||
|
||||
export function OrderCard({ order, onClick, index = 0 }: Props) {
|
||||
const verified = order.verifyStatus === 'verified';
|
||||
const vehicleTag = getVehicleTag(order.plateNo);
|
||||
const isLingniu = vehicleTag === 'lingniu';
|
||||
const verified = isLingniu && order.verifyStatus === 'verified';
|
||||
const verifyLabel = !isLingniu ? null : verified ? '已核对' : '未核对';
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={`h5-card${verified ? ' h5-card--done' : ' h5-card--pending'}`}
|
||||
className={`h5-card${isLingniu ? (verified ? ' h5-card--done' : ' h5-card--pending') : ''}`}
|
||||
data-annotation-id="h5-card"
|
||||
onClick={onClick}
|
||||
style={{ animationDelay: `${Math.min(index, 8) * 40}ms` }}
|
||||
aria-label={`${formatPlateDisplay(order.plateNo)},${verified ? '已核对' : '未核对'}`}
|
||||
aria-label={`${formatPlateDisplay(order.plateNo)}${verifyLabel ? `,${verifyLabel}` : ''}`}
|
||||
>
|
||||
<div className="h5-card-top">
|
||||
<div className="h5-plate-wrap">
|
||||
<div className="h5-plate">{formatPlateDisplay(order.plateNo)}</div>
|
||||
{vehicleTag === 'lingniu' ? (
|
||||
<span className="h5-vehicle-tag h5-vehicle-tag--in">羚牛车辆</span>
|
||||
) : vehicleTag === 'external' ? (
|
||||
<span className="h5-vehicle-tag h5-vehicle-tag--out">非羚牛车辆</span>
|
||||
) : null}
|
||||
</div>
|
||||
{verifyLabel ? (
|
||||
<span className={`h5-tag ${verified ? 'h5-tag--done' : 'h5-tag--pending'}`}>
|
||||
{verified ? <IconCheck size={12} /> : <IconClock size={12} />}
|
||||
{verified ? '已核对' : '未核对'}
|
||||
{verifyLabel}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="h5-card-meta">
|
||||
<IconClock size={13} />
|
||||
@@ -46,7 +59,7 @@ export function OrderCard({ order, onClick, index = 0 }: Props) {
|
||||
</div>
|
||||
<div className="h5-card-foot">
|
||||
<span>单价 {formatMoney(order.unitPrice)} 元/kg</span>
|
||||
{verified ? (
|
||||
{isLingniu && verified ? (
|
||||
<span className="h5-card-foot__verify">核对 {formatDateTimeMinute(order.verifiedAt)}</span>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
|
||||
import type { H5OrderRecord } from '../types';
|
||||
import { buildDemoWatermarkedPhoto } from '../utils/demo-photo';
|
||||
import { formatDateTimeMinute, formatKg, formatMileage, formatMoney, formatPlateDisplay } from '../utils/format';
|
||||
import { getVehicleTag } from '../utils/fleet-plates';
|
||||
import { IconCheck, IconChevronLeft, IconClock } from './Icons';
|
||||
import { PhoneShell } from './PhoneShell';
|
||||
import { isOrderLocked } from '../utils/orders';
|
||||
@@ -14,8 +15,10 @@ type Props = {
|
||||
};
|
||||
|
||||
export function OrderDetail({ order, onBack, onEdit, onDelete }: Props) {
|
||||
const reconciled = order.reconcileStatus === 'reconciled';
|
||||
const verified = order.verifyStatus === 'verified';
|
||||
const vehicleTag = getVehicleTag(order.plateNo);
|
||||
const isLingniu = vehicleTag === 'lingniu';
|
||||
const reconciled = isLingniu && order.reconcileStatus === 'reconciled';
|
||||
const verified = isLingniu && order.verifyStatus === 'verified';
|
||||
const locked = isOrderLocked(order);
|
||||
const [plateSrc, setPlateSrc] = useState(order.platePhotoUrl || '');
|
||||
const [panelSrc, setPanelSrc] = useState(order.panelPhotoUrl || '');
|
||||
@@ -87,11 +90,19 @@ export function OrderDetail({ order, onBack, onEdit, onDelete }: Props) {
|
||||
<section className="h5-panel h5-panel--detail" data-annotation-id="h5-detail">
|
||||
<div className="h5-detail-hero">
|
||||
<div>
|
||||
<div className="h5-plate-wrap">
|
||||
<div className="h5-plate">{formatPlateDisplay(order.plateNo)}</div>
|
||||
{vehicleTag === 'lingniu' ? (
|
||||
<span className="h5-vehicle-tag h5-vehicle-tag--in">羚牛车辆</span>
|
||||
) : vehicleTag === 'external' ? (
|
||||
<span className="h5-vehicle-tag h5-vehicle-tag--out">非羚牛车辆</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="h5-detail-hero__time tabular-nums">
|
||||
{formatDateTimeMinute(order.hydrogenTime)}
|
||||
</div>
|
||||
</div>
|
||||
{isLingniu ? (
|
||||
<div className="h5-tag-row">
|
||||
<span className={`h5-tag ${verified ? 'h5-tag--done' : 'h5-tag--pending'}`}>
|
||||
{verified ? <IconCheck size={12} /> : <IconClock size={12} />}
|
||||
@@ -102,6 +113,7 @@ export function OrderDetail({ order, onBack, onEdit, onDelete }: Props) {
|
||||
{reconciled ? '已对账' : '未对账'}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="h5-detail-metrics" aria-label="加氢金额与用量">
|
||||
<div>
|
||||
@@ -152,19 +164,19 @@ export function OrderDetail({ order, onBack, onEdit, onDelete }: Props) {
|
||||
</div>
|
||||
<div className="h5-detail-row">
|
||||
<span>核对状态</span>
|
||||
<strong>{verified ? '已核对' : '未核对'}</strong>
|
||||
<strong>{isLingniu ? (verified ? '已核对' : '未核对') : '—'}</strong>
|
||||
</div>
|
||||
<div className="h5-detail-row">
|
||||
<span>核对时间</span>
|
||||
<strong>{verified ? formatDateTimeMinute(order.verifiedAt) : '—'}</strong>
|
||||
<strong>{isLingniu && verified ? formatDateTimeMinute(order.verifiedAt) : '—'}</strong>
|
||||
</div>
|
||||
<div className="h5-detail-row">
|
||||
<span>对账状态</span>
|
||||
<strong>{reconciled ? '已对账' : '未对账'}</strong>
|
||||
<strong>{isLingniu ? (reconciled ? '已对账' : '未对账') : '—'}</strong>
|
||||
</div>
|
||||
<div className="h5-detail-row">
|
||||
<span>对账时间</span>
|
||||
<strong>{reconciled ? formatDateTimeMinute(order.reconcileTime) : '—'}</strong>
|
||||
<strong>{isLingniu && reconciled ? formatDateTimeMinute(order.reconcileTime) : '—'}</strong>
|
||||
</div>
|
||||
</section>
|
||||
{reconciled ? (
|
||||
|
||||
@@ -31,7 +31,7 @@ import { OrderDetail } from './components/OrderDetail';
|
||||
import { PhoneShell } from './components/PhoneShell';
|
||||
import type { AppView, CreateDraft, StationOption } from './types';
|
||||
import { formatDateTimeMinute, formatKg, formatMoney, nowDateTimeMinute } from './utils/format';
|
||||
import { hasManualLedgerForDate } from './utils/manual-ledger';
|
||||
import { isManualLedgerGateBlocked, yesterdayDateKey } from './utils/manual-ledger';
|
||||
import {
|
||||
findDuplicateStationOrder,
|
||||
getTotalAmountMismatch,
|
||||
@@ -72,6 +72,7 @@ export default function OneosH5H2Order() {
|
||||
const [tick, setTick] = useState(0);
|
||||
const [view, setView] = useState<AppView>({ name: 'list' });
|
||||
const [mainTab, setMainTab] = useState<'orders' | 'manual'>('orders');
|
||||
const [manualFocusDate, setManualFocusDate] = useState<string | undefined>(undefined);
|
||||
const [toast, setToast] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
@@ -90,8 +91,10 @@ export default function OneosH5H2Order() {
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
if (!hasManualLedgerForDate(station.stationId || station.stationName)) {
|
||||
showToast('请先上传今日手工台账');
|
||||
const stationKey = station.stationId || station.stationName;
|
||||
if (isManualLedgerGateBlocked(stationKey)) {
|
||||
showToast('手工台账有缺失,作为对账重要依据,请先上传手工台账');
|
||||
setManualFocusDate(yesterdayDateKey());
|
||||
setMainTab('manual');
|
||||
setView({ name: 'list' });
|
||||
return;
|
||||
@@ -193,7 +196,7 @@ export default function OneosH5H2Order() {
|
||||
function handleDelete(id: string) {
|
||||
const order = orders.find((item) => item.id === id);
|
||||
if (!order || isOrderLocked(order)) return;
|
||||
if (!window.confirm(`确认删除车牌 ${order.plateNo.replace(/F$/u, '')} 的未对账记录?`)) return;
|
||||
if (!window.confirm(`确认删除车牌 ${order.plateNo.replace(/F$/u, '')} 的未锁定记录?`)) return;
|
||||
removeStationOrder(id);
|
||||
setTick((n) => n + 1);
|
||||
setView({ name: 'list' });
|
||||
@@ -267,7 +270,7 @@ export default function OneosH5H2Order() {
|
||||
</PhoneShell>
|
||||
);
|
||||
} else {
|
||||
const todayUploaded = hasManualLedgerForDate(station.stationId || station.stationName);
|
||||
const gateBlocked = isManualLedgerGateBlocked(station.stationId || station.stationName);
|
||||
content = (
|
||||
<PhoneShell
|
||||
title={mainTab === 'manual' ? '手工台账' : '加氢订单'}
|
||||
@@ -276,8 +279,8 @@ export default function OneosH5H2Order() {
|
||||
<>
|
||||
{mainTab === 'orders' ? (
|
||||
<div className="h5-fab-dock" data-annotation-id="h5-fab-dock">
|
||||
{!todayUploaded ? (
|
||||
<p className="h5-fab-dock__hint">今日未上传手工台账,无法新增电子记录</p>
|
||||
{gateBlocked ? (
|
||||
<p className="h5-fab-dock__hint">手工台账有缺失,作为对账重要依据,请先上传手工台账</p>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
@@ -307,7 +310,7 @@ export default function OneosH5H2Order() {
|
||||
>
|
||||
<IconCamera size={18} />
|
||||
手工台账
|
||||
{!todayUploaded ? <span className="h5-tabbar-dot" aria-label="今日未上传" /> : null}
|
||||
{gateBlocked ? <span className="h5-tabbar-dot" aria-label="前一日手工台账未上传" /> : null}
|
||||
</button>
|
||||
</nav>
|
||||
{toast ? (
|
||||
@@ -322,11 +325,29 @@ export default function OneosH5H2Order() {
|
||||
<ManualLedgerPanel
|
||||
station={station}
|
||||
tick={tick}
|
||||
focusDate={manualFocusDate}
|
||||
showToast={showToast}
|
||||
onUploaded={() => setTick((n) => n + 1)}
|
||||
onUploaded={() => {
|
||||
setTick((n) => n + 1);
|
||||
setManualFocusDate(undefined);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{gateBlocked ? (
|
||||
<button
|
||||
type="button"
|
||||
className="h5-banner-warn"
|
||||
data-annotation-id="h5-ledger-gate-banner"
|
||||
onClick={() => {
|
||||
setManualFocusDate(yesterdayDateKey());
|
||||
setMainTab('manual');
|
||||
}}
|
||||
>
|
||||
手工台账有缺失,作为对账重要依据,请先上传手工台账
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
<div className="h5-hero" data-annotation-id="h5-hero" aria-label="当前站点">
|
||||
<div className="h5-hero-label">
|
||||
<IconMapPin size={13} />
|
||||
|
||||
@@ -35,6 +35,10 @@ const mdDetail = readMd('feature-detail.md');
|
||||
const mdManual = readMd('feature-manual-ledger.md');
|
||||
const mdFleet = readMd('fleet-plate-tag.md');
|
||||
const mdReconcile = readMd('reconcile-linkage.md');
|
||||
const mdRoles = readMd('roles.md');
|
||||
const mdStories = readMd('user-stories.md');
|
||||
const mdLogic = readMd('key-logic.md');
|
||||
const mdFlows = readMd('flows.md');
|
||||
|
||||
const source = {
|
||||
documentVersion: 1,
|
||||
@@ -267,7 +271,7 @@ const source = {
|
||||
'## 识别原图(含水印)\n\n详情展示车牌识别、加氢机面板的原始照片;照片在拍摄/选图时已叠加时间与地点水印并随记录保存。种子数据无实拍时用演示水印图。\n\n详见 [详情编辑删除](feature-detail)。',
|
||||
'h5-manual': mdManual,
|
||||
'h5-manual-calendar':
|
||||
'## 台账日历\n\n自本站首笔加氢记录日起:绿=已上传,橙=未上传;首笔之前无需补传。禁选未来日。点选范围内未传日可补传。仅「今日」是否上传影响新增加氢记录。\n\n详见 [手工台账上传](feature-manual-ledger)。',
|
||||
'## 台账日历\n\n自本站首笔加氢记录日起:绿=已上传,橙=未上传;首笔之前无需补传。禁选未来日。点选范围内未传日可补传。**前一日**未上传时禁止新增加氢记录(今日台账状态不作为门禁)。\n\n详见 [手工台账上传](feature-manual-ledger)。',
|
||||
},
|
||||
assetMap: {},
|
||||
directory: {
|
||||
@@ -285,6 +289,87 @@ const source = {
|
||||
markdown: mdPrd,
|
||||
markdownPath: '.spec/requirements-prd.md',
|
||||
},
|
||||
{
|
||||
type: 'folder',
|
||||
id: 'h5-doc-roles',
|
||||
title: '需求角色',
|
||||
defaultExpanded: true,
|
||||
children: [
|
||||
{
|
||||
type: 'markdown',
|
||||
id: 'h5-doc-roles-md',
|
||||
title: '角色说明',
|
||||
markdown: mdRoles,
|
||||
markdownPath: '.spec/roles.md',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'folder',
|
||||
id: 'h5-doc-stories',
|
||||
title: '用户故事',
|
||||
defaultExpanded: true,
|
||||
children: [
|
||||
{
|
||||
type: 'markdown',
|
||||
id: 'h5-doc-stories-md',
|
||||
title: '用户故事',
|
||||
markdown: mdStories,
|
||||
markdownPath: '.spec/user-stories.md',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'folder',
|
||||
id: 'h5-doc-logic',
|
||||
title: '关键逻辑',
|
||||
defaultExpanded: true,
|
||||
children: [
|
||||
{
|
||||
type: 'markdown',
|
||||
id: 'h5-doc-logic-index',
|
||||
title: '逻辑索引',
|
||||
markdown: mdLogic,
|
||||
markdownPath: '.spec/key-logic.md',
|
||||
},
|
||||
{
|
||||
type: 'markdown',
|
||||
id: 'h5-doc-logic-manual',
|
||||
title: '手工台账门禁',
|
||||
markdown: mdManual,
|
||||
markdownPath: '.spec/feature-manual-ledger.md',
|
||||
},
|
||||
{
|
||||
type: 'markdown',
|
||||
id: 'h5-doc-logic-reconcile',
|
||||
title: '核对与对账联动',
|
||||
markdown: mdReconcile,
|
||||
markdownPath: '.spec/reconcile-linkage.md',
|
||||
},
|
||||
{
|
||||
type: 'markdown',
|
||||
id: 'h5-doc-logic-fleet',
|
||||
title: '羚牛车辆标签',
|
||||
markdown: mdFleet,
|
||||
markdownPath: '.spec/fleet-plate-tag.md',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'folder',
|
||||
id: 'h5-doc-flows',
|
||||
title: '流程图',
|
||||
defaultExpanded: true,
|
||||
children: [
|
||||
{
|
||||
type: 'markdown',
|
||||
id: 'h5-doc-flows-md',
|
||||
title: '操作流程图',
|
||||
markdown: mdFlows,
|
||||
markdownPath: '.spec/flows.md',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'folder',
|
||||
id: 'h5-doc-features',
|
||||
@@ -339,7 +424,7 @@ const source = {
|
||||
type: 'folder',
|
||||
id: 'h5-doc-pages',
|
||||
title: '按页面',
|
||||
defaultExpanded: true,
|
||||
defaultExpanded: false,
|
||||
children: [
|
||||
{
|
||||
type: 'folder',
|
||||
|
||||
@@ -373,6 +373,13 @@
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.h5-plate-wrap {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.h5-tag {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
/** 原型本地「系统车辆列表」车牌(不含尾缀 F),用于羚牛/非羚牛判定 */
|
||||
/** 原型本地「系统车辆列表」车牌(不含尾缀 F),用于羚牛/非羚牛判定。
|
||||
* 与 `src/common/h2VehicleLedgerBridge.js` 的 H2_FLEET_PLATE_KEYS 保持同步。
|
||||
*/
|
||||
const FLEET_PLATES = new Set(
|
||||
[
|
||||
'浙A55666',
|
||||
@@ -21,10 +23,21 @@ const FLEET_PLATES = new Set(
|
||||
'浙F06618',
|
||||
'粤AGR8556',
|
||||
'粤AGP5156',
|
||||
'浙AD12345',
|
||||
'浙AH55660',
|
||||
'浙BK33210',
|
||||
'粤BK33210',
|
||||
'沪AD12345',
|
||||
'苏EF99887',
|
||||
'京CN88771',
|
||||
'川AL55602',
|
||||
].map((p) => p.toUpperCase()),
|
||||
);
|
||||
|
||||
export function normalizePlateKey(plateNo: string): string {
|
||||
if (typeof window !== 'undefined' && window.H2VehicleLedgerBridge?.normalizePlateKey) {
|
||||
return window.H2VehicleLedgerBridge.normalizePlateKey(plateNo);
|
||||
}
|
||||
return String(plateNo || '')
|
||||
.trim()
|
||||
.toUpperCase()
|
||||
@@ -33,6 +46,9 @@ export function normalizePlateKey(plateNo: string): string {
|
||||
|
||||
/** 车牌是否在系统车辆列表中 → 羚牛车辆 */
|
||||
export function isLingniuVehicle(plateNo: string): boolean {
|
||||
if (typeof window !== 'undefined' && window.H2VehicleLedgerBridge?.isLingniuVehicle) {
|
||||
return window.H2VehicleLedgerBridge.isLingniuVehicle(plateNo);
|
||||
}
|
||||
const key = normalizePlateKey(plateNo);
|
||||
if (!key) return false;
|
||||
return FLEET_PLATES.has(key);
|
||||
|
||||
@@ -70,6 +70,32 @@ export function todayDateKey(): string {
|
||||
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
|
||||
}
|
||||
|
||||
export function yesterdayDateKey(): string {
|
||||
const b = bridge();
|
||||
if (b?.yesterdayDateKey) return b.yesterdayDateKey();
|
||||
const d = new Date();
|
||||
d.setDate(d.getDate() - 1);
|
||||
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 前一日手工台账未上传 → 今日不可新增加氢记录。
|
||||
* 若昨日早于本站首笔加氢日(或无首笔),不要求昨日台账。
|
||||
*/
|
||||
export function isManualLedgerGateBlocked(stationIdOrName: string): boolean {
|
||||
const b = bridge();
|
||||
if (b?.isManualLedgerGateBlocked) {
|
||||
return Boolean(b.isManualLedgerGateBlocked(stationIdOrName));
|
||||
}
|
||||
const station = String(stationIdOrName || '').trim();
|
||||
if (!station) return false;
|
||||
const todayKey = todayDateKey();
|
||||
const yKey = yesterdayDateKey();
|
||||
const firstKey = getFirstHydrogenDateKey(station);
|
||||
if (!cellNeedsLedger(yKey, todayKey, firstKey)) return false;
|
||||
return !hasManualLedgerForDate(station, yKey);
|
||||
}
|
||||
|
||||
export function hasManualLedgerForDate(
|
||||
stationIdOrName: string,
|
||||
ledgerDate?: string,
|
||||
@@ -191,6 +217,8 @@ declare global {
|
||||
interface Window {
|
||||
H2VehicleLedgerBridge?: {
|
||||
todayDateKey?: () => string;
|
||||
yesterdayDateKey?: () => string;
|
||||
isManualLedgerGateBlocked?: (stationIdOrName: string) => boolean;
|
||||
getRows?: () => Array<{ stationId?: string; stationName?: string; hydrogenTime?: string }>;
|
||||
hasManualLedgerForDate?: (stationIdOrName: string, ledgerDate?: string) => boolean;
|
||||
getManualLedger?: (stationIdOrName: string, ledgerDate?: string) => ManualLedgerEntry | null;
|
||||
|
||||
@@ -18,6 +18,8 @@ declare global {
|
||||
totalAmount: number,
|
||||
toleranceYuan?: number,
|
||||
) => { expected: number; actual: number; diff: number } | null;
|
||||
isLingniuVehicle?: (plateNo: string) => boolean;
|
||||
normalizePlateKey?: (plateNo: string) => string;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -85,6 +87,12 @@ export function mapRowToOrder(row: Record<string, unknown>): H5OrderRecord {
|
||||
}
|
||||
|
||||
export function isOrderLocked(order: H5OrderRecord): boolean {
|
||||
/* 非羚牛车辆不参与核对/对账业务属性,不以核对对账状态锁定 */
|
||||
const bridge = typeof window !== 'undefined' ? window.H2VehicleLedgerBridge : undefined;
|
||||
const isLingniu = bridge?.isLingniuVehicle
|
||||
? bridge.isLingniuVehicle(order.plateNo)
|
||||
: true;
|
||||
if (!isLingniu) return false;
|
||||
return order.verifyStatus === 'verified' || order.reconcileStatus === 'reconciled';
|
||||
}
|
||||
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
"title": "OneOS",
|
||||
"children": [
|
||||
{
|
||||
"id": "item:prototypes:oneos-web-workbench",
|
||||
"id": "item:prototypes:oneos-web-workbench-new",
|
||||
"kind": "item",
|
||||
"title": "工作台",
|
||||
"itemKey": "prototypes/oneos-web-workbench"
|
||||
"itemKey": "prototypes/oneos-web-workbench-new"
|
||||
},
|
||||
{
|
||||
"id": "folder-oneos-approval",
|
||||
@@ -78,6 +78,12 @@
|
||||
"title": "租赁合同",
|
||||
"itemKey": "prototypes/lease-contract-management"
|
||||
},
|
||||
{
|
||||
"id": "item:prototypes:self-operated-contract",
|
||||
"kind": "item",
|
||||
"title": "自营合同",
|
||||
"itemKey": "prototypes/self-operated-contract"
|
||||
},
|
||||
{
|
||||
"id": "item:prototypes:insurance-procurement",
|
||||
"kind": "item",
|
||||
@@ -221,6 +227,12 @@
|
||||
"kind": "item",
|
||||
"title": "任务工单",
|
||||
"itemKey": "prototypes/task-work-order"
|
||||
},
|
||||
{
|
||||
"id": "item:prototypes:self-operated-dispatch-task",
|
||||
"kind": "item",
|
||||
"title": "调度任务",
|
||||
"itemKey": "prototypes/self-operated-dispatch-task"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -228,14 +240,7 @@
|
||||
"id": "folder-1784341576489-spa7ms",
|
||||
"kind": "folder",
|
||||
"title": "审批中心",
|
||||
"children": [
|
||||
{
|
||||
"id": "item:prototypes:oneos-web-approval",
|
||||
"kind": "item",
|
||||
"title": "审批中心",
|
||||
"itemKey": "prototypes/oneos-web-approval"
|
||||
}
|
||||
]
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"id": "item:prototypes:lease-business-line-overview",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* @name 审批中心
|
||||
*/
|
||||
import { ApprovalApp } from '../../common/oneos-web-approval/ApprovalApp';
|
||||
|
||||
export default function OneosWebApproval() {
|
||||
return <ApprovalApp pageTitle="审批中心" showTabs />;
|
||||
}
|
||||
@@ -37,8 +37,8 @@
|
||||
|
||||
1. 选择**账单开始日期**与**账单结束日期**后,从**车辆氢费明细**获取该时间段内、对应当前站点、且为**已核对且尚未结算(未对账)**的加氢记录。
|
||||
2. 点击「**生成对账单**」时弹出**二次确认**,提示:生成并提交后,车辆氢费明细中对应记录将标记为**已对账**且无法修改。
|
||||
3. 确认后进入结算阶段,填写**收票日期**、**收票金额**及**发票附件**(对账日期在提交时由系统自动回写)。
|
||||
4. 点击「**提交对账单**」后,本批明细在车辆氢费明细中锁定为**已对账**并绑定本对账单,不可再进入后续对账单。
|
||||
3. 确认后进入结算阶段,填写**收票日期**、**收票金额**、**发票附件**及**对账单盖章附件**(对账日期在提交时由系统自动回写)。
|
||||
4. 点击「**提交对账单**」并二次确认后,本批明细在**加氢记录(Web)、加氢订单(H5)、车辆氢费明细**三端同步锁定为**已对账**并绑定本对账单,不可再进入后续对账单。
|
||||
|
||||
**阶段一 · 选期生成**
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
| 收票日期 | 必填 |
|
||||
| 收票金额 | 必填,默认填入成本总金额 |
|
||||
| 发票附件 | 必填,支持上传 |
|
||||
| 对账单盖章附件 | **必填**;支持盖章 PDF、图片(`accept=.pdf,image/*`) |
|
||||
|
||||
---
|
||||
|
||||
@@ -72,19 +73,18 @@
|
||||
|
||||
提交成功后需**原子性**完成:
|
||||
|
||||
1. 写入对账历史(供「查看对账记录」查询)
|
||||
1. 写入对账历史(供「查看对账记录」查询;含发票附件与盖章附件)
|
||||
2. 扣减站点预付余额
|
||||
3. 标记台账记录已结算(绑定 `statementRecordId`)
|
||||
4. **回写车辆氢费明细**:
|
||||
4. **经共享 Bridge 回写三端**(同浏览器会话):
|
||||
|
||||
| 车辆氢费明细字段 | 回写值 |
|
||||
| 模块 | 回写结果 |
|
||||
|---|---|
|
||||
| 对账状态 | **已对账**(`reconcileStatus = reconciled`) |
|
||||
| 对账日期 | 操作完成时间 |
|
||||
| 收票日期 | 操作完成日期 |
|
||||
| 加氢站付款状态 | 已付款 |
|
||||
| 车辆氢费明细 | 对账状态 → **已对账**;对账日期 / 收票日期 / 付款状态 / 对账人 |
|
||||
| 加氢记录(Web) | 对账状态 → **已对账**(订阅 Bridge) |
|
||||
| 加氢订单(H5) | 对账状态 → **已对账**(订阅 Bridge) |
|
||||
|
||||
原型通过 `H2_STATION_STATEMENT_LEDGER_UPDATES` / `h2VehicleLedgerBridge` 模拟跨页回写。
|
||||
原型通过 `H2_STATION_STATEMENT_LEDGER_UPDATES` / `h2VehicleLedgerBridge.applyPatches` 模拟跨页回写(**未接真实 API**)。
|
||||
|
||||
> 「完成核对」只改变核对状态,**不会**把记录写成已对账;已对账仅由本页对账单提交产生。
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
|
||||
历史列表:对账日期、对账人、账单区间、加氢次数、加氢金额、结算后余额、查看明细。
|
||||
|
||||
查看明细:账单区间、统计三卡、收票信息、发票附件、成本明细表。
|
||||
查看明细:账单区间、统计三卡、收票信息、发票附件、对账单盖章附件、成本明细表。
|
||||
|
||||
---
|
||||
|
||||
@@ -108,8 +108,10 @@
|
||||
| 区间内无「已核对且未对账未结算」记录 | 阻止生成,提示调整日期(或先完成核对) |
|
||||
| 区间内存在未核对记录 | **不纳入**对账单;不单独弹窗枚举(静默排除) |
|
||||
| 点击「生成对账单」未确认 | 不进入结算阶段 |
|
||||
| 收票日期/金额/附件未填 | 阻止提交 |
|
||||
| 收票日期/金额/发票附件/盖章附件未填 | 阻止提交 |
|
||||
| 盖章附件非 PDF/图片 | 拒绝该文件并提示 |
|
||||
| 收票金额 ≤ 0 | 阻止提交 |
|
||||
| 点击「提交对账单」未二次确认 | 不执行回写 |
|
||||
| 预付余额结算后为负 | 允许提交,标「已欠费」 |
|
||||
|
||||
---
|
||||
|
||||
@@ -145,8 +145,8 @@
|
||||
| 步骤 | 说明 |
|
||||
|---|---|
|
||||
| 1. 拉取明细 | 按账期从车辆氢费明细取 **已核对** ∧ **未对账** ∧ 未结算 ∧ 本站 的记录;**未核对不纳入** |
|
||||
| 2. 两阶段 | 选期生成 → 填收票提交 |
|
||||
| 3. 提交回写 | 更新预付余额、写对账历史;台账变为 **已对账**(对账日期、收票日期、已付款) |
|
||||
| 2. 两阶段 | 选期生成 → 填收票 / 发票附件 / **对账单盖章附件** → 二次确认提交 |
|
||||
| 3. 提交回写 | 更新预付余额、写对账历史;**加氢记录 Web / H5 / 车辆氢费明细**三端同步为 **已对账** |
|
||||
|
||||
> **核对 ≠ 对账**:核对由车辆氢费明细「完成核对」产生;对账仅由本页「提交对账单」回写。
|
||||
|
||||
@@ -175,8 +175,8 @@
|
||||
### 8.2 对账流程
|
||||
|
||||
- [ ] 仅 **已核对** 且未对账、未结算的记录可进入对账单;未核对明细即使在账期内也不出现
|
||||
- [ ] 两阶段交互与必填校验完整
|
||||
- [ ] 提交后回写车辆氢费明细为 **已对账**,可跨页验收
|
||||
- [ ] 两阶段交互与必填校验完整(含对账单盖章附件:PDF/图片)
|
||||
- [ ] 提交二次确认后,加氢记录(Web)、加氢订单(H5)、车辆氢费明细三端同步为 **已对账**
|
||||
|
||||
### 8.3 期末余额设置
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
| 规则 | 代码 |
|
||||
|---|---|
|
||||
| 筛选入口 | `h2GetAvailableStatementLedgerRows(store, stationName, startDate, endDate)` |
|
||||
| Store 映射 | `h2BridgeMapToStationLedgerRow` **必须透出** `verifyStatus`(否则站点侧会全部当成未核对) |
|
||||
| 已核对 | `verifyStatus === 'verified'`(缺省按 `unverified`) |
|
||||
| 未对账 | `reconcileStatus === 'pending'`(且 ≠ `reconciled`) |
|
||||
| 账期 | `h2ParseDateTimeMs(hydrogenTime)` 落在起止日闭区间 |
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -2329,16 +2329,27 @@ function h2ResolveUploadPreview(file, variant) {
|
||||
};
|
||||
}
|
||||
|
||||
/** 盖章附件:仅允许 PDF / 常见图片 */
|
||||
function h2IsSealAttachmentFile(file) {
|
||||
var name = String((file && (file.name || file.fileName)) || '').toLowerCase();
|
||||
var type = String((file && file.type) || '').toLowerCase();
|
||||
if (type.indexOf('pdf') >= 0 || type.indexOf('image/') === 0) return true;
|
||||
return /\.(pdf|png|jpe?g|gif|webp|bmp)$/i.test(name);
|
||||
}
|
||||
|
||||
/** 合同附件:按钮上传 + 预览/下载 */
|
||||
function ContractFilesUpload(props) {
|
||||
var antd = window.antd;
|
||||
var Upload = antd.Upload;
|
||||
var Button = antd.Button;
|
||||
var messageApi = antd.message;
|
||||
var fileList = props.fileList || [];
|
||||
var disabled = props.disabled;
|
||||
var showHint = props.showHint !== false;
|
||||
var wrapClass = 'h2-contract-upload' + (props.wrapClassName ? ' ' + props.wrapClassName : '');
|
||||
var uploadClass = props.uploadClassName || 'h2-create-upload-btn';
|
||||
var accept = props.accept || undefined;
|
||||
var validateFile = props.validateFile;
|
||||
|
||||
return React.createElement('div', { className: wrapClass },
|
||||
React.createElement('div', { className: 'h2-contract-upload-actions' },
|
||||
@@ -2347,8 +2358,24 @@ function ContractFilesUpload(props) {
|
||||
multiple: true,
|
||||
disabled: disabled,
|
||||
fileList: fileList,
|
||||
beforeUpload: function () { return false; },
|
||||
onChange: props.onChange,
|
||||
accept: accept,
|
||||
beforeUpload: function (file) {
|
||||
if (typeof validateFile === 'function' && !validateFile(file)) {
|
||||
if (messageApi && messageApi.warning) {
|
||||
messageApi.warning(props.rejectMessage || '文件格式不支持');
|
||||
}
|
||||
return Upload.LIST_IGNORE || false;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
onChange: function (info) {
|
||||
if (!props.onChange) return;
|
||||
var nextList = info.fileList || [];
|
||||
if (typeof validateFile === 'function') {
|
||||
nextList = nextList.filter(function (f) { return validateFile(f); });
|
||||
}
|
||||
props.onChange({ fileList: nextList });
|
||||
},
|
||||
showUploadList: false
|
||||
},
|
||||
React.createElement(Button, {
|
||||
@@ -2359,7 +2386,7 @@ function ContractFilesUpload(props) {
|
||||
style: props.buttonStyle
|
||||
}, props.uploadLabel || '上传附件')
|
||||
),
|
||||
showHint ? React.createElement('span', { style: { fontSize: 12, color: '#94a3b8' } }, '点击上传,可多选') : null
|
||||
showHint ? React.createElement('span', { style: { fontSize: 12, color: '#94a3b8' } }, props.hintText || '点击上传,可多选') : null
|
||||
),
|
||||
fileList.length
|
||||
? React.createElement('div', { className: 'h2-contract-file-list' },
|
||||
@@ -5992,7 +6019,8 @@ const Component = function () {
|
||||
balanceAfterSettlement: 0,
|
||||
receiptDate: '',
|
||||
receiptAmount: '',
|
||||
invoiceFiles: []
|
||||
invoiceFiles: [],
|
||||
sealFiles: []
|
||||
});
|
||||
var statementModal = statementModalState[0];
|
||||
var setStatementModal = statementModalState[1];
|
||||
@@ -6753,13 +6781,14 @@ const Component = function () {
|
||||
open: true,
|
||||
record: record,
|
||||
startDate: defaultStart,
|
||||
endDate: '2026-05-31',
|
||||
endDate: '2026-07-31',
|
||||
phase: 'select',
|
||||
draftRows: [],
|
||||
balanceAfterSettlement: h2NumOrZero(record.prepaidBalance),
|
||||
receiptDate: '',
|
||||
receiptAmount: '',
|
||||
invoiceFiles: []
|
||||
invoiceFiles: [],
|
||||
sealFiles: []
|
||||
});
|
||||
}, [statementRecords]);
|
||||
|
||||
@@ -6774,7 +6803,8 @@ const Component = function () {
|
||||
balanceAfterSettlement: 0,
|
||||
receiptDate: '',
|
||||
receiptAmount: '',
|
||||
invoiceFiles: []
|
||||
invoiceFiles: [],
|
||||
sealFiles: []
|
||||
});
|
||||
}, []);
|
||||
|
||||
@@ -6829,7 +6859,8 @@ const Component = function () {
|
||||
balanceAfterSettlement: balanceAfter,
|
||||
receiptAmount: summary.totalCost ? h2FormatYuanNum(summary.totalCost) : '',
|
||||
receiptDate: '',
|
||||
invoiceFiles: []
|
||||
invoiceFiles: [],
|
||||
sealFiles: []
|
||||
});
|
||||
});
|
||||
message.success('已生成对账记录,共 ' + summary.count + ' 笔,请填写结算信息并提交');
|
||||
@@ -6857,7 +6888,18 @@ const Component = function () {
|
||||
message.warning('请上传发票附件');
|
||||
return;
|
||||
}
|
||||
if (!statementModal.sealFiles || !statementModal.sealFiles.length) {
|
||||
message.warning('请上传对账单盖章附件');
|
||||
return;
|
||||
}
|
||||
var summary = h2CalcStatementSummary(rows);
|
||||
Modal.confirm({
|
||||
title: '确认提交对账单?',
|
||||
content: '提交后,本批 ' + summary.count + ' 笔记录将在加氢记录、加氢订单(H5)、车辆氢费明细三端同步为「已对账」,且不可再修改,是否确认?',
|
||||
okText: '确认提交',
|
||||
cancelText: '取消',
|
||||
centered: true,
|
||||
onOk: function () {
|
||||
var operateTime = h2OperateTimestamp();
|
||||
var operateDate = h2FormatOperateDateOnly();
|
||||
var statementId = 'stmt-' + Date.now();
|
||||
@@ -6879,6 +6921,7 @@ const Component = function () {
|
||||
receiptDate: statementModal.receiptDate,
|
||||
receiptAmount: receiptAmountNum,
|
||||
invoiceFiles: (statementModal.invoiceFiles || []).slice(),
|
||||
sealFiles: (statementModal.sealFiles || []).slice(),
|
||||
recordIds: recordIds
|
||||
};
|
||||
var patches = [];
|
||||
@@ -6895,14 +6938,17 @@ const Component = function () {
|
||||
receiptDate: operateDate,
|
||||
paymentStatus: H2_PAYMENT_STATUS_PAID,
|
||||
statementRecordId: statementId,
|
||||
reconcileStatus: H2_RECONCILE_RECONCILED
|
||||
reconcileStatus: H2_RECONCILE_RECONCILED,
|
||||
reconciler: H2_CURRENT_OPERATOR,
|
||||
reconciledByName: H2_CURRENT_OPERATOR
|
||||
});
|
||||
return Object.assign({}, r, {
|
||||
statementRecordId: statementId,
|
||||
reconcileStatus: H2_RECONCILE_RECONCILED,
|
||||
reconcileDate: operateTime,
|
||||
receiptDate: operateDate,
|
||||
paymentStatus: H2_PAYMENT_STATUS_PAID
|
||||
paymentStatus: H2_PAYMENT_STATUS_PAID,
|
||||
reconciledByName: H2_CURRENT_OPERATOR
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -6920,8 +6966,10 @@ const Component = function () {
|
||||
if (typeof window !== 'undefined' && window.H2_VEHICLE_LEDGER_API && typeof window.H2_VEHICLE_LEDGER_API.applyPatches === 'function') {
|
||||
window.H2_VEHICLE_LEDGER_API.applyPatches(patches);
|
||||
}
|
||||
message.success('对账单已提交,共结算 ' + summary.count + ' 笔加氢记录');
|
||||
message.success('对账单已提交,共结算 ' + summary.count + ' 笔;三端已同步为「已对账」');
|
||||
closeStatementModal();
|
||||
}
|
||||
});
|
||||
}, [statementModal, closeStatementModal]);
|
||||
|
||||
var openStatementHistoryModal = useCallback(function (record) {
|
||||
@@ -8458,9 +8506,9 @@ const Component = function () {
|
||||
React.createElement('strong', null, lastEndDate || '暂无')
|
||||
),
|
||||
!isDraft ? React.createElement('div', { className: 'h2-statement-form-hint' },
|
||||
'数据来源:',
|
||||
'数据来源:仅计入',
|
||||
React.createElement('strong', null, '车辆氢费明细'),
|
||||
' 中标记为「已核对」且尚未对账结算的加氢记录;未核对记录不会进入对账单;明细仅展示成本价与成本总价。'
|
||||
' 中所有「已核对」且尚未对账的加氢记录;未核对记录不会进入对账单。提交后,加氢记录(Web)、加氢订单(H5)、车辆氢费明细三端同步为「已对账」。明细仅展示成本价与成本总价。'
|
||||
) : null
|
||||
),
|
||||
isDraft ? React.createElement('div', { className: 'h2-statement-stats', role: 'group', 'aria-label': '对账统计' },
|
||||
@@ -8556,6 +8604,21 @@ const Component = function () {
|
||||
onChange: function (info) {
|
||||
setStatementModal(function (m) { return Object.assign({}, m, { invoiceFiles: info.fileList || [] }); });
|
||||
}
|
||||
})),
|
||||
formItem('对账单盖章附件', true, React.createElement(ContractFilesUpload, {
|
||||
fileList: statementModal.sealFiles || [],
|
||||
showHint: true,
|
||||
hintText: '支持盖章 PDF、图片(可多选)',
|
||||
accept: '.pdf,image/*',
|
||||
validateFile: h2IsSealAttachmentFile,
|
||||
rejectMessage: '盖章附件仅支持 PDF 或图片文件',
|
||||
uploadLabel: '上传盖章附件',
|
||||
wrapClassName: 'h2-statement-seal-upload',
|
||||
uploadClassName: 'h2-statement-upload-btn-wrap',
|
||||
buttonClassName: 'h2-statement-upload-btn',
|
||||
onChange: function (info) {
|
||||
setStatementModal(function (m) { return Object.assign({}, m, { sealFiles: info.fileList || [] }); });
|
||||
}
|
||||
}))
|
||||
) : null
|
||||
);
|
||||
@@ -8635,6 +8698,25 @@ const Component = function () {
|
||||
if (!stmt) return null;
|
||||
var summary = h2CalcStatementSummary(rows);
|
||||
var invoiceFiles = stmt.invoiceFiles || [];
|
||||
var sealFiles = stmt.sealFiles || [];
|
||||
var renderStmtFileList = function (files, emptyText) {
|
||||
if (!files.length) {
|
||||
return React.createElement('div', { style: { fontSize: 13, color: '#94a3b8', marginTop: 8 } }, emptyText);
|
||||
}
|
||||
return React.createElement('div', { className: 'h2-statement-detail-receipt-file-list' },
|
||||
files.map(function (file) {
|
||||
return React.createElement('div', { key: file.uid || file.name, className: 'h2-statement-detail-receipt-file-item' },
|
||||
React.createElement('span', { className: 'h2-statement-detail-receipt-file-name', title: file.name }, file.name || '未命名附件'),
|
||||
React.createElement(Button, {
|
||||
type: 'link',
|
||||
size: 'small',
|
||||
icon: H2_ICONS.download,
|
||||
onClick: function () { h2DownloadUploadFile(file); }
|
||||
}, '下载')
|
||||
);
|
||||
})
|
||||
);
|
||||
};
|
||||
return React.createElement('div', { className: 'h2-statement-panel' },
|
||||
React.createElement('div', { className: 'h2-statement-station-card' },
|
||||
React.createElement('div', { className: 'h2-statement-station-card__name' }, stmt.stationName || '—'),
|
||||
@@ -8672,21 +8754,11 @@ const Component = function () {
|
||||
),
|
||||
React.createElement('div', { className: 'h2-statement-detail-receipt-files' },
|
||||
React.createElement('div', { className: 'h2-statement-detail-receipt-item__label' }, '发票附件'),
|
||||
invoiceFiles.length
|
||||
? React.createElement('div', { className: 'h2-statement-detail-receipt-file-list' },
|
||||
invoiceFiles.map(function (file) {
|
||||
return React.createElement('div', { key: file.uid || file.name, className: 'h2-statement-detail-receipt-file-item' },
|
||||
React.createElement('span', { className: 'h2-statement-detail-receipt-file-name', title: file.name }, file.name || '未命名附件'),
|
||||
React.createElement(Button, {
|
||||
type: 'link',
|
||||
size: 'small',
|
||||
icon: H2_ICONS.download,
|
||||
onClick: function () { h2DownloadUploadFile(file); }
|
||||
}, '下载')
|
||||
);
|
||||
})
|
||||
)
|
||||
: React.createElement('div', { style: { fontSize: 13, color: '#94a3b8', marginTop: 8 } }, '暂无发票附件')
|
||||
renderStmtFileList(invoiceFiles, '暂无发票附件')
|
||||
),
|
||||
React.createElement('div', { className: 'h2-statement-detail-receipt-files', style: { marginTop: 12 } },
|
||||
React.createElement('div', { className: 'h2-statement-detail-receipt-item__label' }, '对账单盖章附件'),
|
||||
renderStmtFileList(sealFiles, '暂无盖章附件')
|
||||
)
|
||||
),
|
||||
React.createElement('div', { className: 'h2-statement-table-wrap' },
|
||||
|
||||
@@ -93,8 +93,8 @@ const NODE_DEFS = [
|
||||
title: '生成对账单',
|
||||
pageId: 'site-info',
|
||||
color: '#7c3aed',
|
||||
aiPrompt: '选期仅拉取已核对未对账氢费明细、二次确认与结算提交。',
|
||||
annotationText: '账期内仅纳入车辆氢费明细「已核对」记录;未核对不进对账单;提交后标为已对账',
|
||||
aiPrompt: '选期仅拉取已核对未对账氢费明细;结算须上传盖章附件;提交二次确认后三端同步已对账。',
|
||||
annotationText: '仅计入车辆氢费明细全部「已核对」且未对账记录;结算阶段必填对账单盖章附件(PDF/图片);提交确认后 Web/H5/台账三端同步「已对账」',
|
||||
locator: {
|
||||
selectors: ['[data-annotation-id="h2-site-statement-form"]', '.h2-statement-form-wrap'],
|
||||
fingerprint: 'form|statement',
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
|---|---|
|
||||
| 共享 Bridge 不可用 | 列表用本地 fallback;保存/删除提示失败 |
|
||||
| 已核对或已对账点编辑/删除 | 对应警告文案 |
|
||||
| PC 独有 | **导出**含核对/对账列;手工台账上传(站点按角色范围;今日双态提示;右侧预览区确认上传) |
|
||||
| PC 独有 | **导出**含车辆来源/数据来源/核对/对账列;手工台账上传(站点按角色范围;**昨日门禁**禁新增;今日双态提示;右侧预览区确认上传) |
|
||||
|
||||
## 4. 数据流
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
- **车辆氢费明细**:车牌号下方标签(站端=绿 / 羚牛=蓝)
|
||||
- **加氢记录 Web**:车牌号下方同口径标签
|
||||
- 导出 CSV 含「来源」列
|
||||
- 导出 CSV 含「数据来源」列(与「车辆来源」分列)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -27,15 +27,17 @@
|
||||
|
||||
| # | 功能 | 说明 |
|
||||
|---|---|---|
|
||||
| 1 | 列表 / 筛选 | 字段与 H5 一致;默认一行(加氢时间/车牌/站名/对账状态);`splitFilterFields` + **更多筛选/收起**(`vm-filter-expand` / `vm-filter-toggle`)对齐物流业务明细;占位符样式见全局设计规范 §4.1.1 |
|
||||
| 2 | 手工台账 | 与 H5 同逻辑:站点按登录角色范围过滤;今日已传/未传双态提示;右侧「手工台账」预览(空态/全屏翻页/已归档角标)+ 确认前二次提示;月历补传与拦截见 [feature-manual-ledger](../oneos-h5-h2-order/.spec/feature-manual-ledger.md) |
|
||||
| 3 | 新增 / 批量 / 编辑 | 字段:时间、站、车牌、**里程(选填)**、单价、量、总额;已对账只读;同站同车牌同时间拦截;总额偏离单价×量超 0.05 元需确认 |
|
||||
| 1 | 列表 / 筛选 / 详情 | 字段与 H5 一致;默认一行(加氢时间/车牌/站名/对账状态);`splitFilterFields` + **更多筛选/收起**;操作列 **详情**(记录字段 + 车牌/面板原图)在「更多」之前,见 [feature-list.md](./feature-list.md) |
|
||||
| 2 | 手工台账 | 与 H5 同逻辑:站点按登录角色范围过滤;今日已传/未传双态提示;**前一日缺失则今日禁新增**(顶栏固定文案,点新增直达台账并选中昨日);右侧「手工台账」预览 + 确认前二次提示;月历补传见 [feature-manual-ledger](../oneos-h5-h2-order/.spec/feature-manual-ledger.md) |
|
||||
| 3 | 新增 / 批量 / 编辑 | 时间精确至秒;单站默认/多站请选择;车牌旁羚牛/外部车辆标签;总额自动只读;支持批量导入(模板+结果页);顶栏无「批量新增加氢记录」标题;详见 [feature-create.md](./feature-create.md) |
|
||||
| 4 | 删除 | 仅未对账;写共享 Store |
|
||||
| 5 | 导出 | 当前筛选结果 CSV(Web 独有) |
|
||||
| 5 | 导出 | 当前筛选结果 CSV;列含**车辆来源**、**数据来源**(与列表标签一致) |
|
||||
|
||||
### 1.2 列表字段
|
||||
|
||||
加氢时间、加氢站名称、车牌号(附来源标签)、里程(km)、氢气单价(元/kg)、加氢量、加氢总额、核对状态、核对时间、核对人、对账状态、对账时间、对账人;操作含 **变更日志**。
|
||||
加氢时间、加氢站名称、车牌号(附**车辆来源**:羚牛车辆/非羚牛车辆 + **数据来源**:站端上传/羚牛上传)、里程(km)、氢气单价(元/kg)、加氢量、加氢总额、核对状态、核对时间、核对人、对账状态、对账时间、对账人。
|
||||
操作列(`OperationActions`):外显 **详情**(弹窗含字段摘要 + 车牌识别原图 / 加氢机面板原图);**更多** 含编辑、变更日志、删除。
|
||||
非羚牛车辆的核对/对账相关列显示为空,详见 [fleet-vehicle-verify.md](./fleet-vehicle-verify.md)。
|
||||
|
||||
### 1.3 工具栏 Tab
|
||||
|
||||
@@ -54,11 +56,14 @@
|
||||
2. 与 H5 读写同一共享 Store;一端增删改后另一端可见(同会话)
|
||||
3. 筛选默认一行;更多筛选/收起对齐物流业务明细;支持核对状态、核对时间、对账状态、对账时间;占位符样式与全局规范一致
|
||||
4. 已核对或已对账不可编辑、删除;**已对账记录对账时间不为空**(对账单回写);核对由车辆氢费明细「完成核对」触发
|
||||
5. 导出列与列表字段一致(含来源、核对人、对账人)
|
||||
5. 导出列与列表字段一致(含车辆来源、数据来源、核对人、对账人)
|
||||
6. 列表左上角两个 Tab:加氢记录 / 手工台账;悬浮与选中样式符合分段 Tab 规范;无预约加氢入口
|
||||
7. 同站同车牌同加氢时间重复保存被拦截;总额与单价×量偏差 > 0.05 元时出现确认
|
||||
8. 来源标签:站端上报→「站端上传」,台账手增→「羚牛上传」;变更日志含时间/操作人/修改项/原值/新值;核对时间后有核对人、对账时间后有对账人
|
||||
8. 数据来源标签:站端上报→「站端上传」,台账手增→「羚牛上传」;变更日志含时间/操作人/修改项/原值/新值;核对时间后有核对人、对账时间后有对账人
|
||||
9. 每日统计默认收起,第一屏以列表为主
|
||||
10. 车牌附「羚牛车辆 / 非羚牛车辆」;非羚牛车辆核对/对账字段显示为空
|
||||
11. 前一日手工台账缺失时,顶栏提示「手工台账有缺失,作为对账重要依据,请先上传手工台账」;点新增直达手工台账并选中昨日;补传后可新增
|
||||
12. 操作列「详情」在「更多」之前;弹窗可查看加氢记录字段及车牌号、加氢机面板照片(无实拍时用演示占位图)
|
||||
|
||||
---
|
||||
|
||||
@@ -69,3 +74,22 @@
|
||||
| 数据结构与对账 | 站端口径字段;**核对 vs 对账**双状态;PC 多导出 | [record-data-model.md](./record-data-model.md)、[../vehicle-h2-fee-ledger/.spec/verify-reconcile.md](../vehicle-h2-fee-ledger/.spec/verify-reconcile.md) |
|
||||
| 重复键 / 总额偏差 | 业务主键拦截;总额偏差二次确认 | [record-data-model.md](./record-data-model.md) §5–6 |
|
||||
| 来源 / 变更日志 / 操作人 | `recordSource`、行级 `changeLogs`、核对人/对账人 | [record-source-changelog.md](./record-source-changelog.md) |
|
||||
| 羚牛车辆与核对对账可见性 | 系统车辆列表判定;非羚牛字段空展示 | [fleet-vehicle-verify.md](./fleet-vehicle-verify.md) |
|
||||
| 手工台账门禁 | 昨日未传 → 今日禁新增;文案与跳转见规格 | [../oneos-h5-h2-order/.spec/feature-manual-ledger.md](../oneos-h5-h2-order/.spec/feature-manual-ledger.md) |
|
||||
|
||||
---
|
||||
|
||||
## 4. 右侧标注目录
|
||||
|
||||
打开原型右侧「原型标注 / 原型目录」,可阅读:
|
||||
|
||||
| 目录 | 内容 |
|
||||
|---|---|
|
||||
| PRD 全文 | [requirements-prd.md](./requirements-prd.md) |
|
||||
| 需求角色 | [roles.md](./roles.md) |
|
||||
| 用户故事 | [user-stories.md](./user-stories.md) |
|
||||
| 关键逻辑 | [key-logic.md](./key-logic.md) 及数据模型、车辆、来源、台账、对账规格 |
|
||||
| 流程图 | [flows.md](./flows.md)(Mermaid) |
|
||||
| 功能说明 | 列表、新增、台账、导出、每日统计等 |
|
||||
|
||||
由 `scripts/build-annotation-source.mjs` 从 `.spec/*.md` 生成 `annotation-source.json`。
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -4,11 +4,15 @@
|
||||
*/
|
||||
import '../../common/oneosWebLegacy/legacyGlobals';
|
||||
import '../../common/h2VehicleLedgerBridge.js';
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import '../vehicle-management/style.css';
|
||||
import Page2 from './pages/02-加氢记录.jsx';
|
||||
import { OneosWebLegacyShell } from '../../common/oneosWebLegacy/OneosWebLegacyShell';
|
||||
import { AnnotationViewer, type AnnotationSourceDocument } from '@axhub/annotation';
|
||||
import {
|
||||
type AnnotationSourceDocument,
|
||||
type AnnotationViewerOptions,
|
||||
} from '@axhub/annotation';
|
||||
import { PrototypeAnnotationHost } from '../../common/prototype-annotation-host';
|
||||
import annotationSourceDocument from './annotation-source.json';
|
||||
|
||||
const pages = [
|
||||
@@ -16,6 +20,18 @@ const pages = [
|
||||
];
|
||||
|
||||
export default function OneosWebH2Station() {
|
||||
const annotationOptions = useMemo<AnnotationViewerOptions>(
|
||||
() => ({
|
||||
currentPageId: 'h2-record',
|
||||
toolbarEdge: 'right',
|
||||
showToolbar: true,
|
||||
showThemeToggle: true,
|
||||
showColorFilter: true,
|
||||
emptyWhenNoData: false,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<OneosWebLegacyShell
|
||||
@@ -23,16 +39,9 @@ export default function OneosWebH2Station() {
|
||||
pages={pages}
|
||||
defaultPageId="h2-record"
|
||||
/>
|
||||
<AnnotationViewer
|
||||
source={annotationSourceDocument as unknown as AnnotationSourceDocument}
|
||||
options={{
|
||||
currentPageId: "h2-record",
|
||||
toolbarEdge: 'right',
|
||||
showToolbar: true,
|
||||
showThemeToggle: true,
|
||||
showColorFilter: true,
|
||||
emptyWhenNoData: true,
|
||||
}}
|
||||
<PrototypeAnnotationHost
|
||||
source={annotationSourceDocument as AnnotationSourceDocument}
|
||||
options={annotationOptions}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,22 +0,0 @@
|
||||
/**
|
||||
* @name 工作台
|
||||
* 自 ONE-OS web端 原稿复刻(1 个页面)
|
||||
*/
|
||||
import '../../common/oneosWebLegacy/legacyGlobals';
|
||||
import React from 'react';
|
||||
import Page1 from './pages/01-工作台.jsx';
|
||||
import { OneosWebLegacyShell } from '../../common/oneosWebLegacy/OneosWebLegacyShell';
|
||||
|
||||
const pages = [
|
||||
{ id: 'u5de5u4f5cu53f0', title: '工作台', component: Page1 },
|
||||
];
|
||||
|
||||
export default function OneosWebWorkbench() {
|
||||
return (
|
||||
<OneosWebLegacyShell
|
||||
moduleTitle="工作台"
|
||||
pages={pages}
|
||||
defaultPageId="u5de5u4f5cu53f0"
|
||||
/>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -39,8 +39,8 @@
|
||||
"fingerprint": "pr-list",
|
||||
"path": []
|
||||
},
|
||||
"aiPrompt": "新增、批量导入、导出与收款列表。",
|
||||
"annotationText": "列表含收款时间、汇款账户名、公司主体、银行、金额、备注、关联账单、未认领金额、操作人、操作时间;操作列支持关联账单。",
|
||||
"aiPrompt": "新增、批量导入、导出与收款列表;导入成功演示入账短信通知。",
|
||||
"annotationText": "列表含收款时间、汇款账户名、公司主体、银行、金额、备注、关联账单、未关联金额、操作人、操作时间;操作列支持关联账单。新增/批量导入成功后按账户名通知客户负责人与合同创建人(原型演示)。",
|
||||
"hasMarkdown": false,
|
||||
"color": "#0f766e",
|
||||
"images": [],
|
||||
@@ -104,6 +104,18 @@
|
||||
"id": "pr-doc-overview",
|
||||
"title": "模块总览",
|
||||
"markdown": "# 收款记录\n\n维护客户到账收款,支持关联租赁账单、自营账单、氢费账单。KPI 按收款记录统计:到账总金额 = 收款金额合计;认领总金额 = 已认领金额合计;未关联总金额 = 到账金额 − 已认领金额。"
|
||||
},
|
||||
{
|
||||
"type": "markdown",
|
||||
"id": "pr-doc-prd",
|
||||
"title": "产品需求摘要",
|
||||
"markdownPath": ".spec/requirements-prd.md"
|
||||
},
|
||||
{
|
||||
"type": "markdown",
|
||||
"id": "pr-doc-notify",
|
||||
"title": "入账短信通知",
|
||||
"markdownPath": ".spec/payment-notify.md"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
sumUnclaimedAmount,
|
||||
type PaymentKpiKey,
|
||||
} from './utils/payment';
|
||||
import { notifyPaymentEntrySms, notifyPaymentImportSms } from './utils/notify-recipients';
|
||||
import annotationSourceDocument from './annotation-source.json';
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
@@ -187,6 +188,7 @@ function PaymentRecordsContent() {
|
||||
setRecords((prev) => [row, ...prev]);
|
||||
setAddOpen(false);
|
||||
message.success('收款记录已新增(原型演示)');
|
||||
notifyPaymentEntrySms(row.customerName, message, { source: 'add' });
|
||||
};
|
||||
|
||||
const handleLinkBills = (recordId: string, linkedBills: LinkedBill[]) => {
|
||||
@@ -249,7 +251,13 @@ function PaymentRecordsContent() {
|
||||
type="button"
|
||||
className="vm-btn vm-btn-ghost ldb-toolbar-btn"
|
||||
data-vm-icon="import"
|
||||
onClick={() => message.info('批量导入:请下载模板后上传 Excel(原型演示)')}
|
||||
onClick={() => {
|
||||
const demoCustomers = filtered.slice(0, 3).map((row) => row.customerName);
|
||||
const names = demoCustomers.length > 0
|
||||
? demoCustomers
|
||||
: ['嘉兴港区韵达快递有限公司', '未知客户(演示未匹配)'];
|
||||
notifyPaymentImportSms(names, message);
|
||||
}}
|
||||
>
|
||||
批量导入
|
||||
</button>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
> 参照《2026年业务二部运营台账总表》物流业务明细子表;行内编辑与导入为原型 Toast 演示。
|
||||
> **界面设计规范**与租赁业务明细、租赁合同列表统一,见 `src/prototypes/ledger-shared/DESIGN.md`(`vm-page ldb-page`)。
|
||||
> **主路径升级**:调度任务办结自动生成明细行;导入降为补录。规则全文见 [ledger-auto-gen.md](./ledger-auto-gen.md)。
|
||||
> 假设:[assumptions.md](../../../resources/self-operated-logistics/assumptions.md)。
|
||||
|
||||
---
|
||||
|
||||
@@ -11,8 +13,9 @@
|
||||
|---|---|
|
||||
| 模块名称 | 台账数据 — 物流业务明细 |
|
||||
| 目标用户 | 业务服务组操作人、主管 |
|
||||
| 核心任务 | 记录出车营收与成本、自动汇总盈亏、导入导出与删改 |
|
||||
| 核心任务 | 承接调度自动出车明细、补录导入、自动汇总盈亏 |
|
||||
| 页面结构 | 筛选区 + KPI 统计 + 工具栏 + 宽表 + 分页 |
|
||||
| 上游 | 自营合同 → 轻量调度任务办结 |
|
||||
|
||||
---
|
||||
|
||||
@@ -73,3 +76,22 @@
|
||||
**总成本** = 氢费 + 人工报销费用 + ETC费用 + 电费 + 薪资 + 车辆费用。
|
||||
|
||||
**盈亏** = 金额 − 总成本;参照 A 股配色:**正数红色、负数绿色**。
|
||||
|
||||
---
|
||||
|
||||
## 6. 调度办结自动生成(摘要)
|
||||
|
||||
| 项 | 说明 |
|
||||
|---|---|
|
||||
| 业务目的 | 办结出车任务后自动落明细,减少手工导入 |
|
||||
| 触发 | `self-operated-dispatch-task` 办结成功(须车辆已交车) |
|
||||
| 粒度 | 一任务一行;备注前缀 `[调度自动·任务号]` |
|
||||
| 成本一期 | 营收侧自动;氢费/ETC 等默认 0,可补录 |
|
||||
| 数据源 | Bridge `ledger-auto` + 种子/导入行合并展示 |
|
||||
| 规格全文 | [ledger-auto-gen.md](./ledger-auto-gen.md) |
|
||||
|
||||
### 验收重点(自动生成)
|
||||
|
||||
- [ ] 调度办结后本页刷新可见新行(同浏览器 localStorage)
|
||||
- [ ] 自动行备注含任务号;金额/盈亏公式正确
|
||||
- [ ] 导入仍可用,不覆盖不同 id 的自动行
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"version": 2,
|
||||
"prototypeName": "self-operated-business-ledger",
|
||||
"pageId": "list",
|
||||
"updatedAt": 1783910257820,
|
||||
"updatedAt": 1784350346385,
|
||||
"nodes": [
|
||||
{
|
||||
"id": "sob-filter",
|
||||
@@ -25,7 +25,7 @@
|
||||
"color": "#2563eb",
|
||||
"images": [],
|
||||
"createdAt": 1740787200000,
|
||||
"updatedAt": 1783910257820
|
||||
"updatedAt": 1784350346385
|
||||
},
|
||||
{
|
||||
"id": "sob-kpi",
|
||||
@@ -45,7 +45,7 @@
|
||||
"color": "#64748b",
|
||||
"images": [],
|
||||
"createdAt": 1740787200000,
|
||||
"updatedAt": 1783910257820
|
||||
"updatedAt": 1784350346385
|
||||
},
|
||||
{
|
||||
"id": "sob-toolbar",
|
||||
@@ -65,7 +65,7 @@
|
||||
"color": "#0f766e",
|
||||
"images": [],
|
||||
"createdAt": 1740787200000,
|
||||
"updatedAt": 1783910257820
|
||||
"updatedAt": 1784350346385
|
||||
},
|
||||
{
|
||||
"id": "sob-table",
|
||||
@@ -85,7 +85,7 @@
|
||||
"color": "#2563eb",
|
||||
"images": [],
|
||||
"createdAt": 1740787200000,
|
||||
"updatedAt": 1783910257820
|
||||
"updatedAt": 1784350346385
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -93,7 +93,7 @@
|
||||
"sob-filter": "## 2. 筛选区\n\n默认展示 4 项,点击「更多筛选」展开。\n\n| 筛选项 | 说明 |\n|---|---|\n| 月份 | YYYY-MM |\n| 出车日期 | 日期区间 |\n| 业务名称 | 精确匹配 |\n| 车牌号码 | 多选 |\n| 车辆承接方 | 我司 / 第三方车队 |\n| 品牌 / 型号 | 级联 |\n| 维护人 | 仅主管可见 |\n\n---",
|
||||
"sob-kpi": "## 3. KPI 统计卡片(6 张)\n\n明细条数、金额合计、总成本合计、氢费总计、ETC费用总计、盈亏合计。\n\n卡片样式对齐租赁业务明细:左侧色条、图标、悬浮说明、盈亏正负着色。\n\n---",
|
||||
"sob-toolbar": "## 4. 列表工具栏\n\n| 按钮 | 行为 |\n|---|---|\n| 批量导入 | 下载模板(23 列手工录入)→ 上传 Excel → 系统车型/金额/总成本/盈亏自动计算 |\n| 导出 | 导出当前筛选结果(原型演示) |\n| 添加行 | 表格底栏新增行(原型演示) |\n\n不支持批量删除与行多选。\n\n### 导入模板列(23 列)\n\n年份、月份、出车日期、业务名称、司机、电话、车牌号码、单价、数量、氢费、ETC费用、薪资、电费、人工报销费用、日社保服务费、日挂车服务费、日挂车费用、日停车费用、日轮胎费用、车辆费用、是否多趟、线路计价、备注。\n\n**必填**:年份、月份、出车日期、业务名称、司机、电话、车牌号码、单价、数量、薪资、是否多趟、线路计价。\n\n**不含**(导入后自动计算/反写):系统车型、金额、总成本、盈亏。\n\n---",
|
||||
"sob-table": "## 5. 列表字段\n\n月份、出车日期、业务名称、司机、电话、车牌号码、系统车型、单价、数量、金额、氢费、ETC费用、薪资、电费、人工报销费用、日社保服务费、日挂车费用、日停车费用、日轮胎费用、车辆费用、总成本、盈亏、是否多趟、线路计价、备注、操作。\n\n**月份展示**:列表合并显示为 `YYYY-MM`;导入模板仍为「年份」「月份」两列,导入后自动合并展示。\n\n**系统车型**:按车牌从车辆主数据反写,展示为「品牌·型号」。\n\n**金额** = 单价 × 数量。\n\n**总成本** = 氢费 + 人工报销费用 + ETC费用 + 电费 + 薪资 + 车辆费用。\n\n**盈亏** = 金额 − 总成本;参照 A 股配色:**正数红色、负数绿色**。"
|
||||
"sob-table": "## 5. 列表字段\n\n月份、出车日期、业务名称、司机、电话、车牌号码、系统车型、单价、数量、金额、氢费、ETC费用、薪资、电费、人工报销费用、日社保服务费、日挂车费用、日停车费用、日轮胎费用、车辆费用、总成本、盈亏、是否多趟、线路计价、备注、操作。\n\n**月份展示**:列表合并显示为 `YYYY-MM`;导入模板仍为「年份」「月份」两列,导入后自动合并展示。\n\n**系统车型**:按车牌从车辆主数据反写,展示为「品牌·型号」。\n\n**金额** = 单价 × 数量。\n\n**总成本** = 氢费 + 人工报销费用 + ETC费用 + 电费 + 薪资 + 车辆费用。\n\n**盈亏** = 金额 − 总成本;参照 A 股配色:**正数红色、负数绿色**。\n\n---"
|
||||
},
|
||||
"assetMap": {},
|
||||
"directory": {
|
||||
@@ -108,7 +108,7 @@
|
||||
"type": "markdown",
|
||||
"id": "sob-doc-overview",
|
||||
"title": "模块总览",
|
||||
"markdown": "## 1. 模块定位\n\n| 项 | 说明 |\n|---|---|\n| 模块名称 | 台账数据 — 物流业务明细 |\n| 目标用户 | 业务服务组操作人、主管 |\n| 核心任务 | 记录出车营收与成本、自动汇总盈亏、导入导出与删改 |\n| 页面结构 | 筛选区 + KPI 统计 + 工具栏 + 宽表 + 分页 |\n\n---"
|
||||
"markdown": "## 1. 模块定位\n\n| 项 | 说明 |\n|---|---|\n| 模块名称 | 台账数据 — 物流业务明细 |\n| 目标用户 | 业务服务组操作人、主管 |\n| 核心任务 | 承接调度自动出车明细、补录导入、自动汇总盈亏 |\n| 页面结构 | 筛选区 + KPI 统计 + 工具栏 + 宽表 + 分页 |\n| 上游 | 自营合同 → 轻量调度任务办结 |\n\n---"
|
||||
},
|
||||
{
|
||||
"type": "link",
|
||||
@@ -121,7 +121,13 @@
|
||||
"type": "markdown",
|
||||
"id": "sob-doc-prd",
|
||||
"title": "PRD 全文",
|
||||
"markdown": "# 物流业务明细 · 产品需求说明(PRD)\n\n> 参照《2026年业务二部运营台账总表》物流业务明细子表;行内编辑与导入为原型 Toast 演示。 \n> **界面设计规范**与租赁业务明细、租赁合同列表统一,见 `src/prototypes/ledger-shared/DESIGN.md`(`vm-page ldb-page`)。\n\n---\n\n## 1. 模块定位\n\n| 项 | 说明 |\n|---|---|\n| 模块名称 | 台账数据 — 物流业务明细 |\n| 目标用户 | 业务服务组操作人、主管 |\n| 核心任务 | 记录出车营收与成本、自动汇总盈亏、导入导出与删改 |\n| 页面结构 | 筛选区 + KPI 统计 + 工具栏 + 宽表 + 分页 |\n\n---\n\n## 2. 筛选区\n\n默认展示 4 项,点击「更多筛选」展开。\n\n| 筛选项 | 说明 |\n|---|---|\n| 月份 | YYYY-MM |\n| 出车日期 | 日期区间 |\n| 业务名称 | 精确匹配 |\n| 车牌号码 | 多选 |\n| 车辆承接方 | 我司 / 第三方车队 |\n| 品牌 / 型号 | 级联 |\n| 维护人 | 仅主管可见 |\n\n---\n\n## 3. KPI 统计卡片(6 张)\n\n明细条数、金额合计、总成本合计、氢费总计、ETC费用总计、盈亏合计。\n\n卡片样式对齐租赁业务明细:左侧色条、图标、悬浮说明、盈亏正负着色。\n\n---\n\n## 4. 列表工具栏\n\n| 按钮 | 行为 |\n|---|---|\n| 批量导入 | 下载模板(23 列手工录入)→ 上传 Excel → 系统车型/金额/总成本/盈亏自动计算 |\n| 导出 | 导出当前筛选结果(原型演示) |\n| 添加行 | 表格底栏新增行(原型演示) |\n\n不支持批量删除与行多选。\n\n### 导入模板列(23 列)\n\n年份、月份、出车日期、业务名称、司机、电话、车牌号码、单价、数量、氢费、ETC费用、薪资、电费、人工报销费用、日社保服务费、日挂车服务费、日挂车费用、日停车费用、日轮胎费用、车辆费用、是否多趟、线路计价、备注。\n\n**必填**:年份、月份、出车日期、业务名称、司机、电话、车牌号码、单价、数量、薪资、是否多趟、线路计价。\n\n**不含**(导入后自动计算/反写):系统车型、金额、总成本、盈亏。\n\n---\n\n## 5. 列表字段\n\n月份、出车日期、业务名称、司机、电话、车牌号码、系统车型、单价、数量、金额、氢费、ETC费用、薪资、电费、人工报销费用、日社保服务费、日挂车费用、日停车费用、日轮胎费用、车辆费用、总成本、盈亏、是否多趟、线路计价、备注、操作。\n\n**月份展示**:列表合并显示为 `YYYY-MM`;导入模板仍为「年份」「月份」两列,导入后自动合并展示。\n\n**系统车型**:按车牌从车辆主数据反写,展示为「品牌·型号」。\n\n**金额** = 单价 × 数量。\n\n**总成本** = 氢费 + 人工报销费用 + ETC费用 + 电费 + 薪资 + 车辆费用。\n\n**盈亏** = 金额 − 总成本;参照 A 股配色:**正数红色、负数绿色**。"
|
||||
"markdown": "# 物流业务明细 · 产品需求说明(PRD)\n\n> 参照《2026年业务二部运营台账总表》物流业务明细子表;行内编辑与导入为原型 Toast 演示。 \n> **界面设计规范**与租赁业务明细、租赁合同列表统一,见 `src/prototypes/ledger-shared/DESIGN.md`(`vm-page ldb-page`)。 \n> **主路径升级**:调度任务办结自动生成明细行;导入降为补录。规则全文见 [ledger-auto-gen.md](./ledger-auto-gen.md)。 \n> 假设:[assumptions.md](../../../resources/self-operated-logistics/assumptions.md)。\n\n---\n\n## 1. 模块定位\n\n| 项 | 说明 |\n|---|---|\n| 模块名称 | 台账数据 — 物流业务明细 |\n| 目标用户 | 业务服务组操作人、主管 |\n| 核心任务 | 承接调度自动出车明细、补录导入、自动汇总盈亏 |\n| 页面结构 | 筛选区 + KPI 统计 + 工具栏 + 宽表 + 分页 |\n| 上游 | 自营合同 → 轻量调度任务办结 |\n\n---\n\n## 2. 筛选区\n\n默认展示 4 项,点击「更多筛选」展开。\n\n| 筛选项 | 说明 |\n|---|---|\n| 月份 | YYYY-MM |\n| 出车日期 | 日期区间 |\n| 业务名称 | 精确匹配 |\n| 车牌号码 | 多选 |\n| 车辆承接方 | 我司 / 第三方车队 |\n| 品牌 / 型号 | 级联 |\n| 维护人 | 仅主管可见 |\n\n---\n\n## 3. KPI 统计卡片(6 张)\n\n明细条数、金额合计、总成本合计、氢费总计、ETC费用总计、盈亏合计。\n\n卡片样式对齐租赁业务明细:左侧色条、图标、悬浮说明、盈亏正负着色。\n\n---\n\n## 4. 列表工具栏\n\n| 按钮 | 行为 |\n|---|---|\n| 批量导入 | 下载模板(23 列手工录入)→ 上传 Excel → 系统车型/金额/总成本/盈亏自动计算 |\n| 导出 | 导出当前筛选结果(原型演示) |\n| 添加行 | 表格底栏新增行(原型演示) |\n\n不支持批量删除与行多选。\n\n### 导入模板列(23 列)\n\n年份、月份、出车日期、业务名称、司机、电话、车牌号码、单价、数量、氢费、ETC费用、薪资、电费、人工报销费用、日社保服务费、日挂车服务费、日挂车费用、日停车费用、日轮胎费用、车辆费用、是否多趟、线路计价、备注。\n\n**必填**:年份、月份、出车日期、业务名称、司机、电话、车牌号码、单价、数量、薪资、是否多趟、线路计价。\n\n**不含**(导入后自动计算/反写):系统车型、金额、总成本、盈亏。\n\n---\n\n## 5. 列表字段\n\n月份、出车日期、业务名称、司机、电话、车牌号码、系统车型、单价、数量、金额、氢费、ETC费用、薪资、电费、人工报销费用、日社保服务费、日挂车费用、日停车费用、日轮胎费用、车辆费用、总成本、盈亏、是否多趟、线路计价、备注、操作。\n\n**月份展示**:列表合并显示为 `YYYY-MM`;导入模板仍为「年份」「月份」两列,导入后自动合并展示。\n\n**系统车型**:按车牌从车辆主数据反写,展示为「品牌·型号」。\n\n**金额** = 单价 × 数量。\n\n**总成本** = 氢费 + 人工报销费用 + ETC费用 + 电费 + 薪资 + 车辆费用。\n\n**盈亏** = 金额 − 总成本;参照 A 股配色:**正数红色、负数绿色**。\n\n---\n\n## 6. 调度办结自动生成(摘要)\n\n| 项 | 说明 |\n|---|---|\n| 业务目的 | 办结出车任务后自动落明细,减少手工导入 |\n| 触发 | `self-operated-dispatch-task` 办结成功(须车辆已交车) |\n| 粒度 | 一任务一行;备注前缀 `[调度自动·任务号]` |\n| 成本一期 | 营收侧自动;氢费/ETC 等默认 0,可补录 |\n| 数据源 | Bridge `ledger-auto` + 种子/导入行合并展示 |\n| 规格全文 | [ledger-auto-gen.md](./ledger-auto-gen.md) |\n\n### 验收重点(自动生成)\n\n- [ ] 调度办结后本页刷新可见新行(同浏览器 localStorage)\n- [ ] 自动行备注含任务号;金额/盈亏公式正确\n- [ ] 导入仍可用,不覆盖不同 id 的自动行"
|
||||
},
|
||||
{
|
||||
"type": "markdown",
|
||||
"id": "sob-doc-auto-gen",
|
||||
"title": "调度自动生成",
|
||||
"markdown": "# 物流业务明细 · 调度办结自动生成\n\n> 实现:`src/common/selfOperatedLogisticsBridge.js` → `completeDispatchTask` / `mergeLedgerWithAutoRows` \n> UI:`self-operated-business-ledger/index.tsx` 合并自动行;触发页在 `self-operated-dispatch-task`\n\n## 业务目的\n\n调度任务办结后自动生成 1 行出车明细,减少人工导入;导入/行编辑保留为补录与纠错。\n\n## 触发条件\n\n| 顺序 | 条件 | 说明 |\n|---|---|---|\n| 1 | 调度任务办结成功 | 见 `dispatch-rules.md` 办结判定 |\n| 2 | 车辆已交车 | 运营态 = self_operated |\n| 3 | 幂等 | 同一 `dispatchTaskId` 覆盖旧自动行 |\n\n## 数据源\n\n| 项 | 说明 |\n|---|---|\n| 自动行存储 | localStorage `oneos.self-operated.ledger-auto.v1` |\n| 列表合并 | `mergeLedgerWithAutoRows(seedOrLocalRows)`:自动行置顶,同 id 去重 |\n| 种子 JSON | `data/rows.json` 仍作历史/演示导入数据 |\n\n原型本地种子 vs **未接真实 API**。\n\n## 用户可见结果\n\n| 结果 | 表现 |\n|---|---|\n| 生成成功 | 调度页 Toast;台账备注前缀 `[调度自动·DD-xxxx]` |\n| 未交车 | 调度页拦截,台账无新行 |\n| 成本一期 | 营收侧(单价/数量/金额/薪资)带出;氢费/ETC 等为 0,可手工补 |\n\n## 与导入关系\n\n| 路径 | 优先级 |\n|---|---|\n| 调度自动生成 | 主路径 |\n| Excel 批量导入 | 补录/历史 |\n| 添加行 | 补录演示 |\n\n取消已办结任务:**禁止**(避免台账孤儿行)。若未来支持作废,须同步删除或标记自动行(本期不做)。\n\n## 公式(与现网一致)\n\n- 金额 = 单价 × 数量 \n- 总成本 = 氢费 + 人工报销 + ETC + 电费 + 薪资 + 车辆费用 \n- 盈亏 = 金额 − 总成本"
|
||||
},
|
||||
{
|
||||
"type": "markdown",
|
||||
@@ -145,7 +151,7 @@
|
||||
"type": "markdown",
|
||||
"id": "sob-doc-sob-table",
|
||||
"title": "物流业务明细宽表",
|
||||
"markdown": "## 5. 列表字段\n\n月份、出车日期、业务名称、司机、电话、车牌号码、系统车型、单价、数量、金额、氢费、ETC费用、薪资、电费、人工报销费用、日社保服务费、日挂车费用、日停车费用、日轮胎费用、车辆费用、总成本、盈亏、是否多趟、线路计价、备注、操作。\n\n**月份展示**:列表合并显示为 `YYYY-MM`;导入模板仍为「年份」「月份」两列,导入后自动合并展示。\n\n**系统车型**:按车牌从车辆主数据反写,展示为「品牌·型号」。\n\n**金额** = 单价 × 数量。\n\n**总成本** = 氢费 + 人工报销费用 + ETC费用 + 电费 + 薪资 + 车辆费用。\n\n**盈亏** = 金额 − 总成本;参照 A 股配色:**正数红色、负数绿色**。"
|
||||
"markdown": "## 5. 列表字段\n\n月份、出车日期、业务名称、司机、电话、车牌号码、系统车型、单价、数量、金额、氢费、ETC费用、薪资、电费、人工报销费用、日社保服务费、日挂车费用、日停车费用、日轮胎费用、车辆费用、总成本、盈亏、是否多趟、线路计价、备注、操作。\n\n**月份展示**:列表合并显示为 `YYYY-MM`;导入模板仍为「年份」「月份」两列,导入后自动合并展示。\n\n**系统车型**:按车牌从车辆主数据反写,展示为「品牌·型号」。\n\n**金额** = 单价 × 数量。\n\n**总成本** = 氢费 + 人工报销费用 + ETC费用 + 电费 + 薪资 + 车辆费用。\n\n**盈亏** = 金额 − 总成本;参照 A 股配色:**正数红色、负数绿色**。\n\n---"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,14 +5,18 @@
|
||||
import '../vehicle-management/style.css';
|
||||
import '../lease-business-ledger/styles/index.css';
|
||||
import './styles/index.css';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { Download, FileUp, Plus } from 'lucide-react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
type AnnotationDirectoryRouteNode,
|
||||
type AnnotationSourceDocument,
|
||||
type AnnotationViewerOptions
|
||||
} from '@axhub/annotation';
|
||||
import { PrototypeAnnotationHost } from '../../common/prototype-annotation-host';
|
||||
import {
|
||||
mergeLedgerWithAutoRows,
|
||||
removeAutoLedgerRow,
|
||||
subscribeSelfOperatedBridge,
|
||||
} from '../../common/selfOperatedLogisticsBridge.js';
|
||||
import seedRows from './data/rows.json';
|
||||
import { FilterPanel } from './components/FilterPanel';
|
||||
import { LedgerKpiRow } from './components/LedgerKpiRow';
|
||||
@@ -36,8 +40,21 @@ import annotationSourceDocument from './annotation-source.json';
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
function loadMergedRecords(): SelfOperatedLedgerRow[] {
|
||||
return mergeLedgerWithAutoRows(seedRows as SelfOperatedLedgerRow[]) as SelfOperatedLedgerRow[];
|
||||
}
|
||||
|
||||
export default function SelfOperatedBusinessLedgerApp() {
|
||||
const [records, setRecords] = useState<SelfOperatedLedgerRow[]>(seedRows as SelfOperatedLedgerRow[]);
|
||||
const [baseRecords, setBaseRecords] = useState<SelfOperatedLedgerRow[]>(
|
||||
() => seedRows as SelfOperatedLedgerRow[],
|
||||
);
|
||||
const [records, setRecords] = useState<SelfOperatedLedgerRow[]>(() => loadMergedRecords());
|
||||
|
||||
useEffect(() => {
|
||||
const sync = () => setRecords(mergeLedgerWithAutoRows(baseRecords) as SelfOperatedLedgerRow[]);
|
||||
sync();
|
||||
return subscribeSelfOperatedBridge(sync);
|
||||
}, [baseRecords]);
|
||||
const [pendingFilters, setPendingFilters] = useState<SelfOperatedFilters>(EMPTY_SELF_FILTERS);
|
||||
const [appliedFilters, setAppliedFilters] = useState<SelfOperatedFilters>(EMPTY_SELF_FILTERS);
|
||||
const [page, setPage] = useState(1);
|
||||
@@ -89,7 +106,7 @@ export default function SelfOperatedBusinessLedgerApp() {
|
||||
}
|
||||
|
||||
if (result.success.length > 0) {
|
||||
setRecords((prev) => [...result.success, ...prev]);
|
||||
setBaseRecords((prev) => [...result.success, ...prev]);
|
||||
setPage(1);
|
||||
}
|
||||
|
||||
@@ -132,6 +149,22 @@ export default function SelfOperatedBusinessLedgerApp() {
|
||||
|
||||
<LedgerKpiRow summary={kpi} />
|
||||
|
||||
<p
|
||||
data-annotation-id="sob-autogen-hint"
|
||||
style={{
|
||||
margin: '0 0 10px',
|
||||
padding: '10px 12px',
|
||||
borderRadius: 8,
|
||||
background: '#f0fdf4',
|
||||
border: '1px solid #bbf7d0',
|
||||
color: '#166534',
|
||||
fontSize: 13,
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
主路径:调度任务办结自动生成明细(备注含「调度自动」);批量导入为补录。可先打开「调度任务」办结种子单 DD-2026-0001 演示。
|
||||
</p>
|
||||
|
||||
<section className="vm-table-section">
|
||||
<div className="vm-table-toolbar ldb-table-toolbar" data-annotation-id="sob-toolbar">
|
||||
<div className="vm-table-actions">
|
||||
@@ -192,7 +225,11 @@ export default function SelfOperatedBusinessLedgerApp() {
|
||||
onCancel={() => setDeleteTarget(null)}
|
||||
onConfirm={() => {
|
||||
if (!deleteTarget) return;
|
||||
setRecords((prev) => prev.filter((row) => row.id !== deleteTarget.id));
|
||||
if (deleteTarget.rowSource === 'auto' || String(deleteTarget.id).startsWith('auto-')) {
|
||||
removeAutoLedgerRow(deleteTarget.id);
|
||||
} else {
|
||||
setBaseRecords((prev) => prev.filter((row) => row.id !== deleteTarget.id));
|
||||
}
|
||||
setDeleteTarget(null);
|
||||
showToast(`已删除 ${deleteTarget.plateNo}(原型)`);
|
||||
}}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { fileURLToPath } from 'node:url';
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const protoRoot = path.resolve(__dirname, '..');
|
||||
const prd = fs.readFileSync(path.join(protoRoot, '.spec/requirements-prd.md'), 'utf8');
|
||||
const autoGen = fs.readFileSync(path.join(protoRoot, '.spec/ledger-auto-gen.md'), 'utf8');
|
||||
|
||||
const NODE_DEFS = [
|
||||
{
|
||||
@@ -112,6 +113,7 @@ const source = {
|
||||
target: 'self',
|
||||
},
|
||||
{ type: 'markdown', id: 'sob-doc-prd', title: 'PRD 全文', markdown: prd.trim() },
|
||||
{ type: 'markdown', id: 'sob-doc-auto-gen', title: '调度自动生成', markdown: autoGen.trim() },
|
||||
...nodes.map((node) => ({
|
||||
type: 'markdown',
|
||||
id: `sob-doc-${node.id}`,
|
||||
|
||||
@@ -29,6 +29,11 @@ export interface SelfOperatedLedgerRow {
|
||||
multiTrip: string;
|
||||
routePricing: string;
|
||||
remark: string;
|
||||
/** seed | import | auto(调度办结) */
|
||||
rowSource?: 'seed' | 'import' | 'auto';
|
||||
dispatchTaskId?: string;
|
||||
dispatchTaskCode?: string;
|
||||
contractCode?: string;
|
||||
}
|
||||
|
||||
export interface SelfOperatedFilters {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
| 产品模块 | 台账数据 → 车辆氢费明细 |
|
||||
| 文档类型 | 产品需求说明 |
|
||||
| 适用读者 | 产品、业务、运营、测试、项目 |
|
||||
| 修订说明 | v2.5:记录来源标签(站端上传/羚牛上传);核对人/对账人;变更日志挂行级字段 |
|
||||
| 修订说明 | v2.6:车牌「羚牛车辆/非羚牛车辆」;非羚牛车辆核对/对账字段空展示 |
|
||||
| 双状态规格 | [verify-reconcile.md](./verify-reconcile.md) |
|
||||
|
||||
---
|
||||
@@ -162,7 +162,9 @@ flowchart LR
|
||||
- **说明:** 合计条含「待保存 / 未对账 / 已对账」等当前列表中的全部可见记录,与导出范围(仅未对账+已对账)不同。
|
||||
|
||||
**列表列顺序(左→右,业务视角):**
|
||||
序号 → 加氢日期 → 加氢时间 → 加氢站名称 → 客户名称 → 车牌号(下来源标签:站端上传 / 羚牛上传)→ 加氢量 → 成本单价 → 成本总价 → 加氢单价 → 加氢总价 → 行驶里程 → 备注 → 业务员 → 承担方式 → 对账日期 → **对账人** → **收票日期** → **加氢站付款状态** → **开票日期** → **客户收款状态** → **开票公司** → 核对状态 → **核对时间** → **核对人** → 对账状态 → 订单编号 → 操作(操作列固定右侧)。
|
||||
序号 → 加氢日期 → 加氢时间 → 加氢站名称 → 客户名称 → 车牌号(附**羚牛车辆/非羚牛车辆** + 来源:站端上传/羚牛上传)→ 加氢量 → 成本单价 → 成本总价 → 加氢单价 → 加氢总价 → 行驶里程 → 备注 → 业务员 → 承担方式 → 对账日期 → **对账人** → **收票日期** → **加氢站付款状态** → **开票日期** → **客户收款状态** → **开票公司** → 核对状态 → **核对时间** → **核对人** → 对账状态 → 订单编号 → 操作(操作列固定右侧)。
|
||||
|
||||
> 非羚牛车辆:核对状态/时间/人、对账状态/日期/人显示为空。判定见 [../oneos-web-h2-station/.spec/fleet-vehicle-verify.md](../oneos-web-h2-station/.spec/fleet-vehicle-verify.md)。
|
||||
|
||||
**系统带出字段(不可手工维护):** 收票日期、加氢站付款状态、开票日期、客户收款状态、开票公司;选择/变更客户等信息后由系统自动刷新,单元格为浅灰底只读样式。
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
|---|---|---|
|
||||
| 1 | 加氢站(H5/Web)新增加氢记录 | 写入共享 Store:`verifyStatus=unverified`,`reconcileStatus=pending`;按站点成本价补 `costUnitPrice`,`costTotal≈单价×量`;同步出现在车辆氢费明细 |
|
||||
| 2 | 能源部在车辆氢费明细确认无误 → **完成核对** | `verifyStatus=verified`,写 `verifiedAt`;站端列表同步「已核对」;**业务员侧不可再改删**(原型规则) |
|
||||
| 3 | 站点信息生成并对账单 **提交** | 仅纳入:**已核对** ∧ **未对账** ∧ 无 `statementRecordId` ∧ 本站 ∧ 账期;提交后 `reconcileStatus=reconciled`,写 `reconcileDate`、`statementRecordId` 等 |
|
||||
| 3 | 站点信息生成并对账单 **提交**(须填收票信息 + **对账单盖章附件**,二次确认) | 仅纳入:**已核对** ∧ **未对账** ∧ 无 `statementRecordId` ∧ 本站 ∧ 账期;提交后三端(台账 / 加氢记录 Web / H5)`reconcileStatus=reconciled`,写 `reconcileDate`、`statementRecordId` 等 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -882,7 +882,9 @@ const Component = function () {
|
||||
'沪AD12345F',
|
||||
'苏EF99887F',
|
||||
'京CN88771F',
|
||||
'川AL55602F'
|
||||
'川AL55602F',
|
||||
/* 非羚牛车辆演示(不在系统车辆列表 / Bridge 车队集合) */
|
||||
'粤Z99999F'
|
||||
];
|
||||
|
||||
/** 原型:按车牌+加氢日期匹配车辆运营态(交还车/库存/异动),联调后接车辆管理 */
|
||||
@@ -2985,6 +2987,28 @@ const Component = function () {
|
||||
return '羚牛上传';
|
||||
}
|
||||
|
||||
function isLingniuVehiclePlate(plateNo) {
|
||||
if (typeof window !== 'undefined' && window.H2VehicleLedgerBridge && typeof window.H2VehicleLedgerBridge.isLingniuVehicle === 'function') {
|
||||
return Boolean(window.H2VehicleLedgerBridge.isLingniuVehicle(plateNo));
|
||||
}
|
||||
var key = String(plateNo || '').trim().toUpperCase().replace(/F$/u, '');
|
||||
if (!key) return false;
|
||||
/* 明确非羚牛演示车牌(未进 Bridge 车队集合) */
|
||||
if (key === '粤Z99999' || key === '浙C77801') return false;
|
||||
return VEHICLE_INFO_TABLE.some(function (p) {
|
||||
return String(p).toUpperCase().replace(/F$/u, '') === key;
|
||||
});
|
||||
}
|
||||
|
||||
function vehicleKindLabel(plateNo) {
|
||||
if (!String(plateNo || '').trim()) return '';
|
||||
return isLingniuVehiclePlate(plateNo) ? '羚牛车辆' : '非羚牛车辆';
|
||||
}
|
||||
|
||||
function showVerifyReconcileFields(row) {
|
||||
return isLingniuVehiclePlate(row && row.plateNo);
|
||||
}
|
||||
|
||||
function recalcRowFields(row) {
|
||||
var mileage = normalizeMileageKm(row.mileageKm);
|
||||
var kg = isEmptyNum(row.hydrogenKg) ? null : roundMoney(row.hydrogenKg);
|
||||
@@ -3211,31 +3235,40 @@ const Component = function () {
|
||||
costTotal: 3168,
|
||||
remark: '成本单价与站点标准价不一致演示'
|
||||
});
|
||||
pushDemoRow('mock-h2-external-plate', {
|
||||
plateNo: '粤Z99999F',
|
||||
hydrogenTime: demoDay && demoDay.subtract ? demoDay.subtract(3, 'day') : demoDay,
|
||||
customerId: '00007',
|
||||
customerName: '上海迅杰物流有限公司',
|
||||
salesperson: '金可鹏',
|
||||
recordSource: 'station',
|
||||
remark: '非羚牛车辆演示:核对/对账字段应显示为空'
|
||||
});
|
||||
return recalcAllRows(rows);
|
||||
}
|
||||
|
||||
var layoutStyle = {
|
||||
padding: '16px 24px 24px',
|
||||
padding: '16px 24px 48px',
|
||||
minHeight: '100vh',
|
||||
background: 'linear-gradient(165deg, #eef4ff 0%, #f5f7fa 42%, #f0f2f5 100%)'
|
||||
background: 'var(--ln-canvas-parchment, #f5f6f6)'
|
||||
};
|
||||
|
||||
var filterLabelStyle = { marginBottom: 6, fontSize: 13, color: 'rgba(0,0,0,0.55)', fontWeight: 500 };
|
||||
var filterLabelStyle = { marginBottom: 6, fontSize: 13, color: 'var(--ln-muted, #71717a)', fontWeight: 500 };
|
||||
var filterItemStyle = { marginBottom: 12 };
|
||||
var filterControlStyle = { width: '100%' };
|
||||
var filterCardStyle = {
|
||||
marginBottom: 20,
|
||||
borderRadius: 16,
|
||||
boxShadow: '0 4px 20px -4px rgba(16,24,40,0.03), 0 0 0 1px rgba(16,24,40,0.06)',
|
||||
border: 'none',
|
||||
background: '#ffffff'
|
||||
marginBottom: 16,
|
||||
borderRadius: 'var(--ln-radius-card, 12px)',
|
||||
boxShadow: '0 1px 2px rgba(15,23,42,0.04), 0 4px 12px rgba(15,23,42,0.03)',
|
||||
border: '1px solid var(--ln-hairline, #e5e7eb)',
|
||||
background: 'var(--ln-surface-card, #ffffff)'
|
||||
};
|
||||
|
||||
var tableCardStyle = {
|
||||
borderRadius: 16,
|
||||
boxShadow: '0 10px 32px -4px rgba(16,24,40,0.06), 0 0 0 1px rgba(16,24,40,0.04)',
|
||||
border: 'none',
|
||||
background: '#ffffff',
|
||||
borderRadius: 'var(--ln-radius-card, 12px)',
|
||||
boxShadow: '0 1px 2px rgba(15,23,42,0.04), 0 4px 12px rgba(15,23,42,0.03)',
|
||||
border: '1px solid var(--ln-hairline, #e5e7eb)',
|
||||
background: 'var(--ln-surface-card, #ffffff)',
|
||||
overflow: 'hidden'
|
||||
};
|
||||
|
||||
@@ -3261,22 +3294,24 @@ const Component = function () {
|
||||
|
||||
// 异常行水印用 background-image,避免 td::after + position 破坏固定列行高同步
|
||||
var ledgerTableStyle =
|
||||
'.h2-ledger-table-wrap{border-radius:12px;overflow:hidden;box-shadow:0 4px 24px -6px rgba(15,23,42,0.05),0 0 0 1px rgba(22,119,255,0.1)}' +
|
||||
'.h2-ledger-table .ant-table-thead>tr>th,.h2-ledger-table .ant-table-thead .ant-table-cell{white-space:nowrap;color:#0f172a!important;font-weight:600!important;font-size:12px!important;' +
|
||||
'background:#e8f4fc!important;border-bottom:1px solid #bae6fd!important;border-inline-end:1px solid #dbeafe!important;padding:0 6px!important;height:36px!important;text-align:center!important}' +
|
||||
'.h2-ledger-table .ant-table-tbody>tr:not(.ant-table-measure-row)>td{padding:4px 6px!important;vertical-align:middle!important;font-size:12px}' +
|
||||
'.h2-seq-cell{line-height:1.35;white-space:nowrap}' +
|
||||
'.h2-ledger-table .ant-table-tbody>tr.h2-row-data:hover>td{background:#f0f9ff!important}' +
|
||||
'.h2-ledger-totals-bar{display:flex;align-items:stretch;gap:0;margin-bottom:10px;border:1px solid #bae6fd;border-radius:10px;overflow:hidden;background:#f8fafc;box-shadow:0 1px 0 rgba(15,23,42,0.04)}' +
|
||||
'.h2-ledger-totals-bar__title{display:flex;align-items:center;justify-content:center;min-width:72px;padding:10px 14px;font-size:14px;font-weight:700;color:#0f172a;background:#e8f4fc;border-right:1px solid #bae6fd}' +
|
||||
'.vh2-page{font-family:Inter,system-ui,-apple-system,sans-serif}' +
|
||||
'.vh2-plate-cell .ant-select{min-width:120px}' +
|
||||
'.h2-ledger-table-wrap{border-radius:var(--ln-radius-card,12px);overflow:hidden;box-shadow:none;border:1px solid var(--ln-hairline,#e5e7eb)}' +
|
||||
'.h2-ledger-table .ant-table-thead>tr>th,.h2-ledger-table .ant-table-thead .ant-table-cell{white-space:nowrap;color:var(--ln-muted,#71717a)!important;font-weight:500!important;font-size:12px!important;' +
|
||||
'background:var(--ln-canvas-soft,#f4f4f5)!important;border-bottom:1px solid var(--ln-hairline,#e5e7eb)!important;border-inline-end:none!important;padding:10px 8px!important;height:auto!important;text-align:center!important}' +
|
||||
'.h2-ledger-table .ant-table-tbody>tr:not(.ant-table-measure-row)>td{padding:8px 8px!important;vertical-align:middle!important;font-size:13px;color:var(--ln-ink,#18181b)}' +
|
||||
'.h2-seq-cell{line-height:1.35;white-space:nowrap;font-variant-numeric:tabular-nums}' +
|
||||
'.h2-ledger-table .ant-table-tbody>tr.h2-row-data:hover>td{background:color-mix(in srgb,var(--ln-primary,#32a06e) 6%,#fff)!important}' +
|
||||
'.h2-ledger-totals-bar{display:flex;align-items:stretch;gap:0;margin-bottom:12px;border:1px solid var(--ln-hairline,#e5e7eb);border-radius:var(--ln-radius-card,12px);overflow:hidden;background:var(--ln-surface-card,#fff);box-shadow:0 1px 2px rgba(15,23,42,0.04)}' +
|
||||
'.h2-ledger-totals-bar__title{display:flex;align-items:center;justify-content:center;min-width:72px;padding:10px 14px;font-size:14px;font-weight:600;color:var(--ln-ink,#18181b);background:var(--ln-canvas-soft,#f4f4f5);border-right:1px solid var(--ln-hairline,#e5e7eb)}' +
|
||||
'.h2-ledger-totals-bar__items{display:flex;flex:1;flex-wrap:wrap}' +
|
||||
'.h2-ledger-totals-bar__item{flex:1;min-width:160px;padding:8px 20px;border-right:1px solid #e2e8f0;display:flex;flex-direction:column;justify-content:center;gap:4px}' +
|
||||
'.h2-ledger-totals-bar__item{flex:1;min-width:160px;padding:8px 20px;border-right:1px solid var(--ln-hairline,#e5e7eb);display:flex;flex-direction:column;justify-content:center;gap:4px}' +
|
||||
'.h2-ledger-totals-bar__item:last-child{border-right:none}' +
|
||||
'.h2-ledger-totals-bar__label{font-size:12px;color:rgba(15,23,42,0.55);font-weight:500;line-height:1.2}' +
|
||||
'.h2-ledger-totals-bar__value{font-size:16px;font-weight:700;color:#0f172a;font-variant-numeric:tabular-nums;line-height:1.3}' +
|
||||
'.h2-cell-readonly{color:#64748b;background:#f8fafc;padding:4px 6px}' +
|
||||
'.h2-cell-auto{color:#475569;background:#f1f5f9;padding:4px 6px}' +
|
||||
'.h2-cell-warn{color:#874d00;background:#fffbe6!important;border:1px solid #ffe58f}' +
|
||||
'.h2-ledger-totals-bar__label{font-size:12px;color:var(--ln-muted,#71717a);font-weight:500;line-height:1.2}' +
|
||||
'.h2-ledger-totals-bar__value{font-size:16px;font-weight:700;color:var(--ln-ink,#18181b);font-variant-numeric:tabular-nums;line-height:1.3}' +
|
||||
'.h2-cell-readonly{color:var(--ln-muted,#71717a);background:var(--ln-canvas-soft,#f4f4f5);padding:4px 6px;border-radius:6px}' +
|
||||
'.h2-cell-auto{color:var(--ln-body,#52525b);background:var(--ln-surface-strong,#f4f4f5);padding:4px 6px;border-radius:6px}' +
|
||||
'.h2-cell-warn{color:#874d00;background:#fffbe6!important;border:1px solid #ffe58f;border-radius:6px}' +
|
||||
'.h2-customer-warn-cell,.h2-cost-warn-cell{display:inline-flex;align-items:center;justify-content:center;gap:4px;max-width:100%}' +
|
||||
'.h2-cost-warn-cell{justify-content:flex-end;gap:4px}' +
|
||||
'.h2-cost-price-type-tag{flex-shrink:0}' +
|
||||
@@ -3322,11 +3357,11 @@ const Component = function () {
|
||||
'.h2-warn-cost-pop .ant-popover-inner-content{padding:10px 12px}' +
|
||||
'.h2-warn-input-wrap .ant-input-number,.h2-warn-input-wrap .ant-input-number-input{background:#fffbe6!important}' +
|
||||
'.h2-ledger-table .ant-picker.h2-datetime-picker{min-width:178px}' +
|
||||
'.h2-row-tier-0>td{background:#f8fafc!important}' +
|
||||
'.h2-row-tier-0>td{background:var(--ln-canvas-soft,#f8fafc)!important}' +
|
||||
'.h2-row-tier-2>td{background:#fffbeb!important}' +
|
||||
'.h2-row-tier-boundary>td{border-top:2px solid #7dd3fc!important}' +
|
||||
'.h2-row-tier-boundary>td{border-top:2px solid color-mix(in srgb,var(--ln-primary,#32a06e) 35%,#fff)!important}' +
|
||||
'.h2-action-icon-btn{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:4px;cursor:pointer;color:rgba(15,23,42,0.55);transition:background .15s,color .15s}' +
|
||||
'.h2-action-icon-btn:hover{background:#f0f9ff;color:#1677ff}' +
|
||||
'.h2-action-icon-btn:hover{background:color-mix(in srgb,var(--ln-primary,#32a06e) 10%,#fff);color:var(--ln-primary,#32a06e)}' +
|
||||
'.h2-action-icon-btn.h2-action-icon-danger:hover{background:#fff1f0;color:#ff4d4f}' +
|
||||
'.h2-action-icon-btn.is-disabled{color:rgba(15,23,42,0.25);cursor:not-allowed;pointer-events:none}' +
|
||||
'.h2-row-more-btn.h2-action-icon-btn:hover{background:#f5f5f5;color:rgba(15,23,42,0.75)}' +
|
||||
@@ -3864,9 +3899,11 @@ const Component = function () {
|
||||
var keySet = {};
|
||||
selectedRowKeys.forEach(function (k) { keySet[k] = true; });
|
||||
var selected = allRows.filter(function (r) { return keySet[r.key]; });
|
||||
var invalid = selected.filter(function (r) { return !isPendingRow(r) || isVerifiedRow(r); });
|
||||
var invalid = selected.filter(function (r) {
|
||||
return !isPendingRow(r) || isVerifiedRow(r) || !showVerifyReconcileFields(r);
|
||||
});
|
||||
if (invalid.length) {
|
||||
message.warning('仅可对「已保存且未核对」的记录完成核对');
|
||||
message.warning('仅可对「已保存且未核对」的羚牛车辆记录完成核对');
|
||||
return;
|
||||
}
|
||||
var verifiedAt = nowDayjs();
|
||||
@@ -4619,14 +4656,21 @@ const Component = function () {
|
||||
title: reqTitle('车牌号'),
|
||||
dataIndex: 'plateNo',
|
||||
key: 'plateNo',
|
||||
width: 132,
|
||||
width: 220,
|
||||
align: 'center',
|
||||
render: function (v, record) {
|
||||
var source = recordSourceLabel(record);
|
||||
var tag = React.createElement(Tag, {
|
||||
var kind = vehicleKindLabel(v);
|
||||
var sourceTag = React.createElement(Tag, {
|
||||
color: source === '站端上传' ? 'green' : 'blue',
|
||||
style: { margin: 0, borderRadius: 4, fontSize: 11, lineHeight: '18px', fontWeight: 600 }
|
||||
}, source);
|
||||
var kindTag = kind
|
||||
? React.createElement(Tag, {
|
||||
color: isLingniuVehiclePlate(v) ? 'green' : 'orange',
|
||||
style: { margin: 0, borderRadius: 4, fontSize: 11, lineHeight: '18px', fontWeight: 600 }
|
||||
}, kind)
|
||||
: null;
|
||||
var plateNode = !canEditRow(record)
|
||||
? readOnlyCell(v)
|
||||
: React.createElement(Select, {
|
||||
@@ -4641,9 +4685,24 @@ const Component = function () {
|
||||
notFoundContent: '车辆不存在',
|
||||
onChange: function (val) { patch(record.key, 'plateNo', val); }
|
||||
});
|
||||
return React.createElement('div', {
|
||||
style: { display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4 }
|
||||
}, plateNode, tag);
|
||||
return React.createElement(
|
||||
'div',
|
||||
{
|
||||
className: 'vh2-plate-cell',
|
||||
style: {
|
||||
display: 'inline-flex',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 4,
|
||||
maxWidth: '100%'
|
||||
}
|
||||
},
|
||||
plateNode,
|
||||
kindTag,
|
||||
sourceTag
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -4827,7 +4886,8 @@ const Component = function () {
|
||||
onHeaderCell: function () {
|
||||
return { 'data-annotation-id': 'vh2-col-reconcile-date' };
|
||||
},
|
||||
render: function (v) {
|
||||
render: function (v, record) {
|
||||
if (!showVerifyReconcileFields(record)) return readOnlyCell('—');
|
||||
if (!v) return readOnlyCell('-');
|
||||
var text = String(v);
|
||||
if (text.indexOf(':') >= 0) return readOnlyCell(text.length > 16 ? text.slice(0, 16) : text);
|
||||
@@ -4840,7 +4900,8 @@ const Component = function () {
|
||||
key: 'reconciledByName',
|
||||
width: 88,
|
||||
align: 'center',
|
||||
render: function (v) {
|
||||
render: function (v, record) {
|
||||
if (!showVerifyReconcileFields(record)) return readOnlyCell('—');
|
||||
return readOnlyCell(v || '—');
|
||||
}
|
||||
},
|
||||
@@ -4901,6 +4962,9 @@ const Component = function () {
|
||||
width: 88,
|
||||
align: 'center',
|
||||
render: function (_, record) {
|
||||
if (!showVerifyReconcileFields(record)) {
|
||||
return React.createElement('span', { style: { color: '#cbd5e1' } }, '—');
|
||||
}
|
||||
var label = rowVerifyLabel(record);
|
||||
if (!label) return React.createElement('span', { style: { color: '#cbd5e1' } }, '—');
|
||||
var color = label === '已核对' ? 'green' : 'orange';
|
||||
@@ -4913,7 +4977,8 @@ const Component = function () {
|
||||
key: 'verifiedAt',
|
||||
width: 148,
|
||||
align: 'center',
|
||||
render: function (v) {
|
||||
render: function (v, record) {
|
||||
if (!showVerifyReconcileFields(record)) return readOnlyCell('—');
|
||||
if (!v) return readOnlyCell('—');
|
||||
return readOnlyCell(formatDateTime(v) || '—');
|
||||
}
|
||||
@@ -4924,7 +4989,8 @@ const Component = function () {
|
||||
key: 'verifiedByName',
|
||||
width: 88,
|
||||
align: 'center',
|
||||
render: function (v) {
|
||||
render: function (v, record) {
|
||||
if (!showVerifyReconcileFields(record)) return readOnlyCell('—');
|
||||
return readOnlyCell(v || '—');
|
||||
}
|
||||
},
|
||||
@@ -4934,6 +5000,9 @@ const Component = function () {
|
||||
width: 88,
|
||||
align: 'center',
|
||||
render: function (_, record) {
|
||||
if (!showVerifyReconcileFields(record)) {
|
||||
return React.createElement('span', { style: { color: '#cbd5e1' } }, '—');
|
||||
}
|
||||
var label = rowStatusLabel(record);
|
||||
if (!label) return React.createElement('span', { style: { color: '#cbd5e1' } }, '—');
|
||||
var color = label === '已对账' ? 'blue' : 'default';
|
||||
@@ -5340,15 +5409,16 @@ const Component = function () {
|
||||
React.createElement('style', null, ledgerTableStyle),
|
||||
React.createElement(
|
||||
'div',
|
||||
{ style: layoutStyle },
|
||||
{ className: 'vm-page ldb-page vh2-page', style: layoutStyle, 'data-annotation-id': 'vh2-page' },
|
||||
React.createElement(
|
||||
'div',
|
||||
{
|
||||
'data-annotation-id': 'vh2-header',
|
||||
className: 'ldb-page-header',
|
||||
style: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
justifyContent: 'flex-end',
|
||||
marginBottom: 12,
|
||||
gap: 16
|
||||
}
|
||||
|
||||
@@ -209,12 +209,12 @@
|
||||
"path": []
|
||||
},
|
||||
"aiPrompt": "列表字段、行内编辑、客户校验与操作列。",
|
||||
"annotationText": "# 列表字段与操作\n\n**核心可编辑字段**(未对账/待保存):加氢时间、站点、车牌、加氢量、成本单价、加氢单价、承担方式等。\n\n**来源标签**(车牌下方):站端主动上报→「站端上传」;本页人工新增→「羚牛上传」。\n\n**系统带出只读**:业务员、开票/付款相关字段;核对时间/核对人(完成核对写入);对账日期/对账人(对账单提交写入)。\n\n## 成本单价列\n\n- 单价前展示 **固定 / 阶梯** 标签(与加氢站价格配置一致)\n- 阶梯价悬浮:当前阶梯量、对应单价\n- 校验图标:正确绿色 ✓ / 异常红色 ⚠\n- **一键替换为标准成本单价**(未对账;创建人或主管),写入变更日志\n\n## 客户名称列\n\n- 与系统登记不一致时整行标红,单元格水印「客户疑似有误」\n- 客户名称后 **警示图标**:悬浮显示异常说明(系统客户、交车时间等)\n- **一键替换为系统客户**(仅「与系统登记客户不一致」场景,未对账数据可操作)\n- 替换写入变更日志\n\n**行操作**(更多菜单):编辑、删除、复制多行、变更日志(时间/操作人/修改项/原值/新值)。\n\n**列表勾选**:仅未对账记录可勾选,用于批量完成核对。\n\n表头支持批量改价(成本单价、加氢单价、承担方式)。",
|
||||
"annotationText": "# 列表字段与操作\n\n**核心可编辑字段**(未对账/待保存):加氢时间、站点、车牌、加氢量、成本单价、加氢单价、承担方式等。\n\n**车辆类型**(车牌下方):系统车辆→「羚牛车辆」,否则→「非羚牛车辆」。\n\n**来源标签**:站端主动上报→「站端上传」;本页人工新增→「羚牛上传」。\n\n**非羚牛车辆**:核对状态/时间/人、对账状态/日期/人显示为空;不可完成核对。\n\n**系统带出只读**:业务员、开票/付款相关字段;核对时间/核对人(完成核对写入);对账日期/对账人(对账单提交写入)。\n\n## 成本单价列\n\n- 单价前展示 **固定 / 阶梯** 标签(与加氢站价格配置一致)\n- 阶梯价悬浮:当前阶梯量、对应单价\n- 校验图标:正确绿色 ✓ / 异常红色 ⚠\n- **一键替换为标准成本单价**(未对账;创建人或主管),写入变更日志\n\n## 客户名称列\n\n- 与系统登记不一致时整行标红,单元格水印「客户疑似有误」\n- 客户名称后 **警示图标**:悬浮显示异常说明(系统客户、交车时间等)\n- **一键替换为系统客户**(仅「与系统登记客户不一致」场景,未对账数据可操作)\n- 替换写入变更日志\n\n**行操作**(更多菜单):编辑、删除、复制多行、变更日志(时间/操作人/修改项/原值/新值)。\n\n**列表勾选**:仅未对账记录可勾选,用于批量完成核对。\n\n表头支持批量改价(成本单价、加氢单价、承担方式)。",
|
||||
"hasMarkdown": true,
|
||||
"color": "#334155",
|
||||
"images": [],
|
||||
"createdAt": 1782806400000,
|
||||
"updatedAt": 1784520000000,
|
||||
"updatedAt": 1784523600000,
|
||||
"controls": []
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/**
|
||||
* @name 车辆氢费明细
|
||||
* 设计规范:ledger-shared/DESIGN.md · vm-page + ldb-page(与维修明细 / 租赁台账对齐)
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
@@ -8,6 +9,8 @@ import {
|
||||
type AnnotationViewerOptions
|
||||
} from '@axhub/annotation';
|
||||
import { PrototypeAnnotationHost } from '../../common/prototype-annotation-host';
|
||||
import '../vehicle-management/style.css';
|
||||
import './styles/index.css';
|
||||
import './globals';
|
||||
import 'antd/dist/reset.css';
|
||||
import H2LedgerPage from './H2LedgerPage.jsx';
|
||||
|
||||
@@ -59,14 +59,10 @@ function FleetModelCard({
|
||||
);
|
||||
}
|
||||
|
||||
function FleetOverviewModal({
|
||||
open,
|
||||
onClose,
|
||||
function FleetShareBody({
|
||||
segmentCounts,
|
||||
onOpenDetail,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
segmentCounts: Record<OnLeaseFleetSegment, number>;
|
||||
onOpenDetail: (item: OnLeaseFleetBucket) => void;
|
||||
}) {
|
||||
@@ -87,29 +83,7 @@ function FleetOverviewModal({
|
||||
? Math.max(...summary.map((item) => item.count))
|
||||
: 1;
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div className="vm-modal-backdrop" role="presentation" onClick={onClose}>
|
||||
<div
|
||||
className="vm-modal vm-fleet-overview-modal"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="vm-fleet-overview-title"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
data-annotation-id="vm-fleet-overview-modal"
|
||||
>
|
||||
<header className="vm-modal-header">
|
||||
<div>
|
||||
<h2 id="vm-fleet-overview-title">在租车型占比</h2>
|
||||
<p>
|
||||
当前筛选共 <span className="tabular-nums">{totalOnLease}</span> 辆在租,展示各品牌型号数量及占当前筛选总数的占比
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" className="vm-modal-close" onClick={onClose} aria-label="关闭">
|
||||
<X size={18} aria-hidden />
|
||||
</button>
|
||||
</header>
|
||||
<div className="vm-fleet-overview-modal__body">
|
||||
<div className="vm-fleet-overview-modal__brands" role="tablist" aria-label="业务线筛选">
|
||||
{ON_LEASE_FLEET_SEGMENTS.map((segment) => (
|
||||
@@ -147,6 +121,42 @@ function FleetOverviewModal({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FleetOverviewModal({
|
||||
open,
|
||||
onClose,
|
||||
segmentCounts,
|
||||
onOpenDetail,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
segmentCounts: Record<OnLeaseFleetSegment, number>;
|
||||
onOpenDetail: (item: OnLeaseFleetBucket) => void;
|
||||
}) {
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div className="vm-modal-backdrop" role="presentation" onClick={onClose}>
|
||||
<div
|
||||
className="vm-modal vm-fleet-overview-modal"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="vm-fleet-overview-title"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
data-annotation-id="vm-fleet-overview-modal"
|
||||
>
|
||||
<header className="vm-modal-header">
|
||||
<div>
|
||||
<h2 id="vm-fleet-overview-title">在租车型占比</h2>
|
||||
<p>按业务线筛选,展示各品牌型号数量及占当前筛选总数的占比</p>
|
||||
</div>
|
||||
<button type="button" className="vm-modal-close" onClick={onClose} aria-label="关闭">
|
||||
<X size={18} aria-hidden />
|
||||
</button>
|
||||
</header>
|
||||
<FleetShareBody segmentCounts={segmentCounts} onOpenDetail={onOpenDetail} />
|
||||
<footer className="vm-modal-footer">
|
||||
<button type="button" className="vm-btn vm-btn-primary" onClick={onClose} data-vm-icon="x">关闭</button>
|
||||
</footer>
|
||||
@@ -155,6 +165,114 @@ function FleetOverviewModal({
|
||||
);
|
||||
}
|
||||
|
||||
/** 工作台等场景:直接内联展示在租车型占比仪表(无需点击 KPI 卡打开弹层) */
|
||||
export function OnLeaseFleetSharePanel({ emptyText }: { emptyText?: string }) {
|
||||
const fleetSourceList = useMemo(() => buildOnLeaseFleetSourceList(), []);
|
||||
const segmentCounts = useMemo(
|
||||
() => countOnLeaseFleetBySegment(fleetSourceList),
|
||||
[fleetSourceList],
|
||||
);
|
||||
const [detailItem, setDetailItem] = useState<OnLeaseFleetBucket | null>(null);
|
||||
const [segmentFilter, setSegmentFilter] = useState<OnLeaseFleetSegment>('全部');
|
||||
|
||||
const summary = useMemo(
|
||||
() => buildOnLeaseFleetSummary(fleetSourceList, segmentFilter),
|
||||
[fleetSourceList, segmentFilter],
|
||||
);
|
||||
const totalOnLease = useMemo(
|
||||
() => summary.reduce((sum, item) => sum + item.count, 0),
|
||||
[summary],
|
||||
);
|
||||
|
||||
if (segmentCounts.全部 <= 0) {
|
||||
return (
|
||||
<p className="wb-insights-fleet-share-empty">
|
||||
{emptyText || '暂无在租车辆,无法查看车型占比'}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="vm-fleet-share-panel" data-annotation-id="vm-fleet-share-panel">
|
||||
<div className="vm-fleet-share-panel__head">
|
||||
<h4 className="vm-fleet-share-panel__title">在租车型占比</h4>
|
||||
<p className="vm-fleet-share-panel__sub">
|
||||
当前筛选共 <span className="tabular-nums">{totalOnLease}</span> 辆在租
|
||||
</p>
|
||||
</div>
|
||||
<div className="vm-fleet-overview-modal__brands" role="tablist" aria-label="业务线筛选">
|
||||
{ON_LEASE_FLEET_SEGMENTS.map((segment) => (
|
||||
<button
|
||||
key={segment}
|
||||
type="button"
|
||||
role="tab"
|
||||
className={`vm-fleet-brand-chip${segmentFilter === segment ? ' is-active' : ''}`}
|
||||
onClick={() => setSegmentFilter(segment)}
|
||||
aria-selected={segmentFilter === segment}
|
||||
>
|
||||
{segment}
|
||||
<span className="vm-fleet-brand-chip__count tabular-nums">{segmentCounts[segment]}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{summary.length ? (
|
||||
<div className="vm-fleet-share-gauges" role="list" aria-label="在租车型占比仪表">
|
||||
{summary.map((item) => {
|
||||
const share = totalOnLease ? Math.round((item.count / totalOnLease) * 100) : 0;
|
||||
const r = 34;
|
||||
const c = 2 * Math.PI * r;
|
||||
const dash = (Math.max(4, Math.min(100, share)) / 100) * c;
|
||||
return (
|
||||
<button
|
||||
key={item.key}
|
||||
type="button"
|
||||
className="vm-fleet-share-gauge"
|
||||
role="listitem"
|
||||
onClick={() => setDetailItem(item)}
|
||||
aria-label={`${item.brand} ${item.model},${item.count} 辆,占 ${share}%`}
|
||||
>
|
||||
<span className="vm-fleet-share-gauge__ring" aria-hidden>
|
||||
<svg viewBox="0 0 96 96" className="vm-fleet-share-gauge__svg">
|
||||
<circle className="vm-fleet-share-gauge__track" cx="48" cy="48" r={r} />
|
||||
<circle
|
||||
className="vm-fleet-share-gauge__arc"
|
||||
cx="48"
|
||||
cy="48"
|
||||
r={r}
|
||||
strokeDasharray={`${dash} ${c - dash}`}
|
||||
transform="rotate(-90 48 48)"
|
||||
/>
|
||||
</svg>
|
||||
<span className="vm-fleet-share-gauge__center">
|
||||
<strong className="tabular-nums">{share}%</strong>
|
||||
<span className="tabular-nums">{item.count} 辆</span>
|
||||
</span>
|
||||
</span>
|
||||
<span className="vm-fleet-share-gauge__brand">{item.brand}</span>
|
||||
<span className="vm-fleet-share-gauge__model" title={item.model}>{item.model}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="vm-fleet-overview-modal__empty">
|
||||
<p>该业务线暂无在租车辆</p>
|
||||
<button type="button" className="vm-btn vm-btn-ghost" data-vm-icon="eye" onClick={() => setSegmentFilter('全部')}>
|
||||
查看全部
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<FleetDetailModal
|
||||
open={!!detailItem}
|
||||
item={detailItem}
|
||||
onClose={() => setDetailItem(null)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function FleetDetailModal({
|
||||
open,
|
||||
item,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,11 @@
|
||||
# 车辆氢费明细 — 产品需求说明(PRD)
|
||||
|
||||
> **与站端加氢订单 / Bridge 口径冲突时的权威顺序(2026-07-18)**
|
||||
> 本文为历史业务版说明;其中「保存→未对账→完成对账」等命名与现行「**核对**(能源部)≠ **对账**(站点对账单)」二分不完全同词。
|
||||
> **站端 H5 / Web 加氢记录 / 车辆氢费现行联动**以以下文档为准,勿用本文覆盖:
|
||||
> - [`vehicle-h2-fee-ledger/.spec/verify-reconcile.md`](../../../prototypes/vehicle-h2-fee-ledger/.spec/verify-reconcile.md)
|
||||
> - [`oneos-h5-h2-order/.spec/reconcile-linkage.md`](../../../prototypes/oneos-h5-h2-order/.spec/reconcile-linkage.md)
|
||||
|
||||
| 项目 | 内容 |
|
||||
|------|------|
|
||||
| 文档版本 | v2.1(业务版) |
|
||||
|
||||
@@ -492,11 +492,30 @@ function createPreviewLoader(
|
||||
const projectId = getSearchParamFromRequestUrl(requestUrl, 'projectId');
|
||||
const importPath = appendSearchParamToModuleSpecifier(previewSource.importPath, 'projectId', projectId);
|
||||
const previewPath = createRawRoutePath(type, name);
|
||||
const enableOneOsTheme =
|
||||
type === 'prototypes' &&
|
||||
name !== 'oneos-prototype-nav' &&
|
||||
![
|
||||
'oneos-web-business',
|
||||
'oneos-web-data-analysis',
|
||||
'oneos-web-finance',
|
||||
'oneos-web-help-center',
|
||||
'oneos-web-lease-contract',
|
||||
'oneos-web-ledger-data',
|
||||
'oneos-web-ops',
|
||||
'oneos-web-procurement',
|
||||
].includes(name);
|
||||
const oneOsThemeBootstrap = enableOneOsTheme
|
||||
? `
|
||||
import { bootstrapOneOsTheme } from ${JSON.stringify('@/common/oneos-app-shell/theme')};
|
||||
bootstrapOneOsTheme({ prototypeId: ${JSON.stringify(name)} });
|
||||
`
|
||||
: '';
|
||||
return `
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import PreviewComponent from ${JSON.stringify(importPath)};
|
||||
|
||||
${oneOsThemeBootstrap}
|
||||
class AxhubPreviewErrorBoundary extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
Reference in New Issue
Block a user