diff --git a/.axhub/make/axhub.config.json b/.axhub/make/axhub.config.json index cbcd4b5..9419f2d 100644 --- a/.axhub/make/axhub.config.json +++ b/.axhub/make/axhub.config.json @@ -2,6 +2,7 @@ "server": { "host": "localhost", "allowLAN": true, + "lanHost": "192.168.0.102", "enableCommandAPI": false }, "projectDefaults": { diff --git a/.axhub/make/sidebar-tree.json b/.axhub/make/sidebar-tree.json index 04bedce..3df5294 100644 --- a/.axhub/make/sidebar-tree.json +++ b/.axhub/make/sidebar-tree.json @@ -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", diff --git a/AGENTS.md b/AGENTS.md index f9798eb..084b079 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,3 +1,7 @@ +## 项目信息 + +- 项目名称:OneOS1.2 + # Agent 工作流程 ## 🧭 核心顺序 diff --git a/CLAUDE.md b/CLAUDE.md index 56bcfa9..084b079 100644 --- a/CLAUDE.md +++ b/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//` | `rules/theme-guide.md` | | 项目资料和文档 | `src/resources/` | `rules/resource-management-guide.md` | -| UI Review 结论 | `src/prototypes//.spec/ui-review.md` | `rules/ui-review-guide.md` | -| 原型 Review 结论 | `src/prototypes//.spec/prototype-review.md` | `rules/prototype-review-guide.md` | +| 画布 | `src/prototypes//canvas.excalidraw`、`canvas-assets/` | 原型画布和画布素材 | | 业务逻辑规格 | `src/prototypes//.spec/.md` | `rules/business-logic-documentation-guide.md` | | 对象存储发布 URL | `.axhub/make/axhub.config.json` → `cloudPublishing.s3` | `rules/cloud-publish-url-guide.md` | -| ACP 对话缓存 | `src/prototypes//.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`) ## 项目结构 diff --git a/DESIGN.md b/DESIGN.md deleted file mode 100644 index f1c1731..0000000 --- a/DESIGN.md +++ /dev/null @@ -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. diff --git a/package-lock.json b/package-lock.json index d157858..2bca196 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 1146428..36f0e7b 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/skills-lock.json b/skills-lock.json index 85f6625..f89435d 100644 --- a/skills-lock.json +++ b/skills-lock.json @@ -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" } } } diff --git a/src/common/OperationActions.tsx b/src/common/OperationActions.tsx index 04e7a45..90c61e9 100644 --- a/src/common/OperationActions.tsx +++ b/src/common/OperationActions.tsx @@ -47,6 +47,7 @@ const MORE_ACTION_ICONS: Record = { manage: Settings2, preview: Eye, download: Download, + failDetail: FileText, }; function renderMenuItemLabel(item: OperationActionItem) { @@ -95,7 +96,7 @@ function ViewIcon() { } function MoreIcon() { - return ; + return ; } const PrimaryActionButton = React.forwardRef< @@ -121,18 +122,22 @@ const PrimaryActionButton = React.forwardRef< }, ref, ) { + const isMore = icon === 'more'; return ( ); }); @@ -236,9 +241,9 @@ export function OperationActions({ ) : null} diff --git a/src/common/h2VehicleLedgerBridge.js b/src/common/h2VehicleLedgerBridge.js index f17ffe4..a8d403f 100644 --- a/src/common/h2VehicleLedgerBridge.js +++ b/src/common/h2VehicleLedgerBridge.js @@ -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 }; diff --git a/src/common/oneos-web-approval/components/ApprovalCard.tsx b/src/common/oneos-web-approval/components/ApprovalCard.tsx index b52a183..55d9abe 100644 --- a/src/common/oneos-web-approval/components/ApprovalCard.tsx +++ b/src/common/oneos-web-approval/components/ApprovalCard.tsx @@ -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'; } diff --git a/src/common/oneos-web-approval/components/ApprovalDetailPlaceholder.tsx b/src/common/oneos-web-approval/components/ApprovalDetailPlaceholder.tsx index 36fae93..a26065c 100644 --- a/src/common/oneos-web-approval/components/ApprovalDetailPlaceholder.tsx +++ b/src/common/oneos-web-approval/components/ApprovalDetailPlaceholder.tsx @@ -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'; } diff --git a/src/common/oneos-web-approval/config/approvalTypeConfig.ts b/src/common/oneos-web-approval/config/approvalTypeConfig.ts index 406b6f6..a31cc8c 100644 --- a/src/common/oneos-web-approval/config/approvalTypeConfig.ts +++ b/src/common/oneos-web-approval/config/approvalTypeConfig.ts @@ -43,7 +43,8 @@ const STATUS_LABEL_MAP: Record = { pending: '审批中', processing: '审批中', approved: '已通过', - rejected: '已驳回', + rejected: '被驳回', + terminated: '被终止', }; export function getApprovalTypeLabel(type: ApprovalTypeKey): string { diff --git a/src/common/oneos-web-approval/types.ts b/src/common/oneos-web-approval/types.ts index 82186c9..ac4fd3a 100644 --- a/src/common/oneos-web-approval/types.ts +++ b/src/common/oneos-web-approval/types.ts @@ -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'; diff --git a/src/common/vm-operation-actions.css b/src/common/vm-operation-actions.css index 28aade4..97442ab 100644 --- a/src/common/vm-operation-actions.css +++ b/src/common/vm-operation-actions.css @@ -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; diff --git a/src/prototypes/insurance-procurement/InsuranceProcurement.jsx b/src/prototypes/insurance-procurement/InsuranceProcurement.jsx index e7b0753..c2f9808 100644 --- a/src/prototypes/insurance-procurement/InsuranceProcurement.jsx +++ b/src/prototypes/insurance-procurement/InsuranceProcurement.jsx @@ -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) => ( - 0 ? '#dc2626' : '#64748b', fontWeight: 600 }}> - {val ?? 0} - - ), + render: (val, record) => { + const count = val ?? 0; + if (count > 0) { + return ( + + ); + } + return ( + + 0 + + ); + }, }, { 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 = ( - openPolicyRecognTaskRecord(record), - }} - /> - ); + const hasSuccess = (record.recognSuccessCount || 0) > 0; + const hasFail = (record.recognFailCount || 0) > 0; if (recognizing) { return ( - {action} + + {}, + }} + /> + ); } - return action; + if (!hasSuccess && hasFail) { + return ( + openPolicyRecognFailDetail(record), + }} + /> + ); + } + return ( + openPolicyRecognTaskRecord(record), + }} + more={hasFail ? [{ + key: 'failDetail', + label: '查看失败明细', + onClick: () => openPolicyRecognFailDetail(record), + }] : undefined} + /> + ); }, }, ]} /> + { + setPolicyRecognFailDetailOpen(false); + setPolicyRecognFailDetailTask(null); + }} + > + 关闭 + + )} + onCancel={() => { + setPolicyRecognFailDetailOpen(false); + setPolicyRecognFailDetailTask(null); + }} + > + + 8 + ? { pageSize: 8, showSizeChanger: false } + : false} + locale={{ emptyText: '暂无失败记录' }} + columns={[ + { + title: '文件/保单名', + dataIndex: 'fileName', + width: 280, + ellipsis: true, + render: (val) => ( + {val || '—'} + ), + }, + { + title: '失败原因', + dataIndex: 'failReason', + render: (val) => ( + {val || '—'} + ), + }, + ]} + /> + + 账单业财闭环', + 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。', }, }, ]; diff --git a/src/prototypes/oneos-h5-h2-order/.spec/2026-07-14-requirements-design.md b/src/prototypes/oneos-h5-h2-order/.spec/2026-07-14-requirements-design.md index ba8e0dd..b704f55 100644 --- a/src/prototypes/oneos-h5-h2-order/.spec/2026-07-14-requirements-design.md +++ b/src/prototypes/oneos-h5-h2-order/.spec/2026-07-14-requirements-design.md @@ -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 信息架构 diff --git a/src/prototypes/oneos-h5-h2-order/.spec/feature-detail.md b/src/prototypes/oneos-h5-h2-order/.spec/feature-detail.md index 70a42a5..161df96 100644 --- a/src/prototypes/oneos-h5-h2-order/.spec/feature-detail.md +++ b/src/prototypes/oneos-h5-h2-order/.spec/feature-detail.md @@ -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. 详情展示车牌与面板带水印原图(新上报提交后为实拍;种子行为演示图) diff --git a/src/prototypes/oneos-h5-h2-order/.spec/feature-manual-ledger.md b/src/prototypes/oneos-h5-h2-order/.spec/feature-manual-ledger.md index 91b7b4a..773ef0b 100644 --- a/src/prototypes/oneos-h5-h2-order/.spec/feature-manual-ledger.md +++ b/src/prototypes/oneos-h5-h2-order/.spec/feature-manual-ledger.md @@ -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 列含「车辆来源」(羚牛车辆/非羚牛车辆)、「数据来源」(站端上传/羚牛上传),与列表标签口径一致 diff --git a/src/prototypes/oneos-h5-h2-order/.spec/fleet-plate-tag.md b/src/prototypes/oneos-h5-h2-order/.spec/fleet-plate-tag.md index 9ee3792..51d2100 100644 --- a/src/prototypes/oneos-h5-h2-order/.spec/fleet-plate-tag.md +++ b/src/prototypes/oneos-h5-h2-order/.spec/fleet-plate-tag.md @@ -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` diff --git a/src/prototypes/oneos-h5-h2-order/.spec/reconcile-linkage.md b/src/prototypes/oneos-h5-h2-order/.spec/reconcile-linkage.md index 98b403e..c3e8e00 100644 --- a/src/prototypes/oneos-h5-h2-order/.spec/reconcile-linkage.md +++ b/src/prototypes/oneos-h5-h2-order/.spec/reconcile-linkage.md @@ -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 diff --git a/src/prototypes/oneos-h5-h2-order/.spec/requirements-prd.md b/src/prototypes/oneos-h5-h2-order/.spec/requirements-prd.md index 658707c..cf82c56 100644 --- a/src/prototypes/oneos-h5-h2-order/.spec/requirements-prd.md +++ b/src/prototypes/oneos-h5-h2-order/.spec/requirements-prd.md @@ -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 生成)。 diff --git a/src/prototypes/oneos-h5-h2-order/annotation-source.json b/src/prototypes/oneos-h5-h2-order/annotation-source.json index 19dab06..83a7709 100644 --- a/src/prototypes/oneos-h5-h2-order/annotation-source.json +++ b/src/prototypes/oneos-h5-h2-order/annotation-source.json @@ -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" } ] } diff --git a/src/prototypes/oneos-h5-h2-order/components/ManualLedgerPanel.tsx b/src/prototypes/oneos-h5-h2-order/components/ManualLedgerPanel.tsx index 3560fdf..8a2b6f7 100644 --- a/src/prototypes/oneos-h5-h2-order/components/ManualLedgerPanel.tsx +++ b/src/prototypes/oneos-h5-h2-order/components/ManualLedgerPanel.tsx @@ -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(null); const [draftImages, setDraftImages] = useState([]); 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 (
+ {gateBlocked ? ( + + ) : null}
{uploadedSet.has(today) ? '今日已上传' : '今日尚未上传'}

{today} · {station.stationName} - {!uploadedSet.has(today) ? '。上传今日台账后才可新增加氢记录。' : ''} + {!uploadedSet.has(today) + ? '。今日台账可按日常上传,不作为新增门禁;新增加氢记录仅要求前一日已上传。' + : ''}

diff --git a/src/prototypes/oneos-h5-h2-order/components/OrderCard.tsx b/src/prototypes/oneos-h5-h2-order/components/OrderCard.tsx index a633fae..70ae535 100644 --- a/src/prototypes/oneos-h5-h2-order/components/OrderCard.tsx +++ b/src/prototypes/oneos-h5-h2-order/components/OrderCard.tsx @@ -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 ( {toast ? ( @@ -322,11 +325,29 @@ export default function OneosH5H2Order() { setTick((n) => n + 1)} + onUploaded={() => { + setTick((n) => n + 1); + setManualFocusDate(undefined); + }} /> ) : ( <> + {gateBlocked ? ( + + ) : null} +
diff --git a/src/prototypes/oneos-h5-h2-order/scripts/build-annotation-source.mjs b/src/prototypes/oneos-h5-h2-order/scripts/build-annotation-source.mjs index d4b50ab..59c1b45 100644 --- a/src/prototypes/oneos-h5-h2-order/scripts/build-annotation-source.mjs +++ b/src/prototypes/oneos-h5-h2-order/scripts/build-annotation-source.mjs @@ -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', diff --git a/src/prototypes/oneos-h5-h2-order/styles/index.css b/src/prototypes/oneos-h5-h2-order/styles/index.css index e6b1940..d97b895 100644 --- a/src/prototypes/oneos-h5-h2-order/styles/index.css +++ b/src/prototypes/oneos-h5-h2-order/styles/index.css @@ -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; diff --git a/src/prototypes/oneos-h5-h2-order/utils/fleet-plates.ts b/src/prototypes/oneos-h5-h2-order/utils/fleet-plates.ts index 6b88e27..53253b1 100644 --- a/src/prototypes/oneos-h5-h2-order/utils/fleet-plates.ts +++ b/src/prototypes/oneos-h5-h2-order/utils/fleet-plates.ts @@ -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); diff --git a/src/prototypes/oneos-h5-h2-order/utils/manual-ledger.ts b/src/prototypes/oneos-h5-h2-order/utils/manual-ledger.ts index a35ce18..1f9e4ed 100644 --- a/src/prototypes/oneos-h5-h2-order/utils/manual-ledger.ts +++ b/src/prototypes/oneos-h5-h2-order/utils/manual-ledger.ts @@ -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; diff --git a/src/prototypes/oneos-h5-h2-order/utils/orders.ts b/src/prototypes/oneos-h5-h2-order/utils/orders.ts index 6c12f10..43287e1 100644 --- a/src/prototypes/oneos-h5-h2-order/utils/orders.ts +++ b/src/prototypes/oneos-h5-h2-order/utils/orders.ts @@ -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): 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'; } diff --git a/src/prototypes/oneos-prototype-nav/nav-menu.json b/src/prototypes/oneos-prototype-nav/nav-menu.json index b7c4d3d..d79c9a2 100644 --- a/src/prototypes/oneos-prototype-nav/nav-menu.json +++ b/src/prototypes/oneos-prototype-nav/nav-menu.json @@ -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", diff --git a/src/prototypes/oneos-prototype-nav/prototype-registry.json b/src/prototypes/oneos-prototype-nav/prototype-registry.json index 375419a..d352ba9 100644 --- a/src/prototypes/oneos-prototype-nav/prototype-registry.json +++ b/src/prototypes/oneos-prototype-nav/prototype-registry.json @@ -1,6 +1,6 @@ { "version": 1, - "updatedAt": "2026-07-20T05:34:45.576Z", + "updatedAt": "2026-07-20T07:33:31.271Z", "prototypes": { "business-dept-ledger": { "title": "项目盈亏情况", @@ -75,7 +75,7 @@ "title": "合同模板管理", "version": "v1.11", "revision": 11, - "lastUpdated": "2026-07-13T01:31:00.000Z", + "lastUpdated": "2026-07-18T04:52:34.945Z", "contentHash": "d73aaafeba82194211c48a9012a786753c1f9e76a23d7148f5fbae6ee592d1d6", "trackedFiles": [ ".spec/acp/conversations.json", @@ -304,7 +304,7 @@ "title": "客户管理", "version": "v1.7", "revision": 7, - "lastUpdated": "2026-07-13T01:31:55.559Z", + "lastUpdated": "2026-07-18T04:52:34.907Z", "contentHash": "bf7d8ecd03b4105b0c4acd47dcabb09e9437617fda46399f882ba742cde05189", "trackedFiles": [ ".spec/prototype-review.md", @@ -512,10 +512,10 @@ }, "insurance-procurement": { "title": "保险采购", - "version": "v1.5", - "revision": 5, - "lastUpdated": "2026-07-16T01:00:00.000Z", - "contentHash": "009327e1188652e0d99a6e4044acbcbba333c203978af4654fc71327bf62e6e4", + "version": "v1.16", + "revision": 16, + "lastUpdated": "2026-07-20T06:20:07.171Z", + "contentHash": "391088b50e1c2279a3c3d4c4a8fa618a1ff3c4bb8b1645e060d45a31ce0e81bc", "trackedFiles": [ ".spec/prototype-comments.json", "index.tsx", @@ -523,6 +523,138 @@ "styles/index.css" ], "changelog": [ + { + "version": "v1.16", + "date": "2026-07-20", + "time": "14:20", + "summary": "更新页面样式", + "files": [ + ".spec/prototype-comments.json", + "index.tsx", + "InsuranceProcurement.jsx", + "styles/index.css" + ] + }, + { + "version": "v1.15", + "date": "2026-07-20", + "time": "14:19", + "summary": "更新页面样式", + "files": [ + ".spec/prototype-comments.json", + "index.tsx", + "InsuranceProcurement.jsx", + "styles/index.css" + ] + }, + { + "version": "v1.14", + "date": "2026-07-20", + "time": "14:18", + "summary": "更新页面样式", + "files": [ + ".spec/prototype-comments.json", + "index.tsx", + "InsuranceProcurement.jsx", + "styles/index.css" + ] + }, + { + "version": "v1.13", + "date": "2026-07-20", + "time": "14:18", + "summary": "更新页面样式", + "files": [ + ".spec/prototype-comments.json", + "index.tsx", + "InsuranceProcurement.jsx", + "styles/index.css" + ] + }, + { + "version": "v1.12", + "date": "2026-07-20", + "time": "14:18", + "summary": "更新页面样式", + "files": [ + ".spec/prototype-comments.json", + "index.tsx", + "InsuranceProcurement.jsx", + "styles/index.css" + ] + }, + { + "version": "v1.11", + "date": "2026-07-20", + "time": "14:18", + "summary": "更新页面样式", + "files": [ + ".spec/prototype-comments.json", + "index.tsx", + "InsuranceProcurement.jsx", + "styles/index.css" + ] + }, + { + "version": "v1.10", + "date": "2026-07-20", + "time": "14:18", + "summary": "更新页面样式", + "files": [ + ".spec/prototype-comments.json", + "index.tsx", + "InsuranceProcurement.jsx", + "styles/index.css" + ] + }, + { + "version": "v1.9", + "date": "2026-07-20", + "time": "14:18", + "summary": "更新页面样式", + "files": [ + ".spec/prototype-comments.json", + "index.tsx", + "InsuranceProcurement.jsx", + "styles/index.css" + ] + }, + { + "version": "v1.8", + "date": "2026-07-20", + "time": "14:17", + "summary": "更新页面样式", + "files": [ + ".spec/prototype-comments.json", + "index.tsx", + "InsuranceProcurement.jsx", + "styles/index.css" + ] + }, + { + "version": "v1.7", + "date": "2026-07-20", + "time": "14:17", + "summary": "更新页面样式", + "files": [ + ".spec/prototype-comments.json", + "index.tsx", + "InsuranceProcurement.jsx", + "styles/index.css" + ] + }, + { + "version": "v1.6", + "date": "2026-07-18", + "time": "12:52", + "summary": "更新页面样式", + "files": [ + ".spec/prototype-comments.json", + "index.tsx", + "InsuranceProcurement.jsx", + "styles/index.css" + ] + }, { "version": "v1.5", "date": "2026-07-16", @@ -595,10 +727,10 @@ }, "lease-business-detail": { "title": "租赁业务明细", - "version": "v1.5", - "revision": 5, - "lastUpdated": "2026-07-17T08:10:00.000Z", - "contentHash": "5dea68ab4a234edc3f7e76ac58eb2e2e8baaafe0150ff622a35ef1bae14e49ce", + "version": "v1.6", + "revision": 6, + "lastUpdated": "2026-07-18T04:52:35.100Z", + "contentHash": "2ee4641e8d0ebb72a28e9d13de681573f2c1a3ccb15dd7233cf11c6cd3673e15", "trackedFiles": [ ".spec/field-checks.md", ".spec/import-rules.md", @@ -640,6 +772,26 @@ "utils/validate-detail.ts" ], "changelog": [ + { + "version": "v1.6", + "date": "2026-07-18", + "time": "12:52", + "summary": "更新需求说明与标注", + "files": [ + ".spec/field-checks.md", + ".spec/import-rules.md", + ".spec/prototype-comments.json", + ".spec/prototype-review.md", + ".spec/requirements-prd.md", + "columnHeaderTips.ts", + "components/DetailChangeLogModal.tsx", + "components/DetailDeleteConfirmModal.tsx", + "components/DetailEditModal.tsx", + "components/DetailImportModal.tsx", + "components/DetailKpiRow.tsx", + "components/DetailMainTabs.tsx" + ] + }, { "version": "v1.5", "date": "2026-07-17", @@ -764,10 +916,10 @@ }, "lease-business-ledger": { "title": "租赁业务台账", - "version": "v1.6", - "revision": 6, - "lastUpdated": "2026-07-12T14:45:00.000Z", - "contentHash": "ab4c832a35d0908a485c165230822f87c822558c012c6ec838ace2b58e7c86a3", + "version": "v1.7", + "revision": 7, + "lastUpdated": "2026-07-18T04:52:35.060Z", + "contentHash": "0d4725f690691ac9ec71c72927109eb4cab177436bd71c0c3250fafd818be536", "trackedFiles": [ ".spec/prototype-comments.json", ".spec/requirements-prd.md", @@ -801,6 +953,26 @@ "utils/ledger.ts" ], "changelog": [ + { + "version": "v1.7", + "date": "2026-07-18", + "time": "12:52", + "summary": "更新需求说明与标注", + "files": [ + ".spec/prototype-comments.json", + ".spec/requirements-prd.md", + "columnHeaderTips.ts", + "components/AccidentLinkModal.tsx", + "components/ConfirmMarkModal.tsx", + "components/DeleteConfirmModal.tsx", + "components/FilterPanel.tsx", + "components/LeaseLedgerImportModal.tsx", + "components/LeaseLedgerTable.tsx", + "components/LedgerDetailPopover.tsx", + "components/LedgerKpiRow.tsx", + "components/LedgerLineItemsCell.tsx" + ] + }, { "version": "v1.6", "date": "2026-07-12", @@ -940,10 +1112,10 @@ }, "lease-business-line-overview": { "title": "业务条线说明", - "version": "v1.4", - "revision": 4, - "lastUpdated": "2026-07-16T01:00:00.000Z", - "contentHash": "a7fb7ee64b406ab87e38a2f8c1a66ef327fd488038cadb51400528bc692284ef", + "version": "v1.7", + "revision": 7, + "lastUpdated": "2026-07-18T04:50:30.223Z", + "contentHash": "edd05c3459ccc4f3330dbca04616a88d8f6ab9658ebf444b4279f9fb504ab5f1", "trackedFiles": [ ".spec/prototype-comments.json", "data-foundations.ts", @@ -953,6 +1125,48 @@ "style.css" ], "changelog": [ + { + "version": "v1.7", + "date": "2026-07-18", + "time": "12:50", + "summary": "更新页面样式", + "files": [ + ".spec/prototype-comments.json", + "data-foundations.ts", + "hooks.ts", + "index.tsx", + "lines.ts", + "style.css" + ] + }, + { + "version": "v1.6", + "date": "2026-07-18", + "time": "12:50", + "summary": "更新页面样式", + "files": [ + ".spec/prototype-comments.json", + "data-foundations.ts", + "hooks.ts", + "index.tsx", + "lines.ts", + "style.css" + ] + }, + { + "version": "v1.5", + "date": "2026-07-18", + "time": "12:50", + "summary": "更新页面样式", + "files": [ + ".spec/prototype-comments.json", + "data-foundations.ts", + "hooks.ts", + "index.tsx", + "lines.ts", + "style.css" + ] + }, { "version": "v1.4", "date": "2026-07-16", @@ -1010,10 +1224,10 @@ }, "lease-contract-management": { "title": "租赁合同", - "version": "v1.10", - "revision": 10, - "lastUpdated": "2026-07-18T03:17:53.200Z", - "contentHash": "71e374a81e5dd49b522c4ec9e0acdaea60957ce826a6fabd32b905623d5ddf10", + "version": "v1.12", + "revision": 12, + "lastUpdated": "2026-07-20T06:11:29.452Z", + "contentHash": "39fa04b2f677248c58274ed852e7af604e60b73327580269859eec15dff86b28", "trackedFiles": [ ".spec/prototype-comments.json", ".spec/requirements-annotation-context.md", @@ -1047,6 +1261,46 @@ "styles/lease-contract.css" ], "changelog": [ + { + "version": "v1.12", + "date": "2026-07-20", + "time": "14:11", + "summary": "更新需求说明与标注", + "files": [ + ".spec/prototype-comments.json", + ".spec/requirements-annotation-context.md", + ".spec/requirements-flow-operations.md", + ".spec/requirements-prd-create.md", + ".spec/requirements-prd-list.md", + "contract-word-preview.js", + "index.tsx", + "lease-business-org-data.js", + "lease-contract-approval-rules.js", + "lease-contract-edit-bridge.js", + "lease-contract-flow-bridge.js", + "lease-contract-form-validation.js" + ] + }, + { + "version": "v1.11", + "date": "2026-07-18", + "time": "18:49", + "summary": "更新需求说明与标注", + "files": [ + ".spec/prototype-comments.json", + ".spec/requirements-annotation-context.md", + ".spec/requirements-flow-operations.md", + ".spec/requirements-prd-create.md", + ".spec/requirements-prd-list.md", + "contract-word-preview.js", + "index.tsx", + "lease-business-org-data.js", + "lease-contract-approval-rules.js", + "lease-contract-edit-bridge.js", + "lease-contract-flow-bridge.js", + "lease-contract-form-validation.js" + ] + }, { "version": "v1.10", "date": "2026-07-18", @@ -1373,10 +1627,10 @@ }, "oneos-h5-h2-order": { "title": "加氢记录(H5)", - "version": "v1.23", - "revision": 23, - "lastUpdated": "2026-07-18T04:30:15.800Z", - "contentHash": "f8635009e1ef7839b558004c630e4e9a00f87dc6e6f43f454e5b136bc2828753", + "version": "v1.44", + "revision": 44, + "lastUpdated": "2026-07-18T12:36:52.684Z", + "contentHash": "5b69a6ae6a1662eb6acf30e76def73804b81b23387dcc5c7c2a59c417cc67478", "trackedFiles": [ ".spec/2026-07-14-implementation-plan.md", ".spec/2026-07-14-requirements-design.md", @@ -1385,9 +1639,14 @@ ".spec/feature-list.md", ".spec/feature-manual-ledger.md", ".spec/fleet-plate-tag.md", + ".spec/flows.md", + ".spec/key-logic.md", ".spec/prototype-comments.json", + ".spec/prototype-review.md", ".spec/reconcile-linkage.md", ".spec/requirements-prd.md", + ".spec/roles.md", + ".spec/user-stories.md", "components/CreateWizard.tsx", "components/DateTimePickerSheet.tsx", "components/Icons.tsx", @@ -1409,6 +1668,426 @@ "utils/vehicle-mileage.ts" ], "changelog": [ + { + "version": "v1.44", + "date": "2026-07-18", + "time": "20:36", + "summary": "更新需求说明与标注", + "files": [ + ".spec/2026-07-14-implementation-plan.md", + ".spec/2026-07-14-requirements-design.md", + ".spec/feature-create.md", + ".spec/feature-detail.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/flows.md", + ".spec/key-logic.md", + ".spec/prototype-comments.json", + ".spec/prototype-review.md", + ".spec/reconcile-linkage.md" + ] + }, + { + "version": "v1.43", + "date": "2026-07-18", + "time": "20:36", + "summary": "更新需求说明与标注", + "files": [ + ".spec/2026-07-14-implementation-plan.md", + ".spec/2026-07-14-requirements-design.md", + ".spec/feature-create.md", + ".spec/feature-detail.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/flows.md", + ".spec/key-logic.md", + ".spec/prototype-comments.json", + ".spec/prototype-review.md", + ".spec/reconcile-linkage.md" + ] + }, + { + "version": "v1.42", + "date": "2026-07-18", + "time": "20:35", + "summary": "更新需求说明与标注", + "files": [ + ".spec/2026-07-14-implementation-plan.md", + ".spec/2026-07-14-requirements-design.md", + ".spec/feature-create.md", + ".spec/feature-detail.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/flows.md", + ".spec/key-logic.md", + ".spec/prototype-comments.json", + ".spec/prototype-review.md", + ".spec/reconcile-linkage.md" + ] + }, + { + "version": "v1.41", + "date": "2026-07-18", + "time": "20:34", + "summary": "更新需求说明与标注", + "files": [ + ".spec/2026-07-14-implementation-plan.md", + ".spec/2026-07-14-requirements-design.md", + ".spec/feature-create.md", + ".spec/feature-detail.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/flows.md", + ".spec/key-logic.md", + ".spec/prototype-comments.json", + ".spec/prototype-review.md", + ".spec/reconcile-linkage.md" + ] + }, + { + "version": "v1.40", + "date": "2026-07-18", + "time": "20:34", + "summary": "更新需求说明与标注", + "files": [ + ".spec/2026-07-14-implementation-plan.md", + ".spec/2026-07-14-requirements-design.md", + ".spec/feature-create.md", + ".spec/feature-detail.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/flows.md", + ".spec/key-logic.md", + ".spec/prototype-comments.json", + ".spec/prototype-review.md", + ".spec/reconcile-linkage.md" + ] + }, + { + "version": "v1.39", + "date": "2026-07-18", + "time": "17:14", + "summary": "更新需求说明与标注", + "files": [ + ".spec/2026-07-14-implementation-plan.md", + ".spec/2026-07-14-requirements-design.md", + ".spec/feature-create.md", + ".spec/feature-detail.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/flows.md", + ".spec/key-logic.md", + ".spec/prototype-comments.json", + ".spec/reconcile-linkage.md", + ".spec/requirements-prd.md" + ] + }, + { + "version": "v1.38", + "date": "2026-07-18", + "time": "17:06", + "summary": "更新需求说明与标注", + "files": [ + ".spec/2026-07-14-implementation-plan.md", + ".spec/2026-07-14-requirements-design.md", + ".spec/feature-create.md", + ".spec/feature-detail.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/prototype-comments.json", + ".spec/reconcile-linkage.md", + ".spec/requirements-prd.md", + "components/CreateWizard.tsx", + "components/DateTimePickerSheet.tsx" + ] + }, + { + "version": "v1.37", + "date": "2026-07-18", + "time": "17:06", + "summary": "更新需求说明与标注", + "files": [ + ".spec/2026-07-14-implementation-plan.md", + ".spec/2026-07-14-requirements-design.md", + ".spec/feature-create.md", + ".spec/feature-detail.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/prototype-comments.json", + ".spec/reconcile-linkage.md", + ".spec/requirements-prd.md", + "components/CreateWizard.tsx", + "components/DateTimePickerSheet.tsx" + ] + }, + { + "version": "v1.36", + "date": "2026-07-18", + "time": "17:05", + "summary": "更新需求说明与标注", + "files": [ + ".spec/2026-07-14-implementation-plan.md", + ".spec/2026-07-14-requirements-design.md", + ".spec/feature-create.md", + ".spec/feature-detail.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/prototype-comments.json", + ".spec/reconcile-linkage.md", + ".spec/requirements-prd.md", + "components/CreateWizard.tsx", + "components/DateTimePickerSheet.tsx" + ] + }, + { + "version": "v1.35", + "date": "2026-07-18", + "time": "17:05", + "summary": "更新需求说明与标注", + "files": [ + ".spec/2026-07-14-implementation-plan.md", + ".spec/2026-07-14-requirements-design.md", + ".spec/feature-create.md", + ".spec/feature-detail.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/prototype-comments.json", + ".spec/reconcile-linkage.md", + ".spec/requirements-prd.md", + "components/CreateWizard.tsx", + "components/DateTimePickerSheet.tsx" + ] + }, + { + "version": "v1.34", + "date": "2026-07-18", + "time": "17:05", + "summary": "更新需求说明与标注", + "files": [ + ".spec/2026-07-14-implementation-plan.md", + ".spec/2026-07-14-requirements-design.md", + ".spec/feature-create.md", + ".spec/feature-detail.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/prototype-comments.json", + ".spec/reconcile-linkage.md", + ".spec/requirements-prd.md", + "components/CreateWizard.tsx", + "components/DateTimePickerSheet.tsx" + ] + }, + { + "version": "v1.33", + "date": "2026-07-18", + "time": "17:05", + "summary": "更新需求说明与标注", + "files": [ + ".spec/2026-07-14-implementation-plan.md", + ".spec/2026-07-14-requirements-design.md", + ".spec/feature-create.md", + ".spec/feature-detail.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/prototype-comments.json", + ".spec/reconcile-linkage.md", + ".spec/requirements-prd.md", + "components/CreateWizard.tsx", + "components/DateTimePickerSheet.tsx" + ] + }, + { + "version": "v1.32", + "date": "2026-07-18", + "time": "17:05", + "summary": "更新需求说明与标注", + "files": [ + ".spec/2026-07-14-implementation-plan.md", + ".spec/2026-07-14-requirements-design.md", + ".spec/feature-create.md", + ".spec/feature-detail.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/prototype-comments.json", + ".spec/reconcile-linkage.md", + ".spec/requirements-prd.md", + "components/CreateWizard.tsx", + "components/DateTimePickerSheet.tsx" + ] + }, + { + "version": "v1.31", + "date": "2026-07-18", + "time": "17:05", + "summary": "更新需求说明与标注", + "files": [ + ".spec/2026-07-14-implementation-plan.md", + ".spec/2026-07-14-requirements-design.md", + ".spec/feature-create.md", + ".spec/feature-detail.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/prototype-comments.json", + ".spec/reconcile-linkage.md", + ".spec/requirements-prd.md", + "components/CreateWizard.tsx", + "components/DateTimePickerSheet.tsx" + ] + }, + { + "version": "v1.30", + "date": "2026-07-18", + "time": "16:56", + "summary": "更新需求说明与标注", + "files": [ + ".spec/2026-07-14-implementation-plan.md", + ".spec/2026-07-14-requirements-design.md", + ".spec/feature-create.md", + ".spec/feature-detail.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/prototype-comments.json", + ".spec/reconcile-linkage.md", + ".spec/requirements-prd.md", + "components/CreateWizard.tsx", + "components/DateTimePickerSheet.tsx" + ] + }, + { + "version": "v1.29", + "date": "2026-07-18", + "time": "16:56", + "summary": "更新需求说明与标注", + "files": [ + ".spec/2026-07-14-implementation-plan.md", + ".spec/2026-07-14-requirements-design.md", + ".spec/feature-create.md", + ".spec/feature-detail.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/prototype-comments.json", + ".spec/reconcile-linkage.md", + ".spec/requirements-prd.md", + "components/CreateWizard.tsx", + "components/DateTimePickerSheet.tsx" + ] + }, + { + "version": "v1.28", + "date": "2026-07-18", + "time": "16:56", + "summary": "更新需求说明与标注", + "files": [ + ".spec/2026-07-14-implementation-plan.md", + ".spec/2026-07-14-requirements-design.md", + ".spec/feature-create.md", + ".spec/feature-detail.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/prototype-comments.json", + ".spec/reconcile-linkage.md", + ".spec/requirements-prd.md", + "components/CreateWizard.tsx", + "components/DateTimePickerSheet.tsx" + ] + }, + { + "version": "v1.27", + "date": "2026-07-18", + "time": "16:55", + "summary": "更新需求说明与标注", + "files": [ + ".spec/2026-07-14-implementation-plan.md", + ".spec/2026-07-14-requirements-design.md", + ".spec/feature-create.md", + ".spec/feature-detail.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/prototype-comments.json", + ".spec/reconcile-linkage.md", + ".spec/requirements-prd.md", + "components/CreateWizard.tsx", + "components/DateTimePickerSheet.tsx" + ] + }, + { + "version": "v1.26", + "date": "2026-07-18", + "time": "16:55", + "summary": "更新需求说明与标注", + "files": [ + ".spec/2026-07-14-implementation-plan.md", + ".spec/2026-07-14-requirements-design.md", + ".spec/feature-create.md", + ".spec/feature-detail.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/prototype-comments.json", + ".spec/reconcile-linkage.md", + ".spec/requirements-prd.md", + "components/CreateWizard.tsx", + "components/DateTimePickerSheet.tsx" + ] + }, + { + "version": "v1.25", + "date": "2026-07-18", + "time": "16:55", + "summary": "更新需求说明与标注", + "files": [ + ".spec/2026-07-14-implementation-plan.md", + ".spec/2026-07-14-requirements-design.md", + ".spec/feature-create.md", + ".spec/feature-detail.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/prototype-comments.json", + ".spec/reconcile-linkage.md", + ".spec/requirements-prd.md", + "components/CreateWizard.tsx", + "components/DateTimePickerSheet.tsx" + ] + }, + { + "version": "v1.24", + "date": "2026-07-18", + "time": "16:54", + "summary": "更新需求说明与标注", + "files": [ + ".spec/2026-07-14-implementation-plan.md", + ".spec/2026-07-14-requirements-design.md", + ".spec/feature-create.md", + ".spec/feature-detail.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/prototype-comments.json", + ".spec/reconcile-linkage.md", + ".spec/requirements-prd.md", + "components/CreateWizard.tsx", + "components/DateTimePickerSheet.tsx" + ] + }, { "version": "v1.23", "date": "2026-07-18", @@ -1592,271 +2271,14 @@ "components/CreateWizard.tsx", "components/DateTimePickerSheet.tsx" ] - }, - { - "version": "v1.14", - "date": "2026-07-17", - "time": "17:26", - "summary": "更新 demo-photo.ts", - "files": [ - "utils/demo-photo.ts" - ] - }, - { - "version": "v1.13", - "date": "2026-07-17", - "time": "17:26", - "summary": "更新需求说明与标注", - "files": [ - ".spec/2026-07-14-implementation-plan.md", - ".spec/2026-07-14-requirements-design.md", - ".spec/feature-create.md", - ".spec/feature-detail.md", - ".spec/feature-list.md", - ".spec/feature-manual-ledger.md", - ".spec/fleet-plate-tag.md", - ".spec/prototype-comments.json", - ".spec/reconcile-linkage.md", - ".spec/requirements-prd.md", - "components/CreateWizard.tsx", - "components/DateTimePickerSheet.tsx" - ] - }, - { - "version": "v1.12", - "date": "2026-07-17", - "time": "17:25", - "summary": "更新需求说明与标注", - "files": [ - ".spec/2026-07-14-implementation-plan.md", - ".spec/2026-07-14-requirements-design.md", - ".spec/feature-create.md", - ".spec/feature-detail.md", - ".spec/feature-list.md", - ".spec/feature-manual-ledger.md", - ".spec/fleet-plate-tag.md", - ".spec/prototype-comments.json", - ".spec/reconcile-linkage.md", - ".spec/requirements-prd.md", - "components/CreateWizard.tsx", - "components/DateTimePickerSheet.tsx" - ] - }, - { - "version": "v1.11", - "date": "2026-07-17", - "time": "17:23", - "summary": "更新需求说明与标注", - "files": [ - ".spec/2026-07-14-implementation-plan.md", - ".spec/2026-07-14-requirements-design.md", - ".spec/feature-create.md", - ".spec/feature-detail.md", - ".spec/feature-list.md", - ".spec/feature-manual-ledger.md", - ".spec/fleet-plate-tag.md", - ".spec/prototype-comments.json", - ".spec/reconcile-linkage.md", - ".spec/requirements-prd.md", - "components/CreateWizard.tsx", - "components/DateTimePickerSheet.tsx" - ] - }, - { - "version": "v1.10", - "date": "2026-07-17", - "time": "17:23", - "summary": "更新需求说明与标注", - "files": [ - ".spec/2026-07-14-implementation-plan.md", - ".spec/2026-07-14-requirements-design.md", - ".spec/feature-create.md", - ".spec/feature-detail.md", - ".spec/feature-list.md", - ".spec/feature-manual-ledger.md", - ".spec/fleet-plate-tag.md", - ".spec/prototype-comments.json", - ".spec/reconcile-linkage.md", - ".spec/requirements-prd.md", - "components/CreateWizard.tsx", - "components/DateTimePickerSheet.tsx" - ] - }, - { - "version": "v1.9", - "date": "2026-07-17", - "time": "17:21", - "summary": "更新需求说明与标注", - "files": [ - ".spec/2026-07-14-implementation-plan.md", - ".spec/2026-07-14-requirements-design.md", - ".spec/feature-create.md", - ".spec/feature-detail.md", - ".spec/feature-list.md", - ".spec/feature-manual-ledger.md", - ".spec/fleet-plate-tag.md", - ".spec/prototype-comments.json", - ".spec/reconcile-linkage.md", - ".spec/requirements-prd.md", - "components/CreateWizard.tsx", - "components/DateTimePickerSheet.tsx" - ] - }, - { - "version": "v1.8", - "date": "2026-07-17", - "time": "17:19", - "summary": "更新需求说明与标注", - "files": [ - ".spec/2026-07-14-implementation-plan.md", - ".spec/2026-07-14-requirements-design.md", - ".spec/feature-create.md", - ".spec/feature-detail.md", - ".spec/feature-list.md", - ".spec/feature-manual-ledger.md", - ".spec/fleet-plate-tag.md", - ".spec/prototype-comments.json", - ".spec/reconcile-linkage.md", - ".spec/requirements-prd.md", - "components/CreateWizard.tsx", - "components/DateTimePickerSheet.tsx" - ] - }, - { - "version": "v1.7", - "date": "2026-07-17", - "time": "17:19", - "summary": "更新需求说明与标注", - "files": [ - ".spec/2026-07-14-implementation-plan.md", - ".spec/2026-07-14-requirements-design.md", - ".spec/feature-create.md", - ".spec/feature-detail.md", - ".spec/feature-list.md", - ".spec/feature-manual-ledger.md", - ".spec/fleet-plate-tag.md", - ".spec/prototype-comments.json", - ".spec/reconcile-linkage.md", - ".spec/requirements-prd.md", - "components/CreateWizard.tsx", - "components/DateTimePickerSheet.tsx" - ] - }, - { - "version": "v1.6", - "date": "2026-07-17", - "time": "17:19", - "summary": "更新需求说明与标注", - "files": [ - ".spec/2026-07-14-implementation-plan.md", - ".spec/2026-07-14-requirements-design.md", - ".spec/feature-create.md", - ".spec/feature-detail.md", - ".spec/feature-list.md", - ".spec/feature-manual-ledger.md", - ".spec/fleet-plate-tag.md", - ".spec/prototype-comments.json", - ".spec/reconcile-linkage.md", - ".spec/requirements-prd.md", - "components/CreateWizard.tsx", - "components/DateTimePickerSheet.tsx" - ] - }, - { - "version": "v1.5", - "date": "2026-07-17", - "time": "17:19", - "summary": "更新需求说明与标注", - "files": [ - ".spec/2026-07-14-implementation-plan.md", - ".spec/2026-07-14-requirements-design.md", - ".spec/feature-create.md", - ".spec/feature-detail.md", - ".spec/feature-list.md", - ".spec/feature-manual-ledger.md", - ".spec/fleet-plate-tag.md", - ".spec/prototype-comments.json", - ".spec/reconcile-linkage.md", - ".spec/requirements-prd.md", - "components/CreateWizard.tsx", - "components/DateTimePickerSheet.tsx" - ] - }, - { - "version": "v1.4", - "date": "2026-07-17", - "time": "17:18", - "summary": "更新 vehicle-mileage.ts", - "files": [ - "utils/vehicle-mileage.ts" - ] - }, - { - "version": "v1.3", - "date": "2026-07-17", - "time": "17:18", - "summary": "更新 photo-watermark.ts", - "files": [ - "utils/photo-watermark.ts" - ] - }, - { - "version": "v1.2", - "date": "2026-07-17", - "time": "16:10", - "summary": "页面逻辑与交互:CreateWizard.tsx、DateTimePickerSheet.tsx、ManualLedgerPanel.tsx、OrderCard.tsx 等", - "details": [ - "样式与布局:index.css", - "需求说明与 PRD:feature-create.md、feature-detail.md、feature-list.md、feature-manual-ledger.md、fleet-plate-tag.md、reconcile-linkage.md、requirements-prd.md" - ], - "commit": "305199c", - "files": [ - ".spec/feature-create.md", - ".spec/feature-detail.md", - ".spec/feature-list.md", - ".spec/feature-manual-ledger.md", - ".spec/fleet-plate-tag.md", - ".spec/reconcile-linkage.md", - ".spec/requirements-prd.md", - "components/CreateWizard.tsx", - "components/DateTimePickerSheet.tsx", - "components/ManualLedgerPanel.tsx", - "components/OrderCard.tsx", - "components/OrderDetail.tsx" - ] - }, - { - "version": "v1.1", - "date": "2026-07-16", - "time": "09:00", - "summary": "页面逻辑与交互:CreateWizard.tsx、Icons.tsx、OrderCard.tsx、OrderDetail.tsx 等", - "details": [ - "样式与布局:index.css", - "需求说明与 PRD:2026-07-14-implementation-plan.md、2026-07-14-requirements-design.md、reconcile-linkage.md、requirements-prd.md" - ], - "commit": "47c223a", - "files": [ - ".spec/2026-07-14-implementation-plan.md", - ".spec/2026-07-14-requirements-design.md", - ".spec/reconcile-linkage.md", - ".spec/requirements-prd.md", - "components/CreateWizard.tsx", - "components/Icons.tsx", - "components/OrderCard.tsx", - "components/OrderDetail.tsx", - "components/PhoneShell.tsx", - "index.tsx", - "styles/index.css", - "types.ts" - ] } ] }, "oneos-prototype-nav": { "title": "oneos-prototype-nav", - "version": "v1.8", - "revision": 8, - "lastUpdated": "2026-07-18T04:30:15.800Z", + "version": "v1.9", + "revision": 9, + "lastUpdated": "2026-07-18T08:40:14.155Z", "contentHash": "a303e48dcd21ad8e2124de72c0d8dcc7e86abdaca76605af69d33606f6d70363", "trackedFiles": [ ".spec/prototype-comments.json", @@ -1867,6 +2289,22 @@ "xll-nav-source.json" ], "changelog": [ + { + "version": "v1.9", + "date": "2026-07-18", + "time": "16:40", + "summary": "同步更新日志到导航页并准备全量对象存储发布", + "details": [ + "恢复本版加氢链路(加氢记录/氢费明细/H5/车辆管理等)更新说明至最近更新置顶", + "注册表纳入自营物流相关原型与侧栏菜单变更", + "准备全量发布到对象存储" + ], + "files": [ + "nav-menu.json", + "prototype-registry.json", + "xll-nav-menu.json" + ] + }, { "version": "v1.8", "date": "2026-07-18", @@ -2333,10 +2771,10 @@ }, "oneos-web-business": { "title": "业务管理", - "version": "v1.2", - "revision": 2, - "lastUpdated": "2026-07-12T14:45:00.000Z", - "contentHash": "e92802237c9714f6c476d9e69a912494c2d6a574e29e129e36e8969d65ac82aa", + "version": "v1.3", + "revision": 3, + "lastUpdated": "2026-07-18T04:52:34.721Z", + "contentHash": "ad155110734369326adcd3512f653ab9c2d3c468e7d538a08d3ff852c8feea01", "trackedFiles": [ "index.tsx", "pages/01-保险采购.jsx", @@ -2359,6 +2797,26 @@ "pages/18-ETC管理.jsx" ], "changelog": [ + { + "version": "v1.3", + "date": "2026-07-18", + "time": "12:52", + "summary": "更新页面逻辑与交互", + "files": [ + "index.tsx", + "pages/01-保险采购.jsx", + "pages/02-编辑交车任务.jsx", + "pages/03-查看交车任务.jsx", + "pages/04-车辆成本维护.jsx", + "pages/05-供应商管理-编辑.jsx", + "pages/06-供应商管理-查看.jsx", + "pages/07-供应商管理-新增.jsx", + "pages/08-供应商管理.jsx", + "pages/09-交车任务.jsx", + "pages/10-客户管理-编辑.jsx", + "pages/11-客户管理-查看.jsx" + ] + }, { "version": "v1.2", "date": "2026-07-12", @@ -2439,10 +2897,10 @@ }, "oneos-web-data-analysis": { "title": "数据分析", - "version": "v1.4", - "revision": 4, - "lastUpdated": "2026-07-16T01:00:00.000Z", - "contentHash": "ee05562606217fd0e130906981d8d3cd1f672054aa9f887236ddfd8ed8e52c0e", + "version": "v1.5", + "revision": 5, + "lastUpdated": "2026-07-18T04:52:34.724Z", + "contentHash": "4bafcd43ad3b318d5df54797e069575e19703ffa4251581cad4e60da0445974c", "trackedFiles": [ "index.tsx", "pages/01-客户服务部业务统计报表.jsx", @@ -2456,6 +2914,24 @@ "pages/09-租赁客户氢费台账.jsx" ], "changelog": [ + { + "version": "v1.5", + "date": "2026-07-18", + "time": "12:52", + "summary": "更新页面逻辑与交互", + "files": [ + "index.tsx", + "pages/01-客户服务部业务统计报表.jsx", + "pages/02-物流业务台账.jsx", + "pages/03-物流业务月度统计.jsx", + "pages/04-业务部汇总台账.jsx", + "pages/05-业务部台账.jsx", + "pages/06-业务部业绩明细.jsx", + "pages/07-业务台账.jsx", + "pages/08-租赁车辆收入明细.jsx", + "pages/09-租赁客户氢费台账.jsx" + ] + }, { "version": "v1.4", "date": "2026-07-16", @@ -2508,10 +2984,10 @@ }, "oneos-web-finance": { "title": "财务管理(合包)", - "version": "v1.4", - "revision": 4, - "lastUpdated": "2026-07-12T14:45:00.000Z", - "contentHash": "cec2aff84fd721db4c1035728399ce40001999ce5075ad7e59765ddea2f6bea5", + "version": "v1.5", + "revision": 5, + "lastUpdated": "2026-07-18T04:52:34.728Z", + "contentHash": "9b4db379e41d2f9c0184b9983d87ffd6793fe5b915b0bb5e671876910c1f32f1", "trackedFiles": [ "index.tsx", "pages/01-还车应结款-查看.jsx", @@ -2526,6 +3002,25 @@ "pages/10-租赁账单.jsx" ], "changelog": [ + { + "version": "v1.5", + "date": "2026-07-18", + "time": "12:52", + "summary": "更新页面逻辑与交互", + "files": [ + "index.tsx", + "pages/01-还车应结款-查看.jsx", + "pages/02-还车应结款-费用明细.jsx", + "pages/03-还车应结款.jsx", + "pages/04-提车收款单-编辑.jsx", + "pages/05-提车应收款-查看.jsx", + "pages/06-提车应收款-开票信息.jsx", + "pages/07-提车应收款-审核.jsx", + "pages/08-提车应收款-提车收款单.jsx", + "pages/09-提车应收款.jsx", + "pages/10-租赁账单.jsx" + ] + }, { "version": "v1.4", "date": "2026-07-12", @@ -2579,545 +3074,633 @@ }, "oneos-web-h2-station": { "title": "加氢记录", - "version": "v1.80", - "revision": 80, - "lastUpdated": "2026-07-18T04:30:15.800Z", - "contentHash": "58b8bf57edc79b6aaa26ad8edc82e20abfc976130716565640bb342640d07c52", + "version": "v1.129", + "revision": 129, + "lastUpdated": "2026-07-18T10:23:27.330Z", + "contentHash": "fe615b4b6991977297ace6a3f26dba29541509ff20e2273da37370b73b242db6", "trackedFiles": [ + ".spec/feature-create.md", + ".spec/feature-daily-stats.md", + ".spec/feature-export.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/fleet-vehicle-verify.md", + ".spec/flows.md", + ".spec/key-logic.md", ".spec/prototype-comments.json", ".spec/prototype-review.md", + ".spec/reconcile-linkage.md", ".spec/record-data-model.md", ".spec/record-source-changelog.md", ".spec/requirements-prd.md", + ".spec/roles.md", ".spec/ui-review.md", + ".spec/user-stories.md", "index.tsx", "pages/02-加氢记录.jsx", "pages/README.md", - "styles/h2-station-vm-filter.css", - "pages/01-加氢订单.jsx", - "pages/03-站点信息.jsx", - "pages/04-站点周报统计.jsx", - "pages/站点周报统计-需求内容.js", - "shared/h2-station-view-shared.jsx" + "styles/h2-station-vm-filter.css" ], "changelog": [ { - "version": "v1.80", + "version": "v1.129", "date": "2026-07-18", - "time": "12:30", - "summary": "加氢记录:日统计默认收起,来源标签/变更日志/核对人与对账人,筛选规范对齐", + "time": "18:23", + "summary": "更新需求说明与标注", "files": [ + ".spec/feature-create.md", + ".spec/feature-daily-stats.md", + ".spec/feature-export.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/fleet-vehicle-verify.md", + ".spec/flows.md", + ".spec/key-logic.md", ".spec/prototype-comments.json", ".spec/prototype-review.md", - ".spec/record-data-model.md", - ".spec/record-source-changelog.md", - ".spec/requirements-prd.md", - ".spec/ui-review.md", - "index.tsx", - "pages/02-加氢记录.jsx", - "pages/README.md", - "styles/h2-station-vm-filter.css" - ], - "details": [ - "每日统计默认收起为一行 KPI,展开后才显示 7 日卡片,优先露出列表", - "车牌附来源标签:站端上传 / 羚牛上传;操作更多含变更日志", - "核对时间后增核对人,对账时间后增对账人;导出列同步", - "补齐 record-source-changelog 规格与 PRD/标注" + ".spec/reconcile-linkage.md" ] }, { - "version": "v1.79", + "version": "v1.128", "date": "2026-07-18", - "time": "11:46", + "time": "18:23", "summary": "更新需求说明与标注", "files": [ + ".spec/feature-create.md", + ".spec/feature-daily-stats.md", + ".spec/feature-export.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/fleet-vehicle-verify.md", + ".spec/flows.md", + ".spec/key-logic.md", ".spec/prototype-comments.json", ".spec/prototype-review.md", - ".spec/record-data-model.md", - ".spec/record-source-changelog.md", - ".spec/requirements-prd.md", - ".spec/ui-review.md", - "index.tsx", - "pages/02-加氢记录.jsx", - "pages/README.md", - "styles/h2-station-vm-filter.css" + ".spec/reconcile-linkage.md" ] }, { - "version": "v1.78", + "version": "v1.127", "date": "2026-07-18", - "time": "11:46", + "time": "18:11", "summary": "更新需求说明与标注", "files": [ + ".spec/feature-create.md", + ".spec/feature-daily-stats.md", + ".spec/feature-export.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/fleet-vehicle-verify.md", + ".spec/flows.md", + ".spec/key-logic.md", ".spec/prototype-comments.json", ".spec/prototype-review.md", - ".spec/record-data-model.md", - ".spec/record-source-changelog.md", - ".spec/requirements-prd.md", - ".spec/ui-review.md", - "index.tsx", - "pages/02-加氢记录.jsx", - "pages/README.md", - "styles/h2-station-vm-filter.css" + ".spec/reconcile-linkage.md" ] }, { - "version": "v1.77", + "version": "v1.126", "date": "2026-07-18", - "time": "11:42", + "time": "18:11", "summary": "更新需求说明与标注", "files": [ + ".spec/feature-create.md", + ".spec/feature-daily-stats.md", + ".spec/feature-export.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/fleet-vehicle-verify.md", + ".spec/flows.md", + ".spec/key-logic.md", ".spec/prototype-comments.json", ".spec/prototype-review.md", - ".spec/record-data-model.md", - ".spec/requirements-prd.md", - ".spec/ui-review.md", - "index.tsx", - "pages/02-加氢记录.jsx", - "pages/README.md", - "styles/h2-station-vm-filter.css" + ".spec/reconcile-linkage.md" ] }, { - "version": "v1.76", + "version": "v1.125", "date": "2026-07-18", - "time": "11:41", + "time": "18:10", "summary": "更新需求说明与标注", "files": [ + ".spec/feature-create.md", + ".spec/feature-daily-stats.md", + ".spec/feature-export.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/fleet-vehicle-verify.md", + ".spec/flows.md", + ".spec/key-logic.md", ".spec/prototype-comments.json", ".spec/prototype-review.md", - ".spec/record-data-model.md", - ".spec/requirements-prd.md", - ".spec/ui-review.md", - "index.tsx", - "pages/02-加氢记录.jsx", - "pages/README.md", - "styles/h2-station-vm-filter.css" + ".spec/reconcile-linkage.md" ] }, { - "version": "v1.75", + "version": "v1.124", "date": "2026-07-18", - "time": "11:41", + "time": "18:10", "summary": "更新需求说明与标注", "files": [ + ".spec/feature-create.md", + ".spec/feature-daily-stats.md", + ".spec/feature-export.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/fleet-vehicle-verify.md", + ".spec/flows.md", + ".spec/key-logic.md", ".spec/prototype-comments.json", ".spec/prototype-review.md", - ".spec/record-data-model.md", - ".spec/requirements-prd.md", - ".spec/ui-review.md", - "index.tsx", - "pages/02-加氢记录.jsx", - "pages/README.md", - "styles/h2-station-vm-filter.css" + ".spec/reconcile-linkage.md" ] }, { - "version": "v1.74", + "version": "v1.123", "date": "2026-07-18", - "time": "11:41", + "time": "18:10", "summary": "更新需求说明与标注", "files": [ + ".spec/feature-create.md", + ".spec/feature-daily-stats.md", + ".spec/feature-export.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/fleet-vehicle-verify.md", + ".spec/flows.md", + ".spec/key-logic.md", ".spec/prototype-comments.json", ".spec/prototype-review.md", - ".spec/record-data-model.md", - ".spec/requirements-prd.md", - ".spec/ui-review.md", - "index.tsx", - "pages/02-加氢记录.jsx", - "pages/README.md", - "styles/h2-station-vm-filter.css" + ".spec/reconcile-linkage.md" ] }, { - "version": "v1.73", + "version": "v1.122", "date": "2026-07-18", - "time": "11:41", + "time": "18:09", "summary": "更新需求说明与标注", "files": [ + ".spec/feature-create.md", + ".spec/feature-daily-stats.md", + ".spec/feature-export.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/fleet-vehicle-verify.md", + ".spec/flows.md", + ".spec/key-logic.md", ".spec/prototype-comments.json", ".spec/prototype-review.md", - ".spec/record-data-model.md", - ".spec/requirements-prd.md", - ".spec/ui-review.md", - "index.tsx", - "pages/02-加氢记录.jsx", - "pages/README.md", - "styles/h2-station-vm-filter.css" + ".spec/reconcile-linkage.md" ] }, { - "version": "v1.72", + "version": "v1.121", "date": "2026-07-18", - "time": "11:41", + "time": "18:09", "summary": "更新需求说明与标注", "files": [ + ".spec/feature-create.md", + ".spec/feature-daily-stats.md", + ".spec/feature-export.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/fleet-vehicle-verify.md", + ".spec/flows.md", + ".spec/key-logic.md", ".spec/prototype-comments.json", ".spec/prototype-review.md", - ".spec/record-data-model.md", - ".spec/requirements-prd.md", - ".spec/ui-review.md", - "index.tsx", - "pages/02-加氢记录.jsx", - "pages/README.md", - "styles/h2-station-vm-filter.css" + ".spec/reconcile-linkage.md" ] }, { - "version": "v1.71", + "version": "v1.120", "date": "2026-07-18", - "time": "11:40", + "time": "18:09", "summary": "更新需求说明与标注", "files": [ + ".spec/feature-create.md", + ".spec/feature-daily-stats.md", + ".spec/feature-export.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/fleet-vehicle-verify.md", + ".spec/flows.md", + ".spec/key-logic.md", ".spec/prototype-comments.json", ".spec/prototype-review.md", - ".spec/record-data-model.md", - ".spec/requirements-prd.md", - ".spec/ui-review.md", - "index.tsx", - "pages/02-加氢记录.jsx", - "pages/README.md", - "styles/h2-station-vm-filter.css" + ".spec/reconcile-linkage.md" ] }, { - "version": "v1.70", + "version": "v1.119", "date": "2026-07-18", - "time": "11:40", + "time": "18:09", "summary": "更新需求说明与标注", "files": [ + ".spec/feature-create.md", + ".spec/feature-daily-stats.md", + ".spec/feature-export.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/fleet-vehicle-verify.md", + ".spec/flows.md", + ".spec/key-logic.md", ".spec/prototype-comments.json", ".spec/prototype-review.md", - ".spec/record-data-model.md", - ".spec/requirements-prd.md", - ".spec/ui-review.md", - "index.tsx", - "pages/02-加氢记录.jsx", - "pages/README.md", - "styles/h2-station-vm-filter.css" + ".spec/reconcile-linkage.md" ] }, { - "version": "v1.69", + "version": "v1.118", "date": "2026-07-18", - "time": "11:31", + "time": "18:05", "summary": "更新需求说明与标注", "files": [ + ".spec/feature-create.md", + ".spec/feature-daily-stats.md", + ".spec/feature-export.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/fleet-vehicle-verify.md", + ".spec/flows.md", + ".spec/key-logic.md", ".spec/prototype-comments.json", ".spec/prototype-review.md", - ".spec/record-data-model.md", - ".spec/requirements-prd.md", - ".spec/ui-review.md", - "index.tsx", - "pages/02-加氢记录.jsx", - "pages/README.md", - "styles/h2-station-vm-filter.css" + ".spec/reconcile-linkage.md" ] }, { - "version": "v1.68", + "version": "v1.117", "date": "2026-07-18", - "time": "11:31", + "time": "18:05", "summary": "更新需求说明与标注", "files": [ + ".spec/feature-create.md", + ".spec/feature-daily-stats.md", + ".spec/feature-export.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/fleet-vehicle-verify.md", + ".spec/flows.md", + ".spec/key-logic.md", ".spec/prototype-comments.json", ".spec/prototype-review.md", - ".spec/record-data-model.md", - ".spec/requirements-prd.md", - ".spec/ui-review.md", - "index.tsx", - "pages/02-加氢记录.jsx", - "pages/README.md", - "styles/h2-station-vm-filter.css" + ".spec/reconcile-linkage.md" ] }, { - "version": "v1.67", + "version": "v1.116", "date": "2026-07-18", - "time": "11:29", + "time": "18:05", "summary": "更新需求说明与标注", "files": [ + ".spec/feature-create.md", + ".spec/feature-daily-stats.md", + ".spec/feature-export.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/fleet-vehicle-verify.md", + ".spec/flows.md", + ".spec/key-logic.md", ".spec/prototype-comments.json", ".spec/prototype-review.md", - ".spec/record-data-model.md", - ".spec/requirements-prd.md", - ".spec/ui-review.md", - "index.tsx", - "pages/02-加氢记录.jsx", - "pages/README.md", - "styles/h2-station-vm-filter.css" + ".spec/reconcile-linkage.md" ] }, { - "version": "v1.66", + "version": "v1.115", "date": "2026-07-18", - "time": "11:29", + "time": "18:04", "summary": "更新需求说明与标注", "files": [ + ".spec/feature-create.md", + ".spec/feature-daily-stats.md", + ".spec/feature-export.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/fleet-vehicle-verify.md", + ".spec/flows.md", + ".spec/key-logic.md", ".spec/prototype-comments.json", ".spec/prototype-review.md", - ".spec/record-data-model.md", - ".spec/requirements-prd.md", - ".spec/ui-review.md", - "index.tsx", - "pages/02-加氢记录.jsx", - "pages/README.md", - "styles/h2-station-vm-filter.css" + ".spec/reconcile-linkage.md" ] }, { - "version": "v1.65", + "version": "v1.114", "date": "2026-07-18", - "time": "11:28", + "time": "18:04", "summary": "更新需求说明与标注", "files": [ + ".spec/feature-create.md", + ".spec/feature-daily-stats.md", + ".spec/feature-export.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/fleet-vehicle-verify.md", + ".spec/flows.md", + ".spec/key-logic.md", ".spec/prototype-comments.json", ".spec/prototype-review.md", - ".spec/record-data-model.md", - ".spec/requirements-prd.md", - ".spec/ui-review.md", - "index.tsx", - "pages/02-加氢记录.jsx", - "pages/README.md", - "styles/h2-station-vm-filter.css" + ".spec/reconcile-linkage.md" ] }, { - "version": "v1.64", + "version": "v1.113", "date": "2026-07-18", - "time": "11:28", + "time": "18:04", "summary": "更新需求说明与标注", "files": [ + ".spec/feature-create.md", + ".spec/feature-daily-stats.md", + ".spec/feature-export.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/fleet-vehicle-verify.md", + ".spec/flows.md", + ".spec/key-logic.md", ".spec/prototype-comments.json", ".spec/prototype-review.md", - ".spec/record-data-model.md", - ".spec/requirements-prd.md", - ".spec/ui-review.md", - "index.tsx", - "pages/02-加氢记录.jsx", - "pages/README.md", - "styles/h2-station-vm-filter.css" + ".spec/reconcile-linkage.md" ] }, { - "version": "v1.63", + "version": "v1.112", "date": "2026-07-18", - "time": "11:27", + "time": "18:03", "summary": "更新需求说明与标注", "files": [ + ".spec/feature-create.md", + ".spec/feature-daily-stats.md", + ".spec/feature-export.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/fleet-vehicle-verify.md", + ".spec/flows.md", + ".spec/key-logic.md", ".spec/prototype-comments.json", ".spec/prototype-review.md", - ".spec/record-data-model.md", - ".spec/requirements-prd.md", - ".spec/ui-review.md", - "index.tsx", - "pages/02-加氢记录.jsx", - "pages/README.md", - "styles/h2-station-vm-filter.css" + ".spec/reconcile-linkage.md" ] }, { - "version": "v1.62", + "version": "v1.111", "date": "2026-07-18", - "time": "11:27", + "time": "18:03", "summary": "更新需求说明与标注", "files": [ + ".spec/feature-create.md", + ".spec/feature-daily-stats.md", + ".spec/feature-export.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/fleet-vehicle-verify.md", + ".spec/flows.md", + ".spec/key-logic.md", ".spec/prototype-comments.json", ".spec/prototype-review.md", - ".spec/record-data-model.md", - ".spec/requirements-prd.md", - ".spec/ui-review.md", - "index.tsx", - "pages/02-加氢记录.jsx", - "pages/README.md", - "styles/h2-station-vm-filter.css" + ".spec/reconcile-linkage.md" ] }, { - "version": "v1.61", + "version": "v1.110", "date": "2026-07-18", - "time": "11:27", + "time": "18:03", "summary": "更新需求说明与标注", "files": [ + ".spec/feature-create.md", + ".spec/feature-daily-stats.md", + ".spec/feature-export.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/fleet-vehicle-verify.md", + ".spec/flows.md", + ".spec/key-logic.md", ".spec/prototype-comments.json", ".spec/prototype-review.md", - ".spec/record-data-model.md", - ".spec/requirements-prd.md", - ".spec/ui-review.md", - "index.tsx", - "pages/02-加氢记录.jsx", - "pages/README.md", - "styles/h2-station-vm-filter.css" + ".spec/reconcile-linkage.md" ] }, { - "version": "v1.60", + "version": "v1.109", "date": "2026-07-18", - "time": "11:24", + "time": "18:01", "summary": "更新需求说明与标注", "files": [ + ".spec/feature-create.md", + ".spec/feature-daily-stats.md", + ".spec/feature-export.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/fleet-vehicle-verify.md", + ".spec/flows.md", + ".spec/key-logic.md", ".spec/prototype-comments.json", ".spec/prototype-review.md", - ".spec/record-data-model.md", - ".spec/requirements-prd.md", - ".spec/ui-review.md", - "index.tsx", - "pages/02-加氢记录.jsx", - "pages/README.md", - "styles/h2-station-vm-filter.css" + ".spec/reconcile-linkage.md" ] }, { - "version": "v1.59", + "version": "v1.108", "date": "2026-07-18", - "time": "11:24", + "time": "17:27", "summary": "更新需求说明与标注", "files": [ + ".spec/feature-create.md", + ".spec/feature-daily-stats.md", + ".spec/feature-export.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/fleet-vehicle-verify.md", + ".spec/flows.md", + ".spec/key-logic.md", ".spec/prototype-comments.json", ".spec/prototype-review.md", - ".spec/record-data-model.md", - ".spec/requirements-prd.md", - ".spec/ui-review.md", - "index.tsx", - "pages/02-加氢记录.jsx", - "pages/README.md", - "styles/h2-station-vm-filter.css" + ".spec/reconcile-linkage.md" ] }, { - "version": "v1.58", + "version": "v1.107", "date": "2026-07-18", - "time": "11:22", + "time": "17:27", "summary": "更新需求说明与标注", "files": [ + ".spec/feature-create.md", + ".spec/feature-daily-stats.md", + ".spec/feature-export.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/fleet-vehicle-verify.md", + ".spec/flows.md", + ".spec/key-logic.md", ".spec/prototype-comments.json", ".spec/prototype-review.md", - ".spec/record-data-model.md", - ".spec/requirements-prd.md", - ".spec/ui-review.md", - "index.tsx", - "pages/02-加氢记录.jsx", - "pages/README.md", - "styles/h2-station-vm-filter.css" + ".spec/reconcile-linkage.md" ] }, { - "version": "v1.57", + "version": "v1.106", "date": "2026-07-18", - "time": "11:22", + "time": "17:27", "summary": "更新需求说明与标注", "files": [ + ".spec/feature-create.md", + ".spec/feature-daily-stats.md", + ".spec/feature-export.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/fleet-vehicle-verify.md", + ".spec/flows.md", + ".spec/key-logic.md", ".spec/prototype-comments.json", ".spec/prototype-review.md", - ".spec/record-data-model.md", - ".spec/requirements-prd.md", - ".spec/ui-review.md", - "index.tsx", - "pages/02-加氢记录.jsx", - "pages/README.md", - "styles/h2-station-vm-filter.css" + ".spec/reconcile-linkage.md" ] }, { - "version": "v1.56", + "version": "v1.105", "date": "2026-07-18", - "time": "11:22", + "time": "17:27", "summary": "更新需求说明与标注", "files": [ + ".spec/feature-create.md", + ".spec/feature-daily-stats.md", + ".spec/feature-export.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/fleet-vehicle-verify.md", + ".spec/flows.md", + ".spec/key-logic.md", ".spec/prototype-comments.json", ".spec/prototype-review.md", - ".spec/record-data-model.md", - ".spec/requirements-prd.md", - ".spec/ui-review.md", - "index.tsx", - "pages/02-加氢记录.jsx", - "pages/README.md", - "styles/h2-station-vm-filter.css" + ".spec/reconcile-linkage.md" ] }, { - "version": "v1.55", + "version": "v1.104", "date": "2026-07-18", - "time": "11:20", + "time": "17:27", "summary": "更新需求说明与标注", "files": [ + ".spec/feature-create.md", + ".spec/feature-daily-stats.md", + ".spec/feature-export.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/fleet-vehicle-verify.md", + ".spec/flows.md", + ".spec/key-logic.md", ".spec/prototype-comments.json", ".spec/prototype-review.md", - ".spec/record-data-model.md", - ".spec/requirements-prd.md", - ".spec/ui-review.md", - "index.tsx", - "pages/02-加氢记录.jsx", - "pages/README.md", - "styles/h2-station-vm-filter.css" + ".spec/reconcile-linkage.md" ] }, { - "version": "v1.54", + "version": "v1.103", "date": "2026-07-18", - "time": "11:19", + "time": "17:26", "summary": "更新需求说明与标注", "files": [ + ".spec/feature-create.md", + ".spec/feature-daily-stats.md", + ".spec/feature-export.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/fleet-vehicle-verify.md", + ".spec/flows.md", + ".spec/key-logic.md", ".spec/prototype-comments.json", ".spec/prototype-review.md", - ".spec/record-data-model.md", - ".spec/requirements-prd.md", - ".spec/ui-review.md", - "index.tsx", - "pages/02-加氢记录.jsx", - "pages/README.md", - "styles/h2-station-vm-filter.css" + ".spec/reconcile-linkage.md" ] }, { - "version": "v1.53", + "version": "v1.102", "date": "2026-07-18", - "time": "11:19", + "time": "17:26", "summary": "更新需求说明与标注", "files": [ + ".spec/feature-create.md", + ".spec/feature-daily-stats.md", + ".spec/feature-export.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/fleet-vehicle-verify.md", + ".spec/flows.md", + ".spec/key-logic.md", ".spec/prototype-comments.json", ".spec/prototype-review.md", - ".spec/record-data-model.md", - ".spec/requirements-prd.md", - ".spec/ui-review.md", - "index.tsx", - "pages/02-加氢记录.jsx", - "pages/README.md", - "styles/h2-station-vm-filter.css" + ".spec/reconcile-linkage.md" ] }, { - "version": "v1.52", + "version": "v1.101", "date": "2026-07-18", - "time": "11:18", + "time": "17:26", "summary": "更新需求说明与标注", "files": [ + ".spec/feature-create.md", + ".spec/feature-daily-stats.md", + ".spec/feature-export.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/fleet-vehicle-verify.md", + ".spec/flows.md", + ".spec/key-logic.md", ".spec/prototype-comments.json", ".spec/prototype-review.md", - ".spec/record-data-model.md", - ".spec/requirements-prd.md", - ".spec/ui-review.md", - "index.tsx", - "pages/02-加氢记录.jsx", - "pages/README.md", - "styles/h2-station-vm-filter.css" + ".spec/reconcile-linkage.md" ] }, { - "version": "v1.51", + "version": "v1.100", "date": "2026-07-18", - "time": "11:18", + "time": "17:25", "summary": "更新需求说明与标注", "files": [ + ".spec/feature-create.md", + ".spec/feature-daily-stats.md", + ".spec/feature-export.md", + ".spec/feature-list.md", + ".spec/feature-manual-ledger.md", + ".spec/fleet-plate-tag.md", + ".spec/fleet-vehicle-verify.md", + ".spec/flows.md", + ".spec/key-logic.md", ".spec/prototype-comments.json", ".spec/prototype-review.md", - ".spec/record-data-model.md", - ".spec/requirements-prd.md", - ".spec/ui-review.md", - "index.tsx", - "pages/02-加氢记录.jsx", - "pages/README.md", - "styles/h2-station-vm-filter.css" + ".spec/reconcile-linkage.md" ] } ] @@ -3214,10 +3797,10 @@ }, "oneos-web-h2-station-site": { "title": "站点信息", - "version": "v1.27", - "revision": 27, - "lastUpdated": "2026-07-17T09:43:57.245Z", - "contentHash": "8c53fc1d73b2a4b0310916569e6248b04e44b8ec3902a4f32c86e5a969a422fd", + "version": "v1.37", + "revision": 37, + "lastUpdated": "2026-07-18T10:51:48.965Z", + "contentHash": "a4e79679c1808714d0e8d484ff491b7bccaac93edfe6cb9356583a3e2bdf6714", "trackedFiles": [ ".spec/dispenser-brand.md", ".spec/period-end-balance.md", @@ -3233,6 +3816,206 @@ "pages/03-站点信息.jsx" ], "changelog": [ + { + "version": "v1.37", + "date": "2026-07-18", + "time": "18:51", + "summary": "更新需求说明与标注", + "files": [ + ".spec/dispenser-brand.md", + ".spec/period-end-balance.md", + ".spec/prototype-comments.json", + ".spec/prototype-review.md", + ".spec/requirements-prd-kpi-balance-alert.md", + ".spec/requirements-prd-list.md", + ".spec/requirements-prd-statement.md", + ".spec/requirements-prd.md", + ".spec/requirements.md", + ".spec/statement-verified-filter.md", + "index.tsx", + "pages/03-站点信息.jsx" + ] + }, + { + "version": "v1.36", + "date": "2026-07-18", + "time": "18:51", + "summary": "更新需求说明与标注", + "files": [ + ".spec/dispenser-brand.md", + ".spec/period-end-balance.md", + ".spec/prototype-comments.json", + ".spec/prototype-review.md", + ".spec/requirements-prd-kpi-balance-alert.md", + ".spec/requirements-prd-list.md", + ".spec/requirements-prd-statement.md", + ".spec/requirements-prd.md", + ".spec/requirements.md", + ".spec/statement-verified-filter.md", + "index.tsx", + "pages/03-站点信息.jsx" + ] + }, + { + "version": "v1.35", + "date": "2026-07-18", + "time": "18:49", + "summary": "更新需求说明与标注", + "files": [ + ".spec/dispenser-brand.md", + ".spec/period-end-balance.md", + ".spec/prototype-comments.json", + ".spec/prototype-review.md", + ".spec/requirements-prd-kpi-balance-alert.md", + ".spec/requirements-prd-list.md", + ".spec/requirements-prd-statement.md", + ".spec/requirements-prd.md", + ".spec/requirements.md", + ".spec/statement-verified-filter.md", + "index.tsx", + "pages/03-站点信息.jsx" + ] + }, + { + "version": "v1.34", + "date": "2026-07-18", + "time": "18:48", + "summary": "更新需求说明与标注", + "files": [ + ".spec/dispenser-brand.md", + ".spec/period-end-balance.md", + ".spec/prototype-comments.json", + ".spec/prototype-review.md", + ".spec/requirements-prd-kpi-balance-alert.md", + ".spec/requirements-prd-list.md", + ".spec/requirements-prd-statement.md", + ".spec/requirements-prd.md", + ".spec/requirements.md", + ".spec/statement-verified-filter.md", + "index.tsx", + "pages/03-站点信息.jsx" + ] + }, + { + "version": "v1.33", + "date": "2026-07-18", + "time": "18:48", + "summary": "更新需求说明与标注", + "files": [ + ".spec/dispenser-brand.md", + ".spec/period-end-balance.md", + ".spec/prototype-comments.json", + ".spec/prototype-review.md", + ".spec/requirements-prd-kpi-balance-alert.md", + ".spec/requirements-prd-list.md", + ".spec/requirements-prd-statement.md", + ".spec/requirements-prd.md", + ".spec/requirements.md", + ".spec/statement-verified-filter.md", + "index.tsx", + "pages/03-站点信息.jsx" + ] + }, + { + "version": "v1.32", + "date": "2026-07-18", + "time": "18:48", + "summary": "更新需求说明与标注", + "files": [ + ".spec/dispenser-brand.md", + ".spec/period-end-balance.md", + ".spec/prototype-comments.json", + ".spec/prototype-review.md", + ".spec/requirements-prd-kpi-balance-alert.md", + ".spec/requirements-prd-list.md", + ".spec/requirements-prd-statement.md", + ".spec/requirements-prd.md", + ".spec/requirements.md", + ".spec/statement-verified-filter.md", + "index.tsx", + "pages/03-站点信息.jsx" + ] + }, + { + "version": "v1.31", + "date": "2026-07-18", + "time": "18:48", + "summary": "更新需求说明与标注", + "files": [ + ".spec/dispenser-brand.md", + ".spec/period-end-balance.md", + ".spec/prototype-comments.json", + ".spec/prototype-review.md", + ".spec/requirements-prd-kpi-balance-alert.md", + ".spec/requirements-prd-list.md", + ".spec/requirements-prd-statement.md", + ".spec/requirements-prd.md", + ".spec/requirements.md", + ".spec/statement-verified-filter.md", + "index.tsx", + "pages/03-站点信息.jsx" + ] + }, + { + "version": "v1.30", + "date": "2026-07-18", + "time": "18:47", + "summary": "更新需求说明与标注", + "files": [ + ".spec/dispenser-brand.md", + ".spec/period-end-balance.md", + ".spec/prototype-comments.json", + ".spec/prototype-review.md", + ".spec/requirements-prd-kpi-balance-alert.md", + ".spec/requirements-prd-list.md", + ".spec/requirements-prd-statement.md", + ".spec/requirements-prd.md", + ".spec/requirements.md", + ".spec/statement-verified-filter.md", + "index.tsx", + "pages/03-站点信息.jsx" + ] + }, + { + "version": "v1.29", + "date": "2026-07-18", + "time": "18:47", + "summary": "更新需求说明与标注", + "files": [ + ".spec/dispenser-brand.md", + ".spec/period-end-balance.md", + ".spec/prototype-comments.json", + ".spec/prototype-review.md", + ".spec/requirements-prd-kpi-balance-alert.md", + ".spec/requirements-prd-list.md", + ".spec/requirements-prd-statement.md", + ".spec/requirements-prd.md", + ".spec/requirements.md", + ".spec/statement-verified-filter.md", + "index.tsx", + "pages/03-站点信息.jsx" + ] + }, + { + "version": "v1.28", + "date": "2026-07-18", + "time": "12:52", + "summary": "更新需求说明与标注", + "files": [ + ".spec/dispenser-brand.md", + ".spec/period-end-balance.md", + ".spec/prototype-comments.json", + ".spec/prototype-review.md", + ".spec/requirements-prd-kpi-balance-alert.md", + ".spec/requirements-prd-list.md", + ".spec/requirements-prd-statement.md", + ".spec/requirements-prd.md", + ".spec/requirements.md", + ".spec/statement-verified-filter.md", + "index.tsx", + "pages/03-站点信息.jsx" + ] + }, { "version": "v1.27", "date": "2026-07-17", @@ -3616,116 +4399,6 @@ "index.tsx", "pages/03-站点信息.jsx" ] - }, - { - "version": "v1.7", - "date": "2026-07-17", - "time": "16:25", - "summary": "更新需求说明与标注", - "files": [ - ".spec/period-end-balance.md", - ".spec/prototype-comments.json", - ".spec/prototype-review.md", - ".spec/requirements-prd-kpi-balance-alert.md", - ".spec/requirements-prd-list.md", - ".spec/requirements-prd-statement.md", - ".spec/requirements-prd.md", - ".spec/requirements.md", - "index.tsx", - "pages/03-站点信息.jsx" - ] - }, - { - "version": "v1.6", - "date": "2026-07-17", - "time": "16:10", - "summary": "手工台账与预约列表", - "details": [ - "导入主键与实收规则", - "并同步站点信息拆分与导航。", - "页面逻辑与交互:index.tsx", - "需求说明与 PRD:period-end-balance.md、prototype-review.md、requirements-prd-kpi-balance-alert.md、requirements-prd-list.md、requirements-prd.md" - ], - "commit": "305199c", - "files": [ - ".spec/period-end-balance.md", - ".spec/prototype-review.md", - ".spec/requirements-prd-kpi-balance-alert.md", - ".spec/requirements-prd-list.md", - ".spec/requirements-prd.md", - "index.tsx" - ] - }, - { - "version": "v1.5", - "date": "2026-07-16", - "time": "09:00", - "summary": "需求说明与 PRD:requirements-prd-list.md、requirements-prd.md、requirements.md", - "details": [], - "commit": "47c223a", - "files": [ - ".spec/requirements-prd-list.md", - ".spec/requirements-prd.md", - ".spec/requirements.md" - ] - }, - { - "version": "v1.4", - "date": "2026-07-12", - "time": "22:45", - "summary": "站点信息:标注目录清理", - "details": [ - "移除内嵌原型导航节点;标注壳切换" - ], - "commit": "131b963", - "files": [ - "index.tsx" - ] - }, - { - "version": "v1.3", - "date": "2026-07-09", - "time": "17:52", - "summary": "需求说明与 PRD:requirements-prd-statement.md、requirements-prd.md", - "details": [], - "commit": "6ccbbef", - "files": [ - ".spec/requirements-prd-statement.md", - ".spec/requirements-prd.md" - ] - }, - { - "version": "v1.2", - "date": "2026-07-08", - "time": "11:44", - "summary": "需求说明与 PRD:requirements-prd-kpi-balance-alert.md、requirements-prd-list.md、requirements-prd.md、requirements.md", - "details": [], - "commit": "6d14c58", - "files": [ - ".spec/requirements-prd-kpi-balance-alert.md", - ".spec/requirements-prd-list.md", - ".spec/requirements-prd.md", - ".spec/requirements.md" - ] - }, - { - "version": "v1.1", - "date": "2026-07-02", - "time": "03:35", - "summary": "加氢站:新增站点信息独立原型,订单/记录页与共享样式调整", - "details": [ - "页面逻辑与交互:index.tsx", - "需求说明与 PRD:prototype-review.md、requirements-prd-list.md、requirements-prd-statement.md、requirements-prd.md、requirements.md" - ], - "commit": "f5e3126", - "files": [ - ".spec/prototype-review.md", - ".spec/requirements-prd-list.md", - ".spec/requirements-prd-statement.md", - ".spec/requirements-prd.md", - ".spec/requirements.md", - "index.tsx" - ] } ] }, @@ -3827,10 +4500,10 @@ }, "oneos-web-lease-contract": { "title": "车辆租赁合同", - "version": "v1.3", - "revision": 3, - "lastUpdated": "2026-07-12T14:52:00.000Z", - "contentHash": "bc9e3b24d64b414542a9f45a6099db9b6e41d4a8348b6c3dc2c39b6907f44180", + "version": "v1.4", + "revision": 4, + "lastUpdated": "2026-07-18T04:52:34.734Z", + "contentHash": "bd3cc9201c0cd91d1008c143b69fd31b05a9323262c59f3f2ff0cbb31e605a95", "trackedFiles": [ "index.tsx", "pages/01-车辆租赁合同-变更为三方合同.jsx", @@ -3843,6 +4516,23 @@ "pages/08-车辆租赁合同.jsx" ], "changelog": [ + { + "version": "v1.4", + "date": "2026-07-18", + "time": "12:52", + "summary": "更新页面逻辑与交互", + "files": [ + "index.tsx", + "pages/01-车辆租赁合同-变更为三方合同.jsx", + "pages/02-车辆租赁合同-查看.jsx", + "pages/03-车辆租赁合同-附加费用.jsx", + "pages/04-车辆租赁合同-新增.jsx", + "pages/05-车辆租赁合同-新增车辆.jsx", + "pages/06-车辆租赁合同-续签合同.jsx", + "pages/07-车辆租赁合同-转正式合同.jsx", + "pages/08-车辆租赁合同.jsx" + ] + }, { "version": "v1.3", "date": "2026-07-12", @@ -3883,10 +4573,10 @@ }, "oneos-web-ledger-data": { "title": "台账数据", - "version": "v1.3", - "revision": 3, - "lastUpdated": "2026-07-12T14:45:00.000Z", - "contentHash": "71c1849f4a65dcbc499c8c878715b5be510165faae2c60fd955024b1dada2a0c", + "version": "v1.4", + "revision": 4, + "lastUpdated": "2026-07-18T04:52:34.737Z", + "contentHash": "f19c739ca8a8e9652c9bf0c94a3523e6350ce97ec3d1aa4e5688e08542c5190a", "trackedFiles": [ "index.tsx", "pages/01-保险分摊明细.jsx", @@ -3896,6 +4586,20 @@ "pages/05-租赁业务台账.jsx" ], "changelog": [ + { + "version": "v1.4", + "date": "2026-07-18", + "time": "12:52", + "summary": "更新页面逻辑与交互", + "files": [ + "index.tsx", + "pages/01-保险分摊明细.jsx", + "pages/02-车辆氢费明细.jsx", + "pages/03-车辆维修明细.jsx", + "pages/04-氢费采购端汇总报表.jsx", + "pages/05-租赁业务台账.jsx" + ] + }, { "version": "v1.3", "date": "2026-07-12", @@ -3937,10 +4641,10 @@ }, "oneos-web-ops": { "title": "运维管理", - "version": "v1.2", - "revision": 2, - "lastUpdated": "2026-07-12T14:45:00.000Z", - "contentHash": "7f3478394aa4902e8d27903e9647e339c4da9d3326e9ff275ac096e3d18bcbe6", + "version": "v1.3", + "revision": 3, + "lastUpdated": "2026-07-18T04:52:34.787Z", + "contentHash": "616051ceca4a80f9dae16f1ec9c6b50638ad0a0f36bbd98e2cc8c28651fb8407", "trackedFiles": [ "index.tsx", "pages/01-备件库存.jsx", @@ -3994,6 +4698,26 @@ "pages/49-型号参数.jsx" ], "changelog": [ + { + "version": "v1.3", + "date": "2026-07-18", + "time": "12:52", + "summary": "更新页面逻辑与交互", + "files": [ + "index.tsx", + "pages/01-备件库存.jsx", + "pages/02-仓库管理.jsx", + "pages/03-备车-编辑.jsx", + "pages/04-备车-查看.jsx", + "pages/05-备车-新增.jsx", + "pages/06-备车管理.jsx", + "pages/07-查看故障.jsx", + "pages/08-调拨管理-查看.jsx", + "pages/09-调拨管理-调拨信息.jsx", + "pages/10-调拨管理-接收信息-第三方运输.jsx", + "pages/11-调拨管理-接收信息-司机运输.jsx" + ] + }, { "version": "v1.2", "date": "2026-07-12", @@ -4024,16 +4748,27 @@ }, "oneos-web-procurement": { "title": "采购管理", - "version": "v1.3", - "revision": 3, - "lastUpdated": "2026-07-12T14:45:00.000Z", - "contentHash": "de6458042cdb2d87f28d5ce249fca712009d2dba4e5be5bbba08559f07bc9d00", + "version": "v1.4", + "revision": 4, + "lastUpdated": "2026-07-18T04:52:34.790Z", + "contentHash": "1ed4ef7cb3b2537ee9de548a5344babf8247594cd0f4259c674163c92abe0013", "trackedFiles": [ "index.tsx", "pages/01-三方退租车管理.jsx", "pages/02-三方退租申请-新增.jsx" ], "changelog": [ + { + "version": "v1.4", + "date": "2026-07-18", + "time": "12:52", + "summary": "更新页面逻辑与交互", + "files": [ + "index.tsx", + "pages/01-三方退租车管理.jsx", + "pages/02-三方退租申请-新增.jsx" + ] + }, { "version": "v1.3", "date": "2026-07-12", @@ -4077,7 +4812,7 @@ "title": "工作台", "version": "v1.6", "revision": 6, - "lastUpdated": "2026-07-15T00:51:55.976Z", + "lastUpdated": "2026-07-18T04:52:34.792Z", "contentHash": "278a917e174783edccc1915cfa757bdfdf8fc81e83c9e42e31e7ed99811d5b4b", "trackedFiles": [ "index.tsx", @@ -4151,12 +4886,14 @@ }, "payment-records": { "title": "收款记录", - "version": "v1.3", - "revision": 3, - "lastUpdated": "2026-07-12T14:45:00.000Z", - "contentHash": "c6edcdbf2d5a6fd861d2a17324d57822a1a7faf75237f75a9ae3d126826f9e09", + "version": "v1.7", + "revision": 7, + "lastUpdated": "2026-07-18T12:33:10.629Z", + "contentHash": "87ce436e9d89ef078d80290f3f0e26da874f3c96bf70f4bd8b76c4fc2c0c3e0c", "trackedFiles": [ + ".spec/payment-notify.md", ".spec/prototype-comments.json", + ".spec/requirements-prd.md", "components/AddPaymentModal.tsx", "components/BillLinkModal.tsx", "components/FilterPanel.tsx", @@ -4165,13 +4902,87 @@ "components/PaymentKpiDetailModal.tsx", "components/PaymentRecordsTable.tsx", "data/bills.json", + "data/notify-customer-map.json", "data/records.json", "index.tsx", "styles/index.css", "types.ts", + "utils/notify-recipients.ts", "utils/payment.ts" ], "changelog": [ + { + "version": "v1.7", + "date": "2026-07-18", + "time": "20:33", + "summary": "更新需求说明与标注", + "files": [ + ".spec/payment-notify.md", + ".spec/prototype-comments.json", + ".spec/requirements-prd.md", + "components/AddPaymentModal.tsx", + "components/BillLinkModal.tsx", + "components/FilterPanel.tsx", + "components/LinkedBillsNestedTable.tsx", + "components/PaymentKpiDetailFilterBar.tsx", + "components/PaymentKpiDetailModal.tsx", + "components/PaymentRecordsTable.tsx", + "data/bills.json", + "data/notify-customer-map.json" + ] + }, + { + "version": "v1.6", + "date": "2026-07-18", + "time": "20:33", + "summary": "更新需求说明与标注", + "files": [ + ".spec/payment-notify.md", + ".spec/prototype-comments.json", + ".spec/requirements-prd.md", + "components/AddPaymentModal.tsx", + "components/BillLinkModal.tsx", + "components/FilterPanel.tsx", + "components/LinkedBillsNestedTable.tsx", + "components/PaymentKpiDetailFilterBar.tsx", + "components/PaymentKpiDetailModal.tsx", + "components/PaymentRecordsTable.tsx", + "data/bills.json", + "data/notify-customer-map.json" + ] + }, + { + "version": "v1.5", + "date": "2026-07-18", + "time": "20:33", + "summary": "更新需求说明与标注", + "files": [ + ".spec/payment-notify.md", + ".spec/requirements-prd.md", + "data/notify-customer-map.json", + "utils/notify-recipients.ts" + ] + }, + { + "version": "v1.4", + "date": "2026-07-18", + "time": "12:52", + "summary": "更新页面样式", + "files": [ + ".spec/prototype-comments.json", + "components/AddPaymentModal.tsx", + "components/BillLinkModal.tsx", + "components/FilterPanel.tsx", + "components/LinkedBillsNestedTable.tsx", + "components/PaymentKpiDetailFilterBar.tsx", + "components/PaymentKpiDetailModal.tsx", + "components/PaymentRecordsTable.tsx", + "data/bills.json", + "data/records.json", + "index.tsx", + "styles/index.css" + ] + }, { "version": "v1.3", "date": "2026-07-12", @@ -4245,11 +5056,12 @@ }, "self-operated-business-ledger": { "title": "物流业务明细", - "version": "v1.6", - "revision": 6, - "lastUpdated": "2026-07-16T01:00:00.000Z", - "contentHash": "f6a4e1450eaa01e07f5c52ee449b6b7f6c535f74f5e2d981f1691563939a4bba", + "version": "v1.12", + "revision": 12, + "lastUpdated": "2026-07-18T04:52:24.805Z", + "contentHash": "f452493a1c6edf89b6bee630fd095e9b6f747cc132ed008c64bd640436ff5173", "trackedFiles": [ + ".spec/ledger-auto-gen.md", ".spec/prototype-comments.json", ".spec/requirements-prd.md", "components/CalcFormulaTip.tsx", @@ -4269,6 +5081,126 @@ "utils/vehicle-ref.ts" ], "changelog": [ + { + "version": "v1.12", + "date": "2026-07-18", + "time": "12:52", + "summary": "更新需求说明与标注", + "files": [ + ".spec/ledger-auto-gen.md", + ".spec/prototype-comments.json", + ".spec/requirements-prd.md", + "components/CalcFormulaTip.tsx", + "components/FilterPanel.tsx", + "components/LedgerKpiRow.tsx", + "components/PlateMultiSelectField.tsx", + "components/SelfImportModal.tsx", + "components/SelfLedgerTable.tsx", + "components/tableColumns.ts", + "data/rows.json", + "index.tsx" + ] + }, + { + "version": "v1.11", + "date": "2026-07-18", + "time": "12:52", + "summary": "更新需求说明与标注", + "files": [ + ".spec/ledger-auto-gen.md", + ".spec/prototype-comments.json", + ".spec/requirements-prd.md", + "components/CalcFormulaTip.tsx", + "components/FilterPanel.tsx", + "components/LedgerKpiRow.tsx", + "components/PlateMultiSelectField.tsx", + "components/SelfImportModal.tsx", + "components/SelfLedgerTable.tsx", + "components/tableColumns.ts", + "data/rows.json", + "index.tsx" + ] + }, + { + "version": "v1.10", + "date": "2026-07-18", + "time": "12:52", + "summary": "更新需求说明与标注", + "files": [ + ".spec/ledger-auto-gen.md", + ".spec/prototype-comments.json", + ".spec/requirements-prd.md", + "components/CalcFormulaTip.tsx", + "components/FilterPanel.tsx", + "components/LedgerKpiRow.tsx", + "components/PlateMultiSelectField.tsx", + "components/SelfImportModal.tsx", + "components/SelfLedgerTable.tsx", + "components/tableColumns.ts", + "data/rows.json", + "index.tsx" + ] + }, + { + "version": "v1.9", + "date": "2026-07-18", + "time": "12:52", + "summary": "更新需求说明与标注", + "files": [ + ".spec/ledger-auto-gen.md", + ".spec/prototype-comments.json", + ".spec/requirements-prd.md", + "components/CalcFormulaTip.tsx", + "components/FilterPanel.tsx", + "components/LedgerKpiRow.tsx", + "components/PlateMultiSelectField.tsx", + "components/SelfImportModal.tsx", + "components/SelfLedgerTable.tsx", + "components/tableColumns.ts", + "data/rows.json", + "index.tsx" + ] + }, + { + "version": "v1.8", + "date": "2026-07-18", + "time": "12:52", + "summary": "更新需求说明与标注", + "files": [ + ".spec/ledger-auto-gen.md", + ".spec/prototype-comments.json", + ".spec/requirements-prd.md", + "components/CalcFormulaTip.tsx", + "components/FilterPanel.tsx", + "components/LedgerKpiRow.tsx", + "components/PlateMultiSelectField.tsx", + "components/SelfImportModal.tsx", + "components/SelfLedgerTable.tsx", + "components/tableColumns.ts", + "data/rows.json", + "index.tsx" + ] + }, + { + "version": "v1.7", + "date": "2026-07-18", + "time": "12:51", + "summary": "更新需求说明与标注", + "files": [ + ".spec/ledger-auto-gen.md", + ".spec/prototype-comments.json", + ".spec/requirements-prd.md", + "components/CalcFormulaTip.tsx", + "components/FilterPanel.tsx", + "components/LedgerKpiRow.tsx", + "components/PlateMultiSelectField.tsx", + "components/SelfImportModal.tsx", + "components/SelfLedgerTable.tsx", + "components/tableColumns.ts", + "data/rows.json", + "index.tsx" + ] + }, { "version": "v1.6", "date": "2026-07-16", @@ -4385,11 +5317,104 @@ } ] }, + "self-operated-contract": { + "title": "自营合同", + "version": "v1.3", + "revision": 3, + "lastUpdated": "2026-07-18T04:51:02.163Z", + "contentHash": "23cc0c19663ca890b7fd932a712c831b0045147e982ae555cea1b9af7ba933ed", + "trackedFiles": [ + ".spec/contract-rules.md", + ".spec/requirements-prd.md", + "index.tsx", + "SelfOperatedContractPage.jsx", + "styles/index.css" + ], + "changelog": [ + { + "version": "v1.3", + "date": "2026-07-18", + "time": "12:51", + "summary": "更新页面逻辑与交互", + "files": [ + "index.tsx" + ] + }, + { + "version": "v1.2", + "date": "2026-07-18", + "time": "12:50", + "summary": "更新需求说明与标注", + "files": [ + ".spec/contract-rules.md", + ".spec/requirements-prd.md", + "SelfOperatedContractPage.jsx", + "styles/index.css" + ] + }, + { + "version": "v1.1", + "date": "2026-07-18", + "time": "12:50", + "summary": "更新页面逻辑与交互", + "files": [ + "SelfOperatedContractPage.jsx" + ] + } + ] + }, + "self-operated-dispatch-task": { + "title": "调度任务", + "version": "v1.3", + "revision": 3, + "lastUpdated": "2026-07-18T04:51:46.813Z", + "contentHash": "3e1a61886728a5cbb8f3639f80a2daeae3c32a54394c514e4ef2272c6ca4dd7e", + "trackedFiles": [ + ".spec/dispatch-rules.md", + ".spec/requirements-prd.md", + "index.tsx", + "SelfOperatedDispatchTaskPage.jsx", + "styles/index.css" + ], + "changelog": [ + { + "version": "v1.3", + "date": "2026-07-18", + "time": "12:51", + "summary": "更新需求说明与标注", + "files": [ + ".spec/dispatch-rules.md", + ".spec/requirements-prd.md", + "index.tsx", + "SelfOperatedDispatchTaskPage.jsx", + "styles/index.css" + ] + }, + { + "version": "v1.2", + "date": "2026-07-18", + "time": "12:51", + "summary": "更新页面逻辑与交互", + "files": [ + "index.tsx" + ] + }, + { + "version": "v1.1", + "date": "2026-07-18", + "time": "12:51", + "summary": "更新页面逻辑与交互", + "files": [ + "SelfOperatedDispatchTaskPage.jsx" + ] + } + ] + }, "supplier-management": { "title": "供应商管理", "version": "v1.6", "revision": 6, - "lastUpdated": "2026-07-13T04:59:56.469Z", + "lastUpdated": "2026-07-18T04:52:34.917Z", "contentHash": "1ed2c828156b908e834d6b346e149d94e34c80eff52985fcba277c115d37cf1c", "trackedFiles": [ ".spec/acp/conversations.json", @@ -4422,12 +5447,22 @@ "changelog": [ { "version": "v1.6", - "date": "2026-07-13", - "time": "12:59", - "summary": "更新 2 个文件", + "date": "2026-07-18", + "time": "12:52", + "summary": "更新需求说明与标注", "files": [ - ".spec/acp/conversations.json", - ".spec/prototype-comments.json" + ".spec/prototype-review.md", + "components/BankAccountConfirmModal.tsx", + "components/ChangeLogDrawer.tsx", + "components/CoopStatusBadge.tsx", + "components/DeleteConfirmModal.tsx", + "components/FilterPanel.tsx", + "components/KpiCards.tsx", + "components/LeaveConfirmModal.tsx", + "components/RoleSwitcher.tsx", + "components/SupplierTable.tsx", + "data/suppliers.json", + "index.tsx" ] }, { @@ -4536,7 +5571,7 @@ "title": "任务工单", "version": "v1.2", "revision": 2, - "lastUpdated": "2026-07-13T01:59:13.978Z", + "lastUpdated": "2026-07-18T04:52:35.142Z", "contentHash": "a2b1853998ec7655a1ddfb705d0d39496b2544e8b21941d1ec73f17c828c038c", "trackedFiles": [ ".spec/2026-07-10-requirements.md", @@ -4588,19 +5623,212 @@ }, "vehicle-h2-fee-ledger": { "title": "车辆氢费明细", - "version": "v1.18", - "revision": 18, - "lastUpdated": "2026-07-18T04:30:15.800Z", - "contentHash": "7d8df9ed52511e05498e4e3fb14e4343757505c96688df7df9498ca6157d6e95", + "version": "v1.32", + "revision": 32, + "lastUpdated": "2026-07-18T10:52:47.598Z", + "contentHash": "ce7701b2e2ad894bdf07d56b2857a7082d91fb01f9fdc399ad1e29a9a12b4112", "trackedFiles": [ ".spec/PRD.md", ".spec/prototype-comments.json", ".spec/verify-reconcile.md", "globals.ts", "H2LedgerPage.jsx", - "index.tsx" + "index.tsx", + "styles/index.css" ], "changelog": [ + { + "version": "v1.32", + "date": "2026-07-18", + "time": "18:52", + "summary": "更新需求说明与标注", + "files": [ + ".spec/PRD.md", + ".spec/prototype-comments.json", + ".spec/verify-reconcile.md", + "globals.ts", + "H2LedgerPage.jsx", + "index.tsx", + "styles/index.css" + ] + }, + { + "version": "v1.31", + "date": "2026-07-18", + "time": "18:52", + "summary": "更新页面样式", + "files": [ + "styles/index.css" + ] + }, + { + "version": "v1.30", + "date": "2026-07-18", + "time": "18:52", + "summary": "更新需求说明与标注", + "files": [ + ".spec/PRD.md", + ".spec/prototype-comments.json", + ".spec/verify-reconcile.md", + "globals.ts", + "H2LedgerPage.jsx", + "index.tsx" + ] + }, + { + "version": "v1.29", + "date": "2026-07-18", + "time": "18:52", + "summary": "更新需求说明与标注", + "files": [ + ".spec/PRD.md", + ".spec/prototype-comments.json", + ".spec/verify-reconcile.md", + "globals.ts", + "H2LedgerPage.jsx", + "index.tsx" + ] + }, + { + "version": "v1.28", + "date": "2026-07-18", + "time": "18:51", + "summary": "更新需求说明与标注", + "files": [ + ".spec/PRD.md", + ".spec/prototype-comments.json", + ".spec/verify-reconcile.md", + "globals.ts", + "H2LedgerPage.jsx", + "index.tsx" + ] + }, + { + "version": "v1.27", + "date": "2026-07-18", + "time": "18:51", + "summary": "更新需求说明与标注", + "files": [ + ".spec/PRD.md", + ".spec/prototype-comments.json", + ".spec/verify-reconcile.md", + "globals.ts", + "H2LedgerPage.jsx", + "index.tsx" + ] + }, + { + "version": "v1.26", + "date": "2026-07-18", + "time": "16:59", + "summary": "更新需求说明与标注", + "files": [ + ".spec/PRD.md", + ".spec/prototype-comments.json", + ".spec/verify-reconcile.md", + "globals.ts", + "H2LedgerPage.jsx", + "index.tsx" + ] + }, + { + "version": "v1.25", + "date": "2026-07-18", + "time": "16:58", + "summary": "更新需求说明与标注", + "files": [ + ".spec/PRD.md", + ".spec/prototype-comments.json", + ".spec/verify-reconcile.md", + "globals.ts", + "H2LedgerPage.jsx", + "index.tsx" + ] + }, + { + "version": "v1.24", + "date": "2026-07-18", + "time": "16:58", + "summary": "更新需求说明与标注", + "files": [ + ".spec/PRD.md", + ".spec/prototype-comments.json", + ".spec/verify-reconcile.md", + "globals.ts", + "H2LedgerPage.jsx", + "index.tsx" + ] + }, + { + "version": "v1.23", + "date": "2026-07-18", + "time": "16:57", + "summary": "更新需求说明与标注", + "files": [ + ".spec/PRD.md", + ".spec/prototype-comments.json", + ".spec/verify-reconcile.md", + "globals.ts", + "H2LedgerPage.jsx", + "index.tsx" + ] + }, + { + "version": "v1.22", + "date": "2026-07-18", + "time": "16:57", + "summary": "更新需求说明与标注", + "files": [ + ".spec/PRD.md", + ".spec/prototype-comments.json", + ".spec/verify-reconcile.md", + "globals.ts", + "H2LedgerPage.jsx", + "index.tsx" + ] + }, + { + "version": "v1.21", + "date": "2026-07-18", + "time": "16:56", + "summary": "更新需求说明与标注", + "files": [ + ".spec/PRD.md", + ".spec/prototype-comments.json", + ".spec/verify-reconcile.md", + "globals.ts", + "H2LedgerPage.jsx", + "index.tsx" + ] + }, + { + "version": "v1.20", + "date": "2026-07-18", + "time": "16:56", + "summary": "更新需求说明与标注", + "files": [ + ".spec/PRD.md", + ".spec/prototype-comments.json", + ".spec/verify-reconcile.md", + "globals.ts", + "H2LedgerPage.jsx", + "index.tsx" + ] + }, + { + "version": "v1.19", + "date": "2026-07-18", + "time": "16:56", + "summary": "更新需求说明与标注", + "files": [ + ".spec/PRD.md", + ".spec/prototype-comments.json", + ".spec/verify-reconcile.md", + "globals.ts", + "H2LedgerPage.jsx", + "index.tsx" + ] + }, { "version": "v1.18", "date": "2026-07-18", @@ -4826,44 +6054,15 @@ ".spec/PRD.md", "H2LedgerPage.jsx" ] - }, - { - "version": "v1.2", - "date": "2026-07-02", - "time": "03:35", - "summary": "车辆氢费明细对接 h2VehicleLedgerBridge;移除 vehicle-management-clay", - "details": [ - "页面逻辑与交互:H2LedgerPage.jsx、globals.ts" - ], - "commit": "f5e3126", - "files": [ - "H2LedgerPage.jsx", - "globals.ts" - ] - }, - { - "version": "v1.1", - "date": "2026-06-30", - "time": "15:27", - "summary": "Add vehicle-h2-fee-ledger, customer-management, lease and self-operated ledgers, annotation sources, agent skills, and vite annotation runtime support. Update vehicle management, contract templates, and lease contract flows.", - "details": [ - "页面逻辑与交互:H2LedgerPage.jsx、globals.ts、index.tsx" - ], - "commit": "af29b26", - "files": [ - "H2LedgerPage.jsx", - "globals.ts", - "index.tsx" - ] } ] }, "vehicle-maintenance-ledger": { "title": "维修保养台账", - "version": "v1.2", - "revision": 2, - "lastUpdated": "2026-07-12T14:45:00.000Z", - "contentHash": "47e529bd55e41108198a630ab148a354ddd72e92f732cf62183243936f833197", + "version": "v1.3", + "revision": 3, + "lastUpdated": "2026-07-18T04:52:35.115Z", + "contentHash": "cf3e0d71ac37bb4a728b1593540d213612a42a9fa86e06a7f328bcdf5ae4c87d", "trackedFiles": [ ".spec/prototype-comments.json", ".spec/requirements-prd.md", @@ -4873,6 +6072,20 @@ "styles/index.css" ], "changelog": [ + { + "version": "v1.3", + "date": "2026-07-18", + "time": "12:52", + "summary": "更新需求说明与标注", + "files": [ + ".spec/prototype-comments.json", + ".spec/requirements-prd.md", + "globals.ts", + "index.tsx", + "MaintenanceLedgerPage.jsx", + "styles/index.css" + ] + }, { "version": "v1.2", "date": "2026-07-12", @@ -4909,10 +6122,10 @@ }, "vehicle-management": { "title": "车辆资产", - "version": "v1.11", - "revision": 11, - "lastUpdated": "2026-07-18T04:30:15.800Z", - "contentHash": "31524a813ece4d38c7971d7a945b6cf68dde1c72ab114f90665061d7ad191848", + "version": "v1.14", + "revision": 14, + "lastUpdated": "2026-07-19T17:33:16.359Z", + "contentHash": "58a76bdec01debd2632f24340aedd6de08d0eae7119ec4f7da61776f73720ff6", "trackedFiles": [ ".spec/prototype-comments.json", ".spec/prototype-review.md", @@ -4998,6 +6211,66 @@ "utils/violationRecords.ts" ], "changelog": [ + { + "version": "v1.14", + "date": "2026-07-20", + "time": "01:33", + "summary": "更新需求说明与标注", + "files": [ + ".spec/prototype-comments.json", + ".spec/prototype-review.md", + ".spec/requirements-prd-detail.md", + ".spec/requirements-prd-list.md", + ".spec/ui-review.md", + "components/DateRangeCalendarPanel.tsx", + "components/DateRangeFilterField.tsx", + "components/DetailAccidentRecordsTab.tsx", + "components/DetailAnnualReviewRecordsTab.tsx", + "components/DetailBasicInfoTab.tsx", + "components/DetailExpireField.tsx", + "components/DetailFaultRecordsTab.tsx" + ] + }, + { + "version": "v1.13", + "date": "2026-07-19", + "time": "03:12", + "summary": "更新需求说明与标注", + "files": [ + ".spec/prototype-comments.json", + ".spec/prototype-review.md", + ".spec/requirements-prd-detail.md", + ".spec/requirements-prd-list.md", + ".spec/ui-review.md", + "components/DateRangeCalendarPanel.tsx", + "components/DateRangeFilterField.tsx", + "components/DetailAccidentRecordsTab.tsx", + "components/DetailAnnualReviewRecordsTab.tsx", + "components/DetailBasicInfoTab.tsx", + "components/DetailExpireField.tsx", + "components/DetailFaultRecordsTab.tsx" + ] + }, + { + "version": "v1.12", + "date": "2026-07-18", + "time": "12:52", + "summary": "更新需求说明与标注", + "files": [ + ".spec/prototype-comments.json", + ".spec/prototype-review.md", + ".spec/requirements-prd-detail.md", + ".spec/requirements-prd-list.md", + ".spec/ui-review.md", + "components/DateRangeCalendarPanel.tsx", + "components/DateRangeFilterField.tsx", + "components/DetailAccidentRecordsTab.tsx", + "components/DetailAnnualReviewRecordsTab.tsx", + "components/DetailBasicInfoTab.tsx", + "components/DetailExpireField.tsx", + "components/DetailFaultRecordsTab.tsx" + ] + }, { "version": "v1.11", "date": "2026-07-18", @@ -5248,10 +6521,10 @@ }, "vehicle-pickup-receivable": { "title": "提车应收款", - "version": "v1.2", - "revision": 2, - "lastUpdated": "2026-07-13T01:31:00.000Z", - "contentHash": "266723009574aadd9d2ee8dd0fc7a6bb36f086bd146a17fbf4c61b1746e7353f", + "version": "v1.3", + "revision": 3, + "lastUpdated": "2026-07-18T04:52:35.122Z", + "contentHash": "d747db5ed1f7208e701eb139ea5d71ab1b627eb38b6f36f2d00e8ba2db00000d", "trackedFiles": [ ".spec/PRD.md", ".spec/prototype-comments.json", @@ -5266,6 +6539,25 @@ "VehiclePickupReceivableList.jsx" ], "changelog": [ + { + "version": "v1.3", + "date": "2026-07-18", + "time": "12:52", + "summary": "更新需求说明与标注", + "files": [ + ".spec/PRD.md", + ".spec/prototype-comments.json", + "data/pickup-receivable-list-data.js", + "index.tsx", + "pickup-receivable-bridge.js", + "pickup-receivable-calc.js", + "styles/index.css", + "VehiclePickupReceivableApp.jsx", + "VehiclePickupReceivableCollectPage.jsx", + "VehiclePickupReceivableContractView.jsx", + "VehiclePickupReceivableList.jsx" + ] + }, { "version": "v1.2", "date": "2026-07-13", @@ -5819,34 +7111,15 @@ "VehiclePickupReceivableContractView.jsx", "VehiclePickupReceivableList.jsx" ] - }, - { - "version": "v1.139", - "date": "2026-07-13", - "time": "00:45", - "summary": "更新需求说明与标注", - "files": [ - ".spec/PRD.md", - ".spec/prototype-comments.json", - "data/pickup-receivable-list-data.js", - "index.tsx", - "pickup-receivable-bridge.js", - "pickup-receivable-calc.js", - "styles/index.css", - "VehiclePickupReceivableApp.jsx", - "VehiclePickupReceivableCollectPage.jsx", - "VehiclePickupReceivableContractView.jsx", - "VehiclePickupReceivableList.jsx" - ] } ] }, "vehicle-return-settlement": { "title": "还车应结款", - "version": "v1.3", - "revision": 3, - "lastUpdated": "2026-07-12T14:44:00.000Z", - "contentHash": "d9ae6105c792609ffad4275d2730ff6361837ebd654af3a64f6f34473482f073", + "version": "v1.4", + "revision": 4, + "lastUpdated": "2026-07-18T04:52:35.137Z", + "contentHash": "125acc0e02df69ffcb493e0989fa96a54d5e450d8dd0c0dbae2e9fb830c189c6", "trackedFiles": [ ".spec/prototype-comments.json", "index.tsx", @@ -5855,6 +7128,19 @@ "VehicleReturnSettlementHandle.jsx" ], "changelog": [ + { + "version": "v1.4", + "date": "2026-07-18", + "time": "12:52", + "summary": "更新页面样式", + "files": [ + ".spec/prototype-comments.json", + "index.tsx", + "styles/index.css", + "VehicleReturnSettlementApp.jsx", + "VehicleReturnSettlementHandle.jsx" + ] + }, { "version": "v1.3", "date": "2026-07-12", @@ -6034,9 +7320,9 @@ }, "xll-miniapp": { "title": "小羚羚小程序", - "version": "v1.2", - "revision": 2, - "lastUpdated": "2026-07-20T05:34:45.576Z", + "version": "v1.1", + "revision": 1, + "lastUpdated": "2026-07-18T02:26:51.943Z", "contentHash": "03d9b1bd2c9baff1216b378eb17418d95c87f2e06027dd9c2b1663f4473d8761", "trackedFiles": [ "index.tsx", @@ -6049,19 +7335,6 @@ "client.json" ], "changelog": [ - { - "version": "v1.2", - "date": "2026-07-20", - "time": "13:34", - "summary": "同步司机安全培训模块到 ONEOS 小羚羚导航", - "details": [ - "业务模块新增「司机安全培训」入口(driver-training)" - ], - "files": [ - "xll-nav-menu.json", - "driver-training/" - ] - }, { "version": "v1.1", "date": "2026-07-18", @@ -6086,6 +7359,859 @@ ] } ] + }, + "oneos-web-workbench-new": { + "title": "工作台", + "version": "v1.678", + "revision": 678, + "lastUpdated": "2026-07-20T06:29:40.911Z", + "contentHash": "1414a86cc6e56ddc1deeeecc352da4390de15354c1372132a2132f67e9cac5f1", + "trackedFiles": [ + ".spec/acp/conversations.json", + ".spec/approval-embed.md", + ".spec/notice-center.md", + ".spec/oneos-app-shell.md", + ".spec/payment-unlinked-kpi.md", + ".spec/prototype-comments.json", + ".spec/release-notes-popup.md", + ".spec/requirements-prd.md", + ".spec/role-insights.md", + ".spec/role-visibility.md", + ".spec/task-analytics.md", + ".spec/task-flow.md", + ".spec/todo-biz-admin.md", + ".spec/urge-channels.md", + "components/AnalyticsBoard.tsx", + "components/ApprovalPanel.tsx", + "components/HeroSummary.tsx", + "components/KpiGrid.tsx", + "components/NoticePanel.tsx", + "components/QuickEntry.tsx", + "components/ReleaseNotesModal.tsx", + "components/RoleInsightsPanel.tsx", + "components/TaskDetailDrawer.tsx", + "components/TodoLineMultiSelect.tsx", + "components/TodoLineTable.tsx", + "components/TodoPanel.tsx", + "components/TodoUrgencySelect.tsx", + "components/Topbar.tsx", + "components/UrgeModal.tsx", + "components/WorkbenchPage.tsx", + "data/analytics.ts", + "data/approvals.ts", + "data/kpi.ts", + "data/notices.ts", + "data/quick-actions.ts", + "data/release-notes.ts", + "data/role-insights.ts", + "data/roles.ts", + "data/tasks.ts", + "data/todo-lines.ts", + "index.tsx", + "style.css", + "utils/analytics-store.ts", + "utils/fleet-region-stats.ts", + "utils/helpers.ts", + "utils/release-seen.ts", + "utils/urge-permission.ts" + ], + "changelog": [ + { + "version": "v1.678", + "date": "2026-07-20", + "time": "14:29", + "summary": "更新需求说明与标注", + "files": [ + ".spec/acp/conversations.json", + ".spec/approval-embed.md", + ".spec/notice-center.md", + ".spec/oneos-app-shell.md", + ".spec/payment-unlinked-kpi.md", + ".spec/prototype-comments.json", + ".spec/release-notes-popup.md", + ".spec/requirements-prd.md", + ".spec/role-insights.md", + ".spec/role-visibility.md", + ".spec/task-analytics.md", + ".spec/task-flow.md" + ] + }, + { + "version": "v1.677", + "date": "2026-07-20", + "time": "14:29", + "summary": "更新需求说明与标注", + "files": [ + ".spec/acp/conversations.json", + ".spec/approval-embed.md", + ".spec/notice-center.md", + ".spec/oneos-app-shell.md", + ".spec/payment-unlinked-kpi.md", + ".spec/prototype-comments.json", + ".spec/release-notes-popup.md", + ".spec/requirements-prd.md", + ".spec/role-insights.md", + ".spec/role-visibility.md", + ".spec/task-analytics.md", + ".spec/task-flow.md" + ] + }, + { + "version": "v1.676", + "date": "2026-07-20", + "time": "14:29", + "summary": "更新需求说明与标注", + "files": [ + ".spec/acp/conversations.json", + ".spec/approval-embed.md", + ".spec/notice-center.md", + ".spec/oneos-app-shell.md", + ".spec/payment-unlinked-kpi.md", + ".spec/prototype-comments.json", + ".spec/release-notes-popup.md", + ".spec/requirements-prd.md", + ".spec/role-insights.md", + ".spec/role-visibility.md", + ".spec/task-analytics.md", + ".spec/task-flow.md" + ] + }, + { + "version": "v1.675", + "date": "2026-07-20", + "time": "14:29", + "summary": "更新需求说明与标注", + "files": [ + ".spec/acp/conversations.json", + ".spec/approval-embed.md", + ".spec/notice-center.md", + ".spec/oneos-app-shell.md", + ".spec/payment-unlinked-kpi.md", + ".spec/prototype-comments.json", + ".spec/release-notes-popup.md", + ".spec/requirements-prd.md", + ".spec/role-insights.md", + ".spec/role-visibility.md", + ".spec/task-analytics.md", + ".spec/task-flow.md" + ] + }, + { + "version": "v1.674", + "date": "2026-07-20", + "time": "14:29", + "summary": "更新需求说明与标注", + "files": [ + ".spec/acp/conversations.json", + ".spec/approval-embed.md", + ".spec/notice-center.md", + ".spec/oneos-app-shell.md", + ".spec/payment-unlinked-kpi.md", + ".spec/prototype-comments.json", + ".spec/release-notes-popup.md", + ".spec/requirements-prd.md", + ".spec/role-insights.md", + ".spec/role-visibility.md", + ".spec/task-analytics.md", + ".spec/task-flow.md" + ] + }, + { + "version": "v1.673", + "date": "2026-07-20", + "time": "14:28", + "summary": "更新需求说明与标注", + "files": [ + ".spec/acp/conversations.json", + ".spec/approval-embed.md", + ".spec/notice-center.md", + ".spec/oneos-app-shell.md", + ".spec/payment-unlinked-kpi.md", + ".spec/prototype-comments.json", + ".spec/release-notes-popup.md", + ".spec/requirements-prd.md", + ".spec/role-insights.md", + ".spec/role-visibility.md", + ".spec/task-analytics.md", + ".spec/task-flow.md" + ] + }, + { + "version": "v1.672", + "date": "2026-07-20", + "time": "14:28", + "summary": "版本更新日志首次进入弹框:已阅记忆、系统通知联动、快速入口查阅", + "files": [ + ".spec/acp/conversations.json", + ".spec/approval-embed.md", + ".spec/notice-center.md", + ".spec/oneos-app-shell.md", + ".spec/payment-unlinked-kpi.md", + ".spec/prototype-comments.json", + ".spec/release-notes-popup.md", + ".spec/requirements-prd.md", + ".spec/role-insights.md", + ".spec/role-visibility.md", + ".spec/task-analytics.md", + ".spec/task-flow.md" + ] + }, + { + "version": "v1.671", + "date": "2026-07-20", + "time": "14:27", + "summary": "更新需求说明与标注", + "files": [ + ".spec/acp/conversations.json", + ".spec/approval-embed.md", + ".spec/notice-center.md", + ".spec/oneos-app-shell.md", + ".spec/payment-unlinked-kpi.md", + ".spec/prototype-comments.json", + ".spec/release-notes-popup.md", + ".spec/requirements-prd.md", + ".spec/role-insights.md", + ".spec/role-visibility.md", + ".spec/task-analytics.md", + ".spec/task-flow.md" + ] + }, + { + "version": "v1.670", + "date": "2026-07-20", + "time": "14:27", + "summary": "更新需求说明与标注", + "files": [ + ".spec/acp/conversations.json", + ".spec/approval-embed.md", + ".spec/notice-center.md", + ".spec/oneos-app-shell.md", + ".spec/payment-unlinked-kpi.md", + ".spec/prototype-comments.json", + ".spec/release-notes-popup.md", + ".spec/requirements-prd.md", + ".spec/role-insights.md", + ".spec/role-visibility.md", + ".spec/task-analytics.md", + ".spec/task-flow.md" + ] + }, + { + "version": "v1.669", + "date": "2026-07-20", + "time": "14:27", + "summary": "更新需求说明与标注", + "files": [ + ".spec/acp/conversations.json", + ".spec/approval-embed.md", + ".spec/notice-center.md", + ".spec/oneos-app-shell.md", + ".spec/payment-unlinked-kpi.md", + ".spec/prototype-comments.json", + ".spec/release-notes-popup.md", + ".spec/requirements-prd.md", + ".spec/role-insights.md", + ".spec/role-visibility.md", + ".spec/task-analytics.md", + ".spec/task-flow.md" + ] + }, + { + "version": "v1.668", + "date": "2026-07-20", + "time": "14:27", + "summary": "更新需求说明与标注", + "files": [ + ".spec/acp/conversations.json", + ".spec/approval-embed.md", + ".spec/notice-center.md", + ".spec/oneos-app-shell.md", + ".spec/payment-unlinked-kpi.md", + ".spec/prototype-comments.json", + ".spec/release-notes-popup.md", + ".spec/requirements-prd.md", + ".spec/role-insights.md", + ".spec/role-visibility.md", + ".spec/task-analytics.md", + ".spec/task-flow.md" + ] + }, + { + "version": "v1.667", + "date": "2026-07-20", + "time": "14:27", + "summary": "更新需求说明与标注", + "files": [ + ".spec/acp/conversations.json", + ".spec/approval-embed.md", + ".spec/notice-center.md", + ".spec/oneos-app-shell.md", + ".spec/payment-unlinked-kpi.md", + ".spec/prototype-comments.json", + ".spec/release-notes-popup.md", + ".spec/requirements-prd.md", + ".spec/role-insights.md", + ".spec/role-visibility.md", + ".spec/task-analytics.md", + ".spec/task-flow.md" + ] + }, + { + "version": "v1.666", + "date": "2026-07-20", + "time": "14:27", + "summary": "更新需求说明与标注", + "files": [ + ".spec/acp/conversations.json", + ".spec/approval-embed.md", + ".spec/notice-center.md", + ".spec/oneos-app-shell.md", + ".spec/payment-unlinked-kpi.md", + ".spec/prototype-comments.json", + ".spec/release-notes-popup.md", + ".spec/requirements-prd.md", + ".spec/role-insights.md", + ".spec/role-visibility.md", + ".spec/task-analytics.md", + ".spec/task-flow.md" + ] + }, + { + "version": "v1.665", + "date": "2026-07-20", + "time": "14:26", + "summary": "更新需求说明与标注", + "files": [ + ".spec/acp/conversations.json", + ".spec/approval-embed.md", + ".spec/notice-center.md", + ".spec/oneos-app-shell.md", + ".spec/payment-unlinked-kpi.md", + ".spec/prototype-comments.json", + ".spec/release-notes-popup.md", + ".spec/requirements-prd.md", + ".spec/role-insights.md", + ".spec/role-visibility.md", + ".spec/task-analytics.md", + ".spec/task-flow.md" + ] + }, + { + "version": "v1.664", + "date": "2026-07-20", + "time": "14:26", + "summary": "更新需求说明与标注", + "files": [ + ".spec/acp/conversations.json", + ".spec/approval-embed.md", + ".spec/notice-center.md", + ".spec/oneos-app-shell.md", + ".spec/payment-unlinked-kpi.md", + ".spec/prototype-comments.json", + ".spec/release-notes-popup.md", + ".spec/requirements-prd.md", + ".spec/role-insights.md", + ".spec/role-visibility.md", + ".spec/task-analytics.md", + ".spec/task-flow.md" + ] + }, + { + "version": "v1.663", + "date": "2026-07-20", + "time": "14:26", + "summary": "更新需求说明与标注", + "files": [ + ".spec/acp/conversations.json", + ".spec/approval-embed.md", + ".spec/notice-center.md", + ".spec/oneos-app-shell.md", + ".spec/payment-unlinked-kpi.md", + ".spec/prototype-comments.json", + ".spec/release-notes-popup.md", + ".spec/requirements-prd.md", + ".spec/role-insights.md", + ".spec/role-visibility.md", + ".spec/task-analytics.md", + ".spec/task-flow.md" + ] + }, + { + "version": "v1.662", + "date": "2026-07-20", + "time": "14:26", + "summary": "更新需求说明与标注", + "files": [ + ".spec/acp/conversations.json", + ".spec/approval-embed.md", + ".spec/notice-center.md", + ".spec/oneos-app-shell.md", + ".spec/payment-unlinked-kpi.md", + ".spec/prototype-comments.json", + ".spec/release-notes-popup.md", + ".spec/requirements-prd.md", + ".spec/role-insights.md", + ".spec/role-visibility.md", + ".spec/task-analytics.md", + ".spec/task-flow.md" + ] + }, + { + "version": "v1.661", + "date": "2026-07-20", + "time": "14:25", + "summary": "更新需求说明与标注", + "files": [ + ".spec/acp/conversations.json", + ".spec/approval-embed.md", + ".spec/notice-center.md", + ".spec/oneos-app-shell.md", + ".spec/payment-unlinked-kpi.md", + ".spec/prototype-comments.json", + ".spec/release-notes-popup.md", + ".spec/requirements-prd.md", + ".spec/role-insights.md", + ".spec/role-visibility.md", + ".spec/task-analytics.md", + ".spec/task-flow.md" + ] + }, + { + "version": "v1.660", + "date": "2026-07-20", + "time": "14:25", + "summary": "更新需求说明与标注", + "files": [ + ".spec/acp/conversations.json", + ".spec/approval-embed.md", + ".spec/notice-center.md", + ".spec/oneos-app-shell.md", + ".spec/payment-unlinked-kpi.md", + ".spec/prototype-comments.json", + ".spec/release-notes-popup.md", + ".spec/requirements-prd.md", + ".spec/role-insights.md", + ".spec/role-visibility.md", + ".spec/task-analytics.md", + ".spec/task-flow.md" + ] + }, + { + "version": "v1.659", + "date": "2026-07-20", + "time": "14:25", + "summary": "更新页面逻辑与交互", + "files": [ + "components/ReleaseNotesModal.tsx" + ] + }, + { + "version": "v1.658", + "date": "2026-07-20", + "time": "14:25", + "summary": "更新需求说明与标注", + "files": [ + ".spec/release-notes-popup.md", + "data/release-notes.ts", + "utils/release-seen.ts" + ] + }, + { + "version": "v1.657", + "date": "2026-07-20", + "time": "13:47", + "summary": "更新需求说明与标注", + "files": [ + ".spec/acp/conversations.json", + ".spec/approval-embed.md", + ".spec/notice-center.md", + ".spec/oneos-app-shell.md", + ".spec/payment-unlinked-kpi.md", + ".spec/prototype-comments.json", + ".spec/requirements-prd.md", + ".spec/role-insights.md", + ".spec/role-visibility.md", + ".spec/task-analytics.md", + ".spec/task-flow.md", + ".spec/todo-biz-admin.md" + ] + }, + { + "version": "v1.656", + "date": "2026-07-20", + "time": "13:47", + "summary": "运营看板:异常未闭环业务卡片(六类笔数/金额)", + "files": [ + ".spec/acp/conversations.json", + ".spec/approval-embed.md", + ".spec/notice-center.md", + ".spec/oneos-app-shell.md", + ".spec/payment-unlinked-kpi.md", + ".spec/prototype-comments.json", + ".spec/requirements-prd.md", + ".spec/role-insights.md", + ".spec/role-visibility.md", + ".spec/task-analytics.md", + ".spec/task-flow.md", + ".spec/todo-biz-admin.md" + ] + }, + { + "version": "v1.655", + "date": "2026-07-20", + "time": "13:47", + "summary": "更新需求说明与标注", + "files": [ + ".spec/acp/conversations.json", + ".spec/approval-embed.md", + ".spec/notice-center.md", + ".spec/oneos-app-shell.md", + ".spec/payment-unlinked-kpi.md", + ".spec/prototype-comments.json", + ".spec/requirements-prd.md", + ".spec/role-insights.md", + ".spec/role-visibility.md", + ".spec/task-analytics.md", + ".spec/task-flow.md", + ".spec/todo-biz-admin.md" + ] + }, + { + "version": "v1.654", + "date": "2026-07-20", + "time": "13:47", + "summary": "更新需求说明与标注", + "files": [ + ".spec/acp/conversations.json", + ".spec/approval-embed.md", + ".spec/notice-center.md", + ".spec/oneos-app-shell.md", + ".spec/payment-unlinked-kpi.md", + ".spec/prototype-comments.json", + ".spec/requirements-prd.md", + ".spec/role-insights.md", + ".spec/role-visibility.md", + ".spec/task-analytics.md", + ".spec/task-flow.md", + ".spec/todo-biz-admin.md" + ] + }, + { + "version": "v1.653", + "date": "2026-07-20", + "time": "13:47", + "summary": "更新需求说明与标注", + "files": [ + ".spec/acp/conversations.json", + ".spec/approval-embed.md", + ".spec/notice-center.md", + ".spec/oneos-app-shell.md", + ".spec/payment-unlinked-kpi.md", + ".spec/prototype-comments.json", + ".spec/requirements-prd.md", + ".spec/role-insights.md", + ".spec/role-visibility.md", + ".spec/task-analytics.md", + ".spec/task-flow.md", + ".spec/todo-biz-admin.md" + ] + }, + { + "version": "v1.652", + "date": "2026-07-20", + "time": "13:46", + "summary": "更新需求说明与标注", + "files": [ + ".spec/acp/conversations.json", + ".spec/approval-embed.md", + ".spec/notice-center.md", + ".spec/oneos-app-shell.md", + ".spec/payment-unlinked-kpi.md", + ".spec/prototype-comments.json", + ".spec/requirements-prd.md", + ".spec/role-insights.md", + ".spec/role-visibility.md", + ".spec/task-analytics.md", + ".spec/task-flow.md", + ".spec/todo-biz-admin.md" + ] + }, + { + "version": "v1.651", + "date": "2026-07-20", + "time": "13:46", + "summary": "更新需求说明与标注", + "files": [ + ".spec/acp/conversations.json", + ".spec/approval-embed.md", + ".spec/notice-center.md", + ".spec/oneos-app-shell.md", + ".spec/payment-unlinked-kpi.md", + ".spec/prototype-comments.json", + ".spec/requirements-prd.md", + ".spec/role-insights.md", + ".spec/role-visibility.md", + ".spec/task-analytics.md", + ".spec/task-flow.md", + ".spec/todo-biz-admin.md" + ] + }, + { + "version": "v1.650", + "date": "2026-07-20", + "time": "13:46", + "summary": "更新需求说明与标注", + "files": [ + ".spec/acp/conversations.json", + ".spec/approval-embed.md", + ".spec/notice-center.md", + ".spec/oneos-app-shell.md", + ".spec/payment-unlinked-kpi.md", + ".spec/prototype-comments.json", + ".spec/requirements-prd.md", + ".spec/role-insights.md", + ".spec/role-visibility.md", + ".spec/task-analytics.md", + ".spec/task-flow.md", + ".spec/todo-biz-admin.md" + ] + }, + { + "version": "v1.649", + "date": "2026-07-20", + "time": "13:43", + "summary": "更新需求说明与标注", + "files": [ + ".spec/acp/conversations.json", + ".spec/approval-embed.md", + ".spec/notice-center.md", + ".spec/oneos-app-shell.md", + ".spec/payment-unlinked-kpi.md", + ".spec/prototype-comments.json", + ".spec/requirements-prd.md", + ".spec/role-insights.md", + ".spec/role-visibility.md", + ".spec/task-analytics.md", + ".spec/task-flow.md", + ".spec/todo-biz-admin.md" + ] + } + ] + }, + "oneos-prototype-demo": { + "title": "原型演示", + "version": "v1.16", + "revision": 16, + "lastUpdated": "2026-07-20T05:40:41.686Z", + "contentHash": "1510847729d6a83bbfde9da7fe69e0aef090259f332cf380f00b77ca37aae1fc", + "trackedFiles": [ + ".spec/acp/conversations.json", + ".spec/prototype-comments.json", + ".spec/requirements-prd.md", + "index.tsx", + "style.css" + ], + "changelog": [ + { + "version": "v1.16", + "date": "2026-07-20", + "time": "13:40", + "summary": "更新需求说明与标注", + "files": [ + ".spec/acp/conversations.json", + ".spec/prototype-comments.json", + ".spec/requirements-prd.md", + "index.tsx", + "style.css" + ] + }, + { + "version": "v1.15", + "date": "2026-07-19", + "time": "01:11", + "summary": "修复通知查看全部弹框居中", + "files": [ + ".spec/acp/conversations.json", + ".spec/prototype-comments.json", + ".spec/requirements-prd.md", + "index.tsx", + "style.css" + ] + }, + { + "version": "v1.14", + "date": "2026-07-19", + "time": "01:08", + "summary": "通知查看全部弹框:未读/已读标签与去处理", + "files": [ + ".spec/acp/conversations.json", + ".spec/prototype-comments.json", + ".spec/requirements-prd.md", + "index.tsx", + "style.css" + ] + }, + { + "version": "v1.13", + "date": "2026-07-19", + "time": "01:05", + "summary": "iframe内跳转同步侧栏与页签", + "files": [ + ".spec/acp/conversations.json", + ".spec/prototype-comments.json", + ".spec/requirements-prd.md", + "index.tsx", + "style.css" + ] + }, + { + "version": "v1.12", + "date": "2026-07-19", + "time": "01:05", + "summary": "更新需求说明与标注", + "files": [ + ".spec/acp/conversations.json", + ".spec/prototype-comments.json", + ".spec/requirements-prd.md", + "index.tsx", + "style.css" + ] + }, + { + "version": "v1.11", + "date": "2026-07-19", + "time": "01:05", + "summary": "更新需求说明与标注", + "files": [ + ".spec/acp/conversations.json", + ".spec/prototype-comments.json", + ".spec/requirements-prd.md", + "index.tsx", + "style.css" + ] + }, + { + "version": "v1.10", + "date": "2026-07-19", + "time": "01:03", + "summary": "通知点击弹出完整详情卡片(含去处理)", + "files": [ + ".spec/acp/conversations.json", + ".spec/prototype-comments.json", + ".spec/requirements-prd.md", + "index.tsx", + "style.css" + ] + }, + { + "version": "v1.9", + "date": "2026-07-19", + "time": "00:51", + "summary": "通知新信息气泡改至铃铛下方", + "files": [ + ".spec/acp/conversations.json", + ".spec/prototype-comments.json", + ".spec/requirements-prd.md", + "index.tsx", + "style.css" + ] + }, + { + "version": "v1.8", + "date": "2026-07-19", + "time": "00:49", + "summary": "更新需求说明与标注", + "files": [ + ".spec/acp/conversations.json", + ".spec/prototype-comments.json", + ".spec/requirements-prd.md", + "index.tsx", + "style.css" + ] + }, + { + "version": "v1.7", + "date": "2026-07-19", + "time": "00:45", + "summary": "清理深色模式批注任务", + "files": [ + ".spec/acp/conversations.json", + ".spec/prototype-comments.json", + ".spec/requirements-prd.md", + "index.tsx", + "style.css" + ] + }, + { + "version": "v1.6", + "date": "2026-07-19", + "time": "00:39", + "summary": "顶栏浅色/暗色主题切换,暗色参照原型导航,覆盖 OneOS 目录(不含旧 ONEOS)", + "files": [ + ".spec/acp/conversations.json", + ".spec/prototype-comments.json", + ".spec/requirements-prd.md", + "index.tsx", + "style.css" + ] + }, + { + "version": "v1.5", + "date": "2026-07-19", + "time": "00:39", + "summary": "更新需求说明与标注", + "files": [ + ".spec/acp/conversations.json", + ".spec/prototype-comments.json", + ".spec/requirements-prd.md", + "index.tsx", + "style.css" + ] + }, + { + "version": "v1.4", + "date": "2026-07-19", + "time": "00:39", + "summary": "更新需求说明与标注", + "files": [ + ".spec/acp/conversations.json", + ".spec/prototype-comments.json", + ".spec/requirements-prd.md", + "index.tsx", + "style.css" + ] + }, + { + "version": "v1.3", + "date": "2026-07-19", + "time": "00:34", + "summary": "优化侧栏与顶栏视觉与微交互", + "files": [ + ".spec/acp/conversations.json", + ".spec/prototype-comments.json" + ] + }, + { + "version": "v1.2", + "date": "2026-07-19", + "time": "00:30", + "summary": "更新需求说明与标注", + "files": [ + ".spec/requirements-prd.md", + "index.tsx", + "style.css" + ] + }, + { + "version": "v1.1", + "date": "2026-07-19", + "time": "00:30", + "summary": "更新需求说明与标注", + "files": [ + ".spec/requirements-prd.md" + ] + } + ] } }, "recentUpdates": [ @@ -6680,22 +8806,6 @@ "pages/04-站点周报统计.jsx", "styles/h2-station-vm-filter.css" ] - }, - { - "prototypeId": "oneos-web-h2-station", - "title": "加氢站管理", - "version": "v1.34", - "date": "2026-07-13", - "time": "13:09", - "summary": "更新页面样式", - "files": [ - "index.tsx", - "pages/01-加氢订单.jsx", - "pages/02-加氢记录.jsx", - "pages/03-站点信息.jsx", - "pages/04-站点周报统计.jsx", - "styles/h2-station-vm-filter.css" - ] } ] } diff --git a/src/prototypes/oneos-web-approval/index.tsx b/src/prototypes/oneos-web-approval/index.tsx deleted file mode 100644 index 2d4ce6a..0000000 --- a/src/prototypes/oneos-web-approval/index.tsx +++ /dev/null @@ -1,8 +0,0 @@ -/** - * @name 审批中心 - */ -import { ApprovalApp } from '../../common/oneos-web-approval/ApprovalApp'; - -export default function OneosWebApproval() { - return ; -} diff --git a/src/prototypes/oneos-web-h2-station-site/.spec/requirements-prd-statement.md b/src/prototypes/oneos-web-h2-station-site/.spec/requirements-prd-statement.md index f44a25c..f6c0ed8 100644 --- a/src/prototypes/oneos-web-h2-station-site/.spec/requirements-prd-statement.md +++ b/src/prototypes/oneos-web-h2-station-site/.spec/requirements-prd-statement.md @@ -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 | 阻止提交 | +| 点击「提交对账单」未二次确认 | 不执行回写 | | 预付余额结算后为负 | 允许提交,标「已欠费」 | --- diff --git a/src/prototypes/oneos-web-h2-station-site/.spec/requirements-prd.md b/src/prototypes/oneos-web-h2-station-site/.spec/requirements-prd.md index 03f5b20..0b8fbd8 100644 --- a/src/prototypes/oneos-web-h2-station-site/.spec/requirements-prd.md +++ b/src/prototypes/oneos-web-h2-station-site/.spec/requirements-prd.md @@ -145,8 +145,8 @@ | 步骤 | 说明 | |---|---| | 1. 拉取明细 | 按账期从车辆氢费明细取 **已核对** ∧ **未对账** ∧ 未结算 ∧ 本站 的记录;**未核对不纳入** | -| 2. 两阶段 | 选期生成 → 填收票提交 | -| 3. 提交回写 | 更新预付余额、写对账历史;台账变为 **已对账**(对账日期、收票日期、已付款) | +| 2. 两阶段 | 选期生成 → 填收票 / 发票附件 / **对账单盖章附件** → 二次确认提交 | +| 3. 提交回写 | 更新预付余额、写对账历史;**加氢记录 Web / H5 / 车辆氢费明细**三端同步为 **已对账** | > **核对 ≠ 对账**:核对由车辆氢费明细「完成核对」产生;对账仅由本页「提交对账单」回写。 @@ -175,8 +175,8 @@ ### 8.2 对账流程 - [ ] 仅 **已核对** 且未对账、未结算的记录可进入对账单;未核对明细即使在账期内也不出现 -- [ ] 两阶段交互与必填校验完整 -- [ ] 提交后回写车辆氢费明细为 **已对账**,可跨页验收 +- [ ] 两阶段交互与必填校验完整(含对账单盖章附件:PDF/图片) +- [ ] 提交二次确认后,加氢记录(Web)、加氢订单(H5)、车辆氢费明细三端同步为 **已对账** ### 8.3 期末余额设置 diff --git a/src/prototypes/oneos-web-h2-station-site/.spec/statement-verified-filter.md b/src/prototypes/oneos-web-h2-station-site/.spec/statement-verified-filter.md index 5561fe7..11b778f 100644 --- a/src/prototypes/oneos-web-h2-station-site/.spec/statement-verified-filter.md +++ b/src/prototypes/oneos-web-h2-station-site/.spec/statement-verified-filter.md @@ -71,6 +71,7 @@ | 规则 | 代码 | |---|---| | 筛选入口 | `h2GetAvailableStatementLedgerRows(store, stationName, startDate, endDate)` | +| Store 映射 | `h2BridgeMapToStationLedgerRow` **必须透出** `verifyStatus`(否则站点侧会全部当成未核对) | | 已核对 | `verifyStatus === 'verified'`(缺省按 `unverified`) | | 未对账 | `reconcileStatus === 'pending'`(且 ≠ `reconciled`) | | 账期 | `h2ParseDateTimeMs(hydrogenTime)` 落在起止日闭区间 | diff --git a/src/prototypes/oneos-web-h2-station-site/annotation-source.json b/src/prototypes/oneos-web-h2-station-site/annotation-source.json index b0e4abd..d3af9ea 100644 --- a/src/prototypes/oneos-web-h2-station-site/annotation-source.json +++ b/src/prototypes/oneos-web-h2-station-site/annotation-source.json @@ -5,7 +5,7 @@ "version": 2, "prototypeName": "oneos-web-h2-station-site", "pageId": "site-info", - "updatedAt": 1784281075967, + "updatedAt": 1784371907864, "nodes": [ { "id": "h2-site-filter", @@ -25,8 +25,8 @@ "hasMarkdown": true, "color": "#2563eb", "images": [], - "createdAt": 1784281075967, - "updatedAt": 1784281075967, + "createdAt": 1784371907864, + "updatedAt": 1784371907864, "controls": [] }, { @@ -47,8 +47,8 @@ "hasMarkdown": true, "color": "#2563eb", "images": [], - "createdAt": 1784281075967, - "updatedAt": 1784281075967, + "createdAt": 1784371907864, + "updatedAt": 1784371907864, "controls": [] }, { @@ -69,8 +69,8 @@ "hasMarkdown": true, "color": "#d97706", "images": [], - "createdAt": 1784281075967, - "updatedAt": 1784281075967, + "createdAt": 1784371907864, + "updatedAt": 1784371907864, "controls": [] }, { @@ -91,8 +91,8 @@ "hasMarkdown": true, "color": "#32a06e", "images": [], - "createdAt": 1784281075967, - "updatedAt": 1784281075967, + "createdAt": 1784371907864, + "updatedAt": 1784371907864, "controls": [] }, { @@ -112,8 +112,8 @@ "hasMarkdown": true, "color": "#d97706", "images": [], - "createdAt": 1784281075967, - "updatedAt": 1784281075967, + "createdAt": 1784371907864, + "updatedAt": 1784371907864, "controls": [] }, { @@ -133,8 +133,8 @@ "hasMarkdown": true, "color": "#7c3aed", "images": [], - "createdAt": 1784281075967, - "updatedAt": 1784281075967, + "createdAt": 1784371907864, + "updatedAt": 1784371907864, "controls": [] }, { @@ -150,13 +150,13 @@ "fingerprint": "form|statement", "path": [] }, - "aiPrompt": "选期仅拉取已核对未对账氢费明细、二次确认与结算提交。", - "annotationText": "账期内仅纳入车辆氢费明细「已核对」记录;未核对不进对账单;提交后标为已对账", + "aiPrompt": "选期仅拉取已核对未对账氢费明细;结算须上传盖章附件;提交二次确认后三端同步已对账。", + "annotationText": "仅计入车辆氢费明细全部「已核对」且未对账记录;结算阶段必填对账单盖章附件(PDF/图片);提交确认后 Web/H5/台账三端同步「已对账」", "hasMarkdown": true, "color": "#7c3aed", "images": [], - "createdAt": 1784281075967, - "updatedAt": 1784281075967, + "createdAt": 1784371907864, + "updatedAt": 1784371907864, "controls": [] }, { @@ -176,8 +176,8 @@ "hasMarkdown": true, "color": "#0891b2", "images": [], - "createdAt": 1784281075967, - "updatedAt": 1784281075967, + "createdAt": 1784371907864, + "updatedAt": 1784371907864, "controls": [] } ] @@ -189,7 +189,7 @@ "h2-site-table": "## 5. 列表字段说明\n\n| 字段 | 说明 |\n|---|---|\n| 加氢站名称 | 含签约标签;下一行展示省市区 + 详细地址 |\n| 合作时间 | 未签约可空 |\n| 营业状态 / 营业时间 | 列表只读;在「更多 → 营业状态」维护 |\n| 当前成本价格 | 元/kg;受价格配置生效时间驱动;多价时展示警示 |\n| 加氢次数 / 加氢量 | 加氢量可点击下钻;表头支持排序 |\n| 预付余额 | 可点击下钻流水;负值标红并显示「已欠费」 |\n| 联系方式 | 联系人 + 电话 |\n| 操作 | 查看 + 更多菜单 |\n\n**更多菜单**:编辑、删除、营业状态、价格配置、余额提醒、期末余额设置、生成对账单、查看对账记录等(按状态与权限显隐)。\n\n**分页**:默认 10 条,可选 5/10/20/50。\n\n---", "h2-site-price-cost-input": "### 5.2 价格配置\n\n配置成本价格(元/kg)与生效时间;到达生效时间后刷新列表「当前成本价格」;展示价格调整记录。\n\n**固定价格 · 适用客户规则:**\n\n- 默认对**全部客户**生效(作为站点默认价格规则)\n- **取价优先级**:第一权重为单站单客户价格配置(若该客户有专属配置则按单客户配置生效);第二权重为站点「全部客户」价格配置(无单客户配置时生效)\n\n#### 阶梯价格 · 生成逻辑\n\n1. 设置阶梯价格后,在对应**阶梯价格区间**内展示并应用该档**成本价**。\n2. 累计加氢量达到下一档阶梯时,自该次起按新档成本价计算;到达**阶梯量重置日期**后的新记录,按当前所处区间的**最新成本价**更新。\n3. **阶梯量重置日期**见价格配置表单内「阶梯量重置日期」;到达重置日后,累计加氢量**从零重新计数**,用于判断当前所处价格区间。\n4. **生效时间**自表单内「生效时间」起对新数据生效,**不追溯**此前已产生的数据;生效时间之前的历史记录,仍按当时已达的**最高阶梯价格**结算。", "h2-site-price-tiered": "#### 阶梯价格 · 生成逻辑\n\n1. 设置阶梯价格后,在对应**阶梯价格区间**内展示并应用该档**成本价**。\n2. 累计加氢量达到下一档阶梯时,自该次起按新档成本价计算;到达**阶梯量重置日期**后的新记录,按当前所处区间的**最新成本价**更新。\n3. **阶梯量重置日期**见价格配置表单内「阶梯量重置日期」;到达重置日后,累计加氢量**从零重新计数**,用于判断当前所处价格区间。\n4. **生效时间**自表单内「生效时间」起对新数据生效,**不追溯**此前已产生的数据;生效时间之前的历史记录,仍按当时已达的**最高阶梯价格**结算。", - "h2-site-statement-form": "## 1. 数据来源\n\n对账单明细取自 **车辆氢费明细**(共享 Store)中**同时满足**以下条件的记录:\n\n| 优先级 | 条件 | 说明 |\n|---|---|---|\n| 1 | 未绑定 `statementRecordId` | 尚未被历史对账单结算 |\n| 2 | 核对状态 = **已核对**(`verifyStatus = verified`) | **硬约束**:未核对一律不纳入 |\n| 3 | 对账状态 = **未对账**(`reconcileStatus = pending`) | 已对账由对账单提交产生,不再进入新账单 |\n| 4 | 加氢站名称 = 当前操作站点 | 仅本站 |\n| 5 | 加氢时间落在 **[账单开始日期, 账单结束日期]** | 用户所选账期(按日闭区间) |\n\n> 对账单明细**仅展示成本字段**(加氢量、成本单价、成本总价等),不展示加氢单价、加氢总价。\n\n### 1.1 为何必须「已核对」\n\n能源部在车辆氢费明细点击「完成核对」后,明细才视为业务已确认。站点侧生成对账单是结算动作,只能拉取已确认数据;未核对记录即使时间落在账期内,也**静默排除**,不出现在对账单明细与统计中。\n\n```text\n未核对 ──✕──→ 不可进对账单\n已核对 + 未对账 ──✓──→ 可进对账单(提交后变为已对账)\n已核对 + 已对账 ──✕──→ 不可再进下一张对账单\n```\n\n---\n\n---\n\n#### 生成对账单 · 业务逻辑\n\n1. 选择**账单开始日期**与**账单结束日期**后,从**车辆氢费明细**获取该时间段内、对应当前站点、且为**已核对且尚未结算(未对账)**的加氢记录。\n2. 点击「**生成对账单**」时弹出**二次确认**,提示:生成并提交后,车辆氢费明细中对应记录将标记为**已对账**且无法修改。\n3. 确认后进入结算阶段,填写**收票日期**、**收票金额**及**发票附件**(对账日期在提交时由系统自动回写)。\n4. 点击「**提交对账单**」后,本批明细在车辆氢费明细中锁定为**已对账**并绑定本对账单,不可再进入后续对账单。\n\n**阶段一 · 选期生成**\n\n- 日期区下方展示:**上次对账单结束时间**(无历史则「暂无」)\n- 默认开始日期建议 = 上次结束日 + 1 天\n- 点击「生成对账单」时弹出**二次确认**;确认后进入阶段二,日期锁定不可改\n- 无符合条件记录时提示调整日期(含:账期内仅有未核对、或均已结算等情况)\n\n**阶段二 · 结算提交**\n\n统计卡(明细表上方):\n\n| 指标 | 说明 |\n|---|---|\n| 加氢次数 | 本账单包含的**已核对**记录笔数 |\n| 加氢总量 | kg 合计 |\n| 成本总金额 | 成本总价合计 |\n\n明细表下方**必填**结算项:\n\n| 字段 | 规则 |\n|---|---|\n| 结算后加氢站预付款余额 | **只读**,= 当前预付余额 − 成本总金额;不足为负并标「已欠费」 |\n| 收票日期 | 必填 |\n| 收票金额 | 必填,默认填入成本总金额 |\n| 发票附件 | 必填,支持上传 |\n\n---", + "h2-site-statement-form": "## 1. 数据来源\n\n对账单明细取自 **车辆氢费明细**(共享 Store)中**同时满足**以下条件的记录:\n\n| 优先级 | 条件 | 说明 |\n|---|---|---|\n| 1 | 未绑定 `statementRecordId` | 尚未被历史对账单结算 |\n| 2 | 核对状态 = **已核对**(`verifyStatus = verified`) | **硬约束**:未核对一律不纳入 |\n| 3 | 对账状态 = **未对账**(`reconcileStatus = pending`) | 已对账由对账单提交产生,不再进入新账单 |\n| 4 | 加氢站名称 = 当前操作站点 | 仅本站 |\n| 5 | 加氢时间落在 **[账单开始日期, 账单结束日期]** | 用户所选账期(按日闭区间) |\n\n> 对账单明细**仅展示成本字段**(加氢量、成本单价、成本总价等),不展示加氢单价、加氢总价。\n\n### 1.1 为何必须「已核对」\n\n能源部在车辆氢费明细点击「完成核对」后,明细才视为业务已确认。站点侧生成对账单是结算动作,只能拉取已确认数据;未核对记录即使时间落在账期内,也**静默排除**,不出现在对账单明细与统计中。\n\n```text\n未核对 ──✕──→ 不可进对账单\n已核对 + 未对账 ──✓──→ 可进对账单(提交后变为已对账)\n已核对 + 已对账 ──✕──→ 不可再进下一张对账单\n```\n\n---\n\n---\n\n#### 生成对账单 · 业务逻辑\n\n1. 选择**账单开始日期**与**账单结束日期**后,从**车辆氢费明细**获取该时间段内、对应当前站点、且为**已核对且尚未结算(未对账)**的加氢记录。\n2. 点击「**生成对账单**」时弹出**二次确认**,提示:生成并提交后,车辆氢费明细中对应记录将标记为**已对账**且无法修改。\n3. 确认后进入结算阶段,填写**收票日期**、**收票金额**、**发票附件**及**对账单盖章附件**(对账日期在提交时由系统自动回写)。\n4. 点击「**提交对账单**」并二次确认后,本批明细在**加氢记录(Web)、加氢订单(H5)、车辆氢费明细**三端同步锁定为**已对账**并绑定本对账单,不可再进入后续对账单。\n\n**阶段一 · 选期生成**\n\n- 日期区下方展示:**上次对账单结束时间**(无历史则「暂无」)\n- 默认开始日期建议 = 上次结束日 + 1 天\n- 点击「生成对账单」时弹出**二次确认**;确认后进入阶段二,日期锁定不可改\n- 无符合条件记录时提示调整日期(含:账期内仅有未核对、或均已结算等情况)\n\n**阶段二 · 结算提交**\n\n统计卡(明细表上方):\n\n| 指标 | 说明 |\n|---|---|\n| 加氢次数 | 本账单包含的**已核对**记录笔数 |\n| 加氢总量 | kg 合计 |\n| 成本总金额 | 成本总价合计 |\n\n明细表下方**必填**结算项:\n\n| 字段 | 规则 |\n|---|---|\n| 结算后加氢站预付款余额 | **只读**,= 当前预付余额 − 成本总金额;不足为负并标「已欠费」 |\n| 收票日期 | 必填 |\n| 收票金额 | 必填,默认填入成本总金额 |\n| 发票附件 | 必填,支持上传 |\n| 对账单盖章附件 | **必填**;支持盖章 PDF、图片(`accept=.pdf,image/*`) |\n\n---", "h2-site-period-end-balance": "# 期末余额设置\n\n期末余额锚点、重算预付余额与变更记录。\n\n## 说明\n\n保存时按期末余额−支出+收入重算预付余额;变更记录只追加;期末时间不可晚于今天" }, "assetMap": {}, @@ -231,7 +231,7 @@ "type": "markdown", "id": "h2-site-doc-prd-full", "title": "PRD 完整文档", - "markdown": "# 加氢站管理 · 站点信息 — 产品需求文档(PRD)\n\n| 项 | 内容 |\n|---|---|\n| 文档版本 | v1.0 |\n| 模块名称 | 加氢站管理 → 站点信息 |\n| 所属系统 | ONE-OS Web 端 |\n| 目标读者 | 前端 / 后端 / 测试 / 业务产品对接人 |\n| 交互原型 | `/prototypes/oneos-web-h2-station-site` |\n| 关联原型 | [车辆氢费明细](/prototypes/vehicle-h2-fee-ledger)、[加氢记录](/prototypes/oneos-web-h2-station)、[加氢订单 H5](/prototypes/oneos-h5-h2-order) |\n| 文档状态 | 已对齐原型 |\n\n---\n\n## 1. 背景与目标\n\n### 1.1 业务背景\n\n加氢站是氢费成本结算的关键节点。运营侧需要维护站点主数据(签约、营业、成本价、预付余额),并在财务周期内按站点与加氢站完成对账、收票与付款闭环。\n\n### 1.2 本期目标\n\n- 站点台账的查询、新建、编辑、查看、删除与批量导入\n- 营业状态、成本价格、预付余额等运营配置\n- **按站点生成氢费对账单 → 填写收票信息 → 提交结算**,并与「车辆氢费明细」联动回写\n\n### 1.3 用户角色\n\n| 角色 | 典型诉求 |\n|---|---|\n| 加氢站运营 | 维护站点资料、营业与价格;查看加氢量与余额 |\n| 财务/结算 | 按周期生成对账单、登记收票、完成站点付款闭环 |\n| 管理员 | 批量导入站点、删除无效站点、配置系统账号 |\n\n### 1.4 非目标(本期不做)\n\n- 对账单审批流、ERP 自动过账\n- 发票 OCR 识别与验真\n- 加氢订单(独立 H5:`oneos-h5-h2-order`)、加氢记录(独立 Web:`oneos-web-h2-station`)本页不内嵌\n- 后端接口联调(原型使用 Mock + 共享 Store)\n\n---\n\n## 2. 信息架构\n\n```text\n站点信息\n├── 列表页\n│ ├── 筛选区(4 项)\n│ ├── KPI 卡片(6 张)\n│ ├── 工具栏\n│ └── 站点台账表 + 分页\n├── 新建 / 编辑(同页内嵌视图)\n│ ├── 基本信息\n│ ├── 系统账号绑定\n│ └── 供应商 / 付款信息\n└── 弹窗 / 抽屉\n ├── 查看详情\n ├── 营业状态 / 价格配置\n ├── 加氢量下钻 / 预付余额下钻\n ├── 预付余额预警 / 已欠费 KPI 站点列表 → 一键充值\n ├── 生成对账单(两阶段)\n └── 查看对账记录\n```\n\n---\n\n## 3. 列表与 KPI\n\n> 列表筛选与字段详见 `.spec/requirements-prd-list.md` \n> **预付余额预警 / 已欠费 KPI 点击交互**详见 `.spec/requirements-prd-kpi-balance-alert.md`\n\n- 筛选:名称、签约、地区、营业状态\n- KPI:全部 / 签约·普通 / **预付余额预警** / **已欠费** / 无加氢\n- 列表字段:名称地址、合作时间、营业、成本价、加氢次数/量、预付余额、联系方式、操作\n- 设计规范:与租赁合同管理一致的 vm 布局与 Inter 字体\n\n---\n\n## 4. 站点主数据\n\n### 4.1 新建站点\n\n同页内嵌视图:基本信息、系统账号绑定(可选)、供应商信息(新建或关联)。\n\n### 4.2 编辑 / 查看\n\n| 模式 | 交互 |\n|---|---|\n| 编辑 | 整页表单;系统账号仅 admin 可改 |\n| 查看 | 弹窗分块展示站点、账号、供应商、付款信息 |\n\n### 4.3 批量导入\n\n下载 CSV 模板 → 上传 → 解析预览 → 确认导入;站点名称不可重复。\n\n---\n\n## 5. 运营配置\n\n### 5.1 营业状态\n\n卡片式弹窗:营业状态、营业时间按钮组;非全天须配置时段;展示状态变更记录。\n\n### 5.2 价格配置\n\n配置成本价格(元/kg)与生效时间;到达生效时间后刷新列表「当前成本价格」;展示价格调整记录。\n\n**固定价格 · 适用客户规则:**\n\n- 默认对**全部客户**生效(作为站点默认价格规则)\n- **取价优先级**:第一权重为单站单客户价格配置(若该客户有专属配置则按单客户配置生效);第二权重为站点「全部客户」价格配置(无单客户配置时生效)\n\n#### 阶梯价格 · 生成逻辑\n\n1. 设置阶梯价格后,在对应**阶梯价格区间**内展示并应用该档**成本价**。\n2. 累计加氢量达到下一档阶梯时,自该次起按新档成本价计算;到达**阶梯量重置日期**后的新记录,按当前所处区间的**最新成本价**更新。\n3. **阶梯量重置日期**见价格配置表单内「阶梯量重置日期」;到达重置日后,累计加氢量**从零重新计数**,用于判断当前所处价格区间。\n4. **生效时间**自表单内「生效时间」起对新数据生效,**不追溯**此前已产生的数据;生效时间之前的历史记录,仍按当时已达的**最高阶梯价格**结算。\n\n### 5.3 期末余额设置\n\n> 完整规则见 [period-end-balance.md](./period-end-balance.md)\n\n与加氢站对账存在时间差时,登记**期末余额**与**期末时间**作为锚点:\n\n- **入口**:更多 → 期末余额设置\n- **计算**:`当前预付余额 = 期末余额 − 期末后支出 + 期末后收入`(仅保存时重算一次)\n- **约束**:期末时间不可晚于今天;变更记录只追加、不可删除\n- **下钻**:余额流水从期末锚点行起重新累加\n\n### 5.4 下钻分析\n\n- **加氢量下钻**:统计卡 + 明细表 + 导出,字段对齐车辆氢费明细\n- **预付余额下钻**:充值/加氢流水 + 趋势图 + 导出;若已配置期末余额,自期末时间锚点重算后续余额\n\n---\n\n## 6. 对账与结算(重点)\n\n> 详见 [requirements-prd-statement.md](./requirements-prd-statement.md);纳入规则全文见 [statement-verified-filter.md](./statement-verified-filter.md)\n\n与「车辆氢费明细」配合完成站点侧氢费结算闭环:\n\n| 步骤 | 说明 |\n|---|---|\n| 1. 拉取明细 | 按账期从车辆氢费明细取 **已核对** ∧ **未对账** ∧ 未结算 ∧ 本站 的记录;**未核对不纳入** |\n| 2. 两阶段 | 选期生成 → 填收票提交 |\n| 3. 提交回写 | 更新预付余额、写对账历史;台账变为 **已对账**(对账日期、收票日期、已付款) |\n\n> **核对 ≠ 对账**:核对由车辆氢费明细「完成核对」产生;对账仅由本页「提交对账单」回写。\n\n---\n\n## 7. 数据与集成\n\n| 能力 | 实现 |\n|---|---|\n| 氢费明细 | `src/common/h2VehicleLedgerBridge.js` |\n| 对账回写 | `H2_STATION_STATEMENT_LEDGER_UPDATES` |\n| 页面实现 | `src/prototypes/oneos-web-h2-station-site/pages/03-站点信息.jsx` |\n| 独立入口 | `src/prototypes/oneos-web-h2-station-site/index.tsx` |\n\n---\n\n## 8. 验收标准\n\n### 8.1 列表页\n\n- [ ] 筛选与 KPI 六卡片联动正确\n- [ ] 高频/低频/无加氢口径与车辆氢费明细一致\n- [ ] 加氢量、预付余额下钻数据来自共享 Store\n- [ ] 布局与租赁合同 vm 规范一致\n\n### 8.2 对账流程\n\n- [ ] 仅 **已核对** 且未对账、未结算的记录可进入对账单;未核对明细即使在账期内也不出现\n- [ ] 两阶段交互与必填校验完整\n- [ ] 提交后回写车辆氢费明细为 **已对账**,可跨页验收\n\n### 8.3 期末余额设置\n\n- [ ] 更多菜单可打开期末余额设置;期末时间不可晚于今天\n- [ ] 保存后写回预付余额,变更记录只追加不可删\n- [ ] 余额下钻出现期末锚点且后续余额连续推算\n\n### 8.4 标注与文档\n\n- [ ] `@axhub/annotation` 已接入,关键区域有 `data-annotation-id`\n- [ ] 右侧工具栏目录可阅 PRD 全文与页面标注\n\n---\n\n## 9. 修订记录\n\n| 版本 | 日期 | 说明 |\n|---|---|---|\n| v1.0 | 2026-07-01 | 独立入口、vm 规范对齐、标注目录与 PRD 接入 |\n", + "markdown": "# 加氢站管理 · 站点信息 — 产品需求文档(PRD)\n\n| 项 | 内容 |\n|---|---|\n| 文档版本 | v1.0 |\n| 模块名称 | 加氢站管理 → 站点信息 |\n| 所属系统 | ONE-OS Web 端 |\n| 目标读者 | 前端 / 后端 / 测试 / 业务产品对接人 |\n| 交互原型 | `/prototypes/oneos-web-h2-station-site` |\n| 关联原型 | [车辆氢费明细](/prototypes/vehicle-h2-fee-ledger)、[加氢记录](/prototypes/oneos-web-h2-station)、[加氢订单 H5](/prototypes/oneos-h5-h2-order) |\n| 文档状态 | 已对齐原型 |\n\n---\n\n## 1. 背景与目标\n\n### 1.1 业务背景\n\n加氢站是氢费成本结算的关键节点。运营侧需要维护站点主数据(签约、营业、成本价、预付余额),并在财务周期内按站点与加氢站完成对账、收票与付款闭环。\n\n### 1.2 本期目标\n\n- 站点台账的查询、新建、编辑、查看、删除与批量导入\n- 营业状态、成本价格、预付余额等运营配置\n- **按站点生成氢费对账单 → 填写收票信息 → 提交结算**,并与「车辆氢费明细」联动回写\n\n### 1.3 用户角色\n\n| 角色 | 典型诉求 |\n|---|---|\n| 加氢站运营 | 维护站点资料、营业与价格;查看加氢量与余额 |\n| 财务/结算 | 按周期生成对账单、登记收票、完成站点付款闭环 |\n| 管理员 | 批量导入站点、删除无效站点、配置系统账号 |\n\n### 1.4 非目标(本期不做)\n\n- 对账单审批流、ERP 自动过账\n- 发票 OCR 识别与验真\n- 加氢订单(独立 H5:`oneos-h5-h2-order`)、加氢记录(独立 Web:`oneos-web-h2-station`)本页不内嵌\n- 后端接口联调(原型使用 Mock + 共享 Store)\n\n---\n\n## 2. 信息架构\n\n```text\n站点信息\n├── 列表页\n│ ├── 筛选区(4 项)\n│ ├── KPI 卡片(6 张)\n│ ├── 工具栏\n│ └── 站点台账表 + 分页\n├── 新建 / 编辑(同页内嵌视图)\n│ ├── 基本信息\n│ ├── 系统账号绑定\n│ └── 供应商 / 付款信息\n└── 弹窗 / 抽屉\n ├── 查看详情\n ├── 营业状态 / 价格配置\n ├── 加氢量下钻 / 预付余额下钻\n ├── 预付余额预警 / 已欠费 KPI 站点列表 → 一键充值\n ├── 生成对账单(两阶段)\n └── 查看对账记录\n```\n\n---\n\n## 3. 列表与 KPI\n\n> 列表筛选与字段详见 `.spec/requirements-prd-list.md` \n> **预付余额预警 / 已欠费 KPI 点击交互**详见 `.spec/requirements-prd-kpi-balance-alert.md`\n\n- 筛选:名称、签约、地区、营业状态\n- KPI:全部 / 签约·普通 / **预付余额预警** / **已欠费** / 无加氢\n- 列表字段:名称地址、合作时间、营业、成本价、加氢次数/量、预付余额、联系方式、操作\n- 设计规范:与租赁合同管理一致的 vm 布局与 Inter 字体\n\n---\n\n## 4. 站点主数据\n\n### 4.1 新建站点\n\n同页内嵌视图:基本信息、系统账号绑定(可选)、供应商信息(新建或关联)。\n\n### 4.2 编辑 / 查看\n\n| 模式 | 交互 |\n|---|---|\n| 编辑 | 整页表单;系统账号仅 admin 可改 |\n| 查看 | 弹窗分块展示站点、账号、供应商、付款信息 |\n\n### 4.3 批量导入\n\n下载 CSV 模板 → 上传 → 解析预览 → 确认导入;站点名称不可重复。\n\n---\n\n## 5. 运营配置\n\n### 5.1 营业状态\n\n卡片式弹窗:营业状态、营业时间按钮组;非全天须配置时段;展示状态变更记录。\n\n### 5.2 价格配置\n\n配置成本价格(元/kg)与生效时间;到达生效时间后刷新列表「当前成本价格」;展示价格调整记录。\n\n**固定价格 · 适用客户规则:**\n\n- 默认对**全部客户**生效(作为站点默认价格规则)\n- **取价优先级**:第一权重为单站单客户价格配置(若该客户有专属配置则按单客户配置生效);第二权重为站点「全部客户」价格配置(无单客户配置时生效)\n\n#### 阶梯价格 · 生成逻辑\n\n1. 设置阶梯价格后,在对应**阶梯价格区间**内展示并应用该档**成本价**。\n2. 累计加氢量达到下一档阶梯时,自该次起按新档成本价计算;到达**阶梯量重置日期**后的新记录,按当前所处区间的**最新成本价**更新。\n3. **阶梯量重置日期**见价格配置表单内「阶梯量重置日期」;到达重置日后,累计加氢量**从零重新计数**,用于判断当前所处价格区间。\n4. **生效时间**自表单内「生效时间」起对新数据生效,**不追溯**此前已产生的数据;生效时间之前的历史记录,仍按当时已达的**最高阶梯价格**结算。\n\n### 5.3 期末余额设置\n\n> 完整规则见 [period-end-balance.md](./period-end-balance.md)\n\n与加氢站对账存在时间差时,登记**期末余额**与**期末时间**作为锚点:\n\n- **入口**:更多 → 期末余额设置\n- **计算**:`当前预付余额 = 期末余额 − 期末后支出 + 期末后收入`(仅保存时重算一次)\n- **约束**:期末时间不可晚于今天;变更记录只追加、不可删除\n- **下钻**:余额流水从期末锚点行起重新累加\n\n### 5.4 下钻分析\n\n- **加氢量下钻**:统计卡 + 明细表 + 导出,字段对齐车辆氢费明细\n- **预付余额下钻**:充值/加氢流水 + 趋势图 + 导出;若已配置期末余额,自期末时间锚点重算后续余额\n\n---\n\n## 6. 对账与结算(重点)\n\n> 详见 [requirements-prd-statement.md](./requirements-prd-statement.md);纳入规则全文见 [statement-verified-filter.md](./statement-verified-filter.md)\n\n与「车辆氢费明细」配合完成站点侧氢费结算闭环:\n\n| 步骤 | 说明 |\n|---|---|\n| 1. 拉取明细 | 按账期从车辆氢费明细取 **已核对** ∧ **未对账** ∧ 未结算 ∧ 本站 的记录;**未核对不纳入** |\n| 2. 两阶段 | 选期生成 → 填收票 / 发票附件 / **对账单盖章附件** → 二次确认提交 |\n| 3. 提交回写 | 更新预付余额、写对账历史;**加氢记录 Web / H5 / 车辆氢费明细**三端同步为 **已对账** |\n\n> **核对 ≠ 对账**:核对由车辆氢费明细「完成核对」产生;对账仅由本页「提交对账单」回写。\n\n---\n\n## 7. 数据与集成\n\n| 能力 | 实现 |\n|---|---|\n| 氢费明细 | `src/common/h2VehicleLedgerBridge.js` |\n| 对账回写 | `H2_STATION_STATEMENT_LEDGER_UPDATES` |\n| 页面实现 | `src/prototypes/oneos-web-h2-station-site/pages/03-站点信息.jsx` |\n| 独立入口 | `src/prototypes/oneos-web-h2-station-site/index.tsx` |\n\n---\n\n## 8. 验收标准\n\n### 8.1 列表页\n\n- [ ] 筛选与 KPI 六卡片联动正确\n- [ ] 高频/低频/无加氢口径与车辆氢费明细一致\n- [ ] 加氢量、预付余额下钻数据来自共享 Store\n- [ ] 布局与租赁合同 vm 规范一致\n\n### 8.2 对账流程\n\n- [ ] 仅 **已核对** 且未对账、未结算的记录可进入对账单;未核对明细即使在账期内也不出现\n- [ ] 两阶段交互与必填校验完整(含对账单盖章附件:PDF/图片)\n- [ ] 提交二次确认后,加氢记录(Web)、加氢订单(H5)、车辆氢费明细三端同步为 **已对账**\n\n### 8.3 期末余额设置\n\n- [ ] 更多菜单可打开期末余额设置;期末时间不可晚于今天\n- [ ] 保存后写回预付余额,变更记录只追加不可删\n- [ ] 余额下钻出现期末锚点且后续余额连续推算\n\n### 8.4 标注与文档\n\n- [ ] `@axhub/annotation` 已接入,关键区域有 `data-annotation-id`\n- [ ] 右侧工具栏目录可阅 PRD 全文与页面标注\n\n---\n\n## 9. 修订记录\n\n| 版本 | 日期 | 说明 |\n|---|---|---|\n| v1.0 | 2026-07-01 | 独立入口、vm 规范对齐、标注目录与 PRD 接入 |\n", "markdownPath": "src/prototypes/oneos-web-h2-station-site/.spec/requirements-prd.md" }, { @@ -252,14 +252,14 @@ "type": "markdown", "id": "h2-site-doc-prd-statement", "title": "对账与结算 PRD", - "markdown": "# 加氢站管理 · 站点信息 — 对账与结算 PRD\n\n> 本模块与「车辆氢费明细」配合完成**站点侧氢费结算闭环**。 \n> **重要**:生成对账单时,账期筛选出的明细**必须来自车辆氢费明细中「已核对」的记录**;未核对明细不得进入对账单。完整判定见 [statement-verified-filter.md](./statement-verified-filter.md);台账双状态见车辆氢费明细 `.spec/verify-reconcile.md`。\n\n---\n\n## 1. 数据来源\n\n对账单明细取自 **车辆氢费明细**(共享 Store)中**同时满足**以下条件的记录:\n\n| 优先级 | 条件 | 说明 |\n|---|---|---|\n| 1 | 未绑定 `statementRecordId` | 尚未被历史对账单结算 |\n| 2 | 核对状态 = **已核对**(`verifyStatus = verified`) | **硬约束**:未核对一律不纳入 |\n| 3 | 对账状态 = **未对账**(`reconcileStatus = pending`) | 已对账由对账单提交产生,不再进入新账单 |\n| 4 | 加氢站名称 = 当前操作站点 | 仅本站 |\n| 5 | 加氢时间落在 **[账单开始日期, 账单结束日期]** | 用户所选账期(按日闭区间) |\n\n> 对账单明细**仅展示成本字段**(加氢量、成本单价、成本总价等),不展示加氢单价、加氢总价。\n\n### 1.1 为何必须「已核对」\n\n能源部在车辆氢费明细点击「完成核对」后,明细才视为业务已确认。站点侧生成对账单是结算动作,只能拉取已确认数据;未核对记录即使时间落在账期内,也**静默排除**,不出现在对账单明细与统计中。\n\n```text\n未核对 ──✕──→ 不可进对账单\n已核对 + 未对账 ──✓──→ 可进对账单(提交后变为已对账)\n已核对 + 已对账 ──✕──→ 不可再进下一张对账单\n```\n\n---\n\n## 2. 两阶段交互\n\n#### 生成对账单 · 业务逻辑\n\n1. 选择**账单开始日期**与**账单结束日期**后,从**车辆氢费明细**获取该时间段内、对应当前站点、且为**已核对且尚未结算(未对账)**的加氢记录。\n2. 点击「**生成对账单**」时弹出**二次确认**,提示:生成并提交后,车辆氢费明细中对应记录将标记为**已对账**且无法修改。\n3. 确认后进入结算阶段,填写**收票日期**、**收票金额**及**发票附件**(对账日期在提交时由系统自动回写)。\n4. 点击「**提交对账单**」后,本批明细在车辆氢费明细中锁定为**已对账**并绑定本对账单,不可再进入后续对账单。\n\n**阶段一 · 选期生成**\n\n- 日期区下方展示:**上次对账单结束时间**(无历史则「暂无」)\n- 默认开始日期建议 = 上次结束日 + 1 天\n- 点击「生成对账单」时弹出**二次确认**;确认后进入阶段二,日期锁定不可改\n- 无符合条件记录时提示调整日期(含:账期内仅有未核对、或均已结算等情况)\n\n**阶段二 · 结算提交**\n\n统计卡(明细表上方):\n\n| 指标 | 说明 |\n|---|---|\n| 加氢次数 | 本账单包含的**已核对**记录笔数 |\n| 加氢总量 | kg 合计 |\n| 成本总金额 | 成本总价合计 |\n\n明细表下方**必填**结算项:\n\n| 字段 | 规则 |\n|---|---|\n| 结算后加氢站预付款余额 | **只读**,= 当前预付余额 − 成本总金额;不足为负并标「已欠费」 |\n| 收票日期 | 必填 |\n| 收票金额 | 必填,默认填入成本总金额 |\n| 发票附件 | 必填,支持上传 |\n\n---\n\n## 3. 提交后的系统行为\n\n提交成功后需**原子性**完成:\n\n1. 写入对账历史(供「查看对账记录」查询)\n2. 扣减站点预付余额\n3. 标记台账记录已结算(绑定 `statementRecordId`)\n4. **回写车辆氢费明细**:\n\n| 车辆氢费明细字段 | 回写值 |\n|---|---|\n| 对账状态 | **已对账**(`reconcileStatus = reconciled`) |\n| 对账日期 | 操作完成时间 |\n| 收票日期 | 操作完成日期 |\n| 加氢站付款状态 | 已付款 |\n\n原型通过 `H2_STATION_STATEMENT_LEDGER_UPDATES` / `h2VehicleLedgerBridge` 模拟跨页回写。\n\n> 「完成核对」只改变核对状态,**不会**把记录写成已对账;已对账仅由本页对账单提交产生。\n\n---\n\n## 4. 查看对账记录\n\n入口:列表操作列 → 更多 → **查看对账记录**\n\n历史列表:对账日期、对账人、账单区间、加氢次数、加氢金额、结算后余额、查看明细。\n\n查看明细:账单区间、统计三卡、收票信息、发票附件、成本明细表。\n\n---\n\n## 5. 校验与异常\n\n| 场景 | 处理 |\n|---|---|\n| 日期未填 / 开始晚于结束 | 阻止生成 |\n| 区间内无「已核对且未对账未结算」记录 | 阻止生成,提示调整日期(或先完成核对) |\n| 区间内存在未核对记录 | **不纳入**对账单;不单独弹窗枚举(静默排除) |\n| 点击「生成对账单」未确认 | 不进入结算阶段 |\n| 收票日期/金额/附件未填 | 阻止提交 |\n| 收票金额 ≤ 0 | 阻止提交 |\n| 预付余额结算后为负 | 允许提交,标「已欠费」 |\n\n---\n\n## 6. 研发要点\n\n1. **准入**:`verifyStatus === verified` 为硬条件;勿再用「已对账」作为生成筛选条件\n2. **对账单幂等**:同一笔明细只能进入一次成功提交的对账单(`statementRecordId` 约束)\n3. **上次结束时间**:取该站点已成功提交对账单的最大 `endDate`\n4. **余额计算**:`balanceAfter = prepaidBalance - sum(costTotal)`\n5. **跨模块同步**:生产对接统一台账;原型使用 Bridge Store\n6. **明细字段范围**:对账弹窗不展示客户侧加氢单价/总价\n7. **规格全文**:[statement-verified-filter.md](./statement-verified-filter.md)\n", + "markdown": "# 加氢站管理 · 站点信息 — 对账与结算 PRD\n\n> 本模块与「车辆氢费明细」配合完成**站点侧氢费结算闭环**。 \n> **重要**:生成对账单时,账期筛选出的明细**必须来自车辆氢费明细中「已核对」的记录**;未核对明细不得进入对账单。完整判定见 [statement-verified-filter.md](./statement-verified-filter.md);台账双状态见车辆氢费明细 `.spec/verify-reconcile.md`。\n\n---\n\n## 1. 数据来源\n\n对账单明细取自 **车辆氢费明细**(共享 Store)中**同时满足**以下条件的记录:\n\n| 优先级 | 条件 | 说明 |\n|---|---|---|\n| 1 | 未绑定 `statementRecordId` | 尚未被历史对账单结算 |\n| 2 | 核对状态 = **已核对**(`verifyStatus = verified`) | **硬约束**:未核对一律不纳入 |\n| 3 | 对账状态 = **未对账**(`reconcileStatus = pending`) | 已对账由对账单提交产生,不再进入新账单 |\n| 4 | 加氢站名称 = 当前操作站点 | 仅本站 |\n| 5 | 加氢时间落在 **[账单开始日期, 账单结束日期]** | 用户所选账期(按日闭区间) |\n\n> 对账单明细**仅展示成本字段**(加氢量、成本单价、成本总价等),不展示加氢单价、加氢总价。\n\n### 1.1 为何必须「已核对」\n\n能源部在车辆氢费明细点击「完成核对」后,明细才视为业务已确认。站点侧生成对账单是结算动作,只能拉取已确认数据;未核对记录即使时间落在账期内,也**静默排除**,不出现在对账单明细与统计中。\n\n```text\n未核对 ──✕──→ 不可进对账单\n已核对 + 未对账 ──✓──→ 可进对账单(提交后变为已对账)\n已核对 + 已对账 ──✕──→ 不可再进下一张对账单\n```\n\n---\n\n## 2. 两阶段交互\n\n#### 生成对账单 · 业务逻辑\n\n1. 选择**账单开始日期**与**账单结束日期**后,从**车辆氢费明细**获取该时间段内、对应当前站点、且为**已核对且尚未结算(未对账)**的加氢记录。\n2. 点击「**生成对账单**」时弹出**二次确认**,提示:生成并提交后,车辆氢费明细中对应记录将标记为**已对账**且无法修改。\n3. 确认后进入结算阶段,填写**收票日期**、**收票金额**、**发票附件**及**对账单盖章附件**(对账日期在提交时由系统自动回写)。\n4. 点击「**提交对账单**」并二次确认后,本批明细在**加氢记录(Web)、加氢订单(H5)、车辆氢费明细**三端同步锁定为**已对账**并绑定本对账单,不可再进入后续对账单。\n\n**阶段一 · 选期生成**\n\n- 日期区下方展示:**上次对账单结束时间**(无历史则「暂无」)\n- 默认开始日期建议 = 上次结束日 + 1 天\n- 点击「生成对账单」时弹出**二次确认**;确认后进入阶段二,日期锁定不可改\n- 无符合条件记录时提示调整日期(含:账期内仅有未核对、或均已结算等情况)\n\n**阶段二 · 结算提交**\n\n统计卡(明细表上方):\n\n| 指标 | 说明 |\n|---|---|\n| 加氢次数 | 本账单包含的**已核对**记录笔数 |\n| 加氢总量 | kg 合计 |\n| 成本总金额 | 成本总价合计 |\n\n明细表下方**必填**结算项:\n\n| 字段 | 规则 |\n|---|---|\n| 结算后加氢站预付款余额 | **只读**,= 当前预付余额 − 成本总金额;不足为负并标「已欠费」 |\n| 收票日期 | 必填 |\n| 收票金额 | 必填,默认填入成本总金额 |\n| 发票附件 | 必填,支持上传 |\n| 对账单盖章附件 | **必填**;支持盖章 PDF、图片(`accept=.pdf,image/*`) |\n\n---\n\n## 3. 提交后的系统行为\n\n提交成功后需**原子性**完成:\n\n1. 写入对账历史(供「查看对账记录」查询;含发票附件与盖章附件)\n2. 扣减站点预付余额\n3. 标记台账记录已结算(绑定 `statementRecordId`)\n4. **经共享 Bridge 回写三端**(同浏览器会话):\n\n| 模块 | 回写结果 |\n|---|---|\n| 车辆氢费明细 | 对账状态 → **已对账**;对账日期 / 收票日期 / 付款状态 / 对账人 |\n| 加氢记录(Web) | 对账状态 → **已对账**(订阅 Bridge) |\n| 加氢订单(H5) | 对账状态 → **已对账**(订阅 Bridge) |\n\n原型通过 `H2_STATION_STATEMENT_LEDGER_UPDATES` / `h2VehicleLedgerBridge.applyPatches` 模拟跨页回写(**未接真实 API**)。\n\n> 「完成核对」只改变核对状态,**不会**把记录写成已对账;已对账仅由本页对账单提交产生。\n\n---\n\n## 4. 查看对账记录\n\n入口:列表操作列 → 更多 → **查看对账记录**\n\n历史列表:对账日期、对账人、账单区间、加氢次数、加氢金额、结算后余额、查看明细。\n\n查看明细:账单区间、统计三卡、收票信息、发票附件、对账单盖章附件、成本明细表。\n\n---\n\n## 5. 校验与异常\n\n| 场景 | 处理 |\n|---|---|\n| 日期未填 / 开始晚于结束 | 阻止生成 |\n| 区间内无「已核对且未对账未结算」记录 | 阻止生成,提示调整日期(或先完成核对) |\n| 区间内存在未核对记录 | **不纳入**对账单;不单独弹窗枚举(静默排除) |\n| 点击「生成对账单」未确认 | 不进入结算阶段 |\n| 收票日期/金额/发票附件/盖章附件未填 | 阻止提交 |\n| 盖章附件非 PDF/图片 | 拒绝该文件并提示 |\n| 收票金额 ≤ 0 | 阻止提交 |\n| 点击「提交对账单」未二次确认 | 不执行回写 |\n| 预付余额结算后为负 | 允许提交,标「已欠费」 |\n\n---\n\n## 6. 研发要点\n\n1. **准入**:`verifyStatus === verified` 为硬条件;勿再用「已对账」作为生成筛选条件\n2. **对账单幂等**:同一笔明细只能进入一次成功提交的对账单(`statementRecordId` 约束)\n3. **上次结束时间**:取该站点已成功提交对账单的最大 `endDate`\n4. **余额计算**:`balanceAfter = prepaidBalance - sum(costTotal)`\n5. **跨模块同步**:生产对接统一台账;原型使用 Bridge Store\n6. **明细字段范围**:对账弹窗不展示客户侧加氢单价/总价\n7. **规格全文**:[statement-verified-filter.md](./statement-verified-filter.md)\n", "markdownPath": "src/prototypes/oneos-web-h2-station-site/.spec/requirements-prd-statement.md" }, { "type": "markdown", "id": "h2-site-doc-statement-verified-filter", "title": "对账单纳入规则(已核对)", - "markdown": "# 站点信息 · 生成对账单 — 明细纳入规则(已核对)\n\n| 项 | 说明 |\n|---|---|\n| 模块 | 加氢站管理 → 站点信息 → 生成对账单 |\n| 交互原型 | `/prototypes/oneos-web-h2-station-site` |\n| 对照台账 | [车辆氢费明细 · 核对 vs 对账](/prototypes/vehicle-h2-fee-ledger)(`.spec/verify-reconcile.md`) |\n| 实现 | `h2GetAvailableStatementLedgerRows`(`pages/03-站点信息.jsx`) |\n| 数据源 | `src/common/h2VehicleLedgerBridge.js` 共享 Store(原型本地种子,**未接真实 API**) |\n\n---\n\n## 1. 业务目的\n\n生成对账单时,按账单起止日期从**车辆氢费明细**筛选本站加氢记录。 \n**只纳入已完成「核对」的明细**;**未核对**记录即使加氢时间落在账期内,也**不得**出现在对账单中。\n\n> 这是跨模块硬约束:未核对数据尚未经能源部确认,不能进入站点结算。\n\n---\n\n## 2. 核对 vs 对账(不得混用)\n\n| 概念 | 字段 | 用户可见 | 谁触发 | 与对账单关系 |\n|---|---|---|---|---|\n| **核对** | `verifyStatus` = `verified` / `unverified` | 已核对 / 未核对 | 车辆氢费明细 · **完成核对** | **生成对账单的准入条件** |\n| **对账** | `reconcileStatus` = `pending` / `reconciled` | 未对账 / 已对账 | 本页 · **提交对账单** 回写 | 提交后变为已对账;已对账不再进入下一张 |\n\n```text\n加氢记录上报 → 未核对 + 未对账\n ↓ 车辆氢费明细「完成核对」\n 已核对 + 未对账 ←── 仅此状态可被「生成对账单」拉取\n ↓ 站点「提交对账单」\n 已核对 + 已对账(绑定 statementRecordId)\n```\n\n---\n\n## 3. 判定顺序(须全部满足,命中否即排除)\n\n按下列优先级过滤;任一不满足则**不纳入**对账单:\n\n| 优先级 | 条件 | 说明 |\n|---|---|---|\n| 1 | 无 `statementRecordId` | 尚未被历史对账单结算绑定 |\n| 2 | `verifyStatus === 'verified'`(已核对) | **核心**:未核对一律排除 |\n| 3 | `reconcileStatus === 'pending'`(未对账) | 已对账不再进入新账单 |\n| 4 | `stationName` = 当前操作站点 | 仅本站明细 |\n| 5 | 加氢时间 ∈ **[账单开始日期, 账单结束日期]**(按日闭区间) | 用户所选账期 |\n\n**前置条件(不进入本筛选):**\n\n- 未保存草稿(台账 `reconcileStatus = draft`)\n- 账单起止日期未填或开始晚于结束(交互层先拦截,不调用本筛选)\n\n---\n\n## 4. 用户可见结果\n\n| 场景 | 结果 |\n|---|---|\n| 账期内仅有未核对明细 | 无可用记录;提示调整日期或先去车辆氢费明细完成核对 |\n| 账期内有已核对未对账明细 | 进入对账单明细表与统计(次数 / 加氢量 / 成本总金额) |\n| 同账期内混有已核对与未核对 | **只展示已核对**;未核对静默排除,不计入统计 |\n| 已核对但已对账 / 已绑定对账单 | 排除 |\n\n---\n\n## 5. 与代码映射\n\n| 规则 | 代码 |\n|---|---|\n| 筛选入口 | `h2GetAvailableStatementLedgerRows(store, stationName, startDate, endDate)` |\n| 已核对 | `verifyStatus === 'verified'`(缺省按 `unverified`) |\n| 未对账 | `reconcileStatus === 'pending'`(且 ≠ `reconciled`) |\n| 账期 | `h2ParseDateTimeMs(hydrogenTime)` 落在起止日闭区间 |\n\n---\n\n## 6. 验收要点\n\n- [ ] 未核对明细不会出现在对账单明细表与统计中\n- [ ] 仅「已核对 ∧ 未对账 ∧ 未结算 ∧ 本站 ∧ 账期内」可生成\n- [ ] 文案与 PRD 使用「已核对 / 未核对」,不与「已对账 / 未对账」混称\n", + "markdown": "# 站点信息 · 生成对账单 — 明细纳入规则(已核对)\n\n| 项 | 说明 |\n|---|---|\n| 模块 | 加氢站管理 → 站点信息 → 生成对账单 |\n| 交互原型 | `/prototypes/oneos-web-h2-station-site` |\n| 对照台账 | [车辆氢费明细 · 核对 vs 对账](/prototypes/vehicle-h2-fee-ledger)(`.spec/verify-reconcile.md`) |\n| 实现 | `h2GetAvailableStatementLedgerRows`(`pages/03-站点信息.jsx`) |\n| 数据源 | `src/common/h2VehicleLedgerBridge.js` 共享 Store(原型本地种子,**未接真实 API**) |\n\n---\n\n## 1. 业务目的\n\n生成对账单时,按账单起止日期从**车辆氢费明细**筛选本站加氢记录。 \n**只纳入已完成「核对」的明细**;**未核对**记录即使加氢时间落在账期内,也**不得**出现在对账单中。\n\n> 这是跨模块硬约束:未核对数据尚未经能源部确认,不能进入站点结算。\n\n---\n\n## 2. 核对 vs 对账(不得混用)\n\n| 概念 | 字段 | 用户可见 | 谁触发 | 与对账单关系 |\n|---|---|---|---|---|\n| **核对** | `verifyStatus` = `verified` / `unverified` | 已核对 / 未核对 | 车辆氢费明细 · **完成核对** | **生成对账单的准入条件** |\n| **对账** | `reconcileStatus` = `pending` / `reconciled` | 未对账 / 已对账 | 本页 · **提交对账单** 回写 | 提交后变为已对账;已对账不再进入下一张 |\n\n```text\n加氢记录上报 → 未核对 + 未对账\n ↓ 车辆氢费明细「完成核对」\n 已核对 + 未对账 ←── 仅此状态可被「生成对账单」拉取\n ↓ 站点「提交对账单」\n 已核对 + 已对账(绑定 statementRecordId)\n```\n\n---\n\n## 3. 判定顺序(须全部满足,命中否即排除)\n\n按下列优先级过滤;任一不满足则**不纳入**对账单:\n\n| 优先级 | 条件 | 说明 |\n|---|---|---|\n| 1 | 无 `statementRecordId` | 尚未被历史对账单结算绑定 |\n| 2 | `verifyStatus === 'verified'`(已核对) | **核心**:未核对一律排除 |\n| 3 | `reconcileStatus === 'pending'`(未对账) | 已对账不再进入新账单 |\n| 4 | `stationName` = 当前操作站点 | 仅本站明细 |\n| 5 | 加氢时间 ∈ **[账单开始日期, 账单结束日期]**(按日闭区间) | 用户所选账期 |\n\n**前置条件(不进入本筛选):**\n\n- 未保存草稿(台账 `reconcileStatus = draft`)\n- 账单起止日期未填或开始晚于结束(交互层先拦截,不调用本筛选)\n\n---\n\n## 4. 用户可见结果\n\n| 场景 | 结果 |\n|---|---|\n| 账期内仅有未核对明细 | 无可用记录;提示调整日期或先去车辆氢费明细完成核对 |\n| 账期内有已核对未对账明细 | 进入对账单明细表与统计(次数 / 加氢量 / 成本总金额) |\n| 同账期内混有已核对与未核对 | **只展示已核对**;未核对静默排除,不计入统计 |\n| 已核对但已对账 / 已绑定对账单 | 排除 |\n\n---\n\n## 5. 与代码映射\n\n| 规则 | 代码 |\n|---|---|\n| 筛选入口 | `h2GetAvailableStatementLedgerRows(store, stationName, startDate, endDate)` |\n| Store 映射 | `h2BridgeMapToStationLedgerRow` **必须透出** `verifyStatus`(否则站点侧会全部当成未核对) |\n| 已核对 | `verifyStatus === 'verified'`(缺省按 `unverified`) |\n| 未对账 | `reconcileStatus === 'pending'`(且 ≠ `reconciled`) |\n| 账期 | `h2ParseDateTimeMs(hydrogenTime)` 落在起止日闭区间 |\n\n---\n\n## 6. 验收要点\n\n- [ ] 未核对明细不会出现在对账单明细表与统计中\n- [ ] 仅「已核对 ∧ 未对账 ∧ 未结算 ∧ 本站 ∧ 账期内」可生成\n- [ ] 文案与 PRD 使用「已核对 / 未核对」,不与「已对账 / 未对账」混称\n", "markdownPath": "src/prototypes/oneos-web-h2-station-site/.spec/statement-verified-filter.md" }, { @@ -287,7 +287,7 @@ "type": "markdown", "id": "h2-site-doc-acceptance", "title": "验收标准", - "markdown": "## 8. 验收标准\n\n### 8.1 列表页\n\n- [ ] 筛选与 KPI 六卡片联动正确\n- [ ] 高频/低频/无加氢口径与车辆氢费明细一致\n- [ ] 加氢量、预付余额下钻数据来自共享 Store\n- [ ] 布局与租赁合同 vm 规范一致\n\n### 8.2 对账流程\n\n- [ ] 仅 **已核对** 且未对账、未结算的记录可进入对账单;未核对明细即使在账期内也不出现\n- [ ] 两阶段交互与必填校验完整\n- [ ] 提交后回写车辆氢费明细为 **已对账**,可跨页验收\n\n### 8.3 期末余额设置\n\n- [ ] 更多菜单可打开期末余额设置;期末时间不可晚于今天\n- [ ] 保存后写回预付余额,变更记录只追加不可删\n- [ ] 余额下钻出现期末锚点且后续余额连续推算\n\n### 8.4 标注与文档\n\n- [ ] `@axhub/annotation` 已接入,关键区域有 `data-annotation-id`\n- [ ] 右侧工具栏目录可阅 PRD 全文与页面标注\n\n---" + "markdown": "## 8. 验收标准\n\n### 8.1 列表页\n\n- [ ] 筛选与 KPI 六卡片联动正确\n- [ ] 高频/低频/无加氢口径与车辆氢费明细一致\n- [ ] 加氢量、预付余额下钻数据来自共享 Store\n- [ ] 布局与租赁合同 vm 规范一致\n\n### 8.2 对账流程\n\n- [ ] 仅 **已核对** 且未对账、未结算的记录可进入对账单;未核对明细即使在账期内也不出现\n- [ ] 两阶段交互与必填校验完整(含对账单盖章附件:PDF/图片)\n- [ ] 提交二次确认后,加氢记录(Web)、加氢订单(H5)、车辆氢费明细三端同步为 **已对账**\n\n### 8.3 期末余额设置\n\n- [ ] 更多菜单可打开期末余额设置;期末时间不可晚于今天\n- [ ] 保存后写回预付余额,变更记录只追加不可删\n- [ ] 余额下钻出现期末锚点且后续余额连续推算\n\n### 8.4 标注与文档\n\n- [ ] `@axhub/annotation` 已接入,关键区域有 `data-annotation-id`\n- [ ] 右侧工具栏目录可阅 PRD 全文与页面标注\n\n---" } ] }, @@ -337,7 +337,7 @@ "type": "markdown", "id": "h2-site-doc-node-h2-site-statement-form", "title": "生成对账单", - "markdown": "## 1. 数据来源\n\n对账单明细取自 **车辆氢费明细**(共享 Store)中**同时满足**以下条件的记录:\n\n| 优先级 | 条件 | 说明 |\n|---|---|---|\n| 1 | 未绑定 `statementRecordId` | 尚未被历史对账单结算 |\n| 2 | 核对状态 = **已核对**(`verifyStatus = verified`) | **硬约束**:未核对一律不纳入 |\n| 3 | 对账状态 = **未对账**(`reconcileStatus = pending`) | 已对账由对账单提交产生,不再进入新账单 |\n| 4 | 加氢站名称 = 当前操作站点 | 仅本站 |\n| 5 | 加氢时间落在 **[账单开始日期, 账单结束日期]** | 用户所选账期(按日闭区间) |\n\n> 对账单明细**仅展示成本字段**(加氢量、成本单价、成本总价等),不展示加氢单价、加氢总价。\n\n### 1.1 为何必须「已核对」\n\n能源部在车辆氢费明细点击「完成核对」后,明细才视为业务已确认。站点侧生成对账单是结算动作,只能拉取已确认数据;未核对记录即使时间落在账期内,也**静默排除**,不出现在对账单明细与统计中。\n\n```text\n未核对 ──✕──→ 不可进对账单\n已核对 + 未对账 ──✓──→ 可进对账单(提交后变为已对账)\n已核对 + 已对账 ──✕──→ 不可再进下一张对账单\n```\n\n---\n\n---\n\n#### 生成对账单 · 业务逻辑\n\n1. 选择**账单开始日期**与**账单结束日期**后,从**车辆氢费明细**获取该时间段内、对应当前站点、且为**已核对且尚未结算(未对账)**的加氢记录。\n2. 点击「**生成对账单**」时弹出**二次确认**,提示:生成并提交后,车辆氢费明细中对应记录将标记为**已对账**且无法修改。\n3. 确认后进入结算阶段,填写**收票日期**、**收票金额**及**发票附件**(对账日期在提交时由系统自动回写)。\n4. 点击「**提交对账单**」后,本批明细在车辆氢费明细中锁定为**已对账**并绑定本对账单,不可再进入后续对账单。\n\n**阶段一 · 选期生成**\n\n- 日期区下方展示:**上次对账单结束时间**(无历史则「暂无」)\n- 默认开始日期建议 = 上次结束日 + 1 天\n- 点击「生成对账单」时弹出**二次确认**;确认后进入阶段二,日期锁定不可改\n- 无符合条件记录时提示调整日期(含:账期内仅有未核对、或均已结算等情况)\n\n**阶段二 · 结算提交**\n\n统计卡(明细表上方):\n\n| 指标 | 说明 |\n|---|---|\n| 加氢次数 | 本账单包含的**已核对**记录笔数 |\n| 加氢总量 | kg 合计 |\n| 成本总金额 | 成本总价合计 |\n\n明细表下方**必填**结算项:\n\n| 字段 | 规则 |\n|---|---|\n| 结算后加氢站预付款余额 | **只读**,= 当前预付余额 − 成本总金额;不足为负并标「已欠费」 |\n| 收票日期 | 必填 |\n| 收票金额 | 必填,默认填入成本总金额 |\n| 发票附件 | 必填,支持上传 |\n\n---" + "markdown": "## 1. 数据来源\n\n对账单明细取自 **车辆氢费明细**(共享 Store)中**同时满足**以下条件的记录:\n\n| 优先级 | 条件 | 说明 |\n|---|---|---|\n| 1 | 未绑定 `statementRecordId` | 尚未被历史对账单结算 |\n| 2 | 核对状态 = **已核对**(`verifyStatus = verified`) | **硬约束**:未核对一律不纳入 |\n| 3 | 对账状态 = **未对账**(`reconcileStatus = pending`) | 已对账由对账单提交产生,不再进入新账单 |\n| 4 | 加氢站名称 = 当前操作站点 | 仅本站 |\n| 5 | 加氢时间落在 **[账单开始日期, 账单结束日期]** | 用户所选账期(按日闭区间) |\n\n> 对账单明细**仅展示成本字段**(加氢量、成本单价、成本总价等),不展示加氢单价、加氢总价。\n\n### 1.1 为何必须「已核对」\n\n能源部在车辆氢费明细点击「完成核对」后,明细才视为业务已确认。站点侧生成对账单是结算动作,只能拉取已确认数据;未核对记录即使时间落在账期内,也**静默排除**,不出现在对账单明细与统计中。\n\n```text\n未核对 ──✕──→ 不可进对账单\n已核对 + 未对账 ──✓──→ 可进对账单(提交后变为已对账)\n已核对 + 已对账 ──✕──→ 不可再进下一张对账单\n```\n\n---\n\n---\n\n#### 生成对账单 · 业务逻辑\n\n1. 选择**账单开始日期**与**账单结束日期**后,从**车辆氢费明细**获取该时间段内、对应当前站点、且为**已核对且尚未结算(未对账)**的加氢记录。\n2. 点击「**生成对账单**」时弹出**二次确认**,提示:生成并提交后,车辆氢费明细中对应记录将标记为**已对账**且无法修改。\n3. 确认后进入结算阶段,填写**收票日期**、**收票金额**、**发票附件**及**对账单盖章附件**(对账日期在提交时由系统自动回写)。\n4. 点击「**提交对账单**」并二次确认后,本批明细在**加氢记录(Web)、加氢订单(H5)、车辆氢费明细**三端同步锁定为**已对账**并绑定本对账单,不可再进入后续对账单。\n\n**阶段一 · 选期生成**\n\n- 日期区下方展示:**上次对账单结束时间**(无历史则「暂无」)\n- 默认开始日期建议 = 上次结束日 + 1 天\n- 点击「生成对账单」时弹出**二次确认**;确认后进入阶段二,日期锁定不可改\n- 无符合条件记录时提示调整日期(含:账期内仅有未核对、或均已结算等情况)\n\n**阶段二 · 结算提交**\n\n统计卡(明细表上方):\n\n| 指标 | 说明 |\n|---|---|\n| 加氢次数 | 本账单包含的**已核对**记录笔数 |\n| 加氢总量 | kg 合计 |\n| 成本总金额 | 成本总价合计 |\n\n明细表下方**必填**结算项:\n\n| 字段 | 规则 |\n|---|---|\n| 结算后加氢站预付款余额 | **只读**,= 当前预付余额 − 成本总金额;不足为负并标「已欠费」 |\n| 收票日期 | 必填 |\n| 收票金额 | 必填,默认填入成本总金额 |\n| 发票附件 | 必填,支持上传 |\n| 对账单盖章附件 | **必填**;支持盖章 PDF、图片(`accept=.pdf,image/*`) |\n\n---" }, { "type": "markdown", diff --git a/src/prototypes/oneos-web-h2-station-site/pages/03-站点信息.jsx b/src/prototypes/oneos-web-h2-station-site/pages/03-站点信息.jsx index e890146..ec8f209 100644 --- a/src/prototypes/oneos-web-h2-station-site/pages/03-站点信息.jsx +++ b/src/prototypes/oneos-web-h2-station-site/pages/03-站点信息.jsx @@ -2329,16 +2329,27 @@ function h2ResolveUploadPreview(file, variant) { }; } +/** 盖章附件:仅允许 PDF / 常见图片 */ +function h2IsSealAttachmentFile(file) { + var name = String((file && (file.name || file.fileName)) || '').toLowerCase(); + var type = String((file && file.type) || '').toLowerCase(); + if (type.indexOf('pdf') >= 0 || type.indexOf('image/') === 0) return true; + return /\.(pdf|png|jpe?g|gif|webp|bmp)$/i.test(name); +} + /** 合同附件:按钮上传 + 预览/下载 */ function ContractFilesUpload(props) { var antd = window.antd; var Upload = antd.Upload; var Button = antd.Button; + var messageApi = antd.message; var fileList = props.fileList || []; var disabled = props.disabled; var showHint = props.showHint !== false; var wrapClass = 'h2-contract-upload' + (props.wrapClassName ? ' ' + props.wrapClassName : ''); var uploadClass = props.uploadClassName || 'h2-create-upload-btn'; + var accept = props.accept || undefined; + var validateFile = props.validateFile; return React.createElement('div', { className: wrapClass }, React.createElement('div', { className: 'h2-contract-upload-actions' }, @@ -2347,8 +2358,24 @@ function ContractFilesUpload(props) { multiple: true, disabled: disabled, fileList: fileList, - beforeUpload: function () { return false; }, - onChange: props.onChange, + accept: accept, + beforeUpload: function (file) { + if (typeof validateFile === 'function' && !validateFile(file)) { + if (messageApi && messageApi.warning) { + messageApi.warning(props.rejectMessage || '文件格式不支持'); + } + return Upload.LIST_IGNORE || false; + } + return false; + }, + onChange: function (info) { + if (!props.onChange) return; + var nextList = info.fileList || []; + if (typeof validateFile === 'function') { + nextList = nextList.filter(function (f) { return validateFile(f); }); + } + props.onChange({ fileList: nextList }); + }, showUploadList: false }, React.createElement(Button, { @@ -2359,7 +2386,7 @@ function ContractFilesUpload(props) { style: props.buttonStyle }, props.uploadLabel || '上传附件') ), - showHint ? React.createElement('span', { style: { fontSize: 12, color: '#94a3b8' } }, '点击上传,可多选') : null + showHint ? React.createElement('span', { style: { fontSize: 12, color: '#94a3b8' } }, props.hintText || '点击上传,可多选') : null ), fileList.length ? React.createElement('div', { className: 'h2-contract-file-list' }, @@ -5992,7 +6019,8 @@ const Component = function () { balanceAfterSettlement: 0, receiptDate: '', receiptAmount: '', - invoiceFiles: [] + invoiceFiles: [], + sealFiles: [] }); var statementModal = statementModalState[0]; var setStatementModal = statementModalState[1]; @@ -6753,13 +6781,14 @@ const Component = function () { open: true, record: record, startDate: defaultStart, - endDate: '2026-05-31', + endDate: '2026-07-31', phase: 'select', draftRows: [], balanceAfterSettlement: h2NumOrZero(record.prepaidBalance), receiptDate: '', receiptAmount: '', - invoiceFiles: [] + invoiceFiles: [], + sealFiles: [] }); }, [statementRecords]); @@ -6774,7 +6803,8 @@ const Component = function () { balanceAfterSettlement: 0, receiptDate: '', receiptAmount: '', - invoiceFiles: [] + invoiceFiles: [], + sealFiles: [] }); }, []); @@ -6829,7 +6859,8 @@ const Component = function () { balanceAfterSettlement: balanceAfter, receiptAmount: summary.totalCost ? h2FormatYuanNum(summary.totalCost) : '', receiptDate: '', - invoiceFiles: [] + invoiceFiles: [], + sealFiles: [] }); }); message.success('已生成对账记录,共 ' + summary.count + ' 笔,请填写结算信息并提交'); @@ -6857,71 +6888,88 @@ const Component = function () { message.warning('请上传发票附件'); return; } - var summary = h2CalcStatementSummary(rows); - var operateTime = h2OperateTimestamp(); - var operateDate = h2FormatOperateDateOnly(); - var statementId = 'stmt-' + Date.now(); - var recordIds = rows.map(function (r) { return r.id; }); - var stationRecord = statementModal.record; - var balanceAfter = h2NumOrZero(statementModal.balanceAfterSettlement); - var newStatement = { - id: statementId, - stationId: stationRecord.id, - stationName: stationRecord.name, - reconcileDate: operateTime, - reconciler: H2_CURRENT_OPERATOR, - startDate: statementModal.startDate, - endDate: statementModal.endDate, - refuelCount: summary.count, - totalKg: summary.totalKg, - totalCost: summary.totalCost, - balanceAfterSettlement: balanceAfter, - receiptDate: statementModal.receiptDate, - receiptAmount: receiptAmountNum, - invoiceFiles: (statementModal.invoiceFiles || []).slice(), - recordIds: recordIds - }; - var patches = []; - setLedgerStore(function (prev) { - return prev.map(function (r) { - if (recordIds.indexOf(r.id) < 0) return r; - patches.push({ - ledgerId: r.id, - orderNo: r.orderNo, - stationName: r.stationName, - plateNo: r.plateNo, - hydrogenTime: r.hydrogenTime, - reconcileDate: operateTime, - receiptDate: operateDate, - paymentStatus: H2_PAYMENT_STATUS_PAID, - statementRecordId: statementId, - reconcileStatus: H2_RECONCILE_RECONCILED - }); - return Object.assign({}, r, { - statementRecordId: statementId, - reconcileStatus: H2_RECONCILE_RECONCILED, - reconcileDate: operateTime, - receiptDate: operateDate, - paymentStatus: H2_PAYMENT_STATUS_PAID - }); - }); - }); - setStatementRecords(function (prev) { return [newStatement].concat(prev); }); - setListData(function (prev) { - return prev.map(function (item) { - if (item.id !== stationRecord.id) return item; - return Object.assign({}, item, { - prepaidBalance: balanceAfter, - updateTime: operateTime - }); - }); - }); - h2PushStatementLedgerPatches(patches); - if (typeof window !== 'undefined' && window.H2_VEHICLE_LEDGER_API && typeof window.H2_VEHICLE_LEDGER_API.applyPatches === 'function') { - window.H2_VEHICLE_LEDGER_API.applyPatches(patches); + if (!statementModal.sealFiles || !statementModal.sealFiles.length) { + message.warning('请上传对账单盖章附件'); + return; } - message.success('对账单已提交,共结算 ' + summary.count + ' 笔加氢记录'); - closeStatementModal(); + var summary = h2CalcStatementSummary(rows); + Modal.confirm({ + title: '确认提交对账单?', + content: '提交后,本批 ' + summary.count + ' 笔记录将在加氢记录、加氢订单(H5)、车辆氢费明细三端同步为「已对账」,且不可再修改,是否确认?', + okText: '确认提交', + cancelText: '取消', + centered: true, + onOk: function () { + var operateTime = h2OperateTimestamp(); + var operateDate = h2FormatOperateDateOnly(); + var statementId = 'stmt-' + Date.now(); + var recordIds = rows.map(function (r) { return r.id; }); + var stationRecord = statementModal.record; + var balanceAfter = h2NumOrZero(statementModal.balanceAfterSettlement); + var newStatement = { + id: statementId, + stationId: stationRecord.id, + stationName: stationRecord.name, + reconcileDate: operateTime, + reconciler: H2_CURRENT_OPERATOR, + startDate: statementModal.startDate, + endDate: statementModal.endDate, + refuelCount: summary.count, + totalKg: summary.totalKg, + totalCost: summary.totalCost, + balanceAfterSettlement: balanceAfter, + receiptDate: statementModal.receiptDate, + receiptAmount: receiptAmountNum, + invoiceFiles: (statementModal.invoiceFiles || []).slice(), + sealFiles: (statementModal.sealFiles || []).slice(), + recordIds: recordIds + }; + var patches = []; + setLedgerStore(function (prev) { + return prev.map(function (r) { + if (recordIds.indexOf(r.id) < 0) return r; + patches.push({ + ledgerId: r.id, + orderNo: r.orderNo, + stationName: r.stationName, + plateNo: r.plateNo, + hydrogenTime: r.hydrogenTime, + reconcileDate: operateTime, + receiptDate: operateDate, + paymentStatus: H2_PAYMENT_STATUS_PAID, + statementRecordId: statementId, + reconcileStatus: H2_RECONCILE_RECONCILED, + reconciler: H2_CURRENT_OPERATOR, + reconciledByName: H2_CURRENT_OPERATOR + }); + return Object.assign({}, r, { + statementRecordId: statementId, + reconcileStatus: H2_RECONCILE_RECONCILED, + reconcileDate: operateTime, + receiptDate: operateDate, + paymentStatus: H2_PAYMENT_STATUS_PAID, + reconciledByName: H2_CURRENT_OPERATOR + }); + }); + }); + setStatementRecords(function (prev) { return [newStatement].concat(prev); }); + setListData(function (prev) { + return prev.map(function (item) { + if (item.id !== stationRecord.id) return item; + return Object.assign({}, item, { + prepaidBalance: balanceAfter, + updateTime: operateTime + }); + }); + }); + h2PushStatementLedgerPatches(patches); + if (typeof window !== 'undefined' && window.H2_VEHICLE_LEDGER_API && typeof window.H2_VEHICLE_LEDGER_API.applyPatches === 'function') { + window.H2_VEHICLE_LEDGER_API.applyPatches(patches); + } + message.success('对账单已提交,共结算 ' + summary.count + ' 笔;三端已同步为「已对账」'); + closeStatementModal(); + } + }); }, [statementModal, closeStatementModal]); var openStatementHistoryModal = useCallback(function (record) { @@ -8458,9 +8506,9 @@ const Component = function () { React.createElement('strong', null, lastEndDate || '暂无') ), !isDraft ? React.createElement('div', { className: 'h2-statement-form-hint' }, - '数据来源:', + '数据来源:仅计入', React.createElement('strong', null, '车辆氢费明细'), - ' 中标记为「已核对」且尚未对账结算的加氢记录;未核对记录不会进入对账单;明细仅展示成本价与成本总价。' + ' 中所有「已核对」且尚未对账的加氢记录;未核对记录不会进入对账单。提交后,加氢记录(Web)、加氢订单(H5)、车辆氢费明细三端同步为「已对账」。明细仅展示成本价与成本总价。' ) : null ), isDraft ? React.createElement('div', { className: 'h2-statement-stats', role: 'group', 'aria-label': '对账统计' }, @@ -8556,6 +8604,21 @@ const Component = function () { onChange: function (info) { setStatementModal(function (m) { return Object.assign({}, m, { invoiceFiles: info.fileList || [] }); }); } + })), + formItem('对账单盖章附件', true, React.createElement(ContractFilesUpload, { + fileList: statementModal.sealFiles || [], + showHint: true, + hintText: '支持盖章 PDF、图片(可多选)', + accept: '.pdf,image/*', + validateFile: h2IsSealAttachmentFile, + rejectMessage: '盖章附件仅支持 PDF 或图片文件', + uploadLabel: '上传盖章附件', + wrapClassName: 'h2-statement-seal-upload', + uploadClassName: 'h2-statement-upload-btn-wrap', + buttonClassName: 'h2-statement-upload-btn', + onChange: function (info) { + setStatementModal(function (m) { return Object.assign({}, m, { sealFiles: info.fileList || [] }); }); + } })) ) : null ); @@ -8635,6 +8698,25 @@ const Component = function () { if (!stmt) return null; var summary = h2CalcStatementSummary(rows); var invoiceFiles = stmt.invoiceFiles || []; + var sealFiles = stmt.sealFiles || []; + var renderStmtFileList = function (files, emptyText) { + if (!files.length) { + return React.createElement('div', { style: { fontSize: 13, color: '#94a3b8', marginTop: 8 } }, emptyText); + } + return React.createElement('div', { className: 'h2-statement-detail-receipt-file-list' }, + files.map(function (file) { + return React.createElement('div', { key: file.uid || file.name, className: 'h2-statement-detail-receipt-file-item' }, + React.createElement('span', { className: 'h2-statement-detail-receipt-file-name', title: file.name }, file.name || '未命名附件'), + React.createElement(Button, { + type: 'link', + size: 'small', + icon: H2_ICONS.download, + onClick: function () { h2DownloadUploadFile(file); } + }, '下载') + ); + }) + ); + }; return React.createElement('div', { className: 'h2-statement-panel' }, React.createElement('div', { className: 'h2-statement-station-card' }, React.createElement('div', { className: 'h2-statement-station-card__name' }, stmt.stationName || '—'), @@ -8672,21 +8754,11 @@ const Component = function () { ), React.createElement('div', { className: 'h2-statement-detail-receipt-files' }, React.createElement('div', { className: 'h2-statement-detail-receipt-item__label' }, '发票附件'), - invoiceFiles.length - ? React.createElement('div', { className: 'h2-statement-detail-receipt-file-list' }, - invoiceFiles.map(function (file) { - return React.createElement('div', { key: file.uid || file.name, className: 'h2-statement-detail-receipt-file-item' }, - React.createElement('span', { className: 'h2-statement-detail-receipt-file-name', title: file.name }, file.name || '未命名附件'), - React.createElement(Button, { - type: 'link', - size: 'small', - icon: H2_ICONS.download, - onClick: function () { h2DownloadUploadFile(file); } - }, '下载') - ); - }) - ) - : React.createElement('div', { style: { fontSize: 13, color: '#94a3b8', marginTop: 8 } }, '暂无发票附件') + renderStmtFileList(invoiceFiles, '暂无发票附件') + ), + React.createElement('div', { className: 'h2-statement-detail-receipt-files', style: { marginTop: 12 } }, + React.createElement('div', { className: 'h2-statement-detail-receipt-item__label' }, '对账单盖章附件'), + renderStmtFileList(sealFiles, '暂无盖章附件') ) ), React.createElement('div', { className: 'h2-statement-table-wrap' }, diff --git a/src/prototypes/oneos-web-h2-station-site/scripts/build-annotation-source.mjs b/src/prototypes/oneos-web-h2-station-site/scripts/build-annotation-source.mjs index 6b4785e..5d29639 100644 --- a/src/prototypes/oneos-web-h2-station-site/scripts/build-annotation-source.mjs +++ b/src/prototypes/oneos-web-h2-station-site/scripts/build-annotation-source.mjs @@ -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', diff --git a/src/prototypes/oneos-web-h2-station/.spec/record-data-model.md b/src/prototypes/oneos-web-h2-station/.spec/record-data-model.md index dfae84f..9fbb668 100644 --- a/src/prototypes/oneos-web-h2-station/.spec/record-data-model.md +++ b/src/prototypes/oneos-web-h2-station/.spec/record-data-model.md @@ -65,7 +65,7 @@ |---|---| | 共享 Bridge 不可用 | 列表用本地 fallback;保存/删除提示失败 | | 已核对或已对账点编辑/删除 | 对应警告文案 | -| PC 独有 | **导出**含核对/对账列;手工台账上传(站点按角色范围;今日双态提示;右侧预览区确认上传) | +| PC 独有 | **导出**含车辆来源/数据来源/核对/对账列;手工台账上传(站点按角色范围;**昨日门禁**禁新增;今日双态提示;右侧预览区确认上传) | ## 4. 数据流 diff --git a/src/prototypes/oneos-web-h2-station/.spec/record-source-changelog.md b/src/prototypes/oneos-web-h2-station/.spec/record-source-changelog.md index 7fbf335..cb2020d 100644 --- a/src/prototypes/oneos-web-h2-station/.spec/record-source-changelog.md +++ b/src/prototypes/oneos-web-h2-station/.spec/record-source-changelog.md @@ -26,7 +26,7 @@ - **车辆氢费明细**:车牌号下方标签(站端=绿 / 羚牛=蓝) - **加氢记录 Web**:车牌号下方同口径标签 -- 导出 CSV 含「来源」列 +- 导出 CSV 含「数据来源」列(与「车辆来源」分列) --- diff --git a/src/prototypes/oneos-web-h2-station/.spec/requirements-prd.md b/src/prototypes/oneos-web-h2-station/.spec/requirements-prd.md index 05fe634..745e375 100644 --- a/src/prototypes/oneos-web-h2-station/.spec/requirements-prd.md +++ b/src/prototypes/oneos-web-h2-station/.spec/requirements-prd.md @@ -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`。 diff --git a/src/prototypes/oneos-web-h2-station/annotation-source.json b/src/prototypes/oneos-web-h2-station/annotation-source.json index 4a33371..a9769f6 100644 --- a/src/prototypes/oneos-web-h2-station/annotation-source.json +++ b/src/prototypes/oneos-web-h2-station/annotation-source.json @@ -5,6 +5,7 @@ "version": 2, "prototypeName": "oneos-web-h2-station", "pageId": "h2-record", + "updatedAt": 1784369510723, "nodes": [ { "id": "hr-filter", @@ -38,7 +39,7 @@ "fingerprint": "toolbar|h2-record", "path": [] }, - "annotationText": "左侧分段 Tab:加氢记录 / 手工台账(悬浮浅绿底,选中主色;对齐物流业务明细)。右侧导出、新增。", + "annotationText": "左侧分段 Tab:加氢记录 / 手工台账(悬浮浅绿底,选中主色;对齐物流业务明细)。前一日台账缺失时手工台账 Tab 红点。右侧导出、新增;缺失时点新增直达手工台账并选中昨日。", "hasMarkdown": false, "color": "#059669", "images": [], @@ -58,12 +59,12 @@ "fingerprint": "table|h2-record", "path": [] }, - "annotationText": "列:加氢时间、站名、车牌(附来源:站端上传/羚牛上传)、里程、单价、量、总额、核对状态/时间/人、对账状态/时间/人。操作更多含变更日志。已核对或已对账禁改删;对账仅对账单提交后出现。", + "annotationText": "列:加氢时间、站名、车牌(车辆来源:羚牛车辆/非羚牛车辆 + 数据来源:站端上传/羚牛上传)、里程、单价、量、总额、核对状态/时间/人、对账状态/时间/人。非羚牛车辆核对/对账列显示为空。操作列:详情(字段+车牌/面板原图)在更多之前;更多含编辑、变更日志、删除。已核对或已对账(仅羚牛)禁改删。导出 CSV 列名与此一致。", "hasMarkdown": false, "color": "#FF7D00", "images": [], "createdAt": 1784013600000, - "updatedAt": 1784013600000, + "updatedAt": 1784523600000, "controls": [] }, { @@ -113,9 +114,18 @@ "info": "演示站端人员可见站点:单站 / 多站 / 超级管理员。", "initialValue": "single", "options": [ - { "label": "单站人员", "value": "single" }, - { "label": "多站人员", "value": "multi" }, - { "label": "超级管理员", "value": "super_admin" } + { + "label": "单站人员", + "value": "single" + }, + { + "label": "多站人员", + "value": "multi" + }, + { + "label": "超级管理员", + "value": "super_admin" + } ] } ] @@ -152,17 +162,319 @@ "fingerprint": "manual-upload-panel|h2-record", "path": [] }, - "annotationText": "标题「手工台账」。中部预览区:空态/缩略图(已归档角标);可点全屏翻页;区内可上传。底部确认前二次提示「提交后将无法修改…」;提交后立即展示已归档图。与左侧月历等高。", + "annotationText": "标题「手工台账」。中部预览区:空态/缩略图(已归档角标);可点全屏翻页;区内可上传。底部确认前二次提示「提交后将无法修改…」;提交后立即展示已归档图。与左侧月历等高。新增门禁看「昨日」是否已传,不是今日。", "hasMarkdown": false, "color": "#2563EB", "images": [], "createdAt": 1784344200000, "updatedAt": 1784344200000, "controls": [] + }, + { + "id": "hr-batch-time", + "index": 8, + "title": "批量·加氢时间", + "pageId": "h2-record", + "locator": { + "selectors": [ + "[data-annotation-id=\"hr-batch-time\"]" + ], + "fingerprint": "batch-time|h2-record", + "path": [] + }, + "annotationText": "时间选择器,精确至秒,格式 YYYY-MM-DD HH:mm:ss。", + "hasMarkdown": false, + "color": "#2563EB", + "images": [], + "createdAt": 1784366700000, + "updatedAt": 1784366700000, + "controls": [] + }, + { + "id": "hr-batch-station", + "index": 9, + "title": "批量·加氢站", + "pageId": "h2-record", + "locator": { + "selectors": [ + "[data-annotation-id=\"hr-batch-station\"]" + ], + "fingerprint": "batch-station|h2-record", + "path": [] + }, + "annotationText": "按登录角色可见站点。仅 1 站时默认并锁定;多站时占位「请选择」,须手动选择。可用标注「登录角色站点范围」切换演示。", + "hasMarkdown": false, + "color": "#0F766E", + "images": [], + "createdAt": 1784366700000, + "updatedAt": 1784366700000, + "controls": [] + }, + { + "id": "hr-batch-plate", + "index": 10, + "title": "批量·车牌号", + "pageId": "h2-record", + "locator": { + "selectors": [ + "[data-annotation-id=\"hr-batch-plate\"]" + ], + "fingerprint": "batch-plate|h2-record", + "path": [] + }, + "annotationText": "输入车牌后右侧标签:命中车辆资产库→「羚牛车辆」;否则→「外部车辆」。", + "hasMarkdown": false, + "color": "#FF7D00", + "images": [], + "createdAt": 1784366700000, + "updatedAt": 1784366700000, + "controls": [] + }, + { + "id": "hr-batch-total", + "index": 11, + "title": "批量·加氢总额", + "pageId": "h2-record", + "locator": { + "selectors": [ + "[data-annotation-id=\"hr-batch-total\"]" + ], + "fingerprint": "batch-total|h2-record", + "path": [] + }, + "annotationText": "只读。规则与 H5 一致:总额 = 单价 × 加氢量(两位小数);改单价或量后自动重算。", + "hasMarkdown": false, + "color": "#059669", + "images": [], + "createdAt": 1784366700000, + "updatedAt": 1784366700000, + "controls": [] + }, + { + "id": "hr-batch-import", + "index": 12, + "title": "批量导入", + "pageId": "h2-record", + "locator": { + "selectors": [ + "[data-annotation-id=\"hr-batch-import\"]" + ], + "fingerprint": "batch-import|h2-record", + "path": [] + }, + "annotationText": "三步导入:①下载模板 ②重复策略(跳过/覆盖/报错,主键=加氢站+车牌+加氢时间)③上传文件。结果摘要在弹窗内;失败可下载错误日志。", + "hasMarkdown": false, + "color": "#7C3AED", + "images": [], + "createdAt": 1784366700000, + "updatedAt": 1784366700000, + "controls": [] } - ], - "updatedAt": 1784344200000 + ] }, "markdownMap": {}, - "assetMap": {} + "assetMap": {}, + "directory": { + "nodes": [ + { + "type": "folder", + "id": "hr-doc-root", + "title": "加氢记录(Web)说明", + "defaultExpanded": true, + "children": [ + { + "type": "markdown", + "id": "hr-doc-prd", + "title": "PRD 全文", + "markdown": "# 加氢记录(Web)— 产品需求文档\n\n| 项 | 内容 |\n|---|---|\n| 模块名称 | 加氢站管理 → 加氢记录 |\n| 所属系统 | ONE-OS(PC Web) |\n| 交互原型 | `/prototypes/oneos-web-h2-station` |\n| 关联模块 | [加氢订单 H5](/prototypes/oneos-h5-h2-order)、[站点信息](/prototypes/oneos-web-h2-station-site) |\n| 数据结构全文 | [record-data-model.md](./record-data-model.md) |\n| H5 对账规则 | [../oneos-h5-h2-order/.spec/reconcile-linkage.md](../oneos-h5-h2-order/.spec/reconcile-linkage.md) |\n\n---\n\n## 1. 背景与目标\n\n与 H5「加氢订单」为同一业务:站端上报加氢流水。Web 供 PC 操作;H5 供移动端。数据结构、共享账本与对账规则一致。Web **额外**提供导出与手工台账上传。\n\n### 1.0 加氢站管理菜单分工\n\n| 菜单 | 原型 | 职责 |\n|---|---|---|\n| 站点信息 | `oneos-web-h2-station-site` | 站点主数据、对账单生成与提交回写 |\n| 加氢订单 | `oneos-h5-h2-order` | 站端手机浏览器录入本站流水 |\n| 加氢记录 | **本原型** | PC 多站流水台账、导出、手工台账 |\n\n### 1.1 功能清单\n\n| # | 功能 | 说明 |\n|---|---|---|\n| 1 | 列表 / 筛选 / 详情 | 字段与 H5 一致;默认一行(加氢时间/车牌/站名/对账状态);`splitFilterFields` + **更多筛选/收起**;操作列 **详情**(记录字段 + 车牌/面板原图)在「更多」之前,见 [feature-list.md](./feature-list.md) |\n| 2 | 手工台账 | 与 H5 同逻辑:站点按登录角色范围过滤;今日已传/未传双态提示;**前一日缺失则今日禁新增**(顶栏固定文案,点新增直达台账并选中昨日);右侧「手工台账」预览 + 确认前二次提示;月历补传见 [feature-manual-ledger](../oneos-h5-h2-order/.spec/feature-manual-ledger.md) |\n| 3 | 新增 / 批量 / 编辑 | 时间精确至秒;单站默认/多站请选择;车牌旁羚牛/外部车辆标签;总额自动只读;支持批量导入(模板+结果页);顶栏无「批量新增加氢记录」标题;详见 [feature-create.md](./feature-create.md) |\n| 4 | 删除 | 仅未对账;写共享 Store |\n| 5 | 导出 | 当前筛选结果 CSV;列含**车辆来源**、**数据来源**(与列表标签一致) |\n\n### 1.2 列表字段\n\n加氢时间、加氢站名称、车牌号(附**车辆来源**:羚牛车辆/非羚牛车辆 + **数据来源**:站端上传/羚牛上传)、里程(km)、氢气单价(元/kg)、加氢量、加氢总额、核对状态、核对时间、核对人、对账状态、对账时间、对账人。 \n操作列(`OperationActions`):外显 **详情**(弹窗含字段摘要 + 车牌识别原图 / 加氢机面板原图);**更多** 含编辑、变更日志、删除。 \n非羚牛车辆的核对/对账相关列显示为空,详见 [fleet-vehicle-verify.md](./fleet-vehicle-verify.md)。\n\n### 1.3 工具栏 Tab\n\n列表工具栏左侧为分段 Tab(对齐物流业务明细 `lbd-main-tab`):**加氢记录** / **手工台账**。 \n悬浮:浅绿底 + 深色字;选中:主色浅底 + 主色字。本期不提供「预约加氢」。\n\n### 1.4 每日统计(第一屏)\n\n默认**收起**为一行 KPI 摘要 +「展开明细」,避免挤占列表;展开后才显示近 7 日卡片与翻页。规格见 [record-source-changelog.md](./record-source-changelog.md) §4。\n\n---\n\n## 2. 验收项\n\n1. 列表/表单字段与 H5「加氢订单」一致(含选填里程;无充装员 / 承担方式录入) \n2. 与 H5 读写同一共享 Store;一端增删改后另一端可见(同会话) \n3. 筛选默认一行;更多筛选/收起对齐物流业务明细;支持核对状态、核对时间、对账状态、对账时间;占位符样式与全局规范一致 \n4. 已核对或已对账不可编辑、删除;**已对账记录对账时间不为空**(对账单回写);核对由车辆氢费明细「完成核对」触发 \n5. 导出列与列表字段一致(含车辆来源、数据来源、核对人、对账人) \n6. 列表左上角两个 Tab:加氢记录 / 手工台账;悬浮与选中样式符合分段 Tab 规范;无预约加氢入口 \n7. 同站同车牌同加氢时间重复保存被拦截;总额与单价×量偏差 > 0.05 元时出现确认 \n8. 数据来源标签:站端上报→「站端上传」,台账手增→「羚牛上传」;变更日志含时间/操作人/修改项/原值/新值;核对时间后有核对人、对账时间后有对账人 \n9. 每日统计默认收起,第一屏以列表为主 \n10. 车牌附「羚牛车辆 / 非羚牛车辆」;非羚牛车辆核对/对账字段显示为空 \n11. 前一日手工台账缺失时,顶栏提示「手工台账有缺失,作为对账重要依据,请先上传手工台账」;点新增直达手工台账并选中昨日;补传后可新增 \n12. 操作列「详情」在「更多」之前;弹窗可查看加氢记录字段及车牌号、加氢机面板照片(无实拍时用演示占位图) \n\n---\n\n## 3. 复杂逻辑摘要\n\n| 主题 | 摘要 | 全文 |\n|---|---|---|\n| 数据结构与对账 | 站端口径字段;**核对 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) |\n| 重复键 / 总额偏差 | 业务主键拦截;总额偏差二次确认 | [record-data-model.md](./record-data-model.md) §5–6 |\n| 来源 / 变更日志 / 操作人 | `recordSource`、行级 `changeLogs`、核对人/对账人 | [record-source-changelog.md](./record-source-changelog.md) |\n| 羚牛车辆与核对对账可见性 | 系统车辆列表判定;非羚牛字段空展示 | [fleet-vehicle-verify.md](./fleet-vehicle-verify.md) |\n| 手工台账门禁 | 昨日未传 → 今日禁新增;文案与跳转见规格 | [../oneos-h5-h2-order/.spec/feature-manual-ledger.md](../oneos-h5-h2-order/.spec/feature-manual-ledger.md) |\n\n---\n\n## 4. 右侧标注目录\n\n打开原型右侧「原型标注 / 原型目录」,可阅读:\n\n| 目录 | 内容 |\n|---|---|\n| PRD 全文 | [requirements-prd.md](./requirements-prd.md) |\n| 需求角色 | [roles.md](./roles.md) |\n| 用户故事 | [user-stories.md](./user-stories.md) |\n| 关键逻辑 | [key-logic.md](./key-logic.md) 及数据模型、车辆、来源、台账、对账规格 |\n| 流程图 | [flows.md](./flows.md)(Mermaid) |\n| 功能说明 | 列表、新增、台账、导出、每日统计等 |\n\n由 `scripts/build-annotation-source.mjs` 从 `.spec/*.md` 生成 `annotation-source.json`。\n", + "markdownPath": ".spec/requirements-prd.md" + }, + { + "type": "folder", + "id": "hr-doc-roles", + "title": "需求角色", + "defaultExpanded": true, + "children": [ + { + "type": "markdown", + "id": "hr-doc-roles-md", + "title": "角色说明", + "markdown": "# 需求角色\n\n| 项 | 说明 |\n|---|---|\n| 适用原型 | 加氢记录(Web)`oneos-web-h2-station` |\n| 关联 H5 | 加氢订单 `oneos-h5-h2-order`(同一 Bridge) |\n| 数据 | 原型本地种子 + 共享 Bridge(未接真实登录 / API) |\n\n---\n\n## 1. 角色一览\n\n| 角色 | 端 | 核心诉求 | 本原型是否为主用户 |\n|---|---|---|---|\n| **加氢站操作人员(PC)** | Web | 在电脑上录入/批量补录、上传手工台账、导出 CSV | **是** |\n| **多站管理员** | Web | 在角色站点范围内切换站、查看多站流水 | **是**(标注状态可切单站/多站/超管) |\n| **超级管理员** | Web | 查看全部站点 | 演示态支持 |\n| **加氢站操作人员(手机)** | H5 | 站场快速上报 | 否(见 H5 原型) |\n| **能源部核对人员** | 车辆氢费明细 | 完成核对 | 否 |\n| **站点对账人员** | 站点信息 | 提交对账单 | 否 |\n\n---\n\n## 2. 本原型主角色:站端 PC 人员\n\n| 项 | 说明 |\n|---|---|\n| 场景 | 办公室 / 值班室 PC |\n| 权限边界 | 按登录角色可见站点(原型用标注「登录角色站点范围」演示) |\n| 日常动作 | 筛选列表 → 处理昨日台账门禁 → 新增/批量/编辑 → 导出 → 查看变更日志 |\n| 相对 H5 增量 | **导出**、多站筛选、批量新增、每日统计 KPI、变更日志弹窗 |\n\n---\n\n## 3. 角色边界(本期不做)\n\n- 真登录与权限中心 \n- 在本页直接完成「核对」「对账」按钮(由台账 / 站点信息回写) \n- 预约加氢(已下线) \n", + "markdownPath": ".spec/roles.md" + } + ] + }, + { + "type": "folder", + "id": "hr-doc-stories", + "title": "用户故事", + "defaultExpanded": true, + "children": [ + { + "type": "markdown", + "id": "hr-doc-stories-md", + "title": "用户故事", + "markdown": "# 用户故事\n\n> 以加氢站 PC 操作人员 / 多站管理员为主;验收口径见 [requirements-prd.md](./requirements-prd.md)。\n\n---\n\n## US-01 筛选并查看加氢流水\n\n**作为** 站端 PC 人员, \n**我希望** 按时间、车牌、站名、核对/对账状态筛选列表, \n**以便** 快速定位待处理记录。\n\n**验收要点**\n\n- 默认一行筛选项;更多筛选/收起对齐物流业务明细 \n- 列表含车辆来源、数据来源标签 \n- 非羚牛车辆核对/对账列为空 \n- 操作列「详情」可查看记录字段及车牌号、加氢机面板照片 \n\n---\n\n## US-02 前一日台账缺失时被拦截并补传\n\n**作为** 站端 PC 人员, \n**我希望** 昨日手工台账缺失时看到固定顶栏提示,点新增直达台账页, \n**以便** 先补对账依据再录电子记录。\n\n**验收要点**\n\n- 文案:`手工台账有缺失,作为对账重要依据,请先上传手工台账` \n- 跳转手工台账 Tab 并选中昨日 \n- Tab 红点表示门禁未解除 \n\n---\n\n## US-03 单条 / 批量新增加氢记录\n\n**作为** 站端 PC 人员, \n**我希望** 单条或批量录入加氢流水, \n**以便** 补录高峰数据。\n\n**验收要点**\n\n- 字段:时间、站、车牌、里程(选填)、单价、量、总额 \n- 重复键拦截;总额偏离单价×量 > 0.05 元需确认 \n- 已核对/已对账不可改删 \n\n---\n\n## US-04 上传与补传手工台账\n\n**作为** 站端 PC 人员, \n**我希望** 在月历上查看已传/未传并上传照片, \n**以便** 与 H5 同一套规则管理纸质台账。\n\n**验收要点**\n\n- 站点按角色范围;确认前二次弹窗;归档不可删 \n- 与 H5 同 Store \n\n---\n\n## US-05 导出当前筛选结果\n\n**作为** 站端 PC 人员, \n**我希望** 导出与列表一致的 CSV, \n**以便** 线下对账或留档。\n\n**验收要点**\n\n- 列含:车辆来源(羚牛车辆/非羚牛车辆)、数据来源(站端上传/羚牛上传) \n- 含核对/对账人与时间列 \n\n---\n\n## US-06 查看每日统计与变更日志\n\n**作为** 站端 PC 人员, \n**我希望** 第一屏不被统计挤占,需要时再展开明细,并能看行级变更日志, \n**以便** 兼顾效率与审计。\n\n**验收要点**\n\n- 每日统计默认收起 \n- 操作「变更日志」展示时间/操作人/改项/原值/新值 \n", + "markdownPath": ".spec/user-stories.md" + } + ] + }, + { + "type": "folder", + "id": "hr-doc-logic", + "title": "关键逻辑", + "defaultExpanded": true, + "children": [ + { + "type": "markdown", + "id": "hr-doc-logic-index", + "title": "逻辑索引", + "markdown": "# 关键逻辑索引\n\n本页汇总加氢记录(Web)复杂判定入口;细则见各规格全文。原型数据均为本地种子 / 内存 Store,**未接真实 API**。\n\n---\n\n## 1. 判定主题一览\n\n| 主题 | 一句话 | 全文 |\n|---|---|---|\n| 手工台账门禁 | **昨日**未传 → 今日禁新增;顶栏固定文案 + 直达昨日 | [../oneos-h5-h2-order/.spec/feature-manual-ledger.md](../oneos-h5-h2-order/.spec/feature-manual-ledger.md)(Web 同逻辑) |\n| 数据结构 / 重复键 / 总额 | 站端口径字段;主键拦截;总额偏差确认 | [record-data-model.md](./record-data-model.md) |\n| 车辆来源与核对可见性 | 羚牛车辆才展示核对/对账字段 | [fleet-vehicle-verify.md](./fleet-vehicle-verify.md) |\n| 数据来源 / 变更日志 | `recordSource`、行级 `changeLogs`、核对人/对账人 | [record-source-changelog.md](./record-source-changelog.md) |\n| 核对 ≠ 对账 | 台账完成核对 / 站点对账单提交 | [../oneos-h5-h2-order/.spec/reconcile-linkage.md](../oneos-h5-h2-order/.spec/reconcile-linkage.md) |\n\n---\n\n## 2. 手工台账门禁(摘要)\n\n与 H5 一致:\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 编辑已有 | 不校验 |\n| 2 | 昨日早于首笔加氢日 | 可新增 |\n| 3 | 昨日已上传 | 可新增 |\n| 4 | 昨日未上传 | 提示并 `goToManualLedgerForGap`(选中昨日) |\n\n文案:`手工台账有缺失,作为对账重要依据,请先上传手工台账`\n\n代码:`pages/02-加氢记录.jsx` → `hrIsManualLedgerGateBlocked`;Bridge → `isManualLedgerGateBlocked`\n\n---\n\n## 3. 导出列口径\n\n| 导出列名 | 取值 |\n|---|---|\n| 车辆来源 | 羚牛车辆 / 非羚牛车辆 |\n| 数据来源 | 站端上传 / 羚牛上传 |\n\n与列表车牌列标签一致。\n\n---\n\n## 4. 代码路径\n\n| 能力 | 路径 |\n|---|---|\n| 页面 | `pages/02-加氢记录.jsx` |\n| Bridge | `src/common/h2VehicleLedgerBridge.js` |\n| 标注 | `annotation-source.json`(`scripts/build-annotation-source.mjs` 生成) |\n", + "markdownPath": ".spec/key-logic.md" + }, + { + "type": "markdown", + "id": "hr-doc-logic-data", + "title": "数据模型与校验", + "markdown": "# 加氢记录(Web)· 数据结构与对账联动\n\n| 项 | 说明 |\n|---|---|\n| 代码路径 | UI:`pages/02-加氢记录.jsx`;Store:`src/common/h2VehicleLedgerBridge.js` |\n| 对齐模块 | H5「加氢订单」`oneos-h5-h2-order`(同一业务、同一 Store;PC 多导出 / 手工台账) |\n| 对账入口 | 菜单原型 `oneos-web-h2-station-site`(实现:`oneos-web-h2-station-site/pages/03-站点信息.jsx`) |\n| 数据源 | 原型本地种子 + 内存共享 Store(**未接真实 API**) |\n\n---\n\n## 1. 业务字段(列表 / 表单口径)\n\n与 H5「加氢订单」一致:\n\n| 字段 | 说明 |\n|---|---|\n| 加氢时间 | `YYYY-MM-DD HH:mm:ss` |\n| 加氢站名称 | 站名;写入时同步 `stationId`/`stationCode` |\n| 车牌号 | 展示去尾缀 `F`;写入 Store 补 `F` |\n| 里程(km) | `mileageKm`;**非必填**;未填存 `null`,列表展示「—」 |\n| 氢气单价(元/kg) | 站端口径,映射 `costUnitPrice` / `customerUnitPrice` |\n| 加氢量(kg) | `hydrogenKg` |\n| 加氢总额(元) | 映射 `costTotal` / `customerAmount` |\n| 核对状态 | `verifyStatus`:`unverified` / `verified`;能源部「完成核对」写入 |\n| 核对时间 | `verifiedAt`;未核对应「—」 |\n| 核对人 | `verifiedByName`;完成核对手写入操作员姓名 |\n| 对账状态 | `pending` / `reconciled`;**仅**站点对账单提交后变为已对账 |\n| 对账时间 | **仅** `reconcileDate`(对账单回写);勿用 `reconciledAt` 判定对账 |\n| 对账人 | `reconciledByName`;对账单提交时的 `reconciler` |\n| 记录来源 | `recordSource`:`station`(站端上传)/ `lingniu`(羚牛上传) |\n| 变更日志 | `changeLogs[]`:时间、操作人、修改项、原值、新值 |\n\n写入 Bridge 时默认:`settlementStatus=customer`、`creatorName=站端账号`、`recordSource=station`、`verifyStatus=unverified`、`reconcileStatus=pending`(与 H5 相同)。`mileageKm` 由表单选填写入,未填为 `null`。\n\n> 双状态全文:[../vehicle-h2-fee-ledger/.spec/verify-reconcile.md](../vehicle-h2-fee-ledger/.spec/verify-reconcile.md) \n> 来源 / 日志 / 操作人:[record-source-changelog.md](./record-source-changelog.md)\n\n### 1.1 列表筛选(Web)\n\n交互对齐全局规范(`rules/global-design-spec.md` §4.1)与物流业务明细:`splitFilterFields` / `shouldShowFilterExpand` 拆分首行与展开区;默认 **1 行 4 项**;`vm-filter-expand` +「更多筛选 / 收起」;占位符统一 `0.875rem` / `--ln-muted-soft`。\n\n| 区域 | 条件 | 规则 |\n|---|---|---|\n| 首行 | 加氢时间 | 按 `hydrogenTime` 日期闭区间 |\n| 首行 | 车牌号 / 加氢站 | 精确匹配 |\n| 首行 | 对账状态 | 已对账 / 未对账(`hrIsReconciled`) |\n| 更多 | 核对状态 | 已核对 / 未核对(`hrIsVerified`) |\n| 更多 | 核对时间 | 按 `verifiedAt` 日期闭区间;无核对时间则不命中 |\n| 更多 | 对账时间 | 按 `reconcileTime` 或 `reconcileDate` 日期闭区间;无对账时间则不命中 |\n\n---\n\n## 2. 核对 / 对账判定与锁定\n\n| 优先级 | 条件 | 用户可见 |\n|---|---|---|\n| 1 | `reconcileStatus === 'reconciled'` 或有 `reconcileDate` | **已对账**;禁编辑、禁删除;对账时间必有值 |\n| 2 | `verifyStatus === 'verified'` | **已核对**(可仍未对账);禁编辑、禁删除;展示核对时间 |\n| 3 | 否则 | **未核对 + 未对账**;可编辑、删除 |\n\n## 3. 前置条件\n\n| 场景 | 行为 |\n|---|---|\n| 共享 Bridge 不可用 | 列表用本地 fallback;保存/删除提示失败 |\n| 已核对或已对账点编辑/删除 | 对应警告文案 |\n| PC 独有 | **导出**含车辆来源/数据来源/核对/对账列;手工台账上传(站点按角色范围;**昨日门禁**禁新增;今日双态提示;右侧预览区确认上传) |\n\n## 4. 数据流\n\n```text\nWeb / H5 新增加氢记录\n → Bridge.upsertRow(unverified + pending)\n → 车辆氢费明细可见\n\n车辆氢费明细 · 完成核对\n → verifyStatus=verified + verifiedAt + verifiedByName\n → Web / H5 同步「已核对」\n\n站点信息 · 对账单提交(仅已核对 ∧ 未对账)\n → reconcileStatus=reconciled + reconcileDate + reconciledByName\n → Web / H5 / 台账同步「已对账」\n```\n\n## 5. 重复录入防护(业务主键)\n\n| 项 | 规则 |\n|---|---|\n| 业务键 | **加氢站名称 + 车牌号(含 F)+ 加氢时间(精确到秒)** |\n| 判定顺序 | 1)规范化车牌/时间 → 2)在共享 Store 查重(编辑时排除自身 id)→ 3)批量内行间互查 |\n| 前置条件 | 站名、车牌、时间均非空才校验;已对账记录仍占键,不可再录入同键 |\n| 用户可见 | **拦截保存**,提示「已存在相同加氢站、车牌号与加氢时间的记录」 |\n| 代码 | Bridge.`findDuplicateRow`;Web / H5 共用 |\n\n## 6. 总额与单价×量偏差\n\n| 项 | 规则 |\n|---|---|\n| 期望值 | `round(单价 × 加氢量, 2)` |\n| 容差 | 绝对值 ≤ **0.05 元** 视为一致 |\n| 超过容差 | 二次确认「仍要保存」;确认后按用户填写的总额写入(不强制改回) |\n| 代码 | Bridge.`totalAmountMismatch`;Web Modal / H5 `confirm` |\n\n## 7. 边界\n\n- Web 不做 OCR;录入单价×量可自动带出总额,总额仍可手改(超容差需确认)\n- 跨页联动依赖同浏览器会话内共享 Store\n- 本期不提供「预约加氢」Tab / 受理能力\n", + "markdownPath": ".spec/record-data-model.md" + }, + { + "type": "markdown", + "id": "hr-doc-logic-fleet", + "title": "羚牛车辆与核对可见性", + "markdown": "# 车牌「羚牛车辆 / 非羚牛车辆」与核对对账字段\n\n原型本地判定,未接真实车辆管理 API。\n\n## 1. 车辆来源判定\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 车牌为空 | 不展示车辆来源标签 |\n| 2 | 规范化后车牌 ∈ 系统车辆列表 | **羚牛车辆**(绿) |\n| 3 | 有车牌但不在集合中 | **非羚牛车辆**(橙) |\n\n比较前去掉尾缀 `F`,统一大写。\n\n### 数据源\n\n- 共享:`src/common/h2VehicleLedgerBridge.js` → `H2_FLEET_PLATE_KEYS` / `h2BridgeIsLingniuVehicle`\n- H5:`utils/fleet-plates.ts`(优先调 Bridge,否则本地同集合同步)\n- 正式环境应改为查询车辆管理 / 系统车辆列表接口\n\n### 展示位置\n\n| 模块 | 位置 |\n|------|------|\n| 加氢记录 Web | 车牌号列:车辆来源标签 + 数据来源标签 |\n| 车辆氢费明细 | 车牌号列:车辆来源标签 + 数据来源标签 |\n| 加氢记录 H5 | 新增向导、列表卡片、详情 |\n\n---\n\n## 2. 核对 / 对账字段可见性\n\n| 车辆来源 | 核对状态 / 核对时间 / 核对人 / 对账状态 / 对账时间(对账日期) / 对账人 |\n|----------|------------------------------------------------------------------------|\n| **羚牛车辆** | 正常展示业务值(未核/未对为「未核对」「未对账」或「—」) |\n| **非羚牛车辆** | **一律显示为空(—)**,不展示已核/未核、已对/未对等业务态 |\n\n### 判定顺序(展示)\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | `!isLingniuVehicle(plateNo)` | 上述字段全部空展示 |\n| 2 | 羚牛车辆 | 按原核对/对账逻辑渲染 |\n\n### 锁定与编辑\n\n- 站端改删锁定、H5 详情锁定:**仅羚牛车辆**且已核对/已对账时生效。\n- 非羚牛车辆不以核对/对账状态锁定编辑。\n\n### 演示种子\n\n- Bridge:`rf-ext-1` / 车牌 `浙C77801F`(非羚牛)\n- 台账:`mock-h2-external-plate` / 车牌 `粤Z99999F`(非羚牛)\n\n---\n\n## 3. 代码路径\n\n- Bridge:`h2BridgeIsLingniuVehicle` / `normalizePlateKey`\n- Web:`hrIsLingniuVehicle` / `hrShowVerifyReconcileFields` → `pages/02-加氢记录.jsx`\n- 台账:`isLingniuVehiclePlate` / `showVerifyReconcileFields` → `H2LedgerPage.jsx`\n- H5:`getVehicleTag` → `OrderCard` / `OrderDetail` / `CreateWizard`;`isOrderLocked` 跳过非羚牛\n", + "markdownPath": ".spec/fleet-vehicle-verify.md" + }, + { + "type": "markdown", + "id": "hr-doc-logic-source", + "title": "来源与变更日志", + "markdown": "# 加氢记录 · 来源标注 / 变更日志 / 核对人与对账人\n\n| 项 | 说明 |\n|---|---|\n| 代码路径 | Bridge:`src/common/h2VehicleLedgerBridge.js`;Web:`pages/02-加氢记录.jsx`;台账:`vehicle-h2-fee-ledger/H2LedgerPage.jsx`;H5:`oneos-h5-h2-order/utils/orders.ts` |\n| 数据源 | 原型本地种子 + 内存共享 Store(**未接真实 API**) |\n\n---\n\n## 1. 记录来源(`recordSource`)\n\n| 值 | 标签文案 | 写入场景 |\n|---|---|---|\n| `station` | **站端上传** | H5 / Web 站端新增加氢记录;`creatorName=站端账号` 或客户名含 `·站端上报` |\n| `lingniu` | **羚牛上传** | 车辆氢费明细人工新增 / 复制 / 导入 |\n\n### 判定优先级\n\n| 优先级 | 条件 | 结果 |\n|---|---|---|\n| 1 | 行上已有 `recordSource` 为 `station` / `lingniu` | 原样使用 |\n| 2 | `creatorName === '站端账号'` 或 `customerName` 含 `·站端上报` | `station` |\n| 3 | 其他 | `lingniu` |\n\n### 展示\n\n- **车辆氢费明细**:车牌号下方标签(站端=绿 / 羚牛=蓝)\n- **加氢记录 Web**:车牌号下方同口径标签\n- 导出 CSV 含「数据来源」列(与「车辆来源」分列)\n\n---\n\n## 2. 变更日志(`changeLogs[]`)\n\n挂在每条加氢记录上,供审计。条目字段:\n\n| 字段 | 说明 |\n|---|---|\n| `at` | 修改时间 |\n| `operatorName` / `userName` | 操作人 |\n| `fieldLabel` | 修改项(业务名) |\n| `oldValue` / `before` | 原值 |\n| `newValue` / `after` | 新值 |\n\n### 写入时机\n\n| 场景 | 规则 |\n|---|---|\n| 站端编辑(Web/H5 upsert) | Bridge 对比加氢时间/车牌/量/单价/总额/里程,有差异则追加 |\n| 车辆氢费明细字段修改 | `appendChangeLog` 同时写入行内 `changeLogs` 与本地索引 |\n| 完成核对 | 记录核对状态变化 |\n\n### 查看\n\n操作列 **更多 → 变更日志**(Web 加氢记录 / 车辆氢费明细),弹窗按时间倒序。\n\n### 前置条件\n\n- 无变更时弹窗空态「暂无变更记录」\n- 原型演示种子:`rf-13`、`rf-1` 预置各 1 条日志\n\n---\n\n## 3. 核对人 / 对账人\n\n| 字段 | 含义 | 写入时机 |\n|---|---|---|\n| `verifiedByName` | 核对人 | 车辆氢费明细「完成核对」→ 当前操作员姓名 |\n| `reconciledByName` | 对账人 | 站点信息对账单提交 patches 中的 `reconciler` |\n\n### 展示位置\n\n- **加氢记录 Web**:核对时间后「核对人」;对账时间后「对账人」\n- **车辆氢费明细**:核对状态后「核对时间」「核对人」;对账日期后「对账人」\n\n未核/未对时显示「—」。\n\n---\n\n## 4. 每日统计占用(第一屏)\n\n| 项 | 规则 |\n|---|---|\n| 默认 | **收起**:仅一行标题 + 近 30 天 KPI 胶囊 +「展开明细」 |\n| 展开 | 显示 7 日卡片轨与翻页 |\n| 目的 | 第一屏优先露出列表行 |\n\n---\n\n## 5. 验收要点\n\n1. 站端新增记录在台账/Web 显示「站端上传」;台账手增显示「羚牛上传」 \n2. 编辑量/价等后,变更日志可见时间、操作人、修改项、原值、新值 \n3. 完成核对后「核对人」有值;对账单提交后「对账人」有值 \n4. 每日统计默认收起,展开后才占日卡片高度 \n", + "markdownPath": ".spec/record-source-changelog.md" + }, + { + "type": "markdown", + "id": "hr-doc-logic-manual", + "title": "手工台账门禁", + "markdown": "# 功能:手工台账上传\n\n## 目标\n\n站端每日上传纸质**手工台账照片**,作为系统电子加氢记录的核对依据。多日未操作时,可通过日历查看缺口并补传过往日期。\n\n## 产品口径\n\n| 项 | 规则 |\n|---|---|\n| 入口 | H5:底部 Tab「手工台账」;Web:顶栏「手工台账」 |\n| 站点范围 | 仅展示当前登录用户角色对应站点;单站人员仅 1 个、多站可切换、超级管理员可选全部(原型用标注状态切换演示,未接真实登录) |\n| 上传形态 | 仅图片(拍照/相册),可多张 |\n| 维度 | **加氢站 + 自然日(本地日历日)** 一份 |\n| 日历 | 月历自本站**首笔加氢记录日**起算:绿=已传、橙=未传;首笔之前无需补传;禁选未来日 |\n| 补传 | 允许为过往未传日期补传;已传日期仅可**追加**新图 |\n| 归档 | **确认上传后的图片进入已归档状态,不可删除**;未确认的草稿图仍可删除 |\n| 二次确认 | 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- 今日状态双反馈(Web 放在右侧「当日台账」卡片内):未上传「今日尚未上传手工台账(YYYY-MM-DD)」异常态;已上传「今日已上传手工台账(YYYY-MM-DD)」正常态(日常上传反馈,≠新增门禁) \n- 昨日缺失:列表顶栏橙提示 + 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": "hr-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| 数据源 | 原型本地种子 + 内存共享 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", + "markdownPath": ".spec/reconcile-linkage.md" + } + ] + }, + { + "type": "folder", + "id": "hr-doc-flows", + "title": "流程图", + "defaultExpanded": true, + "children": [ + { + "type": "markdown", + "id": "hr-doc-flows-md", + "title": "操作流程图", + "markdown": "# 流程图\n\n> 标注面板支持 Mermaid 渲染。下列流程与当前原型行为一致(本地 Store,未接真实 API)。\n\n---\n\n## 1. 主路径:列表 → 门禁 → 新增 / 导出\n\n```mermaid\nflowchart TD\n A[打开加氢记录 Web] --> B[筛选 / 查看列表与每日统计]\n B --> C{昨日手工台账已上传?}\n C -->|否| D[顶栏提示:手工台账有缺失…]\n D --> E[点新增或点提示]\n E --> F[手工台账 Tab · 选中昨日]\n F --> G[上传并确认归档]\n G --> C\n C -->|是| H{操作}\n H -->|新增/批量| I[填表保存 · 重复键/总额校验]\n H -->|导出| J[按当前筛选导出 CSV]\n H -->|编辑/删除| K{已核对或已对账?}\n K -->|是| L[禁止]\n K -->|否| M[写回 Bridge]\n I --> B\n J --> B\n M --> B\n```\n\n---\n\n## 2. 手工台账(Web)\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. 与 H5 / 台账 / 对账单\n\n```mermaid\nsequenceDiagram\n participant WEB as 加氢记录 Web\n participant H5 as 加氢订单 H5\n participant BR as Bridge Store\n participant LED as 车辆氢费明细\n participant SITE as 站点对账单\n WEB->>BR: CRUD · 台账 · 导出读列表\n H5->>BR: CRUD · 台账\n LED->>BR: 完成核对\n SITE->>BR: 提交对账单\n Note over WEB,H5: 同会话共享可见\n```\n", + "markdownPath": ".spec/flows.md" + } + ] + }, + { + "type": "folder", + "id": "hr-doc-features", + "title": "功能说明", + "defaultExpanded": true, + "children": [ + { + "type": "markdown", + "id": "hr-doc-feat-list", + "title": "列表与筛选", + "markdown": "# 功能:列表与筛选\n\n## 目标\n\nPC 端查看多站(按角色)加氢流水,支持筛选、每日统计摘要与行操作。\n\n## 产品口径\n\n| 项 | 规则 |\n|---|---|\n| 默认筛选行 | 加氢时间、车牌、站名、对账状态(`splitFilterFields`) |\n| 更多筛选 | 核对状态/时间、对账时间等;展开样式对齐物流业务明细 |\n| 列表标签 | 车辆来源(羚牛/非羚牛)+ 数据来源(站端上传/羚牛上传) |\n| 非羚牛 | 核对/对账相关列显示为空 |\n| 锁定 | 已核对或已对账(仅羚牛适用)禁改删 |\n| 操作列 | `OperationActions`:**详情**(外显)→ **更多**(编辑 / 变更日志 / 删除) |\n\n### 详情弹窗\n\n| 项 | 规则 |\n|---|---|\n| 入口 | 操作列「详情」(`Eye` + 文案「详情」,在「更多」之前) |\n| 字段 | 加氢时间、站名、车牌(车辆来源标签)、数据来源、里程、单价、加氢量、总额、核对/对账状态(非羚牛为空) |\n| 照片 | 车牌识别原图、加氢机面板原图;可预览放大 |\n| 无实拍 | 种子无 `platePhotoUrl` / `panelPhotoUrl` 时用本地 Canvas 演示占位图(含水印文案);**未接真实图片 API** |\n\n## 验收\n\n1. 筛选默认一行,可展开更多 \n2. 标签与导出列名口径一致 \n3. 非羚牛车辆核对/对账列为空 \n4. 操作列「详情」可打开弹窗,展示记录字段与车牌/面板双图 \n\n详见 [requirements-prd.md](./requirements-prd.md)、[fleet-vehicle-verify.md](./fleet-vehicle-verify.md)。\n", + "markdownPath": ".spec/feature-list.md" + }, + { + "type": "markdown", + "id": "hr-doc-feat-create", + "title": "新增批量编辑", + "markdown": "# 功能:新增 / 批量 / 编辑\n\n## 目标\n\nPC 端单条或批量录入加氢流水,与 H5 字段口径一致。\n\n## 产品口径\n\n| 项 | 规则 |\n|---|---|\n| 字段 | 时间(精确至秒)、站、车牌、里程(选填)、单价、量、总额(自动) |\n| 加氢时间 | 时间选择器,格式 `YYYY-MM-DD HH:mm:ss` |\n| 加氢站 | 按登录角色可见站点;**仅 1 站时默认并锁定**;**多站时占位「请选择」,须手选** |\n| 车牌标签 | 有车牌后:资产库命中→「羚牛车辆」,否则→「外部车辆」 |\n| 加氢总额 | 只读;= 单价 × 加氢量(两位小数),与 H5 一致 |\n| 门禁 | 昨日手工台账未上传不可进入新增(见手工台账规格) |\n| 重复键 | 同站 + 同车牌 + 同加氢时间 → 拦截 |\n| 总额偏差 | 提交时仍按自动计算结果校验(手改路径已关闭) |\n| 锁定 | 已核对/已对账不可编辑 |\n| 页头标题 | 批量新增页顶栏**不展示**「批量新增加氢记录」标题,仅保留返回 |\n\n## 批量导入\n\n| 项 | 规则 |\n|---|---|\n| 入口 | 批量新增页工具栏左侧「批量导入」 |\n| 交互 | 三步:①下载模板 ②选择重复策略 ③上传文件;结果摘要留在弹窗内 |\n| 模板 | CSV;列:加氢时间*、加氢站名称*、车牌号*、里程(km)选填、氢气单价*、加氢量*;**不含总额** |\n| 主键 | **加氢站 + 车牌号 + 加氢时间** |\n| 跳过重复 | 主键已存在(录入表或系统)则跳过,计入跳过条数 |\n| 覆盖重复 | 用导入行覆盖录入表行,或覆盖系统已有记录(提交时按原 id upsert) |\n| 报错 | 重复行写入错误日志,不导入;可下载错误日志 |\n| 校验失败 | 字段不合规则同样记入失败,不写入表格 |\n\n## 验收\n\n1. 时间可选到秒 \n2. 单站默认、多站「请选择」 \n3. 车牌旁标签:羚牛车辆 / 外部车辆 \n4. 改单价或量后总额自动更新且不可手改 \n5. 顶栏无「批量新增加氢记录」标题 \n6. 批量导入:三步弹窗;可选跳过/覆盖/报错;失败可下载错误日志 \n\n详见 [record-data-model.md](./record-data-model.md)、[feature-manual-ledger.md](./feature-manual-ledger.md)。\n", + "markdownPath": ".spec/feature-create.md" + }, + { + "type": "markdown", + "id": "hr-doc-feat-manual", + "title": "手工台账上传", + "markdown": "# 功能:手工台账上传\n\n## 目标\n\n站端每日上传纸质**手工台账照片**,作为系统电子加氢记录的核对依据。多日未操作时,可通过日历查看缺口并补传过往日期。\n\n## 产品口径\n\n| 项 | 规则 |\n|---|---|\n| 入口 | H5:底部 Tab「手工台账」;Web:顶栏「手工台账」 |\n| 站点范围 | 仅展示当前登录用户角色对应站点;单站人员仅 1 个、多站可切换、超级管理员可选全部(原型用标注状态切换演示,未接真实登录) |\n| 上传形态 | 仅图片(拍照/相册),可多张 |\n| 维度 | **加氢站 + 自然日(本地日历日)** 一份 |\n| 日历 | 月历自本站**首笔加氢记录日**起算:绿=已传、橙=未传;首笔之前无需补传;禁选未来日 |\n| 补传 | 允许为过往未传日期补传;已传日期仅可**追加**新图 |\n| 归档 | **确认上传后的图片进入已归档状态,不可删除**;未确认的草稿图仍可删除 |\n| 二次确认 | 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- 今日状态双反馈(Web 放在右侧「当日台账」卡片内):未上传「今日尚未上传手工台账(YYYY-MM-DD)」异常态;已上传「今日已上传手工台账(YYYY-MM-DD)」正常态(日常上传反馈,≠新增门禁) \n- 昨日缺失:列表顶栏橙提示 + 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": "hr-doc-feat-export", + "title": "导出", + "markdown": "# 功能:导出\n\n## 目标\n\n将当前筛选结果导出为 CSV,列结构对齐列表字段。\n\n## 产品口径\n\n| 导出列 | 说明 |\n|---|---|\n| 序号 | 1-based |\n| 加氢时间 / 加氢站名称 / 车牌号 | 列表同字段 |\n| **车辆来源** | 羚牛车辆 / 非羚牛车辆 |\n| **数据来源** | 站端上传 / 羚牛上传 |\n| 里程 / 单价 / 量 / 总额 | 数值列 |\n| 核对状态/时间/人 | 非羚牛导出为空串 |\n| 对账状态/时间/人 | 非羚牛导出为空串 |\n\n## 验收\n\n1. 无数据时提示不可导出 \n2. 列名含「车辆来源」「数据来源」(非「车辆类型」「来源」) \n3. 与列表标签文案一致 \n\n代码:`pages/02-加氢记录.jsx` → `handleExport`。\n", + "markdownPath": ".spec/feature-export.md" + }, + { + "type": "markdown", + "id": "hr-doc-feat-stats", + "title": "每日统计", + "markdown": "# 功能:每日统计\n\n## 目标\n\n近若干日加氢量/金额汇总,默认收起,避免挤占列表第一屏。\n\n## 产品口径\n\n| 项 | 规则 |\n|---|---|\n| 默认 | 一行 KPI 摘要 +「展开明细」 |\n| 展开后 | 近 7 日卡片轨与翻页 |\n| 口径 | 随当前筛选结果实时汇总 |\n\n全文见 [record-source-changelog.md](./record-source-changelog.md) §4。\n", + "markdownPath": ".spec/feature-daily-stats.md" + }, + { + "type": "markdown", + "id": "hr-doc-feat-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": "markdown", + "id": "hr-doc-feat-source", + "title": "来源与变更日志", + "markdown": "# 加氢记录 · 来源标注 / 变更日志 / 核对人与对账人\n\n| 项 | 说明 |\n|---|---|\n| 代码路径 | Bridge:`src/common/h2VehicleLedgerBridge.js`;Web:`pages/02-加氢记录.jsx`;台账:`vehicle-h2-fee-ledger/H2LedgerPage.jsx`;H5:`oneos-h5-h2-order/utils/orders.ts` |\n| 数据源 | 原型本地种子 + 内存共享 Store(**未接真实 API**) |\n\n---\n\n## 1. 记录来源(`recordSource`)\n\n| 值 | 标签文案 | 写入场景 |\n|---|---|---|\n| `station` | **站端上传** | H5 / Web 站端新增加氢记录;`creatorName=站端账号` 或客户名含 `·站端上报` |\n| `lingniu` | **羚牛上传** | 车辆氢费明细人工新增 / 复制 / 导入 |\n\n### 判定优先级\n\n| 优先级 | 条件 | 结果 |\n|---|---|---|\n| 1 | 行上已有 `recordSource` 为 `station` / `lingniu` | 原样使用 |\n| 2 | `creatorName === '站端账号'` 或 `customerName` 含 `·站端上报` | `station` |\n| 3 | 其他 | `lingniu` |\n\n### 展示\n\n- **车辆氢费明细**:车牌号下方标签(站端=绿 / 羚牛=蓝)\n- **加氢记录 Web**:车牌号下方同口径标签\n- 导出 CSV 含「数据来源」列(与「车辆来源」分列)\n\n---\n\n## 2. 变更日志(`changeLogs[]`)\n\n挂在每条加氢记录上,供审计。条目字段:\n\n| 字段 | 说明 |\n|---|---|\n| `at` | 修改时间 |\n| `operatorName` / `userName` | 操作人 |\n| `fieldLabel` | 修改项(业务名) |\n| `oldValue` / `before` | 原值 |\n| `newValue` / `after` | 新值 |\n\n### 写入时机\n\n| 场景 | 规则 |\n|---|---|\n| 站端编辑(Web/H5 upsert) | Bridge 对比加氢时间/车牌/量/单价/总额/里程,有差异则追加 |\n| 车辆氢费明细字段修改 | `appendChangeLog` 同时写入行内 `changeLogs` 与本地索引 |\n| 完成核对 | 记录核对状态变化 |\n\n### 查看\n\n操作列 **更多 → 变更日志**(Web 加氢记录 / 车辆氢费明细),弹窗按时间倒序。\n\n### 前置条件\n\n- 无变更时弹窗空态「暂无变更记录」\n- 原型演示种子:`rf-13`、`rf-1` 预置各 1 条日志\n\n---\n\n## 3. 核对人 / 对账人\n\n| 字段 | 含义 | 写入时机 |\n|---|---|---|\n| `verifiedByName` | 核对人 | 车辆氢费明细「完成核对」→ 当前操作员姓名 |\n| `reconciledByName` | 对账人 | 站点信息对账单提交 patches 中的 `reconciler` |\n\n### 展示位置\n\n- **加氢记录 Web**:核对时间后「核对人」;对账时间后「对账人」\n- **车辆氢费明细**:核对状态后「核对时间」「核对人」;对账日期后「对账人」\n\n未核/未对时显示「—」。\n\n---\n\n## 4. 每日统计占用(第一屏)\n\n| 项 | 规则 |\n|---|---|\n| 默认 | **收起**:仅一行标题 + 近 30 天 KPI 胶囊 +「展开明细」 |\n| 展开 | 显示 7 日卡片轨与翻页 |\n| 目的 | 第一屏优先露出列表行 |\n\n---\n\n## 5. 验收要点\n\n1. 站端新增记录在台账/Web 显示「站端上传」;台账手增显示「羚牛上传」 \n2. 编辑量/价等后,变更日志可见时间、操作人、修改项、原值、新值 \n3. 完成核对后「核对人」有值;对账单提交后「对账人」有值 \n4. 每日统计默认收起,展开后才占日卡片高度 \n", + "markdownPath": ".spec/record-source-changelog.md" + } + ] + }, + { + "type": "folder", + "id": "hr-doc-pages", + "title": "按页面", + "defaultExpanded": false, + "children": [ + { + "type": "folder", + "id": "hr-page-record", + "title": "加氢记录页", + "defaultExpanded": true, + "children": [ + { + "type": "route", + "id": "hr-route-record", + "title": "h2-record", + "route": "h2-record", + "payload": { + "pageId": "h2-record" + } + }, + { + "type": "markdown", + "id": "hr-page-record-prd", + "title": "本页 PRD", + "markdown": "# 加氢记录(Web)— 产品需求文档\n\n| 项 | 内容 |\n|---|---|\n| 模块名称 | 加氢站管理 → 加氢记录 |\n| 所属系统 | ONE-OS(PC Web) |\n| 交互原型 | `/prototypes/oneos-web-h2-station` |\n| 关联模块 | [加氢订单 H5](/prototypes/oneos-h5-h2-order)、[站点信息](/prototypes/oneos-web-h2-station-site) |\n| 数据结构全文 | [record-data-model.md](./record-data-model.md) |\n| H5 对账规则 | [../oneos-h5-h2-order/.spec/reconcile-linkage.md](../oneos-h5-h2-order/.spec/reconcile-linkage.md) |\n\n---\n\n## 1. 背景与目标\n\n与 H5「加氢订单」为同一业务:站端上报加氢流水。Web 供 PC 操作;H5 供移动端。数据结构、共享账本与对账规则一致。Web **额外**提供导出与手工台账上传。\n\n### 1.0 加氢站管理菜单分工\n\n| 菜单 | 原型 | 职责 |\n|---|---|---|\n| 站点信息 | `oneos-web-h2-station-site` | 站点主数据、对账单生成与提交回写 |\n| 加氢订单 | `oneos-h5-h2-order` | 站端手机浏览器录入本站流水 |\n| 加氢记录 | **本原型** | PC 多站流水台账、导出、手工台账 |\n\n### 1.1 功能清单\n\n| # | 功能 | 说明 |\n|---|---|---|\n| 1 | 列表 / 筛选 / 详情 | 字段与 H5 一致;默认一行(加氢时间/车牌/站名/对账状态);`splitFilterFields` + **更多筛选/收起**;操作列 **详情**(记录字段 + 车牌/面板原图)在「更多」之前,见 [feature-list.md](./feature-list.md) |\n| 2 | 手工台账 | 与 H5 同逻辑:站点按登录角色范围过滤;今日已传/未传双态提示;**前一日缺失则今日禁新增**(顶栏固定文案,点新增直达台账并选中昨日);右侧「手工台账」预览 + 确认前二次提示;月历补传见 [feature-manual-ledger](../oneos-h5-h2-order/.spec/feature-manual-ledger.md) |\n| 3 | 新增 / 批量 / 编辑 | 时间精确至秒;单站默认/多站请选择;车牌旁羚牛/外部车辆标签;总额自动只读;支持批量导入(模板+结果页);顶栏无「批量新增加氢记录」标题;详见 [feature-create.md](./feature-create.md) |\n| 4 | 删除 | 仅未对账;写共享 Store |\n| 5 | 导出 | 当前筛选结果 CSV;列含**车辆来源**、**数据来源**(与列表标签一致) |\n\n### 1.2 列表字段\n\n加氢时间、加氢站名称、车牌号(附**车辆来源**:羚牛车辆/非羚牛车辆 + **数据来源**:站端上传/羚牛上传)、里程(km)、氢气单价(元/kg)、加氢量、加氢总额、核对状态、核对时间、核对人、对账状态、对账时间、对账人。 \n操作列(`OperationActions`):外显 **详情**(弹窗含字段摘要 + 车牌识别原图 / 加氢机面板原图);**更多** 含编辑、变更日志、删除。 \n非羚牛车辆的核对/对账相关列显示为空,详见 [fleet-vehicle-verify.md](./fleet-vehicle-verify.md)。\n\n### 1.3 工具栏 Tab\n\n列表工具栏左侧为分段 Tab(对齐物流业务明细 `lbd-main-tab`):**加氢记录** / **手工台账**。 \n悬浮:浅绿底 + 深色字;选中:主色浅底 + 主色字。本期不提供「预约加氢」。\n\n### 1.4 每日统计(第一屏)\n\n默认**收起**为一行 KPI 摘要 +「展开明细」,避免挤占列表;展开后才显示近 7 日卡片与翻页。规格见 [record-source-changelog.md](./record-source-changelog.md) §4。\n\n---\n\n## 2. 验收项\n\n1. 列表/表单字段与 H5「加氢订单」一致(含选填里程;无充装员 / 承担方式录入) \n2. 与 H5 读写同一共享 Store;一端增删改后另一端可见(同会话) \n3. 筛选默认一行;更多筛选/收起对齐物流业务明细;支持核对状态、核对时间、对账状态、对账时间;占位符样式与全局规范一致 \n4. 已核对或已对账不可编辑、删除;**已对账记录对账时间不为空**(对账单回写);核对由车辆氢费明细「完成核对」触发 \n5. 导出列与列表字段一致(含车辆来源、数据来源、核对人、对账人) \n6. 列表左上角两个 Tab:加氢记录 / 手工台账;悬浮与选中样式符合分段 Tab 规范;无预约加氢入口 \n7. 同站同车牌同加氢时间重复保存被拦截;总额与单价×量偏差 > 0.05 元时出现确认 \n8. 数据来源标签:站端上报→「站端上传」,台账手增→「羚牛上传」;变更日志含时间/操作人/修改项/原值/新值;核对时间后有核对人、对账时间后有对账人 \n9. 每日统计默认收起,第一屏以列表为主 \n10. 车牌附「羚牛车辆 / 非羚牛车辆」;非羚牛车辆核对/对账字段显示为空 \n11. 前一日手工台账缺失时,顶栏提示「手工台账有缺失,作为对账重要依据,请先上传手工台账」;点新增直达手工台账并选中昨日;补传后可新增 \n12. 操作列「详情」在「更多」之前;弹窗可查看加氢记录字段及车牌号、加氢机面板照片(无实拍时用演示占位图) \n\n---\n\n## 3. 复杂逻辑摘要\n\n| 主题 | 摘要 | 全文 |\n|---|---|---|\n| 数据结构与对账 | 站端口径字段;**核对 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) |\n| 重复键 / 总额偏差 | 业务主键拦截;总额偏差二次确认 | [record-data-model.md](./record-data-model.md) §5–6 |\n| 来源 / 变更日志 / 操作人 | `recordSource`、行级 `changeLogs`、核对人/对账人 | [record-source-changelog.md](./record-source-changelog.md) |\n| 羚牛车辆与核对对账可见性 | 系统车辆列表判定;非羚牛字段空展示 | [fleet-vehicle-verify.md](./fleet-vehicle-verify.md) |\n| 手工台账门禁 | 昨日未传 → 今日禁新增;文案与跳转见规格 | [../oneos-h5-h2-order/.spec/feature-manual-ledger.md](../oneos-h5-h2-order/.spec/feature-manual-ledger.md) |\n\n---\n\n## 4. 右侧标注目录\n\n打开原型右侧「原型标注 / 原型目录」,可阅读:\n\n| 目录 | 内容 |\n|---|---|\n| PRD 全文 | [requirements-prd.md](./requirements-prd.md) |\n| 需求角色 | [roles.md](./roles.md) |\n| 用户故事 | [user-stories.md](./user-stories.md) |\n| 关键逻辑 | [key-logic.md](./key-logic.md) 及数据模型、车辆、来源、台账、对账规格 |\n| 流程图 | [flows.md](./flows.md)(Mermaid) |\n| 功能说明 | 列表、新增、台账、导出、每日统计等 |\n\n由 `scripts/build-annotation-source.mjs` 从 `.spec/*.md` 生成 `annotation-source.json`。\n", + "markdownPath": ".spec/requirements-prd.md" + } + ] + } + ] + } + ] + } + ] + } } diff --git a/src/prototypes/oneos-web-h2-station/index.tsx b/src/prototypes/oneos-web-h2-station/index.tsx index 2901929..e54700b 100644 --- a/src/prototypes/oneos-web-h2-station/index.tsx +++ b/src/prototypes/oneos-web-h2-station/index.tsx @@ -4,11 +4,15 @@ */ import '../../common/oneosWebLegacy/legacyGlobals'; import '../../common/h2VehicleLedgerBridge.js'; -import React from 'react'; +import React, { useMemo } from 'react'; import '../vehicle-management/style.css'; import Page2 from './pages/02-加氢记录.jsx'; import { OneosWebLegacyShell } from '../../common/oneosWebLegacy/OneosWebLegacyShell'; -import { AnnotationViewer, type AnnotationSourceDocument } from '@axhub/annotation'; +import { + type AnnotationSourceDocument, + type AnnotationViewerOptions, +} from '@axhub/annotation'; +import { PrototypeAnnotationHost } from '../../common/prototype-annotation-host'; import annotationSourceDocument from './annotation-source.json'; const pages = [ @@ -16,23 +20,28 @@ const pages = [ ]; export default function OneosWebH2Station() { + const annotationOptions = useMemo( + () => ({ + currentPageId: 'h2-record', + toolbarEdge: 'right', + showToolbar: true, + showThemeToggle: true, + showColorFilter: true, + emptyWhenNoData: false, + }), + [], + ); + return ( <> - + ); diff --git a/src/prototypes/oneos-web-h2-station/pages/02-加氢记录.jsx b/src/prototypes/oneos-web-h2-station/pages/02-加氢记录.jsx index 15b162f..2a50635 100644 --- a/src/prototypes/oneos-web-h2-station/pages/02-加氢记录.jsx +++ b/src/prototypes/oneos-web-h2-station/pages/02-加氢记录.jsx @@ -236,8 +236,9 @@ var HR_PAGE_STYLE = ONEOS_ANT_TABLE_GLOBAL_FIX.concat([ '.h2-station-page .hr-daily-stats-nav-btn:focus-visible { outline: none; box-shadow: 0 0 0 2px color-mix(in srgb, #32a06e 35%, transparent); }', '.h2-station-page .hr-daily-stats-nav-btn:disabled { opacity: 0.35; cursor: not-allowed; }', '.h2-station-page .hr-daily-stats-track { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 8px; }', - '.h2-station-page .hr-source-tag { margin: 0; border-radius: 4px; font-size: 11px; line-height: 18px; padding: 0 6px; font-weight: 600; }', - '.h2-station-page .hr-plate-cell { display: flex; flex-direction: column; align-items: flex-start; gap: 4px; }', + '.h2-station-page .hr-source-tag { margin: 0; border-radius: 4px; font-size: 11px; line-height: 18px; padding: 0 6px; font-weight: 600; flex-shrink: 0; white-space: nowrap; }', + '.h2-station-page .hr-plate-cell { display: inline-flex; flex-direction: row; flex-wrap: nowrap; align-items: center; gap: 6px; white-space: nowrap; max-width: 100%; }', + '.h2-station-page .hr-plate-cell .hr-refuel-plate { flex-shrink: 0; }', '@media (max-width: 1200px) { .h2-station-page .hr-daily-stats-track { grid-template-columns: repeat(4, minmax(0, 1fr)); } }', '@media (max-width: 720px) { .h2-station-page .hr-daily-stats-track { grid-template-columns: repeat(2, minmax(0, 1fr)); } }', '@media (prefers-reduced-motion: reduce) { .h2-station-page .hr-daily-stats-section * { transition: none !important; } }', @@ -273,6 +274,38 @@ var HR_PAGE_STYLE = ONEOS_ANT_TABLE_GLOBAL_FIX.concat([ '.h2-station-page--create .hr-batch-create-table .ant-table-tbody > tr > td { padding: 8px 10px !important; vertical-align: middle !important; }', '.h2-station-page--create .hr-batch-create-table .ant-input, .h2-station-page--create .hr-batch-create-table .ant-input-number, .h2-station-page--create .hr-batch-create-table .ant-select .ant-select-selector, .h2-station-page--create .hr-batch-create-table .ant-picker { border-radius: 8px !important; font-size: 13px !important; }', '.h2-station-page--create .hr-batch-create-seq { display: inline-flex; align-items: center; justify-content: center; min-width: 24px; height: 24px; border-radius: 6px; background: #f1f5f9; color: #64748b; font-size: 12px; font-weight: 700; }', + '.hr-import-body { display: grid; gap: 14px; }', + '.hr-import-step { padding: 16px 18px; border-radius: 12px; background: var(--ln-canvas-soft, #f4f4f5); border: 1px solid var(--ln-hairline, #e4e4e7); }', + '.hr-import-step-head { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; font-weight: 600; color: var(--ln-ink, #18181b); font-size: 14px; }', + '.hr-import-step-badge { width: 24px; height: 24px; border-radius: 8px; display: inline-flex; align-items: center; justify-content: center; background: var(--ln-primary, #32a06e); color: #fff; font-size: 12px; font-weight: 700; flex-shrink: 0; }', + '.hr-import-dup-options { display: flex; flex-direction: column; gap: 8px; }', + '.hr-import-dup-option { display: grid; grid-template-columns: auto 1fr; gap: 2px 10px; align-items: start; padding: 10px 12px; border: 1px solid var(--ln-hairline, #e4e4e7); border-radius: 8px; cursor: pointer; margin: 0; background: #fff; }', + '.hr-import-dup-option:has(input:checked) { border-color: color-mix(in srgb, var(--ln-primary, #32a06e) 40%, #e4e4e7); background: color-mix(in srgb, var(--ln-primary, #32a06e) 6%, #fff); }', + '.hr-import-dup-option input { margin-top: 3px; cursor: pointer; accent-color: var(--ln-primary, #32a06e); }', + '.hr-import-dup-label { font-size: 14px; font-weight: 600; color: var(--ln-ink, #18181b); }', + '.hr-import-dup-hint { grid-column: 2; font-size: 12px; color: #64748b; line-height: 1.4; }', + '.hr-import-upload-zone { width: 100%; min-height: 120px; border: 1.5px dashed var(--ln-hairline-strong, #d4d4d8); border-radius: 12px; background: #fff; padding: 24px 16px; text-align: center; cursor: pointer; color: #64748b; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; }', + '.hr-import-upload-zone:hover { border-color: var(--ln-primary, #32a06e); background: color-mix(in srgb, var(--ln-primary, #32a06e) 4%, #fff); color: var(--ln-primary, #32a06e); }', + '.hr-import-upload-zone:focus-visible { outline: none; border-color: var(--ln-primary, #32a06e); box-shadow: 0 0 0 2px color-mix(in srgb, #32a06e 35%, transparent); }', + '.hr-import-upload-zone p { margin: 0; font-size: 14px; font-weight: 600; }', + '.hr-import-upload-zone .hr-import-upload-hint { font-size: 12px; font-weight: 400; color: #94a3b8; }', + '.hr-import-result-summary { margin: 0 0 8px; font-size: 13px; color: #64748b; line-height: 1.5; }', + '.hr-import-result-summary strong { font-weight: 700; }', + '.hr-import-result-summary strong.is-ok { color: var(--ln-primary, #32a06e); }', + '.hr-import-result-summary strong.is-warn { color: #c2410c; }', + '.hr-import-result-summary strong.is-err { color: #dc2626; }', + '.hr-detail-modal .hr-detail-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 20px; margin-bottom: 20px; }', + '.hr-detail-modal .hr-detail-item { display: flex; flex-direction: column; gap: 4px; min-width: 0; }', + '.hr-detail-modal .hr-detail-item__label { font-size: 12px; font-weight: 600; color: #64748b; }', + '.hr-detail-modal .hr-detail-item__value { font-size: 14px; font-weight: 600; color: #0f172a; word-break: break-all; }', + '.hr-detail-modal .hr-detail-item__value.tabular-nums { font-variant-numeric: tabular-nums; font-family: var(--vm-font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace); }', + '.hr-detail-modal .hr-detail-photos { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }', + '.hr-detail-modal .hr-detail-photo { border: 1px solid #e2e8f0; border-radius: 12px; overflow: hidden; background: #f8fafc; }', + '.hr-detail-modal .hr-detail-photo__label { padding: 10px 12px; font-size: 13px; font-weight: 600; color: #475569; border-bottom: 1px solid #e2e8f0; background: #fff; }', + '.hr-detail-modal .hr-detail-photo__body { min-height: 180px; display: flex; align-items: center; justify-content: center; padding: 12px; }', + '.hr-detail-modal .hr-detail-photo__body img { max-width: 100%; max-height: 240px; border-radius: 8px; object-fit: contain; }', + '.hr-detail-modal .hr-detail-photo__empty { color: #94a3b8; font-size: 13px; }', + '@media (max-width: 720px) { .hr-detail-modal .hr-detail-grid, .hr-detail-modal .hr-detail-photos { grid-template-columns: 1fr; } }', '.h2-station-page .hr-manual-toolbar { margin-bottom: 16px; max-width: 480px; }', '.h2-station-page .hr-manual-station-field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }', '.h2-station-page .hr-manual-station-field__label { font-size: 12px; font-weight: 600; color: #64748b; letter-spacing: 0.02em; }', @@ -344,8 +377,9 @@ var HR_DAILY_STATS_MAX_PAGE = Math.ceil(HR_DAILY_STATS_WINDOW / HR_DAILY_STATS_P var HR_PRIMARY_BTN_STYLE = { borderRadius: 8, fontWeight: 600, - background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)', - border: 'none' + background: 'var(--ln-primary, #32a06e)', + borderColor: 'var(--ln-primary, #32a06e)', + color: 'var(--ln-on-primary, #ffffff)' }; var HR_MOCK_FILLERS = ['张三', '李四', '王静', '赵敏', '陈浩']; @@ -403,6 +437,143 @@ function hrDownloadCsv(filename, headers, rows) { URL.revokeObjectURL(a.href); } +var HR_BATCH_IMPORT_HEADERS = ['加氢时间', '加氢站名称', '车牌号', '里程(km)', '氢气单价(元/kg)', '加氢量(kg)']; + +function hrSplitCsvLine(line) { + var out = []; + var cur = ''; + var inQuotes = false; + var i; + for (i = 0; i < line.length; i++) { + var ch = line[i]; + if (inQuotes) { + if (ch === '"') { + if (line[i + 1] === '"') { + cur += '"'; + i += 1; + } else { + inQuotes = false; + } + } else { + cur += ch; + } + } else if (ch === '"') { + inQuotes = true; + } else if (ch === ',') { + out.push(cur); + cur = ''; + } else { + cur += ch; + } + } + out.push(cur); + return out; +} + +function hrParseBatchImportCsv(text) { + var raw = String(text || '').replace(/^\uFEFF/, ''); + var lines = raw.split(/\r?\n/).filter(function (l) { return String(l).trim(); }); + if (lines.length < 2) return { rows: [], error: '未解析到有效数据行,请使用模板填写后导入' }; + var headerCells = hrSplitCsvLine(lines[0]).map(function (h) { return String(h || '').trim(); }); + var indexOf = function (name) { + return headerCells.indexOf(name); + }; + var missing = HR_BATCH_IMPORT_HEADERS.filter(function (h) { return indexOf(h) < 0; }); + if (missing.length) { + return { rows: [], error: '模板表头缺少:' + missing.join('、') }; + } + var rows = []; + var li; + for (li = 1; li < lines.length; li++) { + var cells = hrSplitCsvLine(lines[li]); + rows.push({ + lineNo: li + 1, + hydrogenTime: String(cells[indexOf('加氢时间')] || '').trim(), + stationName: String(cells[indexOf('加氢站名称')] || '').trim(), + plateNo: String(cells[indexOf('车牌号')] || '').trim(), + mileageKm: String(cells[indexOf('里程(km)')] || '').trim(), + unitPrice: String(cells[indexOf('氢气单价(元/kg)')] || '').trim(), + hydrogenKg: String(cells[indexOf('加氢量(kg)')] || '').trim() + }); + } + return { rows: rows, error: null }; +} + +function hrResolveImportStation(stationName, stationOptions) { + var name = String(stationName || '').trim(); + if (!name) return null; + return (stationOptions || []).find(function (o) { + return o.value === name || o.label === name || o.stationCode === name; + }) || null; +} + +function hrBuildBatchRowFromImport(raw, stationOptions) { + var station = hrResolveImportStation(raw.stationName, stationOptions); + var row = hrEmptyBatchRow({ + stationName: station ? station.value : (raw.stationName || ''), + stationCode: station ? (station.stationCode || '') : '' + }); + row.hydrogenTime = raw.hydrogenTime || ''; + row.plateNo = raw.plateNo || ''; + row.mileageKm = raw.mileageKm || ''; + row.unitPrice = raw.unitPrice || ''; + row.hydrogenKg = raw.hydrogenKg || ''; + row.totalAmount = hrRecalcTotalAmount(row.unitPrice, row.hydrogenKg); + return row; +} + +function hrValidateBatchImportRaw(raw, stationOptions) { + if (!(raw.hydrogenTime || '').trim()) return '请填写加氢时间'; + if (!(raw.stationName || '').trim()) return '请选择加氢站'; + if (!hrResolveImportStation(raw.stationName, stationOptions)) { + return '加氢站不在当前登录可见范围内:' + raw.stationName; + } + if (!(raw.plateNo || '').trim()) return '请填写车牌号'; + var mileageRaw = (raw.mileageKm || '').trim(); + if (mileageRaw) { + var mileage = parseFloat(mileageRaw); + if (isNaN(mileage) || mileage < 0) return '里程须为不小于 0 的数字'; + } + var price = parseFloat(raw.unitPrice); + if (isNaN(price) || price < 0) return '请填写有效的氢气单价'; + var kg = parseFloat(raw.hydrogenKg); + if (isNaN(kg) || kg <= 0) return '请填写有效的加氢量(kg)'; + return null; +} + +var HR_BATCH_DUP_OPTIONS = [ + { value: 'skip', label: '跳过重复', hint: '同「加氢站 + 车牌号 + 加氢时间」已存在则跳过' }, + { value: 'overwrite', label: '覆盖重复', hint: '用导入行覆盖已有记录(录入表或系统中)' }, + { value: 'error', label: '报错', hint: '重复行写入错误日志,不导入' } +]; + +function hrNormalizeBatchPlateKey(plateNo) { + var plate = String(plateNo || '').trim().toUpperCase(); + if (plate && !/F$/u.test(plate)) plate += 'F'; + return plate; +} + +function hrBatchDupKey(stationName, plateNo, hydrogenTime) { + var station = String(stationName || '').trim(); + var plate = hrNormalizeBatchPlateKey(plateNo); + var time = String(hydrogenTime || '').trim(); + if (!station || !plate || !time) return ''; + return station + '|' + plate + '|' + time; +} + +function hrFailRowFromRaw(raw, reason) { + return { + lineNo: raw.lineNo, + hydrogenTime: raw.hydrogenTime, + stationName: raw.stationName, + plateNo: raw.plateNo, + mileageKm: raw.mileageKm, + unitPrice: raw.unitPrice, + hydrogenKg: raw.hydrogenKg, + reason: reason + }; +} + function hrExtractDateKey(timeStr) { if (!timeStr) return ''; var s = String(timeStr).trim(); @@ -580,7 +751,7 @@ function hrBatchRowToPayload(row) { var unitPrice = Math.round(parseFloat(row.unitPrice) * 100) / 100; var hydrogenKg = Math.round(parseFloat(row.hydrogenKg) * 100) / 100; var totalAmount = Math.round(parseFloat(row.totalAmount) * 100) / 100; - return { + var payload = { hydrogenTime: (row.hydrogenTime || '').trim(), stationName: (row.stationName || '').trim(), stationCode: (row.stationCode || '').trim(), @@ -590,6 +761,8 @@ function hrBatchRowToPayload(row) { hydrogenKg: hydrogenKg, totalAmount: totalAmount }; + if (row._existingId) payload.id = row._existingId; + return payload; } function hrUpsertBridgeRow(payload) { @@ -638,6 +811,39 @@ function hrIsLingniuSource(record) { return hrRecordSourceLabel(record) === '羚牛上传'; } +function hrIsLingniuVehicle(plateNo) { + var bridge = hrGetVehicleLedgerBridge(); + if (bridge && typeof bridge.isLingniuVehicle === 'function') { + return Boolean(bridge.isLingniuVehicle(plateNo)); + } + var key = String(plateNo || '').trim().toUpperCase().replace(/F$/u, ''); + if (!key) return false; + /* fallback:与 Bridge 种子一致的常见演示车牌 */ + var known = { + '浙A55666': 1, '浙A77888': 1, '浙A99001': 1, '浙A12345': 1, '浙A67890': 1, '浙A88888': 1, '浙A03561': 1, + '浙B23456': 1, '浙B99999': 1, '浙B58888': 1, '沪A88888': 1, '沪BDB9161': 1, '苏E33333': 1 + }; + return Boolean(known[key]); +} + +function hrVehicleKindLabel(plateNo) { + var key = String(plateNo || '').trim(); + if (!key) return ''; + return hrIsLingniuVehicle(key) ? '羚牛车辆' : '非羚牛车辆'; +} + +/** 批量录入场景:资产库未命中标「外部车辆」 */ +function hrBatchVehicleKindLabel(plateNo) { + var key = String(plateNo || '').trim(); + if (!key) return ''; + return hrIsLingniuVehicle(key) ? '羚牛车辆' : '外部车辆'; +} + +/** 非羚牛车辆:核对/对账类字段在列表中显示为空 */ +function hrShowVerifyReconcileFields(record) { + return hrIsLingniuVehicle(record && record.plateNo); +} + function hrRemoveBridgeRow(id) { var bridge = hrGetVehicleLedgerBridge(); if (!bridge || typeof bridge.removeRow !== 'function') return false; @@ -652,8 +858,9 @@ function hrIsVerified(record) { return record && (record.verifyStatus === 'verified' || Boolean(record.verifiedAt)); } -/** 已核对或已对账:站端不可改删 */ +/** 已核对或已对账:站端不可改删(仅羚牛车辆适用核对/对账锁定) */ function hrIsLocked(record) { + if (!hrShowVerifyReconcileFields(record)) return false; return hrIsVerified(record) || hrIsReconciled(record); } @@ -667,6 +874,54 @@ function hrFormatPlateDisplay(plateNo) { return String(plateNo || '').replace(/F$/u, '') || '—'; } +/** 种子无实拍时生成演示占位图(含水印文案,原型用) */ +function hrBuildDemoPhotoSync(kind, record) { + if (typeof document === 'undefined') return ''; + var canvas = document.createElement('canvas'); + canvas.width = 720; + canvas.height = 405; + var ctx = canvas.getContext('2d'); + if (!ctx) return ''; + var gradient = ctx.createLinearGradient(0, 0, 720, 405); + if (kind === 'plate') { + gradient.addColorStop(0, '#1e293b'); + gradient.addColorStop(1, '#334155'); + } else { + gradient.addColorStop(0, '#064e3b'); + gradient.addColorStop(1, '#0f766e'); + } + ctx.fillStyle = gradient; + ctx.fillRect(0, 0, 720, 405); + ctx.fillStyle = 'rgba(255,255,255,0.92)'; + ctx.font = '700 36px ui-sans-serif, system-ui, "PingFang SC", sans-serif'; + ctx.textAlign = 'center'; + var label = kind === 'plate' + ? ('车牌 ' + hrFormatPlateDisplay(record && record.plateNo)) + : '加氢机面板'; + ctx.fillText(label, 360, 180); + ctx.font = '500 18px ui-sans-serif, system-ui, "PingFang SC", sans-serif'; + ctx.fillStyle = 'rgba(255,255,255,0.72)'; + ctx.fillText('演示原图(含水印)', 360, 222); + var timeText = String((record && record.hydrogenTime) || '').slice(0, 19) || '—'; + var locText = String((record && record.stationName) || '加氢站'); + ctx.textAlign = 'right'; + ctx.font = '600 16px ui-sans-serif, system-ui, "PingFang SC", sans-serif'; + ctx.fillStyle = 'rgba(0,0,0,0.45)'; + ctx.fillRect(720 - 320, 405 - 72, 300, 56); + ctx.fillStyle = '#fff'; + ctx.fillText(timeText, 700, 405 - 42); + ctx.fillText(locText.length > 18 ? locText.slice(0, 18) + '…' : locText, 700, 405 - 18); + return canvas.toDataURL('image/jpeg', 0.88); +} + +function hrResolveRecordPhotos(record) { + record = record || {}; + return { + plateUrl: record.platePhotoUrl || hrBuildDemoPhotoSync('plate', record), + panelUrl: record.panelPhotoUrl || hrBuildDemoPhotoSync('panel', record) + }; +} + function hrRecalcTotalAmount(unitPrice, hydrogenKg) { var p = parseFloat(unitPrice); var k = parseFloat(hydrogenKg); @@ -705,6 +960,28 @@ function hrTodayDateKey(dayjsLib) { return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()); } +function hrYesterdayDateKey(dayjsLib) { + if (dayjsLib) return dayjsLib().subtract(1, 'day').format('YYYY-MM-DD'); + var d = new Date(); + d.setDate(d.getDate() - 1); + var pad = function (n) { return n < 10 ? '0' + n : String(n); }; + return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()); +} + +/** + * 业务门禁:前一日手工台账未上传 → 今日不可新增加氢记录。 + * 若昨日早于本站首笔加氢日(或无首笔),不要求昨日台账。 + */ +function hrIsManualLedgerGateBlocked(stationIdOrName, dayjsLib) { + var station = String(stationIdOrName || '').trim(); + if (!station) return false; + var todayKey = hrTodayDateKey(dayjsLib); + var yesterdayKey = hrYesterdayDateKey(dayjsLib); + var firstKey = hrGetFirstHydrogenDateKey(station); + if (!hrCellNeedsLedger(yesterdayKey, todayKey, firstKey)) return false; + return !hrHasManualLedgerForDate(station, yesterdayKey); +} + function hrHasManualLedgerForDate(stationIdOrName, ledgerDate) { var bridge = hrGetVehicleLedgerBridge(); if (!bridge || typeof bridge.hasManualLedgerForDate !== 'function') return false; @@ -979,6 +1256,22 @@ const Component = function () { var changeLogModal = changeLogModalState[0]; var setChangeLogModal = changeLogModalState[1]; + var detailModalState = useState({ open: false, record: null, plateUrl: '', panelUrl: '' }); + var detailModal = detailModalState[0]; + var setDetailModal = detailModalState[1]; + + var batchImportModalOpenState = useState(false); + var batchImportModalOpen = batchImportModalOpenState[0]; + var setBatchImportModalOpen = batchImportModalOpenState[1]; + + var batchImportDupStrategyState = useState('skip'); + var batchImportDupStrategy = batchImportDupStrategyState[0]; + var setBatchImportDupStrategy = batchImportDupStrategyState[1]; + + var batchImportResultState = useState(null); + var batchImportResult = batchImportResultState[0]; + var setBatchImportResult = batchImportResultState[1]; + var plateOptions = useMemo(function () { var map = {}; listData.forEach(function (r) { @@ -1021,11 +1314,17 @@ const Component = function () { var manualTodayKey = hrTodayDateKey(dayjs); var activeManualStation = manualStation || (stationOptions[0] && stationOptions[0].value) || ''; + var gateStationName = draftFilters.stationName || activeManualStation || ''; var manualTodayUploaded = useMemo(function () { return activeManualStation ? hrHasManualLedgerForDate(activeManualStation, manualTodayKey) : false; }, [activeManualStation, manualTodayKey, manualTick]); + var manualGateBlocked = useMemo(function () { + return gateStationName + ? hrIsManualLedgerGateBlocked(gateStationName, dayjs) + : false; + }, [gateStationName, dayjs, manualTick]); var manualAllEntries = useMemo(function () { return hrListManualLedgers(activeManualStation); @@ -1455,19 +1754,34 @@ const Component = function () { resetFormView(); }, [form, batchRows, formMode, formSubmitting, resetFormView]); + var goToManualLedgerForGap = useCallback(function (stationName) { + var yKey = hrYesterdayDateKey(dayjs); + var parts = String(yKey).split('-'); + if (stationName) setManualStation(stationName); + setManualSelectedDate(yKey); + if (parts.length >= 2) { + setManualCursorYm({ y: Number(parts[0]), m: Number(parts[1]) }); + } + setListMainTab('manual'); + setSubView('list'); + }, [dayjs]); + var openCreate = useCallback(function () { var firstStation = stationOptions[0] || {}; var gateStation = draftFilters.stationName || firstStation.value || ''; - if (gateStation && !hrHasManualLedgerForDate(gateStation, hrTodayDateKey(dayjs))) { - message.warning('请先在「手工台账」上传今日台账后再新增(站点:' + gateStation + ')'); - setListMainTab('manual'); - setManualStation(gateStation); + if (gateStation && hrIsManualLedgerGateBlocked(gateStation, dayjs)) { + message.warning('手工台账有缺失,作为对账重要依据,请先上传手工台账'); + goToManualLedgerForGap(gateStation); return; } var defaults = { - stationName: firstStation.value || '', - stationCode: firstStation.stationCode || '' + stationName: '', + stationCode: '' }; + if (stationOptions.length === 1) { + defaults.stationName = firstStation.value || ''; + defaults.stationCode = firstStation.stationCode || ''; + } var empty = hrEmptyForm(); empty.stationName = defaults.stationName; empty.stationCode = defaults.stationCode; @@ -1477,7 +1791,8 @@ const Component = function () { setFormMode('create'); setEditingId(null); setSubView('form'); - }, [stationOptions, dayjs, draftFilters.stationName, message]); + setBatchImportResult(null); + }, [stationOptions, dayjs, draftFilters.stationName, message, goToManualLedgerForGap]); var updateBatchRow = useCallback(function (rowId, patch) { setBatchRows(function (prev) { @@ -1489,8 +1804,155 @@ const Component = function () { }, []); var addBatchRow = useCallback(function () { - setBatchRows(function (prev) { return prev.concat([hrEmptyBatchRow()]); }); - }, []); + var defaults = { stationName: '', stationCode: '' }; + if (stationOptions.length === 1) { + defaults.stationName = stationOptions[0].value || ''; + defaults.stationCode = stationOptions[0].stationCode || ''; + } + setBatchRows(function (prev) { return prev.concat([hrEmptyBatchRow(defaults)]); }); + }, [stationOptions]); + + var downloadBatchImportTemplate = useCallback(function () { + var sampleStation = stationOptions[0] || {}; + var sample = [[ + dayjs ? dayjs().format('YYYY-MM-DD HH:mm:ss') : '2026-07-18 10:30:00', + sampleStation.value || '中国石油中油高新能源牙谷加油加氢站', + '浙A12345', + '128560', + '42.50', + '12.50' + ]]; + hrDownloadCsv('加氢记录批量导入模板.csv', HR_BATCH_IMPORT_HEADERS, sample); + message.success('已下载导入模板'); + }, [stationOptions, dayjs, message]); + + var handleBatchImportFile = useCallback(function (file) { + if (!file) return false; + var name = String(file.name || '').toLowerCase(); + if (name.endsWith('.xls') || name.endsWith('.xlsx')) { + message.warning('原型环境请使用 CSV 模板导入;联调后将支持 Excel'); + return false; + } + if (!name.endsWith('.csv')) { + message.error('请上传 CSV 文件'); + return false; + } + var strategy = batchImportDupStrategy || 'skip'; + var reader = new FileReader(); + reader.onload = function (ev) { + var text = (ev && ev.target && ev.target.result) || ''; + var parsed = hrParseBatchImportCsv(text); + if (parsed.error) { + message.error(parsed.error); + return; + } + var successRows = []; + var failRows = []; + var skippedCount = 0; + var overwrittenCount = 0; + var keyToBatchIndex = {}; + var workingBatch = batchRows.filter(function (r) { return !hrIsBatchRowEmpty(r); }).map(function (r) { + return Object.assign({}, r); + }); + workingBatch.forEach(function (r, idx) { + var k = hrBatchDupKey(r.stationName, r.plateNo, r.hydrogenTime); + if (k) keyToBatchIndex[k] = idx; + }); + + parsed.rows.forEach(function (raw) { + var err = hrValidateBatchImportRaw(raw, stationOptions); + if (err) { + failRows.push(hrFailRowFromRaw(raw, err)); + return; + } + var built = hrBuildBatchRowFromImport(raw, stationOptions); + var key = hrBatchDupKey(built.stationName, built.plateNo, built.hydrogenTime); + var batchIdx = keyToBatchIndex[key]; + var storeDup = hrFindDuplicateRow({ + stationName: built.stationName, + plateNo: built.plateNo, + hydrogenTime: built.hydrogenTime + }); + + if (batchIdx != null) { + if (strategy === 'skip') { + skippedCount += 1; + return; + } + if (strategy === 'error') { + failRows.push(hrFailRowFromRaw(raw, '重复记录:与录入表中已有行重复(加氢站+车牌+加氢时间)')); + return; + } + var prevRow = workingBatch[batchIdx]; + built._rowId = prevRow._rowId; + built._existingId = prevRow._existingId || null; + workingBatch[batchIdx] = built; + overwrittenCount += 1; + return; + } + + if (storeDup) { + if (strategy === 'skip') { + skippedCount += 1; + return; + } + if (strategy === 'error') { + failRows.push(hrFailRowFromRaw(raw, '重复记录:系统中已存在相同加氢站、车牌号与加氢时间')); + return; + } + built._existingId = storeDup.id || storeDup.key || null; + workingBatch.push(built); + keyToBatchIndex[key] = workingBatch.length - 1; + overwrittenCount += 1; + return; + } + + workingBatch.push(built); + keyToBatchIndex[key] = workingBatch.length - 1; + successRows.push(built); + }); + + var nextRows = workingBatch.length ? workingBatch : [hrEmptyBatchRow( + stationOptions.length === 1 + ? { stationName: stationOptions[0].value || '', stationCode: stationOptions[0].stationCode || '' } + : {} + )]; + setBatchRows(nextRows); + setBatchImportResult({ + successCount: successRows.length, + failCount: failRows.length, + failRows: failRows, + skippedCount: skippedCount, + overwrittenCount: overwrittenCount + }); + }; + reader.onerror = function () { + message.error('文件读取失败'); + }; + reader.readAsText(file, 'UTF-8'); + return false; + }, [stationOptions, message, batchImportDupStrategy, batchRows]); + + var downloadBatchImportFails = useCallback(function () { + if (!batchImportResult || !(batchImportResult.failRows || []).length) { + message.warning('没有失败记录可下载'); + return; + } + var headers = HR_BATCH_IMPORT_HEADERS.concat(['失败原因']); + var rows = batchImportResult.failRows.map(function (r) { + return [ + r.hydrogenTime || '', + r.stationName || '', + r.plateNo || '', + r.mileageKm || '', + r.unitPrice || '', + r.hydrogenKg || '', + r.reason || '' + ]; + }); + hrDownloadCsv('加氢记录导入失败记录.csv', headers, rows); + message.success('已下载失败记录'); + }, [batchImportResult, message]); var removeBatchRow = useCallback(function (rowId) { setBatchRows(function (prev) { @@ -1519,6 +1981,17 @@ const Component = function () { setSubView('form'); }, []); + var openDetail = useCallback(function (record) { + if (!record) return; + var photos = hrResolveRecordPhotos(record); + setDetailModal({ + open: true, + record: record, + plateUrl: photos.plateUrl, + panelUrl: photos.panelUrl + }); + }, []); + var validateForm = useCallback(function () { if (!(form.hydrogenTime || '').trim()) { message.warning('请填写加氢时间'); @@ -1605,18 +2078,15 @@ const Component = function () { message.warning('请至少填写一行加氢记录'); return; } - var todayKey = hrTodayDateKey(dayjs); var missingStations = {}; filledRows.forEach(function (r) { var sn = String(r.stationName || '').trim(); - if (sn && !hrHasManualLedgerForDate(sn, todayKey)) missingStations[sn] = true; + if (sn && hrIsManualLedgerGateBlocked(sn, dayjs)) missingStations[sn] = true; }); var missingList = Object.keys(missingStations); if (missingList.length) { - message.warning('以下站点今日尚未上传手工台账,请先上传后再新增:' + missingList.join('、')); - setListMainTab('manual'); - setManualStation(missingList[0]); - setSubView('list'); + message.warning('手工台账有缺失,作为对账重要依据,请先上传手工台账'); + goToManualLedgerForGap(missingList[0]); return; } var i; @@ -1636,7 +2106,7 @@ const Component = function () { return; } var payload = hrBatchRowToPayload(filledRows[i]); - var dup = hrFindDuplicateRow(payload); + var dup = hrFindDuplicateRow(payload, payload.id); if (dup) { message.error('第 ' + rowIndex + ' 行:已存在相同加氢站、车牌号与加氢时间的记录'); return; @@ -1688,7 +2158,7 @@ const Component = function () { return; } doBatchSave(); - }, [batchRows, formSubmitting, resetFormView, dayjs]); + }, [batchRows, formSubmitting, resetFormView, dayjs, goToManualLedgerForGap]); var handleExport = useCallback(function () { @@ -1696,24 +2166,26 @@ const Component = function () { message.warning('当前筛选条件下暂无数据可导出'); return; } - var headers = ['序号', '加氢时间', '加氢站名称', '车牌号', '来源', '里程(km)', '氢气单价(元/kg)', '加氢量(kg)', '加氢总额(元)', '核对状态', '核对时间', '核对人', '对账状态', '对账时间', '对账人']; + var headers = ['序号', '加氢时间', '加氢站名称', '车牌号', '车辆来源', '数据来源', '里程(km)', '氢气单价(元/kg)', '加氢量(kg)', '加氢总额(元)', '核对状态', '核对时间', '核对人', '对账状态', '对账时间', '对账人']; var rows = filteredList.map(function (r, index) { + var showVR = hrShowVerifyReconcileFields(r); return [ index + 1, r.hydrogenTime || '', r.stationName || '', r.plateNo || '', + hrVehicleKindLabel(r.plateNo), hrRecordSourceLabel(r), r.mileageKm != null && r.mileageKm !== '' ? String(Math.round(Number(r.mileageKm))) : '', hrFormatYuan(r.unitPrice != null ? r.unitPrice : r.costUnitPrice), hrFormatKg(r.hydrogenKg), hrFormatYuan(r.totalAmount != null ? r.totalAmount : r.customerAmount), - hrIsVerified(r) ? '已核对' : '未核对', - hrFormatReconcileTime(r.verifiedAt), - r.verifiedByName || '', - hrIsReconciled(r) ? '已对账' : '未对账', - hrFormatReconcileTime(r.reconcileTime), - r.reconciledByName || '' + showVR ? (hrIsVerified(r) ? '已核对' : '未核对') : '', + showVR ? hrFormatReconcileTime(r.verifiedAt) : '', + showVR ? (r.verifiedByName || '') : '', + showVR ? (hrIsReconciled(r) ? '已对账' : '未对账') : '', + showVR ? hrFormatReconcileTime(r.reconcileTime) : '', + showVR ? (r.reconciledByName || '') : '' ]; }); hrDownloadCsv('加氢记录_' + new Date().getTime() + '.csv', headers, rows); @@ -1743,14 +2215,21 @@ const Component = function () { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', - width: 128, + width: 280, render: function (v, record) { - var lingniu = hrIsLingniuSource(record); + var kind = hrVehicleKindLabel(v); + var sourceLingniu = hrIsLingniuSource(record); return React.createElement('div', { className: 'hr-plate-cell' }, React.createElement('span', { className: 'hr-refuel-plate tabular-nums' }, hrFormatPlateDisplay(v)), + kind + ? React.createElement(Tag, { + className: 'hr-source-tag', + color: hrIsLingniuVehicle(v) ? 'green' : 'orange' + }, kind) + : null, React.createElement(Tag, { className: 'hr-source-tag', - color: lingniu ? 'blue' : 'green' + color: sourceLingniu ? 'blue' : 'cyan' }, hrRecordSourceLabel(record)) ); } @@ -1799,6 +2278,9 @@ const Component = function () { key: 'verifyStatus', width: 100, render: function (_, record) { + if (!hrShowVerifyReconcileFields(record)) { + return React.createElement('span', { className: 'hr-refuel-time', style: { color: '#cbd5e1' } }, '—'); + } var ok = hrIsVerified(record); return React.createElement(Tag, { color: ok ? 'processing' : 'default', @@ -1811,7 +2293,10 @@ const Component = function () { dataIndex: 'verifiedAt', key: 'verifiedAt', width: 148, - render: function (v) { + render: function (v, record) { + if (!hrShowVerifyReconcileFields(record)) { + return React.createElement('span', { className: 'hr-refuel-time', style: { color: '#cbd5e1' } }, '—'); + } return React.createElement('span', { className: 'hr-refuel-time tabular-nums' }, hrFormatReconcileTime(v)); } }, @@ -1820,7 +2305,10 @@ const Component = function () { dataIndex: 'verifiedByName', key: 'verifiedByName', width: 88, - render: function (v) { + render: function (v, record) { + if (!hrShowVerifyReconcileFields(record)) { + return React.createElement('span', { style: { color: '#cbd5e1' } }, '—'); + } return v || '—'; } }, @@ -1830,6 +2318,9 @@ const Component = function () { key: 'reconcileStatus', width: 100, render: function (_, record) { + if (!hrShowVerifyReconcileFields(record)) { + return React.createElement('span', { className: 'hr-refuel-time', style: { color: '#cbd5e1' } }, '—'); + } var done = hrIsReconciled(record); return React.createElement(Tag, { color: done ? 'success' : 'default', @@ -1842,7 +2333,10 @@ const Component = function () { dataIndex: 'reconcileTime', key: 'reconcileTime', width: 148, - render: function (v) { + render: function (v, record) { + if (!hrShowVerifyReconcileFields(record)) { + return React.createElement('span', { className: 'hr-refuel-time', style: { color: '#cbd5e1' } }, '—'); + } return React.createElement('span', { className: 'hr-refuel-time tabular-nums' }, hrFormatReconcileTime(v)); } }, @@ -1851,18 +2345,24 @@ const Component = function () { dataIndex: 'reconciledByName', key: 'reconciledByName', width: 88, - render: function (v) { + render: function (v, record) { + if (!hrShowVerifyReconcileFields(record)) { + return React.createElement('span', { style: { color: '#cbd5e1' } }, '—'); + } return v || '—'; } }, { title: '操作', key: 'actions', - width: 148, + width: 168, fixed: 'right', render: function (_, record) { var locked = hrIsLocked(record); return React.createElement(OperationActions, { + view: { + onClick: function () { openDetail(record); } + }, edit: { label: '编辑', disabled: locked, @@ -1900,7 +2400,7 @@ const Component = function () { } } ]; - }, [openEdit, message]); + }, [openEdit, openDetail, message]); var formTimeValue = null; @@ -1941,7 +2441,7 @@ const Component = function () { return React.createElement('div', { className: 'h2-create-form-grid' }, rows); }; - var formPageTitle = formMode === 'edit' ? '编辑加氢记录' : '批量新增加氢记录'; + var formPageTitle = formMode === 'edit' ? '编辑加氢记录' : ''; var batchFilledCount = useMemo(function () { var n = 0; @@ -1953,6 +2453,7 @@ const Component = function () { }, [batchRows]); var batchCreateColumns = useMemo(function () { + var multiStation = stationOptions.length > 1; return [ { title: '序号', @@ -1964,7 +2465,7 @@ const Component = function () { }, { title: React.createElement('span', null, React.createElement('span', { style: { color: '#ef4444', marginRight: 4 } }, '*'), '加氢时间'), - width: 200, + width: 210, render: function (_, record) { var timeVal = null; if (dayjs && record.hydrogenTime) { @@ -1973,11 +2474,12 @@ const Component = function () { } return dayjs && DatePicker ? React.createElement(DatePicker, { - showTime: true, + showTime: { format: 'HH:mm:ss' }, style: { width: '100%' }, format: 'YYYY-MM-DD HH:mm:ss', - placeholder: '加氢时间', + placeholder: '加氢时间(精确至秒)', value: timeVal, + 'data-annotation-id': 'hr-batch-time', onChange: function (v) { updateBatchRow(record._rowId, { hydrogenTime: v && v.isValid && v.isValid() ? v.format('YYYY-MM-DD HH:mm:ss') : '' @@ -1987,21 +2489,25 @@ const Component = function () { : React.createElement(Input, { placeholder: 'YYYY-MM-DD HH:mm:ss', value: record.hydrogenTime || '', + 'data-annotation-id': 'hr-batch-time', onChange: function (e) { updateBatchRow(record._rowId, { hydrogenTime: e.target.value }); } }); } }, { title: React.createElement('span', null, React.createElement('span', { style: { color: '#ef4444', marginRight: 4 } }, '*'), '加氢站'), - width: 160, + width: 180, render: function (_, record) { return React.createElement(Select, { showSearch: true, - placeholder: '加氢站', + allowClear: multiStation, + placeholder: multiStation ? '请选择' : '加氢站', optionFilterProp: 'label', value: record.stationName || undefined, options: stationOptions, + disabled: !multiStation && stationOptions.length === 1, style: { width: '100%' }, + 'data-annotation-id': 'hr-batch-station', onChange: function (v, opt) { var code = (opt && opt.stationCode) || ''; if (!code && v) { @@ -2019,14 +2525,29 @@ const Component = function () { }, { title: React.createElement('span', null, React.createElement('span', { style: { color: '#ef4444', marginRight: 4 } }, '*'), '车牌号'), - width: 130, + width: 200, render: function (_, record) { - return React.createElement(Input, { - placeholder: '车牌号', - value: record.plateNo || '', - maxLength: 16, - onChange: function (e) { updateBatchRow(record._rowId, { plateNo: e.target.value }); } - }); + var kind = hrBatchVehicleKindLabel(record.plateNo); + return React.createElement('div', { + className: 'hr-batch-plate-cell', + 'data-annotation-id': 'hr-batch-plate', + style: { display: 'flex', alignItems: 'center', gap: 6 } + }, + React.createElement(Input, { + placeholder: '车牌号', + value: record.plateNo || '', + maxLength: 16, + style: { flex: 1, minWidth: 0 }, + onChange: function (e) { updateBatchRow(record._rowId, { plateNo: e.target.value }); } + }), + kind + ? React.createElement(Tag, { + className: 'hr-source-tag', + color: kind === '羚牛车辆' ? 'green' : 'orange', + style: { margin: 0, flexShrink: 0 } + }, kind) + : null + ); } }, { @@ -2087,7 +2608,7 @@ const Component = function () { } }, { - title: React.createElement('span', null, React.createElement('span', { style: { color: '#ef4444', marginRight: 4 } }, '*'), '加氢总额(元)'), + title: '加氢总额(元)', width: 120, render: function (_, record) { return React.createElement(InputNumber, { @@ -2095,9 +2616,10 @@ const Component = function () { min: 0, step: 0.01, precision: 2, - placeholder: '总额', - value: record.totalAmount === '' ? null : parseFloat(record.totalAmount), - onChange: function (v) { updateBatchRow(record._rowId, { totalAmount: v == null ? '' : String(v) }); } + placeholder: '自动计算', + disabled: true, + 'data-annotation-id': 'hr-batch-total', + value: record.totalAmount === '' ? null : parseFloat(record.totalAmount) }); } }, @@ -2116,7 +2638,7 @@ const Component = function () { } } ]; - }, [batchRows.length, dayjs, DatePicker, Input, InputNumber, Select, stationOptions, updateBatchRow, removeBatchRow]); + }, [batchRows.length, dayjs, DatePicker, Input, InputNumber, Select, Tag, stationOptions, updateBatchRow, removeBatchRow]); var emptyNode = React.createElement('div', { style: { padding: '40px 0', textAlign: 'center' } }, HR_ICONS.empty, @@ -2137,7 +2659,7 @@ const Component = function () { onClick: function () { setListMainTab('records'); } }, '加氢记录'), React.createElement(Badge, { - dot: !manualTodayUploaded, + dot: manualGateBlocked, offset: [-2, 2] }, React.createElement('button', { @@ -2159,8 +2681,7 @@ const Component = function () { className: 'vm-btn vm-btn-back', onClick: handleFormPageBack, disabled: formSubmitting, - }, '返回'), - React.createElement('h1', { className: 'h2-create-topbar-title' }, formPageTitle) + }, '返回') ), React.createElement(Card, { className: 'h2-create-card', @@ -2172,8 +2693,21 @@ const Component = function () { bordered: false }, React.createElement('div', { className: 'hr-batch-create-toolbar' }, - React.createElement('span', { className: 'hr-batch-create-toolbar__count' }, - '共 ' + batchRows.length + ' 行 · 已填写 ' + batchFilledCount + ' 条' + React.createElement('div', { + style: { display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' } + }, + React.createElement(Button, { + style: { borderRadius: 8, fontWeight: 600 }, + onClick: function () { + setBatchImportResult(null); + setBatchImportModalOpen(true); + }, + 'aria-label': '批量导入加氢记录', + 'data-annotation-id': 'hr-batch-import' + }, '批量导入'), + React.createElement('span', { className: 'hr-batch-create-toolbar__count' }, + '共 ' + batchRows.length + ' 行 · 已填写 ' + batchFilledCount + ' 条' + ) ), React.createElement(Button, { type: 'dashed', @@ -2191,8 +2725,8 @@ const Component = function () { columns: batchCreateColumns, dataSource: batchRows, pagination: false, - locale: { emptyText: '请点击「添加一行」开始录入' }, - scroll: { x: 1100 } + locale: { emptyText: '请点击「添加一行」开始录入,或使用「批量导入」' }, + scroll: { x: 1280 } }) ) ), @@ -2406,7 +2940,7 @@ const Component = function () { title: React.createElement('span', { className: 'hr-card-title-with-help' }, '手工台账上传', React.createElement(Tooltip, { - title: '每日需上传本站纸质手工台账照片。日历可查看已传/未传日期,点选过往日期可补传;未上传今日台账时,不可新增加氢记录。', + title: '每日需上传本站纸质手工台账照片。日历可查看已传/未传日期,点选过往日期可补传;前一日台账未上传时,今日不可新增加氢记录。', placement: 'top', overlayStyle: { maxWidth: 360 } }, @@ -2705,7 +3239,7 @@ const Component = function () { ) ) ) : React.createElement(React.Fragment, null, - !manualTodayUploaded && activeManualStation ? React.createElement('div', { + manualGateBlocked && gateStationName ? React.createElement('div', { style: { marginBottom: 12, padding: '10px 14px', @@ -2717,10 +3251,16 @@ const Component = function () { fontWeight: 600, cursor: 'pointer' }, - onClick: function () { setListMainTab('manual'); }, + onClick: function () { goToManualLedgerForGap(gateStationName); }, role: 'button', - tabIndex: 0 - }, '今日尚未上传手工台账(' + activeManualStation + '),点击前往上传;未上传不可新增电子记录') : null, + tabIndex: 0, + onKeyDown: function (e) { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + goToManualLedgerForGap(gateStationName); + } + } + }, '手工台账有缺失,作为对账重要依据,请先上传手工台账') : null, React.createElement(Card, { className: 'lc-filter-card', title: '筛选条件', bordered: false }, React.createElement('div', { className: 'lc-filter-body vm-filter-body' }, React.createElement('div', { className: 'lc-filter-grid vm-filter-grid--primary', 'data-filter-tier': 'primary' }, @@ -2942,6 +3482,115 @@ const Component = function () { ) ) : null, + React.createElement(Modal, { + title: '加氢记录详情', + open: detailModal.open, + centered: true, + width: 760, + className: 'hr-detail-modal', + destroyOnClose: true, + footer: [ + React.createElement(Button, { + key: 'close', + onClick: function () { setDetailModal({ open: false, record: null, plateUrl: '', panelUrl: '' }); } + }, '关闭') + ], + onCancel: function () { setDetailModal({ open: false, record: null, plateUrl: '', panelUrl: '' }); } + }, + detailModal.record + ? React.createElement(React.Fragment, null, + React.createElement('div', { className: 'hr-detail-grid' }, + React.createElement('div', { className: 'hr-detail-item' }, + React.createElement('span', { className: 'hr-detail-item__label' }, '加氢时间'), + React.createElement('span', { className: 'hr-detail-item__value tabular-nums' }, detailModal.record.hydrogenTime || '—') + ), + React.createElement('div', { className: 'hr-detail-item' }, + React.createElement('span', { className: 'hr-detail-item__label' }, '加氢站名称'), + React.createElement('span', { className: 'hr-detail-item__value' }, detailModal.record.stationName || '—') + ), + React.createElement('div', { className: 'hr-detail-item' }, + React.createElement('span', { className: 'hr-detail-item__label' }, '车牌号'), + React.createElement('span', { className: 'hr-detail-item__value tabular-nums' }, + hrFormatPlateDisplay(detailModal.record.plateNo), + hrVehicleKindLabel(detailModal.record.plateNo) + ? React.createElement(Tag, { + style: { marginLeft: 8 }, + color: hrIsLingniuVehicle(detailModal.record.plateNo) ? 'green' : 'orange' + }, hrVehicleKindLabel(detailModal.record.plateNo)) + : null + ) + ), + React.createElement('div', { className: 'hr-detail-item' }, + React.createElement('span', { className: 'hr-detail-item__label' }, '数据来源'), + React.createElement('span', { className: 'hr-detail-item__value' }, hrRecordSourceLabel(detailModal.record)) + ), + React.createElement('div', { className: 'hr-detail-item' }, + React.createElement('span', { className: 'hr-detail-item__label' }, '里程(km)'), + React.createElement('span', { className: 'hr-detail-item__value tabular-nums' }, hrFormatMileage(detailModal.record.mileageKm)) + ), + React.createElement('div', { className: 'hr-detail-item' }, + React.createElement('span', { className: 'hr-detail-item__label' }, '氢气单价(元/kg)'), + React.createElement('span', { className: 'hr-detail-item__value tabular-nums' }, + hrFormatYuan(detailModal.record.unitPrice != null ? detailModal.record.unitPrice : detailModal.record.costUnitPrice) + ) + ), + React.createElement('div', { className: 'hr-detail-item' }, + React.createElement('span', { className: 'hr-detail-item__label' }, '加氢量(kg)'), + React.createElement('span', { className: 'hr-detail-item__value tabular-nums' }, hrFormatKg(detailModal.record.hydrogenKg)) + ), + React.createElement('div', { className: 'hr-detail-item' }, + React.createElement('span', { className: 'hr-detail-item__label' }, '加氢总额(元)'), + React.createElement('span', { className: 'hr-detail-item__value tabular-nums' }, + hrFormatYuanSymbol(detailModal.record.totalAmount != null ? detailModal.record.totalAmount : detailModal.record.customerAmount) + ) + ), + React.createElement('div', { className: 'hr-detail-item' }, + React.createElement('span', { className: 'hr-detail-item__label' }, '核对状态'), + React.createElement('span', { className: 'hr-detail-item__value' }, + hrShowVerifyReconcileFields(detailModal.record) + ? (hrIsVerified(detailModal.record) ? '已核对' : '未核对') + : '—' + ) + ), + React.createElement('div', { className: 'hr-detail-item' }, + React.createElement('span', { className: 'hr-detail-item__label' }, '对账状态'), + React.createElement('span', { className: 'hr-detail-item__value' }, + hrShowVerifyReconcileFields(detailModal.record) + ? (hrIsReconciled(detailModal.record) ? '已对账' : '未对账') + : '—' + ) + ) + ), + React.createElement('div', { className: 'hr-detail-photos', 'aria-label': '识别原图' }, + React.createElement('div', { className: 'hr-detail-photo' }, + React.createElement('div', { className: 'hr-detail-photo__label' }, '车牌识别原图(含水印)'), + React.createElement('div', { className: 'hr-detail-photo__body' }, + detailModal.plateUrl + ? React.createElement(Image, { + src: detailModal.plateUrl, + alt: '车牌识别原图', + style: { maxHeight: 240 } + }) + : React.createElement('div', { className: 'hr-detail-photo__empty' }, '暂无车牌照片') + ) + ), + React.createElement('div', { className: 'hr-detail-photo' }, + React.createElement('div', { className: 'hr-detail-photo__label' }, '加氢机面板原图(含水印)'), + React.createElement('div', { className: 'hr-detail-photo__body' }, + detailModal.panelUrl + ? React.createElement(Image, { + src: detailModal.panelUrl, + alt: '加氢机面板原图', + style: { maxHeight: 240 } + }) + : React.createElement('div', { className: 'hr-detail-photo__empty' }, '暂无面板照片') + ) + ) + ) + ) + : null + ), + React.createElement(Modal, { title: '确认删除', open: deleteModal.open, @@ -3028,6 +3677,137 @@ const Component = function () { } ] }) + ), + + React.createElement(Modal, { + title: '批量导入加氢记录', + open: batchImportModalOpen, + centered: true, + width: 640, + destroyOnClose: true, + onCancel: function () { + setBatchImportModalOpen(false); + }, + footer: [ + React.createElement(Button, { + key: 'close', + onClick: function () { + setBatchImportModalOpen(false); + } + }, '关闭') + ] + }, + React.createElement('div', { className: 'hr-import-body' }, + React.createElement('section', { className: 'hr-import-step' }, + React.createElement('div', { className: 'hr-import-step-head' }, + React.createElement('span', { className: 'hr-import-step-badge' }, '1'), + React.createElement('span', null, '下载导入模板') + ), + React.createElement(Button, { + type: 'primary', + style: HR_PRIMARY_BTN_STYLE, + onClick: downloadBatchImportTemplate, + 'aria-label': '下载加氢记录导入模板' + }, '下载加氢记录导入模板') + ), + React.createElement('section', { className: 'hr-import-step' }, + React.createElement('div', { className: 'hr-import-step-head' }, + React.createElement('span', { className: 'hr-import-step-badge' }, '2'), + React.createElement('span', null, '重复记录处理(主键:加氢站 + 车牌号 + 加氢时间)') + ), + React.createElement('div', { + className: 'hr-import-dup-options', + role: 'radiogroup', + 'aria-label': '重复记录处理' + }, + HR_BATCH_DUP_OPTIONS.map(function (opt) { + return React.createElement('label', { + key: opt.value, + className: 'hr-import-dup-option' + }, + React.createElement('input', { + type: 'radio', + name: 'hr-batch-import-dup', + value: opt.value, + checked: batchImportDupStrategy === opt.value, + onChange: function () { setBatchImportDupStrategy(opt.value); } + }), + React.createElement('span', { className: 'hr-import-dup-label' }, opt.label), + React.createElement('span', { className: 'hr-import-dup-hint' }, opt.hint) + ); + }) + ) + ), + React.createElement('section', { className: 'hr-import-step' }, + React.createElement('div', { className: 'hr-import-step-head' }, + React.createElement('span', { className: 'hr-import-step-badge' }, '3'), + React.createElement('span', null, '上传填写好的文件') + ), + React.createElement(Upload, { + accept: '.csv,.xls,.xlsx', + multiple: false, + maxCount: 1, + showUploadList: false, + beforeUpload: function (file) { + handleBatchImportFile(file); + return false; + } + }, + React.createElement('button', { + type: 'button', + className: 'hr-import-upload-zone', + 'aria-label': '点击选择文件上传' + }, + React.createElement('svg', { + width: 28, + height: 28, + viewBox: '0 0 24 24', + fill: 'none', + stroke: 'currentColor', + strokeWidth: 1.8, + strokeLinecap: 'round', + strokeLinejoin: 'round', + 'aria-hidden': true + }, + React.createElement('path', { d: 'M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4' }), + React.createElement('polyline', { points: '17 8 12 3 7 8' }), + React.createElement('line', { x1: 12, y1: 3, x2: 12, y2: 15 }) + ), + React.createElement('p', null, '点击选择文件上传'), + React.createElement('span', { className: 'hr-import-upload-hint' }, '原型支持 CSV;Excel 请另存为 CSV') + ) + ) + ), + batchImportResult + ? React.createElement('section', { className: 'hr-import-step' }, + React.createElement('p', { className: 'hr-import-result-summary' }, + '新增成功 ', + React.createElement('strong', { className: 'is-ok' }, String(batchImportResult.successCount || 0)), + ' 条' + + (batchImportResult.overwrittenCount + ? ',覆盖 ' + batchImportResult.overwrittenCount + ' 条' + : '') + + (batchImportResult.skippedCount + ? ',跳过 ' + batchImportResult.skippedCount + ' 条' + : '') + + '。' + ), + batchImportResult.failCount + ? React.createElement('p', { className: 'hr-import-result-summary' }, + '失败 ', + React.createElement('strong', { className: 'is-err' }, String(batchImportResult.failCount)), + ' 条,可下载错误日志核对。' + ) + : null, + batchImportResult.failCount + ? React.createElement(Button, { + onClick: downloadBatchImportFails, + style: { borderRadius: 8, fontWeight: 600 } + }, '下载错误日志') + : null + ) + : null + ) ) ); }; diff --git a/src/prototypes/oneos-web-workbench/index.tsx b/src/prototypes/oneos-web-workbench/index.tsx deleted file mode 100644 index c2feea7..0000000 --- a/src/prototypes/oneos-web-workbench/index.tsx +++ /dev/null @@ -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 ( - - ); -} diff --git a/src/prototypes/oneos-web-workbench/pages/01-工作台.jsx b/src/prototypes/oneos-web-workbench/pages/01-工作台.jsx deleted file mode 100644 index 1c45c4e..0000000 --- a/src/prototypes/oneos-web-workbench/pages/01-工作台.jsx +++ /dev/null @@ -1,3407 +0,0 @@ -// 【重要】必须使用 const Component 作为组件变量名 -// 数字化资产 ONEOS 运管平台 - 工作台(参照原型布局 + Dashboard 风格) - -import { OperationActions } from '../../../common/OperationActions'; - -const Component = function () { - var useState = React.useState; - var useMemo = React.useMemo; - var useCallback = React.useCallback; - - var antd = window.antd; - var App = antd.App; - var Row = antd.Row; - var Col = antd.Col; - var Card = antd.Card; - var Statistic = antd.Statistic; - var Tabs = antd.Tabs; - var Badge = antd.Badge; - var Button = antd.Button; - var Dropdown = antd.Dropdown; - var Space = antd.Space; - var Modal = antd.Modal; - var Popover = antd.Popover; - var Table = antd.Table; - var Select = antd.Select; - var DatePicker = antd.DatePicker; - var Tag = antd.Tag; - var Typography = antd.Typography; - var message = antd.message; - var Tooltip = antd.Tooltip; - var Input = antd.Input; - var Checkbox = antd.Checkbox; - - var Text = Typography.Text; - var Title = Typography.Title; - var RangePicker = DatePicker.RangePicker; - - var pageBg = '#f8fafc'; - var cardRadius = 14; - var cardShadow = '0 1px 2px rgba(15,23,42,0.04), 0 8px 24px rgba(15,23,42,0.06)'; - var cardStyle = { - borderRadius: cardRadius, - boxShadow: cardShadow, - border: '1px solid #e2e8f0', - background: '#ffffff' - }; - var pagePadY = 16; - var pagePadX = 24; - var layoutGutter = 16; - var accentBlue = '#2563eb'; - var accentPurple = '#7c3aed'; - var accentCyan = '#0891b2'; - var accentOrange = '#f97316'; - var accentGeekblue = '#1d4ed8'; - var textPrimary = '#1e293b'; - var textSecondary = '#64748b'; - - var todoActionMeta = { - approve: { label: '需审批', color: 'purple' }, - fill: { label: '需填报', color: 'geekblue' }, - initiate: { label: '需发起', color: 'orange' }, - execute: { label: '需执行', color: 'processing' }, - follow: { label: '需跟进', color: 'default' }, - notify: { label: '仅知会', color: 'default' } - }; - var todoRiskMeta = { - high: { label: '高', color: 'error' }, - medium: { label: '中', color: 'warning' }, - low: { label: '低', color: 'success' }, - none: { label: '无', color: 'default' } - }; - - // 工作台卡片内待办 / 通知默认展示条数 - var wbDashPreviewMax = 5; - var wbChartHeight = 168; - var wbBarTrackH = 72; - var wbBarWrapH = 96; - - function protoNav(hint) { - if (typeof hint === 'string' && hint.indexOf('/prototypes/') === 0 && typeof window !== 'undefined') { - window.location.href = hint; - return; - } - message.info('跳转「' + hint + '」(原型,联调配置路由)'); - } - - function formatWorkbenchFinanceYuan(amount) { - if (amount == null || typeof amount !== 'number' || isNaN(amount)) return '—'; - return amount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' 元'; - } - - function formatNoticeNow() { - var d = new Date(); - var pad = function (n) { return n < 10 ? '0' + n : '' + n; }; - return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()); - } - - // 原型:管理员催办后写入操作员「通知中心」;联调时管理员姓名取当前登录用户 - var mockAdminDisplayName = '张明'; - // 原型用例:工作台问候展示名;联调时替换为当前登录用户姓名(如 JWT / 用户信息里的 displayName) - var mockOperatorDisplayName = '陈思远'; - - var noticeModalState = useState(false); - var noticeModalOpen = noticeModalState[0]; - var setNoticeModalOpen = noticeModalState[1]; - - var noticePreviewState = useState(null); - var noticePreviewRecord = noticePreviewState[0]; - var setNoticePreviewRecord = noticePreviewState[1]; - - var quickCustomizeOpenState = useState(false); - var quickCustomizeOpen = quickCustomizeOpenState[0]; - var setQuickCustomizeOpen = quickCustomizeOpenState[1]; - - var quickCustomDraftState = useState([]); - var quickCustomDraft = quickCustomDraftState[0]; - var setQuickCustomDraft = quickCustomDraftState[1]; - - var QUICK_STORAGE_KEY = 'oneos-workbench-quick-v1'; - - function quickItemKey(it) { - if (!it) return ''; - return it.action ? 'action:' + it.action : 'p:' + (it.p || it.t); - } - - function loadQuickCustom() { - if (typeof window === 'undefined' || !window.localStorage) return {}; - try { - var raw = window.localStorage.getItem(QUICK_STORAGE_KEY); - return raw ? JSON.parse(raw) : {}; - } catch (_e) { - return {}; - } - } - - function saveQuickCustom(next) { - if (typeof window !== 'undefined' && window.localStorage) { - try { - window.localStorage.setItem(QUICK_STORAGE_KEY, JSON.stringify(next)); - } catch (_e) { /* ignore */ } - } - } - - var quickCustomState = useState(loadQuickCustom); - var quickCustom = quickCustomState[0]; - var setQuickCustom = quickCustomState[1]; - - var reportTabState = useState('task'); - var reportTab = reportTabState[0]; - var setReportTab = reportTabState[1]; - - var roleTabState = useState('ops'); - var roleTab = roleTabState[0]; - var setRoleTab = roleTabState[1]; - - var todoMoreModalState = useState(false); - var todoMoreModalOpen = todoMoreModalState[0]; - var setTodoMoreModalOpen = todoMoreModalState[1]; - - var overdueDeliveryModalState = useState(false); - var overdueDeliveryModalOpen = overdueDeliveryModalState[0]; - var setOverdueDeliveryModalOpen = overdueDeliveryModalState[1]; - - var overdueReturnModalState = useState(false); - var overdueReturnModalOpen = overdueReturnModalState[0]; - var setOverdueReturnModalOpen = overdueReturnModalState[1]; - - var inspectModalState = useState(false); - var inspectModalOpen = inspectModalState[0]; - var setInspectModalOpen = inspectModalState[1]; - - var genDeliveryPickOpenState = useState(false); - var genDeliveryPickOpen = genDeliveryPickOpenState[0]; - var setGenDeliveryPickOpen = genDeliveryPickOpenState[1]; - var genDeliveryConfigOpenState = useState(false); - var genDeliveryConfigOpen = genDeliveryConfigOpenState[0]; - var setGenDeliveryConfigOpen = genDeliveryConfigOpenState[1]; - var genDeliveryCustomerState = useState(''); - var genDeliveryCustomer = genDeliveryCustomerState[0]; - var setGenDeliveryCustomer = genDeliveryCustomerState[1]; - var genDeliveryProjectIdState = useState(undefined); - var genDeliveryProjectId = genDeliveryProjectIdState[0]; - var setGenDeliveryProjectId = genDeliveryProjectIdState[1]; - var genDeliveryExpectedState = useState(null); - var genDeliveryExpected = genDeliveryExpectedState[0]; - var setGenDeliveryExpected = genDeliveryExpectedState[1]; - var genDeliveryBillingState = useState(null); - var genDeliveryBilling = genDeliveryBillingState[0]; - var setGenDeliveryBilling = genDeliveryBillingState[1]; - var genDeliverySelectedKeysState = useState([]); - var genDeliverySelectedKeys = genDeliverySelectedKeysState[0]; - var setGenDeliverySelectedKeys = genDeliverySelectedKeysState[1]; - - var genReturnPickOpenState = useState(false); - var genReturnPickOpen = genReturnPickOpenState[0]; - var setGenReturnPickOpen = genReturnPickOpenState[1]; - var genReturnConfigOpenState = useState(false); - var genReturnConfigOpen = genReturnConfigOpenState[0]; - var setGenReturnConfigOpen = genReturnConfigOpenState[1]; - var genReturnCustomerState = useState(''); - var genReturnCustomer = genReturnCustomerState[0]; - var setGenReturnCustomer = genReturnCustomerState[1]; - var genReturnProjectIdState = useState(undefined); - var genReturnProjectId = genReturnProjectIdState[0]; - var setGenReturnProjectId = genReturnProjectIdState[1]; - var genReturnDateState = useState(null); - var genReturnDate = genReturnDateState[0]; - var setGenReturnDate = genReturnDateState[1]; - var genReturnSelectedKeysState = useState([]); - var genReturnSelectedKeys = genReturnSelectedKeysState[0]; - var setGenReturnSelectedKeys = genReturnSelectedKeysState[1]; - - // 业管-能源部 · 独立卡片「本日导入加氢明细条数」:0 条时卡片内显示提示文案(联调接接口) - var energyH2ImportTodayState = useState(0); - var energyH2ImportTodayCount = energyH2ImportTodayState[0]; - - // 财务部 · 独立卡片「能源账户充值金额」:元,null 表示无充值记录示意(联调接接口) - var financeEnergyRechargeYuanState = useState(null); - var financeEnergyRechargeYuan = financeEnergyRechargeYuanState[0]; - - var todoMoreTypeState = useState(undefined); - var todoMoreTaskType = todoMoreTypeState[0]; - var setTodoMoreTaskType = todoMoreTypeState[1]; - - var todoMoreDateStartState = useState(''); - var todoMoreDateStart = todoMoreDateStartState[0]; - var setTodoMoreDateStart = todoMoreDateStartState[1]; - - var todoMoreDateEndState = useState(''); - var todoMoreDateEnd = todoMoreDateEndState[0]; - var setTodoMoreDateEnd = todoMoreDateEndState[1]; - - var todoMoreStatusState = useState(undefined); - var todoMoreStatus = todoMoreStatusState[0]; - var setTodoMoreStatus = todoMoreStatusState[1]; - - var todoBoardFilterState = useState('handle'); - var todoBoardFilter = todoBoardFilterState[0]; - var setTodoBoardFilter = todoBoardFilterState[1]; - - var warningDeptState = useState('ops'); - var warningDeptKey = warningDeptState[0]; - var setWarningDeptKey = warningDeptState[1]; - - // KPI 展示方案比稿:legacy=部门 Tab 平铺;radar=角色预警台 - var warningKpiSchemeState = useState('legacy'); - var warningKpiScheme = warningKpiSchemeState[0]; - var setWarningKpiScheme = warningKpiSchemeState[1]; - - var warningRadarFoldOpenState = useState(false); - var warningRadarFoldOpen = warningRadarFoldOpenState[0]; - var setWarningRadarFoldOpen = warningRadarFoldOpenState[1]; - - var roleToWarningDeptKey = useMemo(function () { - return { - ops: 'ops', - ye: 'business', - yeEnergy: 'energy', - finance: 'finance', - safety: 'safety', - legal: 'public' - }; - }, []); - - // 顶部警告卡片:按部门切换(逻辑见需求脑图「警告卡片」,联调接接口) - var warningDeptOrder = useMemo(function () { - return [ - { key: 'ops', label: '运维' }, - { key: 'business', label: '业管' }, - { key: 'energy', label: '业管-能源部' }, - { key: 'safety', label: '安全部' }, - { key: 'finance', label: '财务部' }, - { key: 'public', label: '流程审批' } - ]; - }, []); - - var warningDeptTabItems = useMemo(function () { - return warningDeptOrder.map(function (d) { - return { key: d.key, label: d.label }; - }); - }, [warningDeptOrder]); - - var warningCardsByDept = useMemo(function () { - var g1 = 'linear-gradient(135deg,#fff1f0,#ffccc7)'; - var g2 = 'linear-gradient(135deg,#fff7e6,#ffd591)'; - var g3 = 'linear-gradient(135deg,#f9f0ff,#efdbff)'; - var g4 = 'linear-gradient(135deg,#f0f5ff,#d6e4ff)'; - var g5 = 'linear-gradient(135deg,#e6fffb,#b5f5ec)'; - return { - ops: [ - { key: 'w_ops_delivery', title: '超期未交车', value: 5, iconBg: g1, icon: '交', color: '#f5222d' }, - { key: 'w_ops_return_cnt', title: '超期未还车数量', value: 3, iconBg: g2, icon: '还', color: accentOrange }, - { key: 'w_ops_inspect', title: '年审/等级评定', value: 2, iconBg: g3, icon: '审', color: accentPurple }, - { key: 'w_ops_maint', title: '超期未保养', value: 7, iconBg: g4, icon: '保', color: accentGeekblue }, - { key: 'w_ops_settle_om', title: '超期未核对还车应结款', value: 1, iconBg: g5, icon: '款', color: accentCyan } - ], - business: [ - { key: 'w_biz_lease', title: '超期未核对租赁账单', value: 4, iconBg: g3, icon: '租', color: accentPurple }, - { key: 'w_biz_pickup', title: '超期未核对提车应收款', value: 2, iconBg: g4, icon: '提', color: accentGeekblue }, - { key: 'w_biz_return_bs', title: '超期未核对还车应结款', value: 3, iconBg: g1, icon: '结', color: '#f5222d' }, - { key: 'w_biz_h2', title: '超期未核对氢费账单', value: 1, iconBg: g5, icon: '氢', color: accentCyan } - ], - energy: [ - { key: 'w_en_h2order', title: '超期未核对加氢订单', value: 6, iconBg: g5, icon: '氢', color: accentCyan }, - { key: 'w_en_h2_import_today', title: '本日导入加氢明细条数', value: 0, iconBg: g5, icon: '录', color: accentCyan }, - { key: 'w_en_return', title: '超期未核对还车应结款', value: 2, iconBg: g1, icon: '结', color: '#f5222d' } - ], - safety: [ - { key: 'w_safe_return', title: '超期未核对还车应结款', value: 1, iconBg: g1, icon: '结', color: '#f5222d' } - ], - finance: [ - { key: 'w_fin_lease_od', title: '租赁账单超期未收到款', value: 3, iconBg: g3, icon: '租', color: accentPurple }, - { key: 'w_fin_lease_part', title: '租赁账单未收全款', value: 2, iconBg: g2, icon: '全', color: accentOrange }, - { key: 'w_fin_pickup_od', title: '提车应收款超期未收到款', value: 2, iconBg: g4, icon: '提', color: accentGeekblue }, - { key: 'w_fin_pickup_part', title: '提车应收款未收全款', value: 1, iconBg: g2, icon: '款', color: accentOrange }, - { key: 'w_fin_return_od', title: '还车应结款超期未收到款', value: 4, iconBg: g1, icon: '逾', color: '#f5222d' }, - { key: 'w_fin_return_part', title: '还车应结款未收全款', value: 2, iconBg: g1, icon: '结', color: '#cf1322' }, - { key: 'w_fin_h2_od', title: '氢费账单超期未收到款', value: 1, iconBg: g5, icon: '氢', color: accentCyan }, - { key: 'w_fin_energy_recharge', title: '能源账户充值金额', value: 0, iconBg: g5, icon: '充', color: accentCyan }, - { key: 'w_fin_h2_part', title: '氢费账单未收全款', value: 1, iconBg: g5, icon: '费', color: '#13c2c2' } - ], - public: [ - { key: 'w_pub_audit', title: '超期未审核', value: 9, iconBg: g4, icon: '审', color: accentGeekblue } - ] - }; - }, []); - - var warningCardsVisible = useMemo(function () { - return warningCardsByDept[warningDeptKey] || warningCardsByDept.ops; - }, [warningCardsByDept, warningDeptKey]); - - var warningRadarRoleDeptKey = useMemo(function () { - return roleToWarningDeptKey[roleTab] || 'ops'; - }, [roleToWarningDeptKey, roleTab]); - - // 顶部警告卡标题旁提示:脑图「警告卡片」末级说明原文(与 assets 脑图截图一致) - var workbenchWarningMetricHintByKey = useMemo(function () { - return { - w_ops_delivery: '超过交车结束时间未交', - w_ops_return_cnt: '合同过期车辆未还', - w_ops_inspect: '30天内未处理', - w_ops_maint: '超过保养项维护里程\n超过保养项维护时间', - w_ops_settle_om: '还车应结款超过15天未完成运维部还车应结款提交', - w_biz_lease: '租赁账单生成7天以上未完成费用明细填报', - w_biz_pickup: '提车应收款生成7天以上未完成费用明细填报', - w_biz_return_bs: '还车应结款生成15天以上未完成业务服务部还车应结款提交', - w_biz_h2: '租赁合同氢费为月结算时,超过30天间隔未生成新氢费账单', - w_en_h2order: '加氢记录形成后1天以上未核对', - w_en_h2_import_today: '展示当日已导入的加氢明细条数;为0时表示今日尚未导入,请确认是否有加氢明细需要导入。', - w_en_return: '还车应结款生成15天以上未完成能源采购部还车应结款提交', - w_safe_return: '还车应结款生成15天以上未完成安全部还车应结款提交', - w_fin_lease_od: '租赁账单审核后到财务部7天以上未收到款项', - w_fin_lease_part: '租赁账单审核后到账金额 < 应收金额', - w_fin_pickup_od: '提车应收款审核后到财务部7天以上未收到款项', - w_fin_pickup_part: '提车应收款审核后到账金额 < 应收金额', - w_fin_return_od: '还车应结款为正数时超期未收到款', - w_fin_return_part: '还车应结款审核后到账金额 < 应收金额', - w_fin_h2_od: '氢费账单审核后到财务部7天以上未收到款项', - w_fin_energy_recharge: '展示能源账户已充值金额;无记录时请确认是否有能源账户充值记录。', - w_fin_h2_part: '氢费账单审核后到账金额 < 应收金额', - w_pub_audit: '流程在当前流程超过24小时以上未审核' - }; - }, []); - - // 工作台-超期未交车弹窗:预计交车结束日早于当前日期的示意数据(联调接接口) - var overdueDeliveryMockRows = useMemo(function () { - var pad = function (n) { return n < 10 ? '0' + n : '' + n; }; - var fmt = function (d) { - return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()); - }; - var base = new Date(); - base.setHours(12, 0, 0, 0); - var addDays = function (days) { - var x = new Date(base.getTime()); - x.setDate(x.getDate() + days); - return x; - }; - var d1 = addDays(-9); - var d2s = addDays(-24); - var d2e = addDays(-17); - var d3 = addDays(-5); - var d4s = addDays(-40); - var d4e = addDays(-32); - var d5 = addDays(-2); - return [ - { id: 'wb_od_1', expectedDate: fmt(d1), contractCode: 'HT-ZL-2026-011', projectName: '嘉兴氢能示范项目', customerName: '嘉兴某某物流有限公司', deliveryRegion: '浙江省-嘉兴市', deliveryAddress: '嘉兴市南湖区科技大道1号', deliveryCount: 2, vehicleList: [{ vehicleType: '厢式货车', brand: '东风', model: 'DFH1180', plateNo: '-', deliveryTime: '-', deliveryPerson: '-' }, { vehicleType: '平板货车', brand: '福田', model: 'BJ1180', plateNo: '-', deliveryTime: '-', deliveryPerson: '-' }], createTime: fmt(addDays(-28)) + ' 09:00', createBy: '系统', lastModifyTime: fmt(addDays(-11)) + ' 14:30', lastModifyBy: '李四', assignedDeliveryPerson: '张三' }, - { id: 'wb_od_2', expectedDate: fmt(d2s) + ' 至 ' + fmt(d2e), contractCode: 'HT-ZL-2026-012', projectName: '上海物流租赁项目', customerName: '上海某某运输公司', deliveryRegion: '上海市-上海市', deliveryAddress: '浦东新区张江高科技园区', deliveryCount: 1, vehicleList: [{ vehicleType: '厢式货车', brand: '江淮', model: 'HFC1180', plateNo: '-', deliveryTime: '-', deliveryPerson: '-' }], createTime: fmt(addDays(-30)) + ' 10:30', createBy: '王五', lastModifyTime: fmt(addDays(-20)) + ' 11:00', lastModifyBy: '王五', assignedDeliveryPerson: '李四' }, - { id: 'wb_od_3', expectedDate: fmt(d3), contractCode: 'HT-ZL-2026-013', projectName: '杭州城配租赁项目', customerName: '杭州某某租赁有限公司', deliveryRegion: '浙江省-杭州市', deliveryAddress: '余杭区未来科技城', deliveryCount: 3, vehicleList: [{ vehicleType: '栏板货车', brand: '重汽', model: 'ZZ1180', plateNo: '-', deliveryTime: '-', deliveryPerson: '-' }, { vehicleType: '厢式货车', brand: '东风', model: 'DFH1190', plateNo: '-', deliveryTime: '-', deliveryPerson: '-' }, { vehicleType: '平板货车', brand: '福田', model: 'BJ1190', plateNo: '-', deliveryTime: '-', deliveryPerson: '-' }], createTime: fmt(addDays(-26)) + ' 14:00', createBy: '李四', lastModifyTime: fmt(addDays(-6)) + ' 09:15', lastModifyBy: '王五', assignedDeliveryPerson: '张三' }, - { id: 'wb_od_4', expectedDate: fmt(d4s) + ' 至 ' + fmt(d4e), contractCode: 'HT-ZL-2026-014', projectName: '嘉兴氢能示范项目', customerName: '嘉兴某某物流有限公司', deliveryRegion: '浙江省-嘉兴市', deliveryAddress: '嘉兴市秀洲区洪兴西路288号', deliveryCount: 1, vehicleList: [{ vehicleType: '厢式货车', brand: '重汽', model: 'ZZ1160', plateNo: '-', deliveryTime: '-', deliveryPerson: '-' }], createTime: fmt(addDays(-45)) + ' 08:15', createBy: '张三', lastModifyTime: fmt(addDays(-33)) + ' 16:40', lastModifyBy: '张三', assignedDeliveryPerson: '李四' }, - { id: 'wb_od_5', expectedDate: fmt(d5), contractCode: 'HT-ZL-2026-015', projectName: '上海物流租赁项目', customerName: '上海某某运输公司', deliveryRegion: '上海市-上海市', deliveryAddress: '闵行区莘庄工业区申富路669号', deliveryCount: 2, vehicleList: [{ vehicleType: '平板货车', brand: '福田', model: 'BJ1180', plateNo: '-', deliveryTime: '-', deliveryPerson: '-' }, { vehicleType: '栏板货车', brand: '东风', model: 'DFH1160', plateNo: '-', deliveryTime: '-', deliveryPerson: '-' }], createTime: fmt(addDays(-18)) + ' 11:20', createBy: '王五', lastModifyTime: fmt(addDays(-3)) + ' 10:05', lastModifyBy: '王五', assignedDeliveryPerson: '王五' } - ]; - }, []); - - var openOverdueDeliveryModal = useCallback(function () { - setOverdueDeliveryModalOpen(true); - }, []); - - // 工作台-超期未还车:与还车管理-待处理列表字段一致的示意数据(联调接接口) - var overdueReturnMockRows = useMemo(function () { - var pad = function (n) { return n < 10 ? '0' + n : '' + n; }; - var fmtD = function (d) { - return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()); - }; - var fmtDt = function (d, hh, mm) { - return fmtD(d) + ' ' + pad(hh) + ':' + pad(mm); - }; - var base = new Date(); - base.setHours(12, 0, 0, 0); - var addDays = function (days) { - var x = new Date(base.getTime()); - x.setDate(x.getDate() + days); - return x; - }; - var a = addDays(-52); - var b = addDays(-38); - var c = addDays(-25); - return [ - { id: 'wb_or_1', deliveryTime: fmtDt(a, 9, 30), deliveryPerson: '张三', plateNo: '京A12345', vehicleType: '重型厢式货车', brand: '东风', model: 'DFH1180', vin: 'LGHXCAE28M1234567', contractCode: 'HT-ZL-2026-001', customerName: '嘉兴某某物流有限公司', projectName: '嘉兴氢能示范项目', dept: '华东业务部', bizOwner: '李经理', vehicleArrived: false }, - { id: 'wb_or_2', deliveryTime: fmtDt(b, 14, 0), deliveryPerson: '李四', plateNo: '沪B20001', vehicleType: '厢式货车', brand: '福田', model: 'BJ1180', vin: 'LGHXCAE28M7654321', contractCode: 'HT-ZL-2026-002', customerName: '上海某某运输公司', projectName: '上海物流租赁项目', dept: '华东业务部', bizOwner: '王经理', vehicleArrived: true }, - { id: 'wb_or_3', deliveryTime: fmtDt(c, 10, 15), deliveryPerson: '王五', plateNo: '浙A88888', vehicleType: '轻型厢式货车', brand: '江淮', model: 'HFC1180', vin: 'LGHXCAE28M8888888', contractCode: 'HT-ZL-2026-003', customerName: '杭州某某租赁有限公司', projectName: '杭州城配租赁项目', dept: '浙江业务部', bizOwner: '赵经理', vehicleArrived: false } - ]; - }, []); - - var openOverdueReturnModal = useCallback(function () { - setOverdueReturnModalOpen(true); - }, []); - - // 工作台-年审/等级评定:根据提供的列表要求展示示意数据 - var inspectMockRows = useMemo(function () { - var pad = function (n) { return n < 10 ? '0' + n : '' + n; }; - var fmtD = function (d) { - return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()); - }; - var base = new Date(); - base.setHours(12, 0, 0, 0); - var addDays = function (days) { - var x = new Date(base.getTime()); - x.setDate(x.getDate() + days); - return x; - }; - return [ - { id: 'wb_insp_1', plateNo: '沪A12345', vehicleType: '重型厢式货车', brand: '东风', model: 'DFH1180', nextDate: fmtD(addDays(2)), remainDays: 2 }, - { id: 'wb_insp_2', plateNo: '苏B88888', vehicleType: '厢式货车', brand: '福田', model: 'BJ1180', nextDate: fmtD(addDays(5)), remainDays: 5 }, - { id: 'wb_insp_3', plateNo: '浙C66666', vehicleType: '轻型厢式货车', brand: '江淮', model: 'HFC1180', nextDate: fmtD(addDays(-1)), remainDays: -1 } - ]; - }, []); - - // 待办任务表(行动语义:类型、风险、责任链、分区) - var dashboardTodoRows = useMemo(function () { - return [ - { id: '1', taskType: '还车应结款', taskName: '云通-租赁帕力安18T(粤AGP3649)· 财务终审', genDate: '2026-03-28 11:53', path: 'web端/审批中心.jsx', status: 'pending', zone: 'today', actionType: 'approve', riskLevel: 'high', riskHint: '高金额应退 ¥4,072.50', nextOwner: '赵敏', nextAction: '执行退款(2 工作日内)', slaRemain: '剩余 6h' }, - { id: '2', taskType: '交车', taskName: '顺通运输租赁苏龙18T(交车数:5)', genDate: '2026-03-26 20:08', path: 'web端/运维管理/车辆业务/交车管理.jsx', status: 'pending', zone: 'today', actionType: 'execute', riskLevel: 'medium', riskHint: '交车窗口 6h 内', nextOwner: '客户', nextAction: '签署交车单', slaRemain: '剩余 6h' }, - { id: '3', taskType: '异动', taskName: '异动任务-魏山(异动车数:9)', genDate: '2026-03-27 12:26', path: 'web端/运维管理/车辆业务/异动管理-结束异动.jsx', status: 'overdue', zone: 'handle', actionType: 'approve', riskLevel: 'high', riskHint: '已超时,阻塞车辆调度', nextOwner: '运维一部', nextAction: '现场确认异动结束', slaRemain: '已超时 18h' }, - { id: '4', taskType: '年审', taskName: '年审任务(沪A62261F)', genDate: '2026-03-27 16:32', path: 'web端/运维管理/车辆业务/异动管理.jsx', status: 'pending', zone: 'handle', actionType: 'execute', riskLevel: 'medium', riskHint: '证照即将到期', nextOwner: '张明辉', nextAction: '完成年审办理', slaRemain: '剩余 2 天' }, - { id: '5', taskType: '租赁账单', taskName: '御盛合-租赁苏龙18T(账单编号xxxxxxx)', genDate: '2026-03-28 00:00', path: 'web端/业务管理/租赁账单.jsx', status: 'pending', zone: 'handle', actionType: 'approve', riskLevel: 'low', riskHint: '账单金额无异常', nextOwner: '财务', nextAction: '确认收款', slaRemain: '剩余 3 天' }, - { id: '6', taskType: '还车应结款', taskName: '苏E12345F · 四部门已通过,待发起主流程', genDate: '2026-03-28 09:10', path: 'web端/审批中心.jsx', status: 'pending', zone: 'follow', actionType: 'initiate', riskLevel: 'medium', riskHint: '四部门已全部通过,待你发起', nextOwner: '财务审核', nextAction: '主流程审批', slaRemain: '建议今日发起' }, - { id: '7', taskType: '租赁账单', taskName: '嘉兴某某物流 · 2026年6月账单(抄送)', genDate: '2026-03-28 10:20', path: 'web端/审批中心.jsx', status: 'pending', zone: 'notify', actionType: 'notify', riskLevel: 'none', riskHint: '无需操作,知悉即可', nextOwner: '—', nextAction: '—', slaRemain: '—' } - ]; - }, []); - - var todoSummary = useMemo(function () { - var p = 0, o = 0, today = 0, handle = 0, follow = 0, notify = 0, actionable = 0; - dashboardTodoRows.forEach(function (r) { - if (r.status === 'pending') p++; - else if (r.status === 'overdue') o++; - if (r.zone === 'today') today++; - if (r.zone === 'handle') handle++; - if (r.zone === 'follow') follow++; - if (r.zone === 'notify') notify++; - if (['approve', 'fill', 'initiate', 'execute'].indexOf(r.actionType) >= 0) actionable++; - }); - return { pending: p, overdue: o, today: today, handle: handle, follow: follow, notify: notify, actionable: actionable }; - }, [dashboardTodoRows]); - - var warningRadarSummary = useMemo(function () { - var focus = 0; - var high = 0; - var overdue = 0; - Object.keys(warningCardsByDept).forEach(function (dept) { - (warningCardsByDept[dept] || []).forEach(function (c) { - var v = typeof c.value === 'number' ? c.value : 0; - if (v > 0) focus += v; - if (v >= 3) high += 1; - if (c.key && String(c.key).indexOf('od') >= 0 && v > 0) overdue += v; - }); - }); - if (overdue === 0) overdue = 3; - return { - focus: focus || 12, - high: high || 4, - overdue: overdue, - mine: todoSummary.actionable || 6 - }; - }, [warningCardsByDept, todoSummary.actionable]); - - var warningRadarPrimaryCards = useMemo(function () { - var deptKey = warningRadarRoleDeptKey; - if (deptKey === 'finance') return []; - var cards = (warningCardsByDept[deptKey] || []).slice(); - cards.sort(function (a, b) { - return (b.value || 0) - (a.value || 0); - }); - return cards.filter(function (c) { - if (c.key === 'w_en_h2_import_today' && energyH2ImportTodayCount === 0) return false; - return (c.value || 0) > 0; - }).slice(0, 4); - }, [warningCardsByDept, warningRadarRoleDeptKey, energyH2ImportTodayCount]); - - var warningFinanceAggCards = useMemo(function () { - return [ - { - key: 'fin_agg_lease', title: '租赁 / 提车回款', risk: 'high', - stats: '超期 5 · 未全款 3', amount: '¥128,400', owner: '财务部', - action: '查看回款明细', value: 8 - }, - { - key: 'fin_agg_return', title: '还车应结款回款', risk: 'high', - stats: '超期 4 · 未全款 2', amount: '—', owner: '财务部', - action: '查看回款明细', value: 6 - }, - { - key: 'fin_agg_energy', title: '氢费 / 能源', risk: 'medium', - stats: '超期 1 · 未全款 1', amount: formatWorkbenchFinanceYuan(financeEnergyRechargeYuan), owner: '财务部', - action: '查看能源账单', value: 2 - } - ]; - }, [financeEnergyRechargeYuan]); - - var warningRadarMonitorItems = useMemo(function () { - var items = []; - if (energyH2ImportTodayCount === 0) { - items.push({ - key: 'mon_h2_import', - title: '本日导入加氢明细', - copy: '今日还未导入任何加氢记录,请确认是否有加氢明细需要导入' - }); - } - if (financeEnergyRechargeYuan == null || financeEnergyRechargeYuan === 0) { - items.push({ - key: 'mon_energy_recharge', - title: '能源账户充值金额', - copy: '请确认是否有能源账户充值记录' - }); - } - if (warningRadarRoleDeptKey === 'public') { - items.push({ key: 'mon_audit_types', title: '流程类型分布', copy: '异动 3 · 调拨 2 · 还车应结款 4' }); - } - return items; - }, [energyH2ImportTodayCount, financeEnergyRechargeYuan, warningRadarRoleDeptKey]); - - var dashboardTodoBoardRows = useMemo(function () { - var rows = dashboardTodoRows.filter(function (r) { - if (todoBoardFilter === 'all') return true; - if (todoBoardFilter === 'actionable') return r.actionType !== 'notify' && r.actionType !== 'follow'; - if (todoBoardFilter === 'pending') return r.status === 'pending'; - if (todoBoardFilter === 'overdue') return r.status === 'overdue'; - return r.zone === todoBoardFilter; - }); - rows.sort(function (a, b) { - var riskOrder = { high: 0, medium: 1, low: 2, none: 3 }; - var ra = riskOrder[a.riskLevel] != null ? riskOrder[a.riskLevel] : 9; - var rb = riskOrder[b.riskLevel] != null ? riskOrder[b.riskLevel] : 9; - if (ra !== rb) return ra - rb; - return String(b.genDate || '').localeCompare(String(a.genDate || '')); - }); - return rows; - }, [dashboardTodoRows, todoBoardFilter]); - - var dashboardTodoBoardPreviewRows = useMemo(function () { - return dashboardTodoBoardRows.slice(0, wbDashPreviewMax); - }, [dashboardTodoBoardRows]); - - // 全部待办弹窗:任务类型下拉展示业务全量类型(与示意数据并集,联调可改为接口枚举) - var todoMoreTaskTypeFilterOptions = useMemo(function () { - var catalog = [ - '交车', '调拨', '异动', '年审', '保险', '租赁账单', '审批中心', '还车应结款', - '还车', '备车', '提车应收', '替换车', '违章', '事故', '充电', 'ETC', '能源账户', '氢费', '电费' - ]; - var seen = {}; - catalog.forEach(function (t) { seen[t] = true; }); - dashboardTodoRows.forEach(function (r) { - if (r.taskType) seen[r.taskType] = true; - }); - return Object.keys(seen).sort(function (a, b) { return a.localeCompare(b, 'zh-CN'); }).map(function (t) { - return { value: t, label: t }; - }); - }, [dashboardTodoRows]); - - var todoMoreStatusFilterOptions = useMemo(function () { - return [ - { value: 'pending', label: '待处理' }, - { value: 'overdue', label: '已超时' }, - { value: 'done', label: '已完成' } - ]; - }, []); - - var noticeListState = useState(function () { - return [ - { - id: 'n1', time: '2026-02-27 08:00', type: '商业险到期提醒', priority: 'high', - content: '「粤A12345」商业险将在2026-04-15到期,请尽快处理', read: false, - detail: '车牌粤A12345 的商业险保单将于 2026-04-15 到期。请核对保单信息与承保公司,在到期前完成续保,避免车辆脱保影响运营。', - target: { label: '前往证照管理', path: 'web端/运维管理/车辆业务/证照管理.jsx' } - }, - { - id: 'n2', time: '2026-02-26 18:00', type: '营运证到期提醒', priority: 'high', - content: '「粤B88888」营运证还有90天到期,请尽快更新营运证', read: false, - detail: '车牌粤B88888 的营运证剩余有效期 90 天。请提前准备年审/换证材料,并安排车辆送检时间。', - target: { label: '前往证照管理', path: 'web端/运维管理/车辆业务/证照管理.jsx' } - }, - { - id: 'n3', time: '2026-02-26 10:00', type: '行驶证到期提醒', priority: 'medium', - content: '「浙A11111」行驶证还有90天到期,请尽快进行年审', read: false, - detail: '车牌浙A11111 行驶证将于 90 天内到期。请确认车辆检验状态,并同步更新行驶证影像与关键日期。', - target: { label: '前往证照管理', path: 'web端/运维管理/车辆业务/证照管理.jsx' } - }, - { - id: 'n4', time: '2026-02-25 14:00', type: '租赁合同到期提醒', priority: 'high', - content: '「HT-2025-088」「某某产业园项目」还有30天到期,请尽快处理', read: false, - detail: '合同 HT-2025-088(某某产业园项目)将于 30 天后到期。请与业管确认续签或还车计划,避免超期运营。', - target: { label: '查看租赁合同', path: 'web端/车辆租赁合同/车辆租赁合同.jsx' } - }, - { - id: 'n5', time: '2026-02-25 09:00', type: '租赁账单生成提醒', priority: 'medium', - content: '「HT-2025-088」「某某产业园项目」「ZD-202602-031」已生成,请尽快处理', read: false, - detail: '账单 ZD-202602-031 已生成,关联合同 HT-2025-088 / 某某产业园项目。请核对账期与应收金额,并跟进客户付款。', - target: { label: '查看租赁账单', path: 'web端/业务管理/租赁账单.jsx' } - }, - { - id: 'n6', time: '2026-02-24 11:00', type: '氢费余额不足提醒', priority: 'high', - content: '「某某物流」「华南干线项目」氢费余额已不足500元,请尽快通知客户处理', read: false, - detail: '客户某某物流(华南干线项目)氢费账户余额低于 500 元。请通知客户充值,避免加氢订单中断。', - target: { label: '查看氢费账单', path: 'web端/财务管理/氢费账单.jsx' } - }, - { - id: 'n7', time: '2026-02-24 08:30', type: '审批流程提醒', priority: 'medium', - content: '「李四」「异动审核」审批节点已到达,请进行审批', read: false, - detail: '异动审核流程(申请人:李四)已流转至您的审批节点。请查看申请详情并在工作台或审批中心完成处理。', - target: { label: '前往审批中心', path: 'web端/审批中心.jsx' } - } - ]; - }); - var noticeList = noticeListState[0]; - var setNoticeList = noticeListState[1]; - - var noticeNewCount = useMemo(function () { - var n = 0; - (noticeList || []).forEach(function (it) { - if (!it.read) n++; - }); - return n; - }, [noticeList]); - - var markNoticeRead = useCallback(function (id) { - setNoticeList(function (prev) { - return (prev || []).map(function (n) { - if (n.id !== id) return n; - if (n.read) return n; - return Object.assign({}, n, { read: true }); - }); - }); - }, []); - - var openNoticeModal = useCallback(function () { - setNoticeModalOpen(true); - }, []); - - var openNoticePreview = useCallback(function (record) { - if (!record) return; - setNoticePreviewRecord(record); - }, []); - - var closeNoticePreview = useCallback(function () { - setNoticePreviewRecord(null); - }, []); - - var handleNoticePreviewNavigate = useCallback(function (record) { - if (!record || !record.target || !record.target.path) { - message.info('该通知暂无关联页面(原型)'); - return; - } - protoNav(record.target.path); - closeNoticePreview(); - }, [closeNoticePreview]); - - var pushUrgeNotice = useCallback(function (taskRow) { - var adminName = mockAdminDisplayName; - var content = '(' + adminName + ')已对(' + taskRow.taskName + ')进行催办,请尽快处理'; - setNoticeList(function (prev) { - return [{ - id: 'urge-' + Date.now(), - time: formatNoticeNow(), - type: '催办提醒', - priority: 'high', - content: content, - detail: '管理员 ' + adminName + ' 对任务「' + taskRow.taskName + '」发起了催办。请优先处理该待办,完成后状态将同步更新。', - read: false - }].concat(prev || []); - }); - message.success('催办提醒已推送至通知列表'); - }, []); - - var vehicleMonthBars = useMemo(function () { - return [ - { m: '3月', op: 62, idle: 18 }, { m: '4月', op: 65, idle: 16 }, { m: '5月', op: 68, idle: 15 }, - { m: '6月', op: 70, idle: 14 }, { m: '7月', op: 72, idle: 13 }, { m: '8月', op: 75, idle: 12 }, - { m: '9月', op: 78, idle: 12 }, { m: '10月', op: 80, idle: 11 }, { m: '11月', op: 82, idle: 11 }, - { m: '12月', op: 85, idle: 10 }, { m: '1月', op: 88, idle: 10 }, { m: '2月', op: 90, idle: 9 } - ]; - }, []); - - var contractMonthBars = useMemo(function () { - return [ - { m: '3月', lease: 8, self: 3, renew: 2 }, { m: '4月', lease: 9, self: 3, renew: 2 }, { m: '5月', lease: 10, self: 4, renew: 3 }, - { m: '6月', lease: 10, self: 4, renew: 3 }, { m: '7月', lease: 11, self: 4, renew: 4 }, { m: '8月', lease: 12, self: 5, renew: 3 }, - { m: '9月', lease: 12, self: 5, renew: 4 }, { m: '10月', lease: 13, self: 5, renew: 5 }, { m: '11月', lease: 14, self: 6, renew: 4 }, - { m: '12月', lease: 14, self: 6, renew: 5 }, { m: '1月', lease: 15, self: 6, renew: 5 }, { m: '2月', lease: 16, self: 7, renew: 6 } - ]; - }, []); - - // 任务处理情况:横轴员工、纵轴任务数(示意,联调接接口) - var taskEmployeeBars = useMemo(function () { - return [ - { m: '陈思远', done: 18, overdue: 2 }, - { m: '张明', done: 22, overdue: 1 }, - { m: '李小琳', done: 15, overdue: 4 }, - { m: '王强', done: 9, overdue: 3 }, - { m: '赵敏', done: 24, overdue: 0 }, - { m: '周杰', done: 11, overdue: 5 } - ]; - }, []); - - // 合同到账:应收 / 已收(元,示意;联调接接口) - var contractPaymentDonut = useMemo(function () { - return { receivable: 128500000, received: 94200000 }; - }, []); - - function renderBarGroup(items, k1, k2, c1, c2, labelMinWidth) { - var max = 1; - items.forEach(function (it) { max = Math.max(max, it[k1] + it[k2]); }); - var colMinW = labelMinWidth != null ? labelMinWidth : 28; - return React.createElement('div', { style: { display: 'flex', alignItems: 'flex-end', gap: 5, height: wbBarWrapH, paddingTop: 8, overflowX: 'auto' } }, - items.map(function (it) { - var h1 = Math.round((it[k1] / max) * 100); - var h2 = Math.round((it[k2] / max) * 100); - return React.createElement('div', { key: it.m, style: { display: 'flex', flexDirection: 'column', alignItems: 'center', minWidth: colMinW } }, - React.createElement('div', { style: { display: 'flex', alignItems: 'flex-end', gap: 3, height: wbBarTrackH } }, - React.createElement('div', { style: { width: 10, height: Math.max(h1, 4), background: c1, borderRadius: 3 } }), - React.createElement('div', { style: { width: 10, height: Math.max(h2, 4), background: c2, borderRadius: 3 } }) - ), - React.createElement('span', { style: { fontSize: 11, color: 'rgba(0,0,0,0.45)', marginTop: 6, textAlign: 'center', maxWidth: colMinW + 24, wordBreak: 'break-all', lineHeight: 1.2 } }, it.m) - ); - }) - ); - } - - // 合同情况:每月三组柱 — 租赁 / 自营 / 续签(数量);纵轴刻度按全局最大单柱值 - function renderBarGroupTriple(items, k1, k2, k3, c1, c2, c3, labelMinWidth) { - var max = 1; - items.forEach(function (it) { - max = Math.max(max, it[k1] || 0, it[k2] || 0, it[k3] || 0); - }); - var colMinW = labelMinWidth != null ? labelMinWidth : 40; - var barW = 8; - return React.createElement('div', { style: { display: 'flex', alignItems: 'flex-end', gap: 6, height: wbBarWrapH, paddingTop: 8, overflowX: 'auto' } }, - items.map(function (it) { - var h1 = Math.round(((it[k1] || 0) / max) * 100); - var h2 = Math.round(((it[k2] || 0) / max) * 100); - var h3 = Math.round(((it[k3] || 0) / max) * 100); - return React.createElement('div', { key: it.m, style: { display: 'flex', flexDirection: 'column', alignItems: 'center', minWidth: colMinW } }, - React.createElement('div', { style: { display: 'flex', alignItems: 'flex-end', gap: 2, height: wbBarTrackH } }, - React.createElement('div', { style: { width: barW, height: Math.max(h1, (it[k1] || 0) > 0 ? 4 : 0), background: c1, borderRadius: 2 } }), - React.createElement('div', { style: { width: barW, height: Math.max(h2, (it[k2] || 0) > 0 ? 4 : 0), background: c2, borderRadius: 2 } }), - React.createElement('div', { style: { width: barW, height: Math.max(h3, (it[k3] || 0) > 0 ? 4 : 0), background: c3, borderRadius: 2 } }) - ), - React.createElement('span', { style: { fontSize: 11, color: 'rgba(0,0,0,0.45)', marginTop: 6, textAlign: 'center', maxWidth: colMinW + 20, wordBreak: 'break-all', lineHeight: 1.2 } }, it.m) - ); - }) - ); - } - - // 任务处理情况:双柱在统计卡片内随 Tab 区域伸缩;含 Y/X 轴刻度与悬浮数据标签 - function renderTaskEmployeeBarChart(items, k1, k2, c1, c2, labelMinWidth) { - var maxVal = 1; - items.forEach(function (it) { maxVal = Math.max(maxVal, it[k1] + it[k2]); }); - var midVal = Math.max(0, Math.round(maxVal / 2)); - var colMinW = labelMinWidth != null ? labelMinWidth : 40; - var axisMuted = '#707d8f'; - var axisLine = 'rgba(0,0,0,0.12)'; - - function wrapColTooltip(it, rowKey, colNode) { - var tipTitle = React.createElement('div', { style: { fontSize: 12, lineHeight: 1.6 } }, - React.createElement('div', { style: { fontWeight: 600, marginBottom: 4, color: 'rgba(0,0,0,0.88)' } }, it.m), - React.createElement('div', { style: { color: c1 } }, '完成任务:' + it[k1]), - React.createElement('div', { style: { color: c2 } }, '超时任务:' + it[k2]) - ); - if (Tooltip) { - return React.createElement(Tooltip, { key: rowKey, title: tipTitle, placement: 'top', mouseEnterDelay: 0.05 }, colNode); - } - return React.cloneElement(colNode, { key: rowKey }); - } - - var barColumns = items.map(function (it) { - var p1 = maxVal > 0 ? Math.round((it[k1] / maxVal) * 1000) / 10 : 0; - var p2 = maxVal > 0 ? Math.round((it[k2] / maxVal) * 1000) / 10 : 0; - var colInner = React.createElement('div', { - style: { - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - flex: '1 1 0', - minWidth: colMinW, - minHeight: 0, - overflow: 'hidden', - cursor: 'default' - } - }, - React.createElement('div', { - className: 'workbench-task-bar-track', - style: { - flex: 1, - width: '100%', - minHeight: 48, - display: 'flex', - alignItems: 'flex-end', - justifyContent: 'center', - gap: 'clamp(2px, 6%, 8px)', - boxSizing: 'border-box', - padding: '4px 2px 0', - position: 'relative', - background: 'linear-gradient(to top, transparent 0%, transparent calc(50% - 0.5px), rgba(0,0,0,0.05) calc(50%), transparent calc(50% + 0.5px), transparent 100%)' - } - }, - React.createElement('div', { - className: 'workbench-task-bar-pillar', - style: { - width: 'clamp(7px, 32%, 16px)', - flex: '0 0 auto', - height: it[k1] > 0 ? p1 + '%' : 0, - minHeight: it[k1] > 0 ? 4 : 0, - background: c1, - borderRadius: 3, - alignSelf: 'flex-end', - transition: 'opacity .15s' - } - }), - React.createElement('div', { - className: 'workbench-task-bar-pillar', - style: { - width: 'clamp(7px, 32%, 16px)', - flex: '0 0 auto', - height: it[k2] > 0 ? p2 + '%' : 0, - minHeight: it[k2] > 0 ? 4 : 0, - background: c2, - borderRadius: 3, - alignSelf: 'flex-end', - transition: 'opacity .15s' - } - }) - ), - React.createElement('span', { - style: { - flexShrink: 0, - fontSize: 11, - color: 'rgba(0,0,0,0.45)', - marginTop: 6, - textAlign: 'center', - maxWidth: '100%', - wordBreak: 'break-all', - lineHeight: 1.2, - paddingBottom: 2 - } - }, it.m) - ); - return wrapColTooltip(it, it.m, colInner); - }); - - return React.createElement('div', { className: 'workbench-task-chart-axes-wrap' }, - React.createElement('div', { className: 'workbench-task-chart-plot-row' }, - React.createElement('div', { - className: 'workbench-task-y-title', - style: { - width: 22, - flexShrink: 0, - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - fontSize: 11, - color: axisMuted, - fontWeight: 400, - writingMode: 'vertical-rl', - transform: 'rotate(180deg)', - letterSpacing: 1, - userSelect: 'none' - } - }, '任务数量'), - React.createElement('div', { - className: 'workbench-task-y-ticks', - style: { - width: 30, - flexShrink: 0, - display: 'flex', - flexDirection: 'column', - justifyContent: 'space-between', - alignItems: 'flex-end', - padding: '6px 6px 6px 0', - borderRight: '1px solid ' + axisLine, - fontSize: 10, - color: axisMuted, - lineHeight: 1.2, - boxSizing: 'border-box', - userSelect: 'none' - } - }, - React.createElement('span', null, maxVal), - React.createElement('span', null, midVal), - React.createElement('span', null, '0') - ), - React.createElement('div', { - style: { - flex: 1, - minWidth: 0, - display: 'flex', - flexDirection: 'column', - minHeight: 0 - } - }, - React.createElement('div', { className: 'workbench-task-bar-chart' }, barColumns), - React.createElement('div', { - className: 'workbench-task-x-axis', - style: { - flexShrink: 0, - borderTop: '1px solid ' + axisLine, - textAlign: 'center', - fontSize: 11, - color: axisMuted, - fontWeight: 400, - paddingTop: 6, - marginTop: 2, - userSelect: 'none' - } - }, '员工') - ) - ) - ); - } - - // 合同到账情况:环形图 — 全环面积对应应收款;分色为已收/未收;中心为已收款金额;扇区悬浮标签 - function formatContractMoneyBrief(n) { - if (n == null || isNaN(n)) return '-'; - var v = Number(n); - if (Math.abs(v) >= 100000000) return (v / 100000000).toFixed(2) + ' 亿元'; - if (Math.abs(v) >= 10000) { - var w = v / 10000; - var s = w % 1 === 0 ? w.toFixed(0) : w.toFixed(1); - return s + ' 万元'; - } - return v.toLocaleString('zh-CN') + ' 元'; - } - - function renderContractPaymentDonutChart(d) { - var rec = Math.max(0, d.receivable || 0); - var got = Math.max(0, Math.min(d.received || 0, rec)); - var pend = Math.max(0, rec - got); - var ratio = rec > 0 ? got / rec : 0; - var angleReceived = ratio * 360; - var cReceived = '#1677ff'; - var cPending = '#d9d9d9'; - - function donutSeg(cx, cy, r0, r1, a0, a1) { - if (a1 - a0 < 0.04) return ''; - var rad = function (deg) { return (deg - 90) * Math.PI / 180; }; - var x1 = cx + r1 * Math.cos(rad(a0)); - var y1 = cy + r1 * Math.sin(rad(a0)); - var x2 = cx + r1 * Math.cos(rad(a1)); - var y2 = cy + r1 * Math.sin(rad(a1)); - var x3 = cx + r0 * Math.cos(rad(a1)); - var y3 = cy + r0 * Math.sin(rad(a1)); - var x4 = cx + r0 * Math.cos(rad(a0)); - var y4 = cy + r0 * Math.sin(rad(a0)); - var large = (a1 - a0) > 180 ? 1 : 0; - return 'M' + x1 + ',' + y1 + ' A' + r1 + ',' + r1 + ' 0 ' + large + ' 1 ' + x2 + ',' + y2 + ' L' + x3 + ',' + y3 + ' A' + r0 + ',' + r0 + ' 0 ' + large + ' 0 ' + x4 + ',' + y4 + ' Z'; - } - - var cx = 100; - var cy = 100; - var r0 = 54; - var r1 = 90; - var pathReceived = - angleReceived >= 359.95 ? donutSeg(cx, cy, r0, r1, 0, 360) : (angleReceived > 0.08 ? donutSeg(cx, cy, r0, r1, 0, angleReceived) : ''); - var pathPending = - angleReceived <= 0.05 ? donutSeg(cx, cy, r0, r1, 0, 360) : (angleReceived < 359.92 ? donutSeg(cx, cy, r0, r1, angleReceived, 360) : ''); - - var pctGot = rec > 0 ? ((got / rec) * 100).toFixed(1) : '0'; - var pctPend = rec > 0 ? ((pend / rec) * 100).toFixed(1) : '0'; - - var tipReceived = React.createElement('div', { style: { fontSize: 12, lineHeight: 1.65 } }, - React.createElement('div', { style: { fontWeight: 600 } }, '已收款'), - React.createElement('div', null, '金额:' + formatContractMoneyBrief(got)), - React.createElement('div', { style: { color: 'rgba(0,0,0,0.55)' } }, '占应收款:' + pctGot + '%') - ); - var tipPending = React.createElement('div', { style: { fontSize: 12, lineHeight: 1.65 } }, - React.createElement('div', { style: { fontWeight: 600 } }, '未到账'), - React.createElement('div', null, '金额:' + formatContractMoneyBrief(pend)), - React.createElement('div', { style: { color: 'rgba(0,0,0,0.55)' } }, '占应收款:' + pctPend + '%') - ); - - var pathRecvEl = pathReceived - ? React.createElement('path', { - d: pathReceived, - fill: cReceived, - stroke: '#fff', - strokeWidth: 1.5, - style: { cursor: 'pointer' } - }) - : null; - var pathPendEl = pathPending - ? React.createElement('path', { - d: pathPending, - fill: cPending, - stroke: '#fff', - strokeWidth: 1.5, - style: { cursor: 'pointer' } - }) - : null; - - if (Tooltip && pathRecvEl) { - pathRecvEl = React.createElement(Tooltip, { title: tipReceived, placement: 'top', mouseEnterDelay: 0.05 }, pathRecvEl); - } - if (Tooltip && pathPendEl) { - pathPendEl = React.createElement(Tooltip, { title: tipPending, placement: 'top', mouseEnterDelay: 0.05 }, pathPendEl); - } - - return React.createElement('div', { className: 'workbench-payment-donut-hold' }, - React.createElement('div', { - className: 'workbench-payment-donut-inner', - style: { - position: 'relative', - width: 'min(280px, 78%)', - maxWidth: 300, - aspectRatio: '1', - flexShrink: 0 - } - }, - React.createElement('svg', { - viewBox: '0 0 200 200', - width: '100%', - height: '100%', - style: { display: 'block' } - }, - React.createElement('g', null, pathPendEl, pathRecvEl) - ), - React.createElement('div', { - style: { - position: 'absolute', - left: 0, - right: 0, - top: 0, - bottom: 0, - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - justifyContent: 'center', - pointerEvents: 'none', - padding: '0 12%', - textAlign: 'center' - } - }, - React.createElement('span', { style: { fontSize: 12, color: 'rgba(0,0,0,0.45)', lineHeight: 1.4 } }, '已收款金额'), - React.createElement('span', { style: { fontSize: 20, fontWeight: 600, color: 'rgba(0,0,0,0.88)', lineHeight: 1.35, marginTop: 4 } }, formatContractMoneyBrief(got)), - React.createElement('span', { style: { fontSize: 11, color: 'rgba(0,0,0,0.38)', marginTop: 6 } }, '应收款 ' + formatContractMoneyBrief(rec)) - ) - ), - React.createElement('div', { - style: { - display: 'flex', - flexWrap: 'wrap', - justifyContent: 'center', - gap: '16px 24px', - marginTop: 14, - fontSize: 12, - color: 'rgba(0,0,0,0.55)' - } - }, - React.createElement('span', null, - React.createElement('span', { style: { display: 'inline-block', width: 8, height: 8, borderRadius: 2, background: cReceived, marginRight: 6, verticalAlign: 'middle' } }), - '已收款 ', - React.createElement('span', { style: { color: 'rgba(0,0,0,0.75)', fontWeight: 500 } }, formatContractMoneyBrief(got)) - ), - React.createElement('span', null, - React.createElement('span', { style: { display: 'inline-block', width: 8, height: 8, borderRadius: 2, background: cPending, marginRight: 6, verticalAlign: 'middle' } }), - '未到账 ', - React.createElement('span', { style: { fontWeight: 500, color: 'rgba(0,0,0,0.75)' } }, formatContractMoneyBrief(pend)) - ) - ) - ); - } - - // 原型「图示」区:仿 Dashboard 面积图占位 - function renderChartPlaceholder(caption, chartH) { - var h = chartH != null ? chartH : wbChartHeight; - var svgH = Math.max(72, Math.round(h * 0.55)); - return React.createElement('div', { - style: { - position: 'relative', - height: h, - borderRadius: 8, - overflow: 'hidden', - background: 'linear-gradient(180deg, rgba(22,119,255,0.08) 0%, rgba(22,119,255,0.02) 45%, #fff 100%)', - border: '1px solid rgba(0,0,0,0.06)' - } - }, - React.createElement('svg', { viewBox: '0 0 800 200', preserveAspectRatio: 'none', style: { position: 'absolute', left: 0, right: 0, bottom: 28, height: svgH, width: '100%' } }, - React.createElement('defs', null, - React.createElement('linearGradient', { id: 'areaGrad', x1: '0', y1: '0', x2: '0', y2: '1' }, - React.createElement('stop', { offset: '0%', stopColor: '#1677ff', stopOpacity: 0.35 }), - React.createElement('stop', { offset: '100%', stopColor: '#1677ff', stopOpacity: 0.02 }) - ) - ), - React.createElement('path', { - fill: 'url(#areaGrad)', - d: 'M0,140 Q120,100 200,120 T400,80 T600,95 T800,70 L800,200 L0,200 Z' - }), - React.createElement('path', { - fill: 'none', - stroke: '#1677ff', - strokeWidth: 2.5, - d: 'M0,140 Q120,100 200,120 T400,80 T600,95 T800,70' - }) - ), - React.createElement('div', { - style: { - position: 'absolute', - top: '50%', - left: '50%', - transform: 'translate(-50%,-50%)', - fontSize: 14, - color: 'rgba(0,0,0,0.25)', - pointerEvents: 'none', - fontWeight: 500 - } - }, caption || '图示 · 联调接入图表') - ); - } - - var deliveryTaskProjectList = useMemo(function () { - return [ - { - id: 'p1', - name: '嘉兴某某物流氢能运输项目', - contractCode: 'JXZL20260216YW101235A', - customerName: '嘉兴某某物流有限公司', - deliveryRegion: '浙江省 / 嘉兴市', - deliveryLocation: '浙江省嘉兴市南湖区科技大道1号', - vehicles: [ - { key: 'v1', seq: 1, brand: '东风', model: '氢燃料电池重卡 H31', plateNo: '浙A10001', vin: 'LFV2BJCH8K3123456', monthRent: '12800', serviceFee: '800', deposit: '30000', remark: '首车', deliveryStatus: 'submitted' }, - { key: 'v1c', seq: 1, brand: '东风', model: '氢燃料电池重卡 H31', plateNo: '浙A10000', vin: 'LFV2BJCH8K3000001', monthRent: '12800', serviceFee: '800', deposit: '30000', remark: '已交车样例', deliveryStatus: 'completed' }, - { key: 'v2', seq: 2, brand: '东风', model: '氢燃料电池重卡 H31', plateNo: '浙A10002', vin: 'LFV2BJCH8K3123457', monthRent: '12800', serviceFee: '800', deposit: '30000', remark: '', deliveryStatus: 'submitted' }, - { key: 'v3', seq: 3, brand: '福田', model: '智蓝氢能轻卡', plateNo: '', vin: 'LZYTBACR2M1234567', monthRent: '8500', serviceFee: '500', deposit: '20000', remark: '待上牌', deliveryStatus: 'none' }, - { key: 'v4', seq: 4, brand: '重汽', model: '豪沃氢能牵引车', plateNo: '浙F20001', vin: 'ZZ4257N386FZ12345', monthRent: '15000', serviceFee: '1000', deposit: '35000', remark: '', deliveryStatus: 'none' }, - { key: 'v5', seq: 5, brand: '陕汽', model: '德龙氢能自卸', plateNo: '浙F20002', vin: 'SX1313GR456123456', monthRent: '13200', serviceFee: '880', deposit: '32000', remark: '固定线路', deliveryStatus: 'none' } - ] - }, - { - id: 'p2', - name: '上海某某运输氢能租赁项目', - contractCode: 'SHZL20260201YW200123A', - customerName: '上海某某运输公司', - deliveryRegion: '上海市 / 上海市', - deliveryLocation: '上海市浦东新区张江高科技园区', - vehicles: [ - { key: 'v7', seq: 2, brand: '宇通', model: '氢能公交 ZK6126', plateNo: '沪B40001', vin: 'LZYTAGCF8K4567890', monthRent: '22000', serviceFee: '1200', deposit: '50000', remark: '示范线路', deliveryStatus: 'none' }, - { key: 'v8', seq: 3, brand: '福田', model: '欧辉氢能大巴', plateNo: '', vin: 'LZYTBACR2M2345678', monthRent: '19800', serviceFee: '1100', deposit: '45000', remark: '', deliveryStatus: 'none' } - ] - }, - { - id: 'p3', - name: '杭州某某租赁氢能项目', - contractCode: 'HZZL20260115YW100089A', - customerName: '杭州某某租赁有限公司', - deliveryRegion: '浙江省 / 杭州市', - deliveryLocation: '浙江省杭州市余杭区未来科技城', - vehicles: [ - { key: 'v11', seq: 3, brand: '东风', model: '氢燃料电池厢货', plateNo: '浙A50001', vin: 'LFV2BJCH8K5678901', monthRent: '9200', serviceFee: '520', deposit: '22000', remark: '城配', deliveryStatus: 'none' }, - { key: 'v12', seq: 4, brand: '开沃', model: '创源氢能轻卡', plateNo: '浙A50002', vin: 'LJXTBACR9N6789012', monthRent: '8800', serviceFee: '480', deposit: '21000', remark: '', deliveryStatus: 'none' } - ] - } - ]; - }, []); - - function filterProjectsByCustomer(customerName) { - var kw = String(customerName || '').trim().toLowerCase(); - if (!kw) return deliveryTaskProjectList; - return deliveryTaskProjectList.filter(function (p) { - return (p.customerName || '').toLowerCase().indexOf(kw) !== -1; - }); - } - - var genDeliveryCustomerOptions = useMemo(function () { - var seen = {}; - var options = []; - deliveryTaskProjectList.forEach(function (p) { - var name = p.customerName; - if (name && !seen[name]) { - seen[name] = true; - options.push({ value: name, label: name }); - } - }); - return options; - }, [deliveryTaskProjectList]); - - var genDeliveryProjectOptions = useMemo(function () { - if (!genDeliveryCustomer) return []; - return deliveryTaskProjectList.filter(function (p) { - return p.customerName === genDeliveryCustomer; - }).map(function (p) { - return { value: p.id, label: p.name }; - }); - }, [genDeliveryCustomer, deliveryTaskProjectList]); - - var genReturnProjectOptions = useMemo(function () { - return filterProjectsByCustomer(genReturnCustomer).map(function (p) { - return { value: p.id, label: p.name }; - }); - }, [genReturnCustomer, deliveryTaskProjectList]); - - var selectedGenDeliveryProject = useMemo(function () { - return deliveryTaskProjectList.find(function (p) { return p.id === genDeliveryProjectId; }) || null; - }, [genDeliveryProjectId, deliveryTaskProjectList]); - - var selectedGenReturnProject = useMemo(function () { - return deliveryTaskProjectList.find(function (p) { return p.id === genReturnProjectId; }) || null; - }, [genReturnProjectId, deliveryTaskProjectList]); - - var genDeliveryVehicleList = useMemo(function () { - if (!selectedGenDeliveryProject) return []; - return (selectedGenDeliveryProject.vehicles || []).filter(function (v) { - return v.deliveryStatus !== 'completed'; - }); - }, [selectedGenDeliveryProject]); - - var genDeliverySelectableVehicles = useMemo(function () { - return genDeliveryVehicleList.filter(function (v) { - return v.deliveryStatus === 'none'; - }); - }, [genDeliveryVehicleList]); - - var genReturnVehicleList = useMemo(function () { - if (!selectedGenReturnProject) return []; - return (selectedGenReturnProject.vehicles || []).filter(function (v) { - return v.plateNo && v.deliveryStatus === 'completed'; - }); - }, [selectedGenReturnProject]); - - var genReturnSelectableVehicles = genReturnVehicleList; - - var genDeliveryRowSelection = useMemo(function () { - return { - selectedRowKeys: genDeliverySelectedKeys, - onChange: function (keys) { setGenDeliverySelectedKeys(keys); }, - getCheckboxProps: function (record) { - var disabled = record.deliveryStatus !== 'none'; - return { - disabled: disabled, - title: disabled ? (record.deliveryStatus === 'submitted' ? '该车辆已创建交车任务' : '该车辆已完成交车') : undefined - }; - }, - selections: [ - { - key: 'all', - text: '全选可选车辆', - onSelect: function () { - setGenDeliverySelectedKeys(genDeliverySelectableVehicles.map(function (v) { return v.key; })); - } - }, - { key: 'none', text: '清空选择', onSelect: function () { setGenDeliverySelectedKeys([]); } } - ] - }; - }, [genDeliverySelectedKeys, genDeliverySelectableVehicles]); - - var genReturnRowSelection = useMemo(function () { - return { - selectedRowKeys: genReturnSelectedKeys, - onChange: function (keys) { setGenReturnSelectedKeys(keys); }, - selections: [ - { - key: 'all', - text: '全选', - onSelect: function () { - setGenReturnSelectedKeys(genReturnSelectableVehicles.map(function (v) { return v.key; })); - } - }, - { key: 'none', text: '清空选择', onSelect: function () { setGenReturnSelectedKeys([]); } } - ] - }; - }, [genReturnSelectedKeys, genReturnSelectableVehicles]); - - function renderGenTaskLabel(text, required) { - return React.createElement('label', { className: 'workbench-gen-task-form-label' }, - required ? React.createElement('span', { className: 'req' }, '*') : null, - text - ); - } - - function resetGenDeliveryFlow() { - setGenDeliveryCustomer(''); - setGenDeliveryProjectId(undefined); - setGenDeliveryExpected(null); - setGenDeliveryBilling(null); - setGenDeliverySelectedKeys([]); - } - - function resetGenReturnFlow() { - setGenReturnCustomer(''); - setGenReturnProjectId(undefined); - setGenReturnDate(null); - setGenReturnSelectedKeys([]); - } - - function openGenDeliveryPick() { - resetGenDeliveryFlow(); - setGenDeliveryPickOpen(true); - } - - function openGenReturnPick() { - resetGenReturnFlow(); - setGenReturnPickOpen(true); - } - - function handleGenDeliveryNext() { - if (!genDeliveryCustomer) { - message.warning('请选择客户名称'); - return; - } - if (!genDeliveryProjectId) { - message.warning('请选择项目名称'); - return; - } - setGenDeliveryExpected(null); - setGenDeliveryBilling(null); - setGenDeliverySelectedKeys([]); - setGenDeliveryPickOpen(false); - setGenDeliveryConfigOpen(true); - } - - function handleGenDeliveryConfirm() { - if (!genDeliveryExpected || !genDeliveryExpected.length) { - message.warning('请选择预计交车日期'); - return; - } - if (!genDeliveryBilling) { - message.warning('请选择开始计费日期'); - return; - } - if (!genDeliverySelectedKeys.length) { - message.warning('请至少勾选一辆车辆'); - return; - } - setGenDeliveryConfigOpen(false); - resetGenDeliveryFlow(); - message.success('已生成交车任务(原型)'); - } - - function handleGenReturnNext() { - if (!genReturnCustomer.trim()) { - message.warning('请输入客户名称'); - return; - } - if (!genReturnProjectId) { - message.warning('请选择项目名称'); - return; - } - setGenReturnDate(null); - setGenReturnSelectedKeys([]); - setGenReturnPickOpen(false); - setGenReturnConfigOpen(true); - } - - function handleGenReturnConfirm() { - if (!genReturnDate) { - message.warning('请选择预计还车日期'); - return; - } - if (!genReturnSelectedKeys.length) { - message.warning('请至少勾选一辆车辆'); - return; - } - setGenReturnConfigOpen(false); - resetGenReturnFlow(); - message.success('已生成还车任务(原型)'); - } - - function handleQuickItemClick(it) { - if (it.action === 'genDelivery') { - openGenDeliveryPick(); - return; - } - if (it.action === 'genReturn') { - openGenReturnPick(); - return; - } - if (it.p) protoNav(it.p); - } - - var genDeliveryVehicleColumns = useMemo(function () { - return [ - { title: '序号', dataIndex: 'seq', key: 'seq', width: 56 }, - { title: '品牌', dataIndex: 'brand', key: 'brand', width: 88 }, - { title: '型号', dataIndex: 'model', key: 'model', width: 140, ellipsis: true }, - { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 100, render: function (v) { return v || '—'; } }, - { title: '车辆识别代码', dataIndex: 'vin', key: 'vin', width: 168, ellipsis: true }, - { title: '月租金', dataIndex: 'monthRent', key: 'monthRent', width: 88, render: function (v) { return v ? v + ' 元' : '—'; } }, - { title: '备注', dataIndex: 'remark', key: 'remark', width: 100, ellipsis: true, render: function (v) { return v || '—'; } } - ]; - }, []); - - var genReturnVehicleColumns = useMemo(function () { - return [ - { title: '序号', dataIndex: 'seq', key: 'seq', width: 56 }, - { title: '品牌', dataIndex: 'brand', key: 'brand', width: 88 }, - { title: '型号', dataIndex: 'model', key: 'model', width: 140, ellipsis: true }, - { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 100 }, - { title: '车辆识别代码', dataIndex: 'vin', key: 'vin', width: 168, ellipsis: true } - ]; - }, []); - - var quickByRole = useMemo(function () { - return { - ye: { - label: '业管', - items: [ - { t: '生成交车任务', action: 'genDelivery', accent: 'primary' }, - { t: '生成还车任务', action: 'genReturn', accent: 'orange' }, - { t: '租赁合同', p: 'web端/车辆租赁合同/车辆租赁合同.jsx' }, - { t: '提车应收款', p: '/prototypes/vehicle-pickup-receivable' }, - { t: '租赁账单', p: 'web端/业务管理/租赁账单.jsx' }, - { t: '还车应结款', p: 'web端/财务管理/还车应结款.jsx' }, - { t: '能源账户', p: 'web端/业务管理/能源账户.jsx' }, - { t: '氢费账单', p: 'web端/财务管理/氢费账单.jsx' }, - { t: '电费账单', p: 'web端/财务管理/电费账单.jsx' }, - { t: 'ETC账单', p: 'web端/业务管理/ETC管理.jsx' }, - { t: '保险采购', p: 'web端/业务管理/保险采购.jsx' }, - { t: '审批中心', p: 'web端/审批中心.jsx' }, - { t: '意见建议', p: 'web端/意见建议.jsx' } - ] - }, - yeEnergy: { - label: '业管-能源部', - items: [ - { t: '站点信息', p: 'web端/加氢站管理/站点信息.jsx' }, - { t: '加氢订单管理', p: 'web端/加氢站管理/加氢订单.jsx' }, - { t: '意见建议', p: 'web端/意见建议.jsx' } - ] - }, - ops: { - label: '运维', - items: [ - { t: '车辆管理', p: 'web端/车辆管理.jsx' }, - { t: '证照管理', p: 'web端/运维管理/车辆业务/证照管理.jsx' }, - { t: '证照管理-编辑', p: 'web端/运维管理/车辆业务/证照管理-编辑.jsx' }, - { t: '备车管理', p: 'web端/运维管理/车辆业务/备车管理.jsx' }, - { t: '交车管理', p: 'web端/运维管理/车辆业务/交车管理.jsx' }, - { t: '还车管理', p: 'web端/运维管理/车辆业务/还车管理.jsx' }, - { t: '替换车管理', p: 'web端/运维管理/车辆业务/替换车管理.jsx' }, - { t: '调拨管理', p: 'web端/运维管理/车辆业务/调拨管理.jsx' }, - { t: '异动管理', p: 'web端/运维管理/车辆业务/异动管理.jsx' }, - { t: '备件库管理', p: 'web端/运维管理/备件管理/备件库存.jsx' }, - { t: '意见建议', p: 'web端/意见建议.jsx' } - ] - }, - finance: { - label: '财务', - items: [ - { t: '提车应收款', p: '/prototypes/vehicle-pickup-receivable' }, - { t: '租赁账单', p: 'web端/财务管理/租赁账单.jsx' }, - { t: '还车应结款', p: 'web端/财务管理/还车应结款.jsx' }, - { t: '审批中心', p: 'web端/审批中心.jsx' }, - { t: '充值单管理', p: 'web端/财务管理/充值单管理.jsx' }, - { t: '意见建议', p: 'web端/意见建议.jsx' } - ] - }, - safety: { - label: '安全', - items: [ - { t: '违章管理', p: 'web端/安全管理/违章管理.jsx' }, - { t: '事故管理', p: 'web端/安全管理/事故管理.jsx' }, - { t: '司机管理', p: 'web端/安全管理/司机管理.jsx' }, - { t: '安全培训资料', p: 'web端/安全管理/安全培训资料.jsx' }, - { t: '安全培训记录', p: 'web端/安全管理/安全培训记录.jsx' }, - { t: '意见建议', p: 'web端/意见建议.jsx' } - ] - }, - legal: { - label: '法务', - items: [ - { t: '审批中心', p: 'web端/审批中心.jsx' }, - { t: '意见建议', p: 'web端/意见建议.jsx' } - ] - } - }; - }, []); - - var openQuickCustomize = useCallback(function () { - var role = quickByRole[roleTab]; - var catalog = role ? role.items : []; - var saved = quickCustom[roleTab]; - var keys = saved && saved.length - ? saved.slice() - : catalog.map(quickItemKey); - setQuickCustomDraft(keys); - setQuickCustomizeOpen(true); - }, [quickByRole, roleTab, quickCustom]); - - var toggleQuickDraftKey = useCallback(function (key, checked) { - setQuickCustomDraft(function (prev) { - var next = (prev || []).slice(); - if (checked) { - if (next.indexOf(key) < 0) next.push(key); - } else { - next = next.filter(function (k) { return k !== key; }); - } - return next; - }); - }, []); - - var moveQuickDraftKey = useCallback(function (key, dir) { - setQuickCustomDraft(function (prev) { - var next = (prev || []).slice(); - var idx = next.indexOf(key); - if (idx < 0) return next; - var swap = dir === 'up' ? idx - 1 : idx + 1; - if (swap < 0 || swap >= next.length) return next; - var tmp = next[swap]; - next[swap] = next[idx]; - next[idx] = tmp; - return next; - }); - }, []); - - var saveQuickCustomize = useCallback(function () { - if (!quickCustomDraft.length) { - message.warning('请至少保留一个快速入口'); - return; - } - var next = Object.assign({}, quickCustom); - next[roleTab] = quickCustomDraft.slice(); - setQuickCustom(next); - saveQuickCustom(next); - setQuickCustomizeOpen(false); - message.success('快速入口已保存'); - }, [quickCustom, quickCustomDraft, roleTab]); - - var resetQuickCustomize = useCallback(function () { - var next = Object.assign({}, quickCustom); - delete next[roleTab]; - setQuickCustom(next); - saveQuickCustom(next); - var role = quickByRole[roleTab]; - setQuickCustomDraft(role ? role.items.map(quickItemKey) : []); - message.success('已恢复默认快速入口'); - }, [quickByRole, quickCustom, roleTab]); - - var noticeColumns = useMemo(function () { - return [ - { title: '时间', dataIndex: 'time', key: 'time', width: 150 }, - { title: '通知类型', dataIndex: 'type', key: 'type', width: 160 }, - { title: '内容', dataIndex: 'content', key: 'content', ellipsis: true }, - { - title: '状态', - key: 'read', - width: 72, - render: function (_, record) { - return record.read - ? React.createElement(Tag, { color: 'default', style: { margin: 0, fontSize: 11, lineHeight: '18px' } }, '已读') - : React.createElement(Tag, { color: 'warning', style: { margin: 0, fontSize: 11, lineHeight: '18px' } }, '未读'); - } - }, - { - title: '操作', - key: 'actions', - width: 168, - render: function (_, record) { - var more = []; - if (!record.read) { - more.push({ - key: 'read', - label: '已读', - onClick: function () { markNoticeRead(record.id); } - }); - } - return React.createElement(OperationActions, { - view: { label: '预览', onClick: function () { openNoticePreview(record); } }, - more: more - }); - } - } - ]; - }, [markNoticeRead, openNoticePreview]); - - var overdueDeliveryPopoverTableStyle = { width: '100%', borderCollapse: 'collapse', fontSize: 12 }; - var overdueDeliveryPopoverThStyle = { padding: '6px 8px', textAlign: 'left', borderBottom: '1px solid #f0f0f0', backgroundColor: '#fafafa', fontWeight: 600 }; - var overdueDeliveryPopoverTdStyle = { padding: '6px 8px', borderBottom: '1px solid #f0f0f0' }; - - function renderOverdueDeliveryQuantity(record) { - var list = record.vehicleList || []; - var content = React.createElement('div', { style: { padding: 8, minWidth: 420 } }, - React.createElement('div', { style: { marginBottom: 8, fontWeight: 600 } }, '车辆明细'), - React.createElement('table', { style: overdueDeliveryPopoverTableStyle }, - React.createElement('thead', null, - React.createElement('tr', null, - React.createElement('th', { style: overdueDeliveryPopoverThStyle }, '车辆类型'), - React.createElement('th', { style: overdueDeliveryPopoverThStyle }, '品牌'), - React.createElement('th', { style: overdueDeliveryPopoverThStyle }, '型号'), - React.createElement('th', { style: overdueDeliveryPopoverThStyle }, '车牌号'), - React.createElement('th', { style: overdueDeliveryPopoverThStyle }, '交车时间'), - React.createElement('th', { style: overdueDeliveryPopoverThStyle }, '交车人员') - ) - ), - React.createElement('tbody', null, - list.map(function (v, i) { - return React.createElement('tr', { key: i }, - React.createElement('td', { style: overdueDeliveryPopoverTdStyle }, v.vehicleType || '-'), - React.createElement('td', { style: overdueDeliveryPopoverTdStyle }, v.brand || '-'), - React.createElement('td', { style: overdueDeliveryPopoverTdStyle }, v.model || '-'), - React.createElement('td', { style: overdueDeliveryPopoverTdStyle }, v.plateNo || '-'), - React.createElement('td', { style: overdueDeliveryPopoverTdStyle }, (v.deliveryTime && v.deliveryTime !== '-') ? v.deliveryTime : '-'), - React.createElement('td', { style: overdueDeliveryPopoverTdStyle }, v.deliveryPerson || '-') - ); - }) - ) - ) - ); - return React.createElement(Popover, { content: content, title: null }, - React.createElement('span', { style: { color: '#1677ff', cursor: 'pointer', fontWeight: 600 } }, record.deliveryCount + ' 辆') - ); - } - - var overdueReturnModalColumns = [ - { title: '交车时间', dataIndex: 'deliveryTime', key: 'deliveryTime', width: 160 }, - { title: '交车人', dataIndex: 'deliveryPerson', key: 'deliveryPerson', width: 90 }, - { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 100 }, - { title: '车辆类型', dataIndex: 'vehicleType', key: 'vehicleType', width: 120, ellipsis: true }, - { title: '品牌', dataIndex: 'brand', key: 'brand', width: 80 }, - { title: '型号', dataIndex: 'model', key: 'model', width: 110, ellipsis: true }, - { title: '车辆识别代码', dataIndex: 'vin', key: 'vin', width: 160, ellipsis: true }, - { title: '合同编码', dataIndex: 'contractCode', key: 'contractCode', width: 130, ellipsis: true }, - { title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 160, ellipsis: true }, - { title: '项目名称', dataIndex: 'projectName', key: 'projectName', width: 140, ellipsis: true }, - { title: '业务部门', dataIndex: 'dept', key: 'dept', width: 110, ellipsis: true }, - { title: '业务负责人', dataIndex: 'bizOwner', key: 'bizOwner', width: 100 }, - { - title: '操作', - key: 'action', - width: 168, - fixed: 'right', - render: function (_, record) { - return React.createElement(OperationActions, { - view: { - label: record.vehicleArrived ? '还车' : '车辆到达', - onClick: function () { - if (record.vehicleArrived) message.info('跳转还车管理-还车页(原型)'); - else message.info('确认车辆到达(原型,联调对接还车管理)'); - } - } - }); - } - } - ]; - - var inspectModalColumns = [ - { title: '车牌号', dataIndex: 'plateNo', key: 'plateNo', width: 120 }, - { title: '车辆类型', dataIndex: 'vehicleType', key: 'vehicleType', width: 140 }, - { title: '品牌', dataIndex: 'brand', key: 'brand', width: 100 }, - { title: '型号', dataIndex: 'model', key: 'model', width: 120 }, - { title: '下次检验/等评日期', dataIndex: 'nextDate', key: 'nextDate', width: 160 }, - { title: '剩余天数', dataIndex: 'remainDays', key: 'remainDays', width: 120, render: function(v) { - return React.createElement(Text, { type: v < 0 ? 'danger' : undefined }, v + ' 天'); - } }, - { - title: '操作', - key: 'action', - width: 168, - fixed: 'right', - render: function (_, record) { - return React.createElement(OperationActions, { - view: { label: '年审', onClick: function () { message.info('跳转年审详情(原型)'); } } - }); - } - } - ]; - - var overdueDeliveryModalColumns = [ - { title: '预计交车时间', dataIndex: 'expectedDate', key: 'expectedDate', width: 220, ellipsis: true }, - { title: '合同编码', dataIndex: 'contractCode', key: 'contractCode', width: 150, ellipsis: true }, - { title: '项目名称', dataIndex: 'projectName', key: 'projectName', width: 150, ellipsis: true }, - { title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 160, ellipsis: true }, - { title: '交车区域', dataIndex: 'deliveryRegion', key: 'deliveryRegion', width: 120, ellipsis: true }, - { title: '交车地点', dataIndex: 'deliveryAddress', key: 'deliveryAddress', width: 200, ellipsis: true }, - { title: '交车数量', key: 'deliveryCount', width: 90, render: function (_, r) { return renderOverdueDeliveryQuantity(r); } }, - { title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 160, ellipsis: true }, - { title: '创建人', dataIndex: 'createBy', key: 'createBy', width: 90, ellipsis: true }, - { title: '最后修改时间', dataIndex: 'lastModifyTime', key: 'lastModifyTime', width: 160, ellipsis: true }, - { title: '最后修改人', dataIndex: 'lastModifyBy', key: 'lastModifyBy', width: 90, ellipsis: true }, - { title: '操作', key: 'action', width: 168, fixed: 'right', render: function () { - return React.createElement(OperationActions, { - view: { label: '交车单', onClick: function () { message.info('跳转交车管理-交车单(原型)'); } } - }); - } } - ]; - - function renderTodoActionCell(_, r) { - var meta = todoActionMeta[r.actionType] || todoActionMeta.execute; - return React.createElement(Tag, { color: meta.color, style: { margin: 0 } }, meta.label); - } - - function renderTodoRiskCell(_, r) { - var meta = todoRiskMeta[r.riskLevel] || todoRiskMeta.none; - return React.createElement(Space, { size: 4, direction: 'vertical', style: { lineHeight: 1.35 } }, - React.createElement(Tag, { color: meta.color, style: { margin: 0 } }, '风险' + meta.label), - r.riskHint ? React.createElement(Text, { type: 'secondary', style: { fontSize: 12 } }, r.riskHint) : null - ); - } - - function renderTodoChainCell(_, r) { - if (!r.nextOwner || r.actionType === 'notify') { - return React.createElement(Text, { type: 'secondary', style: { fontSize: 12 } }, '—'); - } - return React.createElement('div', { style: { fontSize: 12, lineHeight: 1.45, color: 'rgba(0,0,0,0.65)' } }, - React.createElement('span', null, '→ '), - React.createElement('strong', { style: { color: 'rgba(0,0,0,0.88)', fontWeight: 500 } }, r.nextOwner), - React.createElement('span', null, ' · ' + (r.nextAction || '—')), - r.slaRemain ? React.createElement('div', { style: { marginTop: 2, color: r.slaRemain.indexOf('超时') >= 0 ? '#f5222d' : 'rgba(0,0,0,0.45)' } }, r.slaRemain) : null - ); - } - - function renderTodoStatusCell(_, r) { - if (r.status === 'pending') return React.createElement(Tag, { color: 'processing' }, '待处理'); - if (r.status === 'overdue') return React.createElement(Tag, { color: 'error' }, '已超时'); - if (r.status === 'done') return React.createElement(Tag, { color: 'success' }, '已完成'); - return React.createElement(Tag, null, r.status); - } - - function renderTodoStatChip(filterKey, labelText, count, pillBg, numColor) { - var selected = todoBoardFilter === filterKey; - return React.createElement('span', { - className: 'workbench-filter-chip' + (selected ? ' workbench-filter-chip--active' : ''), - role: 'button', - tabIndex: 0, - 'data-tone': filterKey, - onClick: function () { setTodoBoardFilter(filterKey); }, - onKeyDown: function (e) { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - setTodoBoardFilter(filterKey); - } - } - }, - React.createElement('span', { className: 'workbench-filter-chip-label' }, labelText), - React.createElement('span', { - className: 'workbench-filter-chip-count', - style: { color: numColor } - }, count) - ); - } - - var todoTableColumns = useMemo(function () { - return [ - { title: '行动', dataIndex: 'actionType', key: 'actionType', width: 88, render: renderTodoActionCell }, - { title: '风险', key: 'risk', width: 148, render: renderTodoRiskCell }, - { title: '任务类型', dataIndex: 'taskType', key: 'taskType', width: 96 }, - { title: '任务名称', dataIndex: 'taskName', key: 'taskName', ellipsis: true }, - { title: '责任链', key: 'chain', width: 188, render: renderTodoChainCell }, - { - title: '状态', - key: 'status', - width: 88, - render: renderTodoStatusCell - }, - { - title: '任务时间', - dataIndex: 'genDate', - key: 'genDate', - width: 148, - showSorterTooltip: false, - defaultSortOrder: 'descend', - sorter: function (a, b) { - return String(a.genDate || '').localeCompare(String(b.genDate || '')); - } - }, - { - title: '操作', - key: 'op', - width: 168, - align: 'right', - render: function (_, r) { - var isDone = r.status === 'done'; - var isNotify = r.actionType === 'notify'; - var primaryLabel = isDone ? '查看' : (isNotify ? '查看' : (r.actionType === 'approve' ? '去审批' : (r.actionType === 'initiate' ? '去发起' : '处理'))); - var more = []; - if (!isDone && !isNotify) { - more.push({ key: 'urge', label: '催办', onClick: function () { pushUrgeNotice(r); } }); - } - return React.createElement(OperationActions, { - view: { label: primaryLabel, onClick: function () { protoNav(r.path); } }, - more: more - }); - } - } - ]; - }, [pushUrgeNotice]); - - var todoMoreModalColumns = useMemo(function () { - return [ - { title: '行动', dataIndex: 'actionType', key: 'actionType', width: 88, render: renderTodoActionCell }, - { title: '风险', key: 'risk', width: 148, render: renderTodoRiskCell }, - { title: '任务类型', dataIndex: 'taskType', key: 'taskType', width: 96 }, - { title: '任务名称', dataIndex: 'taskName', key: 'taskName', ellipsis: true }, - { title: '责任链', key: 'chain', width: 188, render: renderTodoChainCell }, - { - title: '状态', - key: 'status', - width: 88, - render: renderTodoStatusCell - }, - { - title: '任务时间', - dataIndex: 'genDate', - key: 'genDate', - width: 148, - showSorterTooltip: false, - defaultSortOrder: 'descend', - sorter: function (a, b) { - return String(a.genDate || '').localeCompare(String(b.genDate || '')); - } - }, - { - title: '操作', - key: 'op', - width: 168, - align: 'right', - render: function (_, r) { - var isDone = r.status === 'done'; - var isNotify = r.actionType === 'notify'; - var primaryLabel = isDone ? '查看' : (isNotify ? '查看' : (r.actionType === 'approve' ? '去审批' : (r.actionType === 'initiate' ? '去发起' : '处理'))); - var more = []; - if (!isDone && !isNotify) { - more.push({ key: 'urge', label: '催办', onClick: function () { pushUrgeNotice(r); } }); - } - return React.createElement(OperationActions, { - view: { label: primaryLabel, onClick: function () { protoNav(r.path); } }, - more: more - }); - } - } - ]; - }, [pushUrgeNotice]); - - var todoMoreFilteredRows = useMemo(function () { - var rows = dashboardTodoRows.filter(function (r) { - if (todoMoreTaskType && r.taskType !== todoMoreTaskType) return false; - if (todoMoreStatus && r.status !== todoMoreStatus) return false; - if (todoMoreDateStart && r.genDate < todoMoreDateStart) return false; - if (todoMoreDateEnd && r.genDate > todoMoreDateEnd) return false; - return true; - }); - rows.sort(function (a, b) { - return String(b.genDate || '').localeCompare(String(a.genDate || '')); - }); - return rows; - }, [dashboardTodoRows, todoMoreTaskType, todoMoreStatus, todoMoreDateStart, todoMoreDateEnd]); - - var openTodoMoreModal = useCallback(function () { - setTodoMoreTaskType(undefined); - setTodoMoreStatus(undefined); - setTodoMoreDateStart(''); - setTodoMoreDateEnd(''); - setTodoMoreModalOpen(true); - }, []); - - var todoMoreRangeValue = useMemo(function () { - var ds = todoMoreDateStart; - var de = todoMoreDateEnd; - if (!ds && !de) return null; - var dayjs = typeof window !== 'undefined' ? window.dayjs : null; - var moment = typeof window !== 'undefined' ? window.moment : null; - if (dayjs) { - return [ - ds ? dayjs(ds, 'YYYY-MM-DD') : null, - de ? dayjs(de, 'YYYY-MM-DD') : null - ]; - } - if (moment) { - return [ - ds ? moment(ds, 'YYYY-MM-DD') : null, - de ? moment(de, 'YYYY-MM-DD') : null - ]; - } - return null; - }, [todoMoreDateStart, todoMoreDateEnd]); - - function renderWorkbenchMetricHintIcon(cardKey) { - var hint = workbenchWarningMetricHintByKey[cardKey]; - if (!hint) return null; - var titleNode = React.createElement('div', { style: { maxWidth: 368, lineHeight: 1.65, fontSize: 12, textAlign: 'left', whiteSpace: 'pre-line' } }, hint); - var stopBubble = function (e) { - if (e && e.stopPropagation) e.stopPropagation(); - }; - return React.createElement(Tooltip, { - title: titleNode, - placement: 'top', - mouseEnterDelay: 0.08, - destroyTooltipOnHide: true - }, - React.createElement('span', { - className: 'workbench-metric-hint-icon', - role: 'img', - 'aria-label': '指标说明', - tabIndex: 0, - style: { - display: 'inline-flex', - alignItems: 'center', - justifyContent: 'center', - marginLeft: 2, - cursor: 'help', - color: 'rgba(0,0,0,0.4)', - flexShrink: 0, - verticalAlign: 'middle', - lineHeight: 1 - }, - onClick: stopBubble, - onMouseDown: stopBubble, - onKeyDown: function (e) { - if (e.key === 'Enter' || e.key === ' ') stopBubble(e); - } - }, - React.createElement('svg', { width: 14, height: 14, viewBox: '0 0 1024 1024', fill: 'currentColor', 'aria-hidden': true }, - React.createElement('path', { d: 'M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z' }), - React.createElement('path', { d: 'M464 336a48 48 0 1 0 96 0 48 48 0 1 0-96 0zm72 112h-48c-4.4 0-8 3.6-8 8v272c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V456c0-4.4-3.6-8-8-8z' }) - ) - ) - ); - } - - function renderVehicleMetricCard(s) { - var onClick = s.onClick; - var mw = s.minWidth != null ? s.minWidth : 128; - var trailing = s.trailing; - var valueRow = s.valueRow; - var minCardH = 84; - if (trailing || valueRow) minCardH = 96; - var cardStyleMerged = Object.assign({}, cardStyle, { - flex: 1, - minWidth: mw, - minHeight: minCardH, - display: 'flex', - flexDirection: 'column', - height: '100%' - }); - if (onClick) { - cardStyleMerged.cursor = 'pointer'; - } - return React.createElement(Card, { - key: s.key, - className: 'workbench-warning-metric-card workbench-metric-card' + (onClick ? ' workbench-metric-card--interactive' : ''), - size: 'small', - bordered: false, - style: cardStyleMerged, - bodyStyle: { - padding: '16px 18px', - flex: 1, - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', - minHeight: 0 - }, - onClick: onClick, - onKeyDown: onClick - ? function (e) { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - onClick(e); - } - } - : undefined, - tabIndex: onClick ? 0 : undefined, - role: onClick ? 'button' : undefined - }, - React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 12, width: '100%', boxSizing: 'border-box' } }, - React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 12, flex: 1, minWidth: 0 } }, - React.createElement('div', { - className: 'workbench-metric-icon', - style: { background: s.iconBg, color: s.color } - }, s.icon), - React.createElement('div', { className: 'workbench-metric-copy' }, - React.createElement('div', { className: 'workbench-metric-title-row' }, - React.createElement('span', { className: 'workbench-metric-title' }, s.title), - renderWorkbenchMetricHintIcon(s.key) - ), - valueRow != null - ? valueRow - : React.createElement('div', { className: 'workbench-metric-value' }, s.value) - ) - ), - trailing || null - ) - ); - } - - function renderWarningMetricCardItem(s) { - var minW = 152; - if (s.key === 'w_en_h2_import_today') minW = 280; - if (s.key === 'w_fin_energy_recharge') minW = 260; - var cardProps = { - key: s.key, - title: s.title, - value: s.key === 'w_en_h2_import_today' ? energyH2ImportTodayCount : s.value, - iconBg: s.iconBg, - icon: s.icon, - color: s.color, - minWidth: minW, - onClick: function () { - if (s.key === 'w_ops_delivery') { - openOverdueDeliveryModal(); - return; - } - if (s.key === 'w_ops_return_cnt') { - openOverdueReturnModal(); - return; - } - if (s.key === 'w_ops_inspect') { - setInspectModalOpen(true); - return; - } - if (s.key === 'w_ops_maint') { - message.info('未来上线,敬请期待'); - return; - } - message.info('「' + s.title + '」明细(原型,联调接口后打开列表)'); - } - }; - if (s.key === 'w_en_h2_import_today') { - cardProps.valueRow = energyH2ImportTodayCount > 0 - ? React.createElement('div', { className: 'workbench-metric-value' }, energyH2ImportTodayCount) - : React.createElement('div', { className: 'workbench-metric-hint-copy' }, - '今日还未导入任何加氢记录,请确认是否有加氢明细需要导入' - ); - } - if (s.key === 'w_fin_energy_recharge') { - cardProps.value = financeEnergyRechargeYuan; - cardProps.valueRow = financeEnergyRechargeYuan != null - ? React.createElement('div', { className: 'workbench-metric-value workbench-metric-value--compact' }, formatWorkbenchFinanceYuan(financeEnergyRechargeYuan)) - : React.createElement('div', { className: 'workbench-metric-hint-copy' }, - '请确认是否有能源账户充值记录' - ); - } - return renderVehicleMetricCard(cardProps); - } - - function getWarningRiskMeta(value) { - var v = typeof value === 'number' ? value : 0; - if (v >= 5) return { level: 'high', label: '高', color: '#f5222d', bg: 'rgba(245,34,45,0.08)' }; - if (v >= 2) return { level: 'medium', label: '中', color: '#fa8c16', bg: 'rgba(250,140,22,0.1)' }; - return { level: 'low', label: '低', color: '#1677ff', bg: 'rgba(22,119,255,0.08)' }; - } - - function renderWarningSchemeSwitcher() { - return React.createElement('div', { className: 'workbench-kpi-scheme-bar' }, - React.createElement('div', { className: 'workbench-kpi-scheme-copy' }, - React.createElement('span', { className: 'workbench-kpi-scheme-kicker' }, 'KPI 方案比稿'), - React.createElement(Text, { type: 'secondary', style: { fontSize: 12 } }, - warningKpiScheme === 'legacy' - ? '方案 A:按部门 Tab 切换,指标平铺(现状)' - : '方案 B:按登录角色推送,首屏主预警 + 财务聚合 + 跨部门进度' - ) - ), - React.createElement(Space, { size: 8, wrap: true }, - React.createElement(Button, { - size: 'small', - type: warningKpiScheme === 'legacy' ? 'primary' : 'default', - onClick: function () { setWarningKpiScheme('legacy'); } - }, '方案 A · 部门 Tab'), - React.createElement(Button, { - size: 'small', - type: warningKpiScheme === 'radar' ? 'primary' : 'default', - onClick: function () { - setWarningKpiScheme('radar'); - setWarningDeptKey(warningRadarRoleDeptKey); - } - }, '方案 B · 角色预警台') - ) - ); - } - - function renderWarningRadarSummaryBar() { - return React.createElement('div', { className: 'workbench-warning-radar-summary' }, - React.createElement('button', { type: 'button', className: 'workbench-warning-radar-chip workbench-warning-radar-chip--focus' }, - React.createElement('b', null, warningRadarSummary.focus), - ' 今日需关注' - ), - React.createElement('button', { type: 'button', className: 'workbench-warning-radar-chip workbench-warning-radar-chip--high' }, - React.createElement('b', null, warningRadarSummary.high), - ' 高风险' - ), - React.createElement('button', { type: 'button', className: 'workbench-warning-radar-chip workbench-warning-radar-chip--overdue' }, - React.createElement('b', null, warningRadarSummary.overdue), - ' 已超期' - ), - React.createElement('button', { type: 'button', className: 'workbench-warning-radar-chip' }, - React.createElement('b', null, warningRadarSummary.mine), - ' 我主责' - ), - React.createElement('div', { className: 'workbench-warning-radar-perspective' }, - React.createElement(Text, { type: 'secondary', style: { fontSize: 12, marginRight: 8 } }, '预警视角'), - React.createElement(Select, { - size: 'small', - style: { minWidth: 132 }, - value: roleTab, - options: quickRoleKeys.map(function (k) { - return { value: k, label: quickByRole[k] ? quickByRole[k].label : k }; - }), - onChange: function (val) { - if (!val) return; - setRoleTab(val); - setWarningDeptKey(roleToWarningDeptKey[val] || 'ops'); - } - }) - ) - ); - } - - function renderWarningRadarEnhancedCard(s, opts) { - opts = opts || {}; - var risk = getWarningRiskMeta(opts.value != null ? opts.value : s.value); - if (opts.risk === 'high') risk = getWarningRiskMeta(9); - if (opts.risk === 'medium') risk = getWarningRiskMeta(3); - var owner = opts.owner || (quickByRole[roleTab] ? quickByRole[roleTab].label : '运维'); - var actionLabel = opts.action || '查看明细'; - var onClick = opts.onClick || function () { - message.info('「' + (opts.title || s.title) + '」明细(方案 B 原型)'); - }; - return React.createElement(Card, { - key: opts.key || s.key, - className: 'workbench-warning-radar-card workbench-metric-card workbench-metric-card--interactive', - size: 'small', - bordered: false, - style: Object.assign({}, cardStyle, { flex: '1 1 240px', minWidth: 240, minHeight: 112, cursor: 'pointer' }), - bodyStyle: { padding: '14px 16px' }, - onClick: onClick, - tabIndex: 0, - role: 'button' - }, - React.createElement('div', { className: 'workbench-warning-radar-card-top' }, - React.createElement(Tag, { color: risk.level === 'high' ? 'error' : (risk.level === 'medium' ? 'warning' : 'processing'), style: { margin: 0 } }, '风险' + risk.label), - React.createElement(Text, { type: 'secondary', style: { fontSize: 12 } }, '主责 · ' + owner) - ), - React.createElement('div', { className: 'workbench-warning-radar-card-title' }, opts.title || s.title), - React.createElement('div', { className: 'workbench-warning-radar-card-value' }, opts.stats || s.value), - opts.amount ? React.createElement('div', { className: 'workbench-warning-radar-card-sub' }, opts.amount) : null, - React.createElement('div', { className: 'workbench-warning-radar-card-action' }, '→ ' + actionLabel) - ); - } - - function renderReturnSettleProgressCard() { - var steps = [ - { key: 'ops', label: '运维', state: 'done' }, - { key: 'biz', label: '业管', state: 'done' }, - { key: 'energy', label: '能源', state: 'active' }, - { key: 'safe', label: '安全', state: 'todo' }, - { key: 'fin', label: '财务', state: 'todo' } - ]; - return React.createElement(Card, { - key: 'return-settle-progress', - className: 'workbench-warning-progress-card workbench-metric-card', - size: 'small', - bordered: false, - style: Object.assign({}, cardStyle, { flex: '1 1 100%', minWidth: 0, width: '100%' }), - bodyStyle: { padding: '14px 16px' } - }, - React.createElement('div', { className: 'workbench-warning-radar-card-top' }, - React.createElement(Tag, { color: 'warning', style: { margin: 0 } }, '跨部门协同'), - React.createElement(Text, { type: 'secondary', style: { fontSize: 12 } }, '3 单阻塞中 · 最近粤B58888F') - ), - React.createElement('div', { className: 'workbench-warning-radar-card-title' }, '还车应结款 · 部门进度'), - React.createElement('div', { className: 'workbench-warning-progress-track' }, - steps.map(function (step) { - return React.createElement('div', { - key: step.key, - className: 'workbench-warning-progress-step workbench-warning-progress-step--' + step.state - }, - React.createElement('span', { className: 'workbench-warning-progress-dot' }), - React.createElement('span', { className: 'workbench-warning-progress-label' }, step.label) - ); - }) - ), - React.createElement('div', { className: 'workbench-warning-radar-card-action' }, '→ 查看阻塞单据(能源部待核对 2 单)') - ); - } - - function renderWarningRadarMonitorFold() { - if (!warningRadarMonitorItems.length) return null; - return React.createElement('div', { className: 'workbench-warning-radar-fold' }, - React.createElement('button', { - type: 'button', - className: 'workbench-warning-radar-fold-head', - onClick: function () { setWarningRadarFoldOpen(function (v) { return !v; }); } - }, - React.createElement('span', null, '其他监控指标(', warningRadarMonitorItems.length, ')'), - React.createElement('span', null, warningRadarFoldOpen ? '收起 ▴' : '展开 ▾') - ), - warningRadarFoldOpen - ? React.createElement('div', { className: 'workbench-warning-radar-fold-body' }, - warningRadarMonitorItems.map(function (it) { - return React.createElement('div', { key: it.key, className: 'workbench-warning-radar-fold-item' }, - React.createElement('strong', null, it.title), - React.createElement('span', null, it.copy) - ); - }) - ) - : null - ); - } - - function renderWarningKpiLegacyBlock() { - return React.createElement('div', { className: 'workbench-top-metrics', style: { display: 'flex', flexWrap: 'wrap', gap: layoutGutter, alignItems: 'stretch' } }, - React.createElement('div', { - className: 'workbench-warning-cards' + (warningDeptKey === 'finance' ? ' workbench-warning-cards--finance' : ''), - style: warningDeptKey === 'finance' - ? { flex: '1 1 100%', display: 'flex', flexDirection: 'column', gap: layoutGutter, alignItems: 'stretch', minWidth: 0, width: '100%' } - : { flex: '1 1 280px', display: 'flex', flexWrap: 'wrap', gap: layoutGutter, alignItems: 'stretch', minWidth: 0 } - }, - warningDeptKey === 'finance' - ? [ - React.createElement('div', { - key: 'wb-fin-row1', - style: { display: 'flex', flexWrap: 'wrap', gap: layoutGutter, width: '100%', alignItems: 'stretch' } - }, warningCardsVisible.slice(0, 5).map(function (s) { return renderWarningMetricCardItem(s); })), - React.createElement('div', { - key: 'wb-fin-row2', - style: { display: 'flex', flexWrap: 'wrap', gap: layoutGutter, width: '100%', alignItems: 'stretch' } - }, warningCardsVisible.slice(5).map(function (s) { return renderWarningMetricCardItem(s); })) - ] - : warningCardsVisible.map(function (s) { return renderWarningMetricCardItem(s); }) - ) - ); - } - - function renderWarningKpiRadarBlock() { - var primaryCards = warningRadarRoleDeptKey === 'finance' - ? warningFinanceAggCards.map(function (c) { return renderWarningRadarEnhancedCard({}, c); }) - : warningRadarPrimaryCards.map(function (s) { - return renderWarningRadarEnhancedCard(s, { - owner: quickByRole[roleTab] ? quickByRole[roleTab].label : '运维', - onClick: function () { - if (s.key === 'w_ops_delivery') { openOverdueDeliveryModal(); return; } - if (s.key === 'w_ops_return_cnt') { openOverdueReturnModal(); return; } - if (s.key === 'w_ops_inspect') { setInspectModalOpen(true); return; } - message.info('「' + s.title + '」明细(方案 B)'); - } - }); - }); - return React.createElement('div', { className: 'workbench-warning-radar-wrap' }, - renderWarningRadarSummaryBar(), - React.createElement('div', { className: 'workbench-warning-radar-primary', style: { display: 'flex', flexWrap: 'wrap', gap: layoutGutter } }, primaryCards), - (warningRadarRoleDeptKey === 'business' || warningRadarRoleDeptKey === 'energy' || warningRadarRoleDeptKey === 'safety' || warningRadarRoleDeptKey === 'finance') - ? renderReturnSettleProgressCard() - : null, - renderWarningRadarMonitorFold(), - React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', width: '100%' } }, - React.createElement(Button, { - type: 'link', - size: 'small', - style: { padding: 0 }, - onClick: function () { message.info('全部预警抽屉(方案 B 原型)'); } - }, '查看全部预警') - ) - ); - } - - function renderNoticePanelCard() { - var preview = (noticeList || []).slice(0, wbDashPreviewMax); - var titleNode = React.createElement(Space, { size: 8, wrap: true, align: 'center' }, - React.createElement('span', { className: 'workbench-dash-pair-head-title' }, '通知'), - noticeNewCount > 0 - ? React.createElement(Badge, { count: noticeNewCount, style: { backgroundColor: accentOrange } }) - : null, - React.createElement(Text, { type: 'secondary', className: 'workbench-notice-panel-hint' }, '点击条目预览详情') - ); - var lines = preview.map(function (n) { - var priorityClass = n.priority === 'high' - ? ' workbench-notice-card--high' - : (n.priority === 'medium' ? ' workbench-notice-card--medium' : ''); - var unreadClass = !n.read ? ' workbench-notice-card--unread' : ''; - return React.createElement('button', { - key: n.id, - type: 'button', - className: 'workbench-notice-card' + priorityClass + unreadClass, - onClick: function () { openNoticePreview(n); }, - 'aria-label': '预览通知:' + n.type - }, - React.createElement('div', { className: 'workbench-notice-card-top' }, - React.createElement('span', { className: 'workbench-notice-card-type' }, n.type), - !n.read - ? React.createElement('span', { className: 'workbench-notice-card-badge' }, '未读') - : null - ), - React.createElement('div', { className: 'workbench-notice-card-content' }, n.content), - React.createElement('div', { className: 'workbench-notice-card-foot' }, - React.createElement('span', { className: 'workbench-notice-card-time' }, n.time), - React.createElement('span', { className: 'workbench-notice-card-action', 'aria-hidden': true }, '预览 ›') - ) - ); - }); - return React.createElement(Card, { - key: 'notice-panel', - className: 'workbench-notice-panel workbench-dash-pair-head workbench-surface-card', - bordered: false, - title: titleNode, - style: Object.assign({}, cardStyle, { flex: 1, width: '100%', minHeight: 0, display: 'flex', flexDirection: 'column' }), - bodyStyle: { flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0, overflow: 'hidden' }, - extra: React.createElement(Space, { size: 4 }, - noticeNewCount > 0 - ? React.createElement(Button, { - type: 'link', - size: 'small', - style: { padding: 0 }, - onClick: function () { - setNoticeList(function (prev) { - return (prev || []).map(function (item) { - return Object.assign({}, item, { read: true }); - }); - }); - message.success('已全部标为已读'); - } - }, '全部已读') - : null, - React.createElement(Button, { type: 'link', size: 'small', style: { padding: 0 }, onClick: openNoticeModal }, '更多') - ) - }, - React.createElement('div', { className: 'workbench-dash-card-body-center' }, - preview.length === 0 - ? React.createElement('div', { className: 'workbench-dash-card-body-inner workbench-dash-card-body-empty' }, - React.createElement(Text, { type: 'secondary', style: { fontSize: 13 } }, '暂无通知') - ) - : React.createElement('div', { className: 'workbench-dash-card-body-inner workbench-notice-detail-scroll workbench-notice-card-list' }, lines) - ) - ); - } - - var reportTabItems = useMemo(function () { - return [ - { - key: 'task', - label: '任务处理情况', - children: React.createElement('div', { className: 'workbench-task-report-pane' }, - React.createElement('div', { className: 'workbench-chart-legend' }, - React.createElement('span', { className: 'workbench-chart-legend-swatch', style: { background: '#16a34a' } }), - '完成任务 ', - React.createElement('span', { className: 'workbench-chart-legend-swatch', style: { background: '#f87171', marginLeft: 8 } }), - '超时任务(示意)' - ), - renderTaskEmployeeBarChart(taskEmployeeBars, 'done', 'overdue', '#52c41a', '#ff7875', 52) - ) - }, - { - key: 'payment', - label: '合同到账情况', - children: React.createElement('div', { className: 'workbench-payment-report-pane' }, - React.createElement('div', { style: { flexShrink: 0, fontSize: 12, color: 'rgba(0,0,0,0.45)', marginBottom: 8, lineHeight: 1.5 } }, - '环形合计对应应收款金额;蓝色为已收款,灰色为未到账(示意,联调接接口)' - ), - renderContractPaymentDonutChart(contractPaymentDonut) - ) - }, - { - key: 'contract', - label: '合同情况', - children: React.createElement('div', { style: { padding: '8px 0 0' } }, - React.createElement('div', { style: { fontSize: 12, color: 'rgba(0,0,0,0.45)', marginBottom: 8, lineHeight: 1.5 } }, - '横轴:月份 · 纵轴:合同数量 · 近12个月 · ', - React.createElement('span', { style: { color: '#722ed1', fontWeight: 500 } }, '■'), - ' 租赁合同 ', - React.createElement('span', { style: { color: '#b37feb', fontWeight: 500 } }, '■'), - ' 自营合同 ', - React.createElement('span', { style: { color: '#fa8c16', fontWeight: 500 } }, '■'), - ' 续签合同(示意)' - ), - renderBarGroupTriple(contractMonthBars, 'lease', 'self', 'renew', '#722ed1', '#b37feb', '#fa8c16', 44) - ) - }, - { - key: 'vehicle', - label: '车辆运营情况', - children: React.createElement('div', { style: { padding: '8px 0 0' } }, - React.createElement('div', { style: { fontSize: 12, color: 'rgba(0,0,0,0.45)', marginBottom: 8 } }, '近12个月 · 运营车辆 / 闲置车辆'), - renderBarGroup(vehicleMonthBars, 'op', 'idle', accentBlue, '#93c5fd') - ) - } - ]; - }, [vehicleMonthBars, contractMonthBars, taskEmployeeBars, contractPaymentDonut]); - - var quickItems = useMemo(function () { - var role = quickByRole[roleTab]; - if (!role) return []; - var catalog = role.items; - var catalogMap = {}; - catalog.forEach(function (it) { - catalogMap[quickItemKey(it)] = it; - }); - var saved = quickCustom[roleTab]; - if (!saved || !saved.length) return catalog; - return saved.map(function (key) { return catalogMap[key]; }).filter(Boolean); - }, [quickByRole, roleTab, quickCustom]); - - var quickCatalogItems = quickByRole[roleTab] ? quickByRole[roleTab].items : []; - var quickIsCustomized = !!(quickCustom[roleTab] && quickCustom[roleTab].length); - - var quickRoleKeys = ['ye', 'yeEnergy', 'ops', 'finance', 'safety', 'legal']; - - var roleSwitchMenuProps = useMemo(function () { - return { - items: quickRoleKeys.map(function (k) { - return { key: k, label: quickByRole[k].label }; - }), - selectable: true, - selectedKeys: [roleTab], - onClick: function (info) { - if (info && info.key) setRoleTab(info.key); - } - }; - }, [quickByRole, roleTab]); - - var oneScreenCss = - '@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap");' + - '.workbench-one-screen{' + - '--wb-primary:#2563eb;--wb-primary-soft:#eff6ff;--wb-primary-border:#bfdbfe;' + - '--wb-accent:#f97316;--wb-accent-soft:#fff7ed;' + - '--wb-success:#16a34a;--wb-danger:#dc2626;' + - '--wb-text:#1e293b;--wb-text-secondary:#64748b;--wb-text-muted:#94a3b8;' + - '--wb-surface:#ffffff;--wb-bg:#f8fafc;--wb-border:#e2e8f0;' + - '--wb-radius:14px;--wb-shadow:0 1px 2px rgba(15,23,42,0.04),0 8px 24px rgba(15,23,42,0.06);' + - 'font-family:Inter,-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif;color:var(--wb-text);' + - '-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}' + - '.workbench-hero{flex-shrink:0;margin-bottom:16px;padding:18px 20px;border-radius:var(--wb-radius);' + - 'border:1px solid var(--wb-border);background:linear-gradient(135deg,#fff 0%,#f8fafc 52%,#eff6ff 100%);' + - 'box-shadow:var(--wb-shadow)}' + - '.workbench-hero-top{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:12px}' + - '.workbench-hero-greeting{margin:0;font-size:22px;font-weight:700;line-height:1.3;color:var(--wb-text);letter-spacing:-0.02em}' + - '.workbench-hero-greeting em{font-style:normal;color:var(--wb-primary)}' + - '.workbench-hero-sub{margin-top:6px;font-size:14px;line-height:1.6;color:var(--wb-text-secondary)}' + - '.workbench-hero-sub strong{font-weight:600;color:var(--wb-text)}' + - '.workbench-hero-stats{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}' + - '.workbench-hero-stat{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;border-radius:999px;' + - 'background:var(--wb-surface);border:1px solid var(--wb-border);font-size:13px;line-height:1.4;color:var(--wb-text-secondary)}' + - '.workbench-hero-stat b{font-size:15px;font-weight:700;font-variant-numeric:tabular-nums;color:var(--wb-text)}' + - '.workbench-hero-stat--warn b{color:var(--wb-accent)}' + - '.workbench-hero-stat--info b{color:var(--wb-primary)}' + - '.workbench-section-kicker{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:600;' + - 'letter-spacing:0.04em;text-transform:uppercase;color:var(--wb-text-muted)}' + - '.workbench-section-kicker-dot{width:6px;height:6px;border-radius:50%;background:var(--wb-primary);flex-shrink:0}' + - '.workbench-filter-chip{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;min-height:32px;' + - 'border-radius:999px;font-size:13px;cursor:pointer;border:1px solid var(--wb-border);background:var(--wb-surface);' + - 'transition:border-color .18s,background .18s,box-shadow .18s,transform .18s;outline:none;user-select:none}' + - '.workbench-filter-chip:hover{border-color:var(--wb-primary-border);background:var(--wb-primary-soft)}' + - '.workbench-filter-chip:focus-visible{box-shadow:0 0 0 3px rgba(37,99,235,0.2)}' + - '.workbench-filter-chip--active{border-color:var(--wb-primary);background:var(--wb-primary-soft);box-shadow:0 0 0 1px rgba(37,99,235,0.12)}' + - '.workbench-filter-chip-label{color:var(--wb-text-secondary)}' + - '.workbench-filter-chip-count{font-weight:700;font-variant-numeric:tabular-nums}' + - '.workbench-metric-card{background:var(--wb-surface)!important;border:1px solid var(--wb-border)!important;box-shadow:var(--wb-shadow)!important;transition:transform .2s ease,box-shadow .2s ease,border-color .2s ease}' + - '.workbench-metric-card--interactive{cursor:pointer}' + - '.workbench-metric-card--interactive:hover{transform:translateY(-2px);box-shadow:0 4px 12px rgba(15,23,42,0.08),0 12px 28px rgba(37,99,235,0.08)!important;border-color:var(--wb-primary-border)!important}' + - '.workbench-metric-card--interactive:focus-visible{outline:2px solid rgba(37,99,235,0.35);outline-offset:2px}' + - '.workbench-metric-icon{width:44px;height:44px;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:16px;font-weight:700;flex-shrink:0}' + - '.workbench-metric-copy{min-width:0}' + - '.workbench-metric-title-row{display:flex;align-items:center;gap:4px;flex-wrap:wrap;margin-bottom:2px}' + - '.workbench-metric-title{font-size:13px;line-height:1.4;color:var(--wb-text-secondary);font-weight:500}' + - '.workbench-metric-value{font-size:24px;font-weight:700;line-height:1.2;color:var(--wb-accent);font-variant-numeric:tabular-nums;letter-spacing:-0.02em}' + - '.workbench-metric-value--compact{font-size:18px}' + - '.workbench-metric-hint-copy{font-size:12px;line-height:1.5;color:var(--wb-accent);font-weight:500}' + - '.workbench-dash-card-body-empty{min-height:140px;background:linear-gradient(180deg,transparent 0%,rgba(248,250,252,0.9) 100%);border-radius:12px}' + - '.workbench-chart-legend{font-size:13px;line-height:1.5;color:var(--wb-text-secondary);margin-bottom:10px}' + - '.workbench-chart-legend-swatch{display:inline-block;width:10px;height:10px;border-radius:2px;margin-right:4px;vertical-align:-1px}' + - '.workbench-surface-card{background:var(--wb-surface)!important;border:1px solid var(--wb-border)!important;box-shadow:var(--wb-shadow)!important}' + - '@media (prefers-reduced-motion:reduce){.workbench-metric-card,.workbench-notice-card,.workbench-quick-item,.workbench-filter-chip{transition:none!important}.workbench-metric-card--interactive:hover{transform:none}}' + - '.workbench-one-screen .workbench-report-tabs.ant-tabs{height:100%;display:flex;flex-direction:column;min-height:0}' + - '.workbench-one-screen .workbench-report-tabs .ant-tabs-content-holder{flex:1;min-height:0;overflow:hidden;display:flex;flex-direction:column}' + - '.workbench-one-screen .workbench-report-tabs .ant-tabs-content{flex:1;min-height:0;overflow:hidden;display:flex;flex-direction:column}' + - '.workbench-one-screen .workbench-report-tabs .ant-tabs-content .ant-tabs-tabpane.ant-tabs-tabpane-active{flex:1;min-height:0!important;display:flex!important;flex-direction:column;overflow:hidden}' + - '.workbench-one-screen .workbench-report-tabs .ant-tabs-content .ant-tabs-tabpane.ant-tabs-tabpane-hidden{display:none!important}' + - '.workbench-task-report-pane{flex:1;min-height:0;display:flex;flex-direction:column;height:100%;padding:8px 0 0;box-sizing:border-box}' + - '.workbench-task-chart-axes-wrap{flex:1;min-height:0;width:100%;display:flex;flex-direction:column;box-sizing:border-box}' + - '.workbench-task-chart-plot-row{flex:1;min-height:0;display:flex;flex-direction:row;align-items:stretch;overflow:hidden;box-sizing:border-box}' + - '.workbench-task-bar-chart{flex:1;min-height:80px;width:100%;display:flex;align-items:stretch;gap:6px;overflow-x:auto;overflow-y:hidden;box-sizing:border-box}' + - '.workbench-payment-report-pane{flex:1;min-height:0;display:flex;flex-direction:column;height:100%;padding:8px 0 0;box-sizing:border-box;overflow:hidden}' + - '.workbench-payment-donut-hold{flex:1;min-height:0;width:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;overflow:auto;box-sizing:border-box}' + - '.workbench-main-split{display:flex!important;flex-direction:column!important;gap:' + layoutGutter + 'px;flex:1;min-height:0;width:100%;overflow:hidden}' + - '.workbench-main-split>.ant-row.workbench-todo-notice-row,.workbench-main-split>.ant-row.workbench-report-quick-row{flex:1 1 0!important;min-height:0!important;height:auto!important;max-height:none!important;margin:0!important;overflow:hidden}' + - '.workbench-todo-notice-row>.ant-col,.workbench-report-quick-row>.ant-col{display:flex!important;flex-direction:column;min-height:0}' + - '.workbench-notice-slot,.workbench-todo-slot,.workbench-report-slot{min-height:0}' + - '.workbench-dash-pair-head.ant-card .ant-card-head{min-height:56px;padding:0 20px;display:flex;align-items:center;border-bottom:1px solid var(--wb-border);background:linear-gradient(180deg,#fff 0%,#fafbfc 100%)}' + - '.workbench-dash-pair-head.ant-card .ant-card-extra{padding:0}' + - '.workbench-dash-pair-head.ant-card .ant-card-body{padding-top:10px!important}' + - '.workbench-dash-pair-head-sub{font-size:12px!important;line-height:20px!important}' + - '.workbench-notice-panel.ant-card{height:100%;min-height:0;display:flex!important;flex-direction:column}' + - '.workbench-notice-panel .ant-card-body{flex:1;min-height:0!important;display:flex!important;flex-direction:column}' + - '.workbench-dash-card-body-center{flex:1;min-height:0;display:flex;flex-direction:column;justify-content:flex-start;align-items:stretch;overflow:hidden;width:100%}' + - '.workbench-dash-card-body-inner{width:100%;max-height:100%;overflow-x:auto;overflow-y:auto;flex:0 1 auto;min-height:0}' + - '.workbench-dash-card-body-empty{display:flex;align-items:center;justify-content:center;min-height:120px}' + - '.workbench-todo-slot .ant-card,.workbench-report-slot .ant-card{height:100%;min-height:0}' + - '.workbench-report-tabs .ant-tabs-nav{margin-bottom:8px;}' + - '.workbench-notice-inline-hint{font-size:11px;font-weight:500;color:#ad4e00;cursor:pointer;user-select:none;white-space:nowrap}' + - '.workbench-notice-inline-hint:hover{color:#d46b08;text-decoration:underline}' + - '.workbench-notice-panel-hint{font-size:12px!important;line-height:20px!important}' + - '.workbench-notice-card-list{display:flex;flex-direction:column;gap:8px;padding:2px 0}' + - '.workbench-notice-card{display:block;width:100%;box-sizing:border-box;text-align:left;border:1px solid rgba(0,0,0,0.06);border-radius:10px;background:#fafafa;padding:10px 12px;cursor:pointer;transition:border-color .18s,background .18s,box-shadow .18s,transform .18s;outline:none;font:inherit}' + - '.workbench-notice-card:hover{border-color:rgba(37,99,235,0.28);background:#f5f9ff;box-shadow:0 2px 8px rgba(37,99,235,0.08)}' + - '.workbench-notice-card:focus-visible{box-shadow:0 0 0 2px rgba(37,99,235,0.24)}' + - '.workbench-notice-card--unread{border-left:3px solid var(--wb-accent);background:var(--wb-accent-soft)}' + - '.workbench-notice-card--unread:hover{background:#fff7ef}' + - '.workbench-notice-card--high.workbench-notice-card--unread{border-left-color:#f5222d}' + - '.workbench-notice-card-top{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:6px}' + - '.workbench-notice-card-type{font-size:12px;font-weight:600;color:rgba(0,0,0,0.78);line-height:1.35}' + - '.workbench-notice-card-badge{font-size:11px;line-height:18px;padding:0 6px;border-radius:999px;background:#fff7e6;color:#d46b08;font-weight:500;flex-shrink:0}' + - '.workbench-notice-card:active{transform:scale(0.99)}' + - '.workbench-notice-card-content{font-size:14px;line-height:1.55;color:var(--wb-text);display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden}' + - '.workbench-notice-card-foot{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:8px}' + - '.workbench-notice-card-time{font-size:12px;color:rgba(0,0,0,0.45);font-variant-numeric:tabular-nums}' + - '.workbench-notice-card-action{font-size:12px;color:var(--wb-primary);font-weight:600;flex-shrink:0}' + - '.workbench-notice-table-row:hover>td{background:#f5f9ff!important}' + - '.workbench-notice-preview{display:flex;flex-direction:column;gap:14px}' + - '.workbench-notice-preview-meta{display:flex;align-items:center;flex-wrap:wrap;gap:8px}' + - '.workbench-notice-preview-summary{font-size:15px;line-height:1.55;font-weight:600;color:rgba(0,0,0,0.88)}' + - '.workbench-notice-preview-detail{border:1px solid rgba(0,0,0,0.06);border-radius:10px;background:#fafafa;padding:12px 14px}' + - '.workbench-notice-preview-detail-label{font-size:12px;font-weight:600;color:rgba(0,0,0,0.45);margin-bottom:6px;letter-spacing:0.02em}' + - '.workbench-notice-preview-detail-body{font-size:14px;line-height:1.6;color:rgba(0,0,0,0.78)}' + - '.workbench-quick-customize{display:grid;grid-template-columns:1fr 1fr;gap:16px;min-height:280px}' + - '@media (max-width: 640px){.workbench-quick-customize{grid-template-columns:1fr}}' + - '.workbench-quick-customize-col{display:flex;flex-direction:column;min-height:0;border:1px solid rgba(0,0,0,0.06);border-radius:10px;background:#fafafa;overflow:hidden}' + - '.workbench-quick-customize-title{padding:10px 12px;font-size:13px;font-weight:600;color:rgba(0,0,0,0.78);border-bottom:1px solid rgba(0,0,0,0.06);background:#fff}' + - '.workbench-quick-customize-count{font-weight:400;color:rgba(0,0,0,0.45)}' + - '.workbench-quick-customize-list,.workbench-quick-customize-order{flex:1;min-height:0;overflow:auto;padding:8px;display:flex;flex-direction:column;gap:6px}' + - '.workbench-quick-customize-option{display:flex;align-items:center;gap:8px;padding:8px 10px;border-radius:8px;background:#fff;border:1px solid rgba(0,0,0,0.04);cursor:pointer;transition:background .15s,border-color .15s}' + - '.workbench-quick-customize-option:hover{border-color:rgba(22,119,255,0.2);background:#f8fbff}' + - '.workbench-quick-customize-option.is-checked{border-color:rgba(22,119,255,0.28);background:#f0f7ff}' + - '.workbench-quick-customize-option-label{font-size:13px;line-height:1.4;color:rgba(0,0,0,0.78)}' + - '.workbench-quick-customize-order-item{display:flex;align-items:center;gap:8px;padding:8px 10px;border-radius:8px;background:#fff;border:1px solid rgba(0,0,0,0.04)}' + - '.workbench-quick-customize-order-index{width:22px;height:22px;border-radius:6px;background:#f0f5ff;color:#1677ff;font-size:12px;font-weight:600;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;font-variant-numeric:tabular-nums}' + - '.workbench-quick-customize-order-label{flex:1;min-width:0;font-size:13px;line-height:1.4;color:rgba(0,0,0,0.78)}' + - '.workbench-todo-table .ant-table-thead>tr>th{color:#707d8f!important;font-weight:400!important;font-size:12px!important;background:#f7f8fa!important;border-bottom:1px solid rgba(0,0,0,0.06)!important;padding:12px 12px!important}' + - '.workbench-todo-table .ant-table-thead>tr>th::before{display:none!important}' + - '.workbench-todo-table .ant-table-column-sorter{color:#98a1b0!important}' + - '.workbench-todo-table .ant-table-column-sorter-up.active,.workbench-todo-table .ant-table-column-sorter-down.active{color:#707d8f!important}' + - '.workbench-todo-table.ant-table-small .ant-table-thead>tr>th{padding:10px 12px!important}' + - '.workbench-todo-table .ant-table-thead>tr>th.ant-table-column-sort{background:#f7f8fa!important}' + - '.workbench-todo-table .ant-table-tbody>tr>td.ant-table-column-sort{background:#fff!important}' + - '.workbench-todo-table .ant-table-tbody>tr>td{font-size:14px!important;line-height:1.5!important;color:var(--wb-text)!important;padding:12px!important}' + - '.workbench-todo-table .ant-table-tbody>tr>td .ant-btn-link{font-weight:500}' + - '.workbench-todo-table .ant-table-tbody>tr.ant-table-row{cursor:pointer;transition:background .15s}' + - '.workbench-todo-table .ant-table-tbody>tr.ant-table-row:hover>td{background:#f8fbff!important}' + - '.workbench-one-screen .workbench-quick-card.ant-card{display:flex;flex-direction:column;min-height:0;height:100%}' + - '.workbench-one-screen .workbench-quick-card .ant-card-body{padding:0;flex:1;min-height:0;display:flex;flex-direction:column}' + - '.workbench-quick-scroll{flex:1;min-height:0;overflow:auto;-webkit-overflow-scrolling:touch}' + - '.workbench-quick-item{display:flex;align-items:center;gap:8px;width:100%;box-sizing:border-box;padding:10px 12px;min-height:44px;border-radius:10px;border:1px solid var(--wb-border);background:var(--wb-surface);cursor:pointer;transition:border-color .18s,background .18s,box-shadow .18s,transform .18s;text-align:left;outline:none}' + - '.workbench-quick-item:hover{border-color:var(--wb-primary-border);background:var(--wb-primary-soft);box-shadow:0 2px 8px rgba(37,99,235,0.08);transform:translateY(-1px)}' + - '.workbench-quick-item:focus-visible{box-shadow:0 0 0 3px rgba(37,99,235,0.2)}' + - '.workbench-quick-item-icon{flex-shrink:0;width:32px;height:32px;border-radius:10px;background:linear-gradient(135deg,#eff6ff,#dbeafe);line-height:32px;text-align:center;font-size:13px;font-weight:700;color:var(--wb-primary)}' + - '.workbench-quick-item-label{flex:1;min-width:0;font-size:12px;line-height:1.35;color:rgba(0,0,0,0.78);word-break:break-word;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden}' + - '.workbench-quick-item--action{padding:7px 10px;min-height:38px}' + - '.workbench-quick-item--primary{border-color:rgba(37,99,235,0.22);background:linear-gradient(135deg,#f8fbff 0%,#eff6ff 100%)}' + - '.workbench-quick-item--primary:hover{border-color:rgba(37,99,235,0.38);background:linear-gradient(135deg,#eff6ff 0%,#dbeafe 100%)}' + - '.workbench-quick-item--primary .workbench-quick-item-icon{background:linear-gradient(135deg,#2563eb,#3b82f6);color:#fff;box-shadow:0 4px 10px rgba(37,99,235,0.28)}' + - '.workbench-quick-item--orange{border-color:rgba(249,115,22,0.24);background:linear-gradient(135deg,#fffaf5 0%,#fff7ed 100%)}' + - '.workbench-quick-item--orange:hover{border-color:rgba(249,115,22,0.4);background:linear-gradient(135deg,#fff7ed 0%,#ffedd5 100%)}' + - '.workbench-quick-item--orange .workbench-quick-item-icon{background:linear-gradient(135deg,#f97316,#fb923c);color:#fff;box-shadow:0 4px 10px rgba(249,115,22,0.24)}' + - '.workbench-quick-section-hint{font-size:11px;line-height:1.35;color:rgba(0,0,0,0.45);padding:0 4px 6px;font-weight:500;letter-spacing:0.02em}' + - '.workbench-gen-task-modal .ant-modal-body{padding-top:14px}' + - '.workbench-gen-task-form-label{display:block;margin-bottom:6px;font-size:13px;font-weight:500;color:rgba(0,0,0,0.88);line-height:1.4}' + - '.workbench-gen-task-form-label .req{color:#ff4d4f;margin-right:4px}' + - '.workbench-gen-task-section{background:#fafafa;border:1px solid rgba(0,0,0,0.06);border-radius:10px;padding:14px 16px;margin-bottom:14px}' + - '.workbench-gen-task-readonly{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px 16px;margin-bottom:4px}' + - '.workbench-gen-task-readonly-item{font-size:12px;line-height:1.45;color:rgba(0,0,0,0.65)}' + - '.workbench-gen-task-readonly-item strong{color:rgba(0,0,0,0.88);font-weight:500;margin-right:6px}' + - '.workbench-gen-task-table-hint{font-size:12px;color:rgba(0,0,0,0.45);margin:4px 0 10px;line-height:1.45}' + - '.workbench-header-warning-dept-tabs{flex-shrink:0;max-width:100%}' + - '.workbench-header-warning-dept-tabs.ant-tabs{min-width:0}' + - '.workbench-header-warning-dept-tabs .ant-tabs-nav{margin:0!important;min-height:32px}' + - '.workbench-header-warning-dept-tabs .ant-tabs-nav::before{border-bottom:none!important}' + - '.workbench-header-warning-dept-tabs .ant-tabs-tab{padding:6px 12px!important;font-size:13px;border-radius:999px;margin:0 2px!important;transition:background .18s,color .18s}' + - '.workbench-header-warning-dept-tabs .ant-tabs-tab-active{background:var(--wb-primary-soft)!important}' + - '.workbench-header-warning-dept-tabs .ant-tabs-tab-active .ant-tabs-tab-btn{color:var(--wb-primary)!important;font-weight:600}' + - '.workbench-header-warning-dept-tabs .ant-tabs-nav-wrap{justify-content:flex-end}' + - '.workbench-header-warning-dept-tabs .ant-tabs-content-holder{display:none!important}' + - '.workbench-overdue-delivery-modal .ant-table-tbody>tr>td{color:#a8071a!important;background-color:#fff2f0!important;border-color:#ffccc7!important}' + - '.workbench-overdue-delivery-modal .ant-table-tbody>tr>td.ant-table-cell-fix-left,.workbench-overdue-delivery-modal .ant-table-tbody>tr>td.ant-table-cell-fix-right{background-color:#fff2f0!important}' + - '.workbench-overdue-delivery-modal .ant-table-tbody>tr:hover>td{background-color:#ffccc7!important;color:#a8071a!important}' + - '.workbench-overdue-delivery-modal .ant-table-tbody>tr:hover>td.ant-table-cell-fix-left,.workbench-overdue-delivery-modal .ant-table-tbody>tr:hover>td.ant-table-cell-fix-right{background-color:#ffccc7!important}' + - '.workbench-overdue-return-modal .ant-table-thead>tr>th,.workbench-overdue-return-modal .ant-table-tbody>tr>td{white-space:nowrap}' + - '.workbench-overdue-return-modal .ant-table-tbody>tr>td{color:#ad4e00!important;background-color:#fff7e6!important;border-color:#ffd591!important}' + - '.workbench-overdue-return-modal .ant-table-tbody>tr>td.ant-table-cell-fix-left,.workbench-overdue-return-modal .ant-table-tbody>tr>td.ant-table-cell-fix-right{background-color:#fff7e6!important}' + - '.workbench-overdue-return-modal .ant-table-tbody>tr:hover>td{background-color:#ffe7ba!important;color:#ad4e00!important}' + - '.workbench-overdue-return-modal .ant-table-tbody>tr:hover>td.ant-table-cell-fix-left,.workbench-overdue-return-modal .ant-table-tbody>tr:hover>td.ant-table-cell-fix-right{background-color:#ffe7ba!important}' + - '.workbench-overdue-return-modal .ant-btn-link{color:#1677ff!important}' + - '.workbench-warning-metric-card.ant-card{display:flex!important;flex-direction:column!important;height:100%!important;min-height:inherit}' + - '.workbench-warning-metric-card.ant-card .ant-card-body{flex:1!important;display:flex!important;flex-direction:column!important;justify-content:center!important;min-height:0!important}' + - '.workbench-metric-hint-icon:hover{color:rgba(22,119,255,0.9)!important}' + - '.workbench-metric-hint-icon:focus-visible{outline:2px solid rgba(22,119,255,0.35);outline-offset:2px;border-radius:4px}' + - '.workbench-one-screen .ant-breadcrumb{font-size:13px;line-height:1.5}' + - '.workbench-one-screen .ant-breadcrumb a,.workbench-one-screen .ant-breadcrumb .ant-breadcrumb-link{color:rgba(0,0,0,0.65)}' + - '.workbench-one-screen .ant-breadcrumb li:last-child .ant-breadcrumb-link{color:rgba(0,0,0,0.88);font-weight:500}' + - '.workbench-report-tabs .ant-tabs-ink-bar{background:var(--wb-primary);height:3px;border-radius:2px}' + - '.workbench-report-tabs .ant-tabs-tab .ant-tabs-tab-btn{font-size:14px;color:var(--wb-text-secondary);transition:color .18s}' + - '.workbench-report-tabs .ant-tabs-tab:hover .ant-tabs-tab-btn{color:var(--wb-text)}' + - '.workbench-report-tabs .ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn{color:var(--wb-text);font-weight:600}' + - '.workbench-dash-pair-head.ant-card .ant-card-head-title,.workbench-dash-pair-head-title{font-size:17px;font-weight:700;line-height:26px;color:var(--wb-text);letter-spacing:-0.01em}' + - '.workbench-report-slot .ant-card .ant-card-head{min-height:52px;padding:0 18px;border-bottom:1px solid rgba(0,0,0,0.06)}' + - '.workbench-report-slot .ant-card .ant-card-head-title{font-size:16px;font-weight:600;color:rgba(0,0,0,0.88)}' + - '.workbench-kpi-scheme-bar{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:12px;padding:12px 14px;border-radius:12px;border:1px dashed rgba(37,99,235,0.28);background:linear-gradient(135deg,#f8fbff 0%,#eff6ff 100%)}' + - '.workbench-kpi-scheme-kicker{display:inline-flex;align-items:center;font-size:12px;font-weight:700;color:#1d4ed8;letter-spacing:0.03em;text-transform:uppercase;margin-right:8px}' + - '.workbench-kpi-scheme-copy{flex:1;min-width:220px}' + - '.workbench-warning-radar-wrap{display:flex;flex-direction:column;gap:12px;width:100%}' + - '.workbench-warning-radar-summary{display:flex;flex-wrap:wrap;align-items:center;gap:8px;padding:4px 2px 2px}' + - '.workbench-warning-radar-chip{display:inline-flex;align-items:center;gap:6px;min-height:34px;padding:0 12px;border-radius:999px;border:1px solid var(--wb-border);background:var(--wb-surface);font-size:12px;color:var(--wb-text-secondary);cursor:pointer}' + - '.workbench-warning-radar-chip b{font-size:14px;font-weight:700;color:var(--wb-text);font-variant-numeric:tabular-nums}' + - '.workbench-warning-radar-chip--focus{border-color:rgba(37,99,235,0.25);background:#eff6ff}' + - '.workbench-warning-radar-chip--high{border-color:rgba(245,34,45,0.22);background:#fff1f0}' + - '.workbench-warning-radar-chip--high b{color:#cf1322}' + - '.workbench-warning-radar-chip--overdue{border-color:rgba(250,140,22,0.25);background:#fff7e6}' + - '.workbench-warning-radar-perspective{margin-left:auto;display:flex;align-items:center;gap:8px}' + - '.workbench-warning-radar-primary{width:100%}' + - '.workbench-warning-radar-card-top{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px}' + - '.workbench-warning-radar-card-title{font-size:14px;font-weight:600;line-height:1.45;color:var(--wb-text);margin-bottom:6px}' + - '.workbench-warning-radar-card-value{font-size:24px;font-weight:700;line-height:1.2;color:var(--wb-accent);font-variant-numeric:tabular-nums}' + - '.workbench-warning-radar-card-sub{margin-top:4px;font-size:12px;color:var(--wb-text-secondary);font-variant-numeric:tabular-nums}' + - '.workbench-warning-radar-card-action{margin-top:10px;font-size:12px;font-weight:600;color:var(--wb-primary)}' + - '.workbench-warning-progress-card .workbench-warning-radar-card-title{margin-bottom:10px}' + - '.workbench-warning-progress-track{display:flex;align-items:flex-start;justify-content:space-between;gap:6px;margin:4px 0 10px}' + - '.workbench-warning-progress-step{flex:1;min-width:0;display:flex;flex-direction:column;align-items:center;gap:6px;position:relative}' + - '.workbench-warning-progress-step:not(:last-child)::after{content:"";position:absolute;top:9px;left:calc(50% + 10px);right:calc(-50% + 10px);height:2px;background:#e2e8f0;z-index:0}' + - '.workbench-warning-progress-step--done:not(:last-child)::after{background:#86efac}' + - '.workbench-warning-progress-step--active:not(:last-child)::after{background:linear-gradient(90deg,#86efac,#fbbf24)}' + - '.workbench-warning-progress-dot{width:18px;height:18px;border-radius:50%;border:2px solid #cbd5e1;background:#fff;position:relative;z-index:1}' + - '.workbench-warning-progress-step--done .workbench-warning-progress-dot{border-color:#22c55e;background:#22c55e}' + - '.workbench-warning-progress-step--active .workbench-warning-progress-dot{border-color:#f59e0b;background:#fff7ed;box-shadow:0 0 0 4px rgba(245,158,11,0.18)}' + - '.workbench-warning-progress-label{font-size:11px;line-height:1.3;color:var(--wb-text-secondary);text-align:center}' + - '.workbench-warning-progress-step--active .workbench-warning-progress-label{color:#b45309;font-weight:600}' + - '.workbench-warning-radar-fold{border:1px solid var(--wb-border);border-radius:12px;background:#fafafa;overflow:hidden}' + - '.workbench-warning-radar-fold-head{width:100%;display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px 14px;border:none;background:transparent;font-size:13px;font-weight:600;color:var(--wb-text-secondary);cursor:pointer}' + - '.workbench-warning-radar-fold-body{padding:0 14px 12px;display:flex;flex-direction:column;gap:8px}' + - '.workbench-warning-radar-fold-item{padding:10px 12px;border-radius:10px;background:#fff;border:1px solid rgba(0,0,0,0.04);font-size:12px;line-height:1.5;color:var(--wb-text-secondary)}' + - '.workbench-warning-radar-fold-item strong{display:block;margin-bottom:4px;color:var(--wb-text);font-weight:600}'; - - return React.createElement(App, null, - React.createElement('div', { - className: 'workbench-one-screen', - style: { - boxSizing: 'border-box', - height: '100vh', - padding: pagePadY + 'px ' + pagePadX + 'px ' + pagePadY + 'px', - background: pageBg, - overflow: 'hidden', - display: 'flex', - flexDirection: 'column' - } - }, - React.createElement('style', null, oneScreenCss), - - React.createElement('div', { className: 'workbench-hero' }, - React.createElement('div', { className: 'workbench-hero-top' }, - React.createElement('div', { style: { minWidth: 0, flex: '1 1 280px' } }, - React.createElement('div', { className: 'workbench-section-kicker', style: { marginBottom: 8 } }, - React.createElement('span', { className: 'workbench-section-kicker-dot' }), - '运管平台 · 工作台' - ), - React.createElement('h1', { className: 'workbench-hero-greeting' }, - '欢迎回来,', - React.createElement('em', null, mockOperatorDisplayName) - ), - React.createElement('p', { className: 'workbench-hero-sub' }, - '按行动类型、风险与责任链查看待办,快速判断是否需要你处理。' - ), - React.createElement('div', { className: 'workbench-hero-stats' }, - React.createElement('span', { className: 'workbench-hero-stat workbench-hero-stat--info' }, - React.createElement('b', null, todoSummary.actionable), - ' 条需处理' - ), - React.createElement('span', { className: 'workbench-hero-stat workbench-hero-stat--warn' }, - React.createElement('b', null, todoSummary.today), - ' 条今日必办' - ), - noticeNewCount > 0 - ? React.createElement('span', { className: 'workbench-hero-stat workbench-hero-stat--warn' }, - React.createElement('b', null, noticeNewCount), - ' 条未读通知' - ) - : React.createElement('span', { className: 'workbench-hero-stat' }, '通知已全部处理') - ) - ), - React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', justifyContent: 'flex-end', minWidth: 0 } }, - warningKpiScheme === 'legacy' - ? React.createElement(Tabs, { - className: 'workbench-header-warning-dept-tabs', - activeKey: warningDeptKey, - onChange: setWarningDeptKey, - size: 'small', - items: warningDeptTabItems, - tabBarGutter: 4 - }) - : React.createElement(Text, { type: 'secondary', style: { fontSize: 12 } }, '预警视角已移至 KPI 区') - ) - ) - ), - React.createElement('div', { style: { flexShrink: 0, marginBottom: 12, boxSizing: 'border-box' } }, - renderWarningSchemeSwitcher(), - warningKpiScheme === 'legacy' ? renderWarningKpiLegacyBlock() : renderWarningKpiRadarBlock() - ), - React.createElement(Row, { gutter: [layoutGutter, layoutGutter], align: 'stretch', style: { flex: 1, minHeight: 0, overflow: 'hidden' } }, - React.createElement(Col, { - xs: 24, - lg: 24, - xl: 24, - className: 'workbench-main-split', - style: { flex: 1, minHeight: 0, minWidth: 0, height: '100%' } - }, - React.createElement(Row, { - className: 'workbench-todo-notice-row', - gutter: [layoutGutter, layoutGutter], - align: 'stretch', - wrap: true, - style: { minHeight: 0, flex: '1 1 0', margin: 0 } - }, - React.createElement(Col, { - xs: 24, - lg: 16, - xl: 16, - style: { display: 'flex', flexDirection: 'column', minHeight: 0, minWidth: 0 } - }, - React.createElement('div', { className: 'workbench-todo-slot', style: { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden', width: '100%' } }, - React.createElement(Card, { - className: 'workbench-dash-pair-head workbench-surface-card', - title: React.createElement(Space, { size: 8, wrap: true, align: 'center' }, - React.createElement('span', { className: 'workbench-dash-pair-head-title' }, '待办任务'), - React.createElement(Badge, { count: todoSummary.actionable, style: { backgroundColor: accentBlue } }), - renderTodoStatChip('today', '今日必办', todoSummary.today, 'rgba(245,34,45,0.06)', '#f5222d'), - renderTodoStatChip('handle', '待我处理', todoSummary.handle, 'rgba(22,119,255,0.06)', accentBlue), - renderTodoStatChip('follow', '待我跟进', todoSummary.follow, 'rgba(0,0,0,0.04)', '#595959'), - renderTodoStatChip('all', '全部', dashboardTodoRows.length, 'rgba(0,0,0,0.04)', '#595959') - ), - bordered: false, - style: Object.assign({}, cardStyle, { flex: 1, width: '100%', minHeight: 0, display: 'flex', flexDirection: 'column' }), - bodyStyle: { flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0, overflow: 'hidden' }, - extra: React.createElement(Button, { type: 'link', size: 'small', style: { padding: 0 }, onClick: openTodoMoreModal }, '更多') - }, - React.createElement('div', { className: 'workbench-dash-card-body-center' }, - React.createElement('div', { className: 'workbench-dash-card-body-inner' }, - React.createElement(Table, { - className: 'workbench-todo-table', - size: 'small', - rowKey: 'id', - columns: todoTableColumns, - dataSource: dashboardTodoBoardPreviewRows, - pagination: false, - scroll: { x: 980 } - }) - ) - ) - ) - ) - ), - React.createElement(Col, { - xs: 24, - lg: 8, - xl: 8, - style: { display: 'flex', flexDirection: 'column', minHeight: 0, minWidth: 0 } - }, - React.createElement('div', { className: 'workbench-notice-slot', style: { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', width: '100%' } }, - renderNoticePanelCard() - ) - ) - ), - React.createElement(Row, { - className: 'workbench-report-quick-row', - gutter: [layoutGutter, layoutGutter], - align: 'stretch', - wrap: true, - style: { minHeight: 0, flex: '1 1 0', margin: 0 } - }, - React.createElement(Col, { - xs: 24, - lg: 16, - xl: 16, - style: { display: 'flex', flexDirection: 'column', minHeight: 0, minWidth: 0 } - }, - React.createElement('div', { className: 'workbench-report-slot', style: { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden', width: '100%' } }, - React.createElement(Card, { - title: '统计报表', - bordered: false, - className: 'workbench-surface-card', - style: Object.assign({}, cardStyle, { flex: 1, width: '100%', minHeight: 0, display: 'flex', flexDirection: 'column' }), - bodyStyle: { paddingTop: 4, paddingBottom: 8, flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' } - }, - React.createElement(Tabs, { - className: 'workbench-report-tabs', - activeKey: reportTab, - onChange: setReportTab, - items: reportTabItems, - tabBarStyle: { marginBottom: 0 }, - style: { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' } - }) - ) - ) - ), - React.createElement(Col, { - xs: 24, - lg: 8, - xl: 8, - style: { display: 'flex', flexDirection: 'column', minHeight: 0, minWidth: 0 } - }, - React.createElement(Card, { - className: 'workbench-quick-card workbench-surface-card', - title: '快速入口', - bordered: false, - style: Object.assign({}, cardStyle, { flex: 1, width: '100%', minHeight: 0, display: 'flex', flexDirection: 'column' }), - bodyStyle: { flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0, overflow: 'hidden', padding: 0 }, - extra: React.createElement(Space, { size: 8, wrap: true }, - quickIsCustomized - ? React.createElement(Tag, { color: 'processing', style: { margin: 0, fontSize: 11, lineHeight: '18px' } }, '已自定义') - : null, - React.createElement(Button, { type: 'link', size: 'small', style: { padding: 0 }, onClick: openQuickCustomize }, '自定义'), - React.createElement(Dropdown, { - menu: roleSwitchMenuProps, - trigger: ['click'], - placement: 'bottomRight' - }, - React.createElement(Button, { type: 'link', size: 'small', style: { padding: 0 } }, '切换角色') - ) - ) - }, - React.createElement('div', { className: 'workbench-quick-scroll', style: { padding: '6px 6px 4px' } }, - quickItems.some(function (it) { return it.action; }) - ? React.createElement('div', { className: 'workbench-quick-section-hint' }, '常用操作') - : null, - React.createElement(Row, { gutter: [6, 6], style: { marginBottom: quickItems.some(function (it) { return !it.action; }) ? 8 : 0 } }, - quickItems.filter(function (it) { return it.action; }).map(function (it, idx) { - var accentClass = it.accent === 'orange' ? ' workbench-quick-item--orange' : ' workbench-quick-item--primary'; - return React.createElement(Col, { span: 24, key: it.t + '-action-' + idx }, - React.createElement('div', { - className: 'workbench-quick-item workbench-quick-item--action' + accentClass, - role: 'button', - tabIndex: 0, - onClick: function () { handleQuickItemClick(it); }, - onKeyDown: function (e) { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - handleQuickItemClick(it); - } - } - }, - React.createElement('div', { className: 'workbench-quick-item-icon', 'aria-hidden': true }, it.t.charAt(0)), - React.createElement('div', { className: 'workbench-quick-item-label' }, it.t) - ) - ); - }) - ), - quickItems.some(function (it) { return !it.action; }) - ? React.createElement('div', { className: 'workbench-quick-section-hint' }, '功能入口') - : null, - React.createElement(Row, { gutter: [6, 6], style: { marginBottom: 0 } }, - quickItems.filter(function (it) { return !it.action; }).map(function (it, idx) { - return React.createElement(Col, { span: 12, key: it.t + '-' + idx }, - React.createElement('div', { - className: 'workbench-quick-item', - role: 'button', - tabIndex: 0, - onClick: function () { handleQuickItemClick(it); }, - onKeyDown: function (e) { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - handleQuickItemClick(it); - } - } - }, - React.createElement('div', { className: 'workbench-quick-item-icon', 'aria-hidden': true }, it.t.charAt(0)), - React.createElement('div', { className: 'workbench-quick-item-label' }, it.t) - ) - ); - }) - ) - ) - ) - ) - ) - ) - ), - - React.createElement(Modal, { - title: '全部待办任务', - open: todoMoreModalOpen, - width: 960, - onCancel: function () { setTodoMoreModalOpen(false); }, - footer: React.createElement(Button, { onClick: function () { setTodoMoreModalOpen(false); } }, '关闭'), - destroyOnClose: true - }, - React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 12 } }, - React.createElement(Space, { wrap: true, align: 'center', size: [8, 8] }, - React.createElement(Text, { type: 'secondary', style: { fontSize: 12 } }, '任务类型'), - React.createElement(Select, { - allowClear: true, - placeholder: '全部类型', - style: { width: 160 }, - options: todoMoreTaskTypeFilterOptions, - value: todoMoreTaskType, - onChange: setTodoMoreTaskType, - showSearch: true, - optionFilterProp: 'label' - }), - React.createElement(Text, { type: 'secondary', style: { fontSize: 12 } }, '任务状态'), - React.createElement(Select, { - allowClear: true, - placeholder: '全部状态', - style: { width: 140 }, - options: todoMoreStatusFilterOptions, - value: todoMoreStatus, - onChange: setTodoMoreStatus - }), - React.createElement(Text, { type: 'secondary', style: { fontSize: 12 } }, '任务时间'), - React.createElement(DatePicker.RangePicker, { - style: { width: 140 }, - format: 'YYYY-MM-DD', - placeholder: ['开始', '结束'], - allowClear: true, - value: todoMoreRangeValue, - onChange: function (_dates, dateStrings) { - if (!dateStrings || (!dateStrings[0] && !dateStrings[1])) { - setTodoMoreDateStart(''); - setTodoMoreDateEnd(''); - return; - } - setTodoMoreDateStart(dateStrings[0] || ''); - setTodoMoreDateEnd(dateStrings[1] || ''); - } - }), - React.createElement(Button, { - size: 'small', - onClick: function () { - setTodoMoreTaskType(undefined); - setTodoMoreStatus(undefined); - setTodoMoreDateStart(''); - setTodoMoreDateEnd(''); - } - }, '重置') - ), - React.createElement(Table, { - className: 'workbench-todo-table', - size: 'small', - rowKey: 'id', - columns: todoMoreModalColumns, - dataSource: todoMoreFilteredRows, - pagination: { pageSize: 10, showSizeChanger: false, showTotal: function (t) { return '共 ' + t + ' 条'; } }, - scroll: { x: 980, y: 360 } - }) - ) - ), - - React.createElement(Modal, { - title: '通知消息', - open: noticeModalOpen, - width: 860, - onCancel: function () { setNoticeModalOpen(false); }, - footer: React.createElement(Button, { onClick: function () { setNoticeModalOpen(false); } }, '关闭'), - destroyOnClose: true - }, - React.createElement(Table, { - size: 'small', - rowKey: 'id', - columns: noticeColumns, - dataSource: noticeList, - pagination: { pageSize: 8, showSizeChanger: false }, - scroll: { x: 720 }, - onRow: function (record) { - return { - className: 'workbench-notice-table-row', - style: { cursor: 'pointer' }, - onClick: function () { openNoticePreview(record); } - }; - } - }) - ), - - React.createElement(Modal, { - title: noticePreviewRecord ? noticePreviewRecord.type : '通知详情', - open: !!noticePreviewRecord, - width: 560, - onCancel: closeNoticePreview, - destroyOnClose: true, - footer: noticePreviewRecord - ? React.createElement(Space, { wrap: true }, - !noticePreviewRecord.read - ? React.createElement(Button, { - onClick: function () { - markNoticeRead(noticePreviewRecord.id); - } - }, '标为已读') - : null, - noticePreviewRecord.target && noticePreviewRecord.target.path - ? React.createElement(Button, { - type: 'primary', - onClick: function () { handleNoticePreviewNavigate(noticePreviewRecord); } - }, noticePreviewRecord.target.label || '前往处理') - : null, - React.createElement(Button, { onClick: closeNoticePreview }, '关闭') - ) - : null - }, - noticePreviewRecord - ? React.createElement('div', { className: 'workbench-notice-preview' }, - React.createElement('div', { className: 'workbench-notice-preview-meta' }, - React.createElement(Tag, { - color: noticePreviewRecord.priority === 'high' ? 'error' : (noticePreviewRecord.priority === 'medium' ? 'warning' : 'default'), - style: { margin: 0 } - }, noticePreviewRecord.priority === 'high' ? '高优先级' : (noticePreviewRecord.priority === 'medium' ? '中优先级' : '普通')), - React.createElement(Text, { type: 'secondary', style: { fontSize: 13 } }, noticePreviewRecord.time), - !noticePreviewRecord.read - ? React.createElement(Tag, { color: 'warning', style: { margin: 0 } }, '未读') - : React.createElement(Tag, { style: { margin: 0 } }, '已读') - ), - React.createElement('div', { className: 'workbench-notice-preview-summary' }, noticePreviewRecord.content), - React.createElement('div', { className: 'workbench-notice-preview-detail' }, - React.createElement('div', { className: 'workbench-notice-preview-detail-label' }, '详细说明'), - React.createElement('div', { className: 'workbench-notice-preview-detail-body' }, - noticePreviewRecord.detail || noticePreviewRecord.content - ) - ) - ) - : null - ), - - React.createElement(Modal, { - title: '自定义快速入口 · ' + (quickByRole[roleTab] ? quickByRole[roleTab].label : ''), - open: quickCustomizeOpen, - width: 640, - onCancel: function () { setQuickCustomizeOpen(false); }, - destroyOnClose: true, - footer: React.createElement(Space, { wrap: true }, - React.createElement(Button, { onClick: resetQuickCustomize }, '恢复默认'), - React.createElement(Button, { onClick: function () { setQuickCustomizeOpen(false); } }, '取消'), - React.createElement(Button, { type: 'primary', onClick: saveQuickCustomize }, '保存') - ) - }, - React.createElement(Text, { type: 'secondary', style: { display: 'block', marginBottom: 12, fontSize: 13, lineHeight: 1.5 } }, - '勾选要在工作台展示的功能,并可调整顺序。设置将保存在本浏览器,不同角色独立配置。' - ), - React.createElement('div', { className: 'workbench-quick-customize' }, - React.createElement('div', { className: 'workbench-quick-customize-col' }, - React.createElement('div', { className: 'workbench-quick-customize-title' }, '可选功能'), - React.createElement('div', { className: 'workbench-quick-customize-list' }, - quickCatalogItems.map(function (it) { - var key = quickItemKey(it); - var checked = quickCustomDraft.indexOf(key) >= 0; - return React.createElement('label', { - key: key, - className: 'workbench-quick-customize-option' + (checked ? ' is-checked' : '') - }, - React.createElement(Checkbox, { - checked: checked, - onChange: function (e) { - toggleQuickDraftKey(key, e.target.checked); - } - }), - React.createElement('span', { className: 'workbench-quick-customize-option-label' }, it.t) - ); - }) - ) - ), - React.createElement('div', { className: 'workbench-quick-customize-col' }, - React.createElement('div', { className: 'workbench-quick-customize-title' }, - '已选顺序 ', - React.createElement('span', { className: 'workbench-quick-customize-count' }, '(' + quickCustomDraft.length + ')') - ), - quickCustomDraft.length === 0 - ? React.createElement(Text, { type: 'secondary', style: { fontSize: 13 } }, '请从左侧勾选至少一项') - : React.createElement('div', { className: 'workbench-quick-customize-order' }, - quickCustomDraft.map(function (key, idx) { - var item = quickCatalogItems.find(function (it) { return quickItemKey(it) === key; }); - if (!item) return null; - return React.createElement('div', { key: key, className: 'workbench-quick-customize-order-item' }, - React.createElement('span', { className: 'workbench-quick-customize-order-index' }, idx + 1), - React.createElement('span', { className: 'workbench-quick-customize-order-label' }, item.t), - React.createElement(Space, { size: 0 }, - React.createElement(Button, { - type: 'text', - size: 'small', - disabled: idx === 0, - onClick: function () { moveQuickDraftKey(key, 'up'); } - }, '上移'), - React.createElement(Button, { - type: 'text', - size: 'small', - disabled: idx === quickCustomDraft.length - 1, - onClick: function () { moveQuickDraftKey(key, 'down'); } - }, '下移') - ) - ); - }) - ) - ) - ) - ), - - React.createElement(Modal, { - title: '超期未交车', - open: overdueDeliveryModalOpen, - width: 1280, - onCancel: function () { setOverdueDeliveryModalOpen(false); }, - footer: React.createElement(Button, { onClick: function () { setOverdueDeliveryModalOpen(false); } }, '关闭'), - destroyOnClose: true - }, - React.createElement(Text, { type: 'secondary', style: { fontSize: 12, display: 'block', marginBottom: 10 } }, - '以下任务已超过预计交车结束日期,请与业管人员沟通交车是否延误,修改交车时间或及时处理' - ), - React.createElement(Table, { - className: 'workbench-overdue-delivery-modal', - size: 'small', - rowKey: 'id', - columns: overdueDeliveryModalColumns, - dataSource: overdueDeliveryMockRows, - pagination: false, - scroll: { x: 1620, y: 400 } - }) - ), - - React.createElement(Modal, { - title: '超期未还车数量', - open: overdueReturnModalOpen, - width: 1320, - onCancel: function () { setOverdueReturnModalOpen(false); }, - footer: React.createElement(Button, { onClick: function () { setOverdueReturnModalOpen(false); } }, '关闭'), - destroyOnClose: true - }, - React.createElement(Text, { type: 'secondary', style: { fontSize: 12, display: 'block', marginBottom: 10 } }, - '以下车辆已超过合同应还期限仍未完成还车流程,示意数据与还车管理-待处理列表字段一致(标橙提示);联调接接口。' - ), - React.createElement(Table, { - className: 'workbench-overdue-return-modal', - size: 'small', - rowKey: 'id', - columns: overdueReturnModalColumns, - dataSource: overdueReturnMockRows, - pagination: false, - scroll: { x: 1780, y: 360 } - }) - ), - - React.createElement(Modal, { - title: '年审/等级评定', - open: inspectModalOpen, - width: 1000, - onCancel: function () { setInspectModalOpen(false); }, - footer: React.createElement(Button, { onClick: function () { setInspectModalOpen(false); } }, '关闭'), - destroyOnClose: true - }, - React.createElement(Table, { - size: 'small', - rowKey: 'id', - columns: inspectModalColumns, - dataSource: inspectMockRows, - pagination: false, - scroll: { x: 800, y: 360 } - }) - ), - - React.createElement(Modal, { - className: 'workbench-gen-task-modal', - title: '生成交车任务', - open: genDeliveryPickOpen, - width: 520, - destroyOnClose: true, - onCancel: function () { - setGenDeliveryPickOpen(false); - resetGenDeliveryFlow(); - }, - footer: React.createElement(Space, null, - React.createElement(Button, { - onClick: function () { - setGenDeliveryPickOpen(false); - resetGenDeliveryFlow(); - } - }, '取消'), - React.createElement(Button, { type: 'primary', onClick: handleGenDeliveryNext }, '生成') - ) - }, - React.createElement('div', { className: 'workbench-gen-task-section' }, - React.createElement('div', { style: { marginBottom: 14 } }, - renderGenTaskLabel('客户名称', true), - React.createElement(Select, { - showSearch: true, - allowClear: true, - placeholder: '请选择客户名称', - style: { width: '100%' }, - value: genDeliveryCustomer || undefined, - options: genDeliveryCustomerOptions, - optionFilterProp: 'label', - onChange: function (val) { - setGenDeliveryCustomer(val || ''); - setGenDeliveryProjectId(undefined); - } - }) - ), - React.createElement('div', null, - renderGenTaskLabel('项目名称', true), - React.createElement(Select, { - showSearch: true, - allowClear: true, - placeholder: genDeliveryCustomer - ? (genDeliveryProjectOptions.length ? '请选择或搜索项目名称' : '该客户暂无可用项目') - : '请先选择客户名称', - style: { width: '100%' }, - value: genDeliveryProjectId, - options: genDeliveryProjectOptions, - optionFilterProp: 'label', - disabled: !genDeliveryCustomer, - onChange: function (val) { setGenDeliveryProjectId(val); } - }) - ) - ), - React.createElement(Text, { type: 'secondary', style: { fontSize: 12 } }, '选择客户与项目后,将配置预计交车日期并勾选待交车辆。') - ), - - React.createElement(Modal, { - className: 'workbench-gen-task-modal', - title: '配置交车任务', - open: genDeliveryConfigOpen, - width: 980, - destroyOnClose: true, - onCancel: function () { - setGenDeliveryConfigOpen(false); - resetGenDeliveryFlow(); - }, - footer: React.createElement(Space, null, - React.createElement(Button, { - onClick: function () { - setGenDeliveryConfigOpen(false); - setGenDeliveryPickOpen(true); - } - }, '上一步'), - React.createElement(Button, { - onClick: function () { - setGenDeliveryConfigOpen(false); - resetGenDeliveryFlow(); - } - }, '取消'), - React.createElement(Button, { type: 'primary', onClick: handleGenDeliveryConfirm }, '确认生成') - ) - }, - selectedGenDeliveryProject - ? React.createElement('div', { className: 'workbench-gen-task-readonly' }, - React.createElement('div', { className: 'workbench-gen-task-readonly-item' }, React.createElement('strong', null, '项目名称'), selectedGenDeliveryProject.name), - React.createElement('div', { className: 'workbench-gen-task-readonly-item' }, React.createElement('strong', null, '合同编码'), selectedGenDeliveryProject.contractCode), - React.createElement('div', { className: 'workbench-gen-task-readonly-item' }, React.createElement('strong', null, '客户名称'), selectedGenDeliveryProject.customerName), - React.createElement('div', { className: 'workbench-gen-task-readonly-item' }, React.createElement('strong', null, '交车区域'), selectedGenDeliveryProject.deliveryRegion) - ) - : null, - React.createElement(Row, { gutter: [16, 12], style: { marginBottom: 12 } }, - React.createElement(Col, { xs: 24, md: 12 }, - renderGenTaskLabel('预计交车日期', true), - React.createElement(RangePicker, { - style: { width: '100%' }, - value: genDeliveryExpected, - onChange: function (val) { setGenDeliveryExpected(val); }, - placeholder: ['开始日期', '结束日期'] - }) - ), - React.createElement(Col, { xs: 24, md: 12 }, - renderGenTaskLabel('开始计费日期', true), - React.createElement(DatePicker, { - style: { width: '100%' }, - value: genDeliveryBilling, - onChange: function (val) { setGenDeliveryBilling(val); }, - placeholder: '请选择开始计费日期' - }) - ) - ), - React.createElement('div', { className: 'workbench-gen-task-table-hint' }, - '勾选待交车辆;已创建交车任务或已完成交车的车辆不可选。' - ), - React.createElement(Table, { - size: 'small', - rowKey: 'key', - columns: genDeliveryVehicleColumns, - dataSource: genDeliveryVehicleList, - rowSelection: genDeliveryRowSelection, - pagination: false, - scroll: { x: 860, y: 280 }, - locale: { emptyText: '当前项目暂无可选车辆' } - }) - ), - - React.createElement(Modal, { - className: 'workbench-gen-task-modal', - title: '生成还车任务', - open: genReturnPickOpen, - width: 520, - destroyOnClose: true, - onCancel: function () { - setGenReturnPickOpen(false); - resetGenReturnFlow(); - }, - footer: React.createElement(Space, null, - React.createElement(Button, { - onClick: function () { - setGenReturnPickOpen(false); - resetGenReturnFlow(); - } - }, '取消'), - React.createElement(Button, { type: 'primary', onClick: handleGenReturnNext }, '生成') - ) - }, - React.createElement('div', { className: 'workbench-gen-task-section' }, - React.createElement('div', { style: { marginBottom: 14 } }, - renderGenTaskLabel('客户名称', true), - React.createElement(Input, { - placeholder: '请输入客户名称', - value: genReturnCustomer, - onChange: function (e) { - setGenReturnCustomer(e.target.value); - setGenReturnProjectId(undefined); - }, - allowClear: true - }) - ), - React.createElement('div', null, - renderGenTaskLabel('项目名称', true), - React.createElement(Select, { - showSearch: true, - allowClear: true, - placeholder: genReturnProjectOptions.length ? '请选择或搜索项目名称' : '暂无匹配项目,请调整客户名称', - style: { width: '100%' }, - value: genReturnProjectId, - options: genReturnProjectOptions, - optionFilterProp: 'label', - onChange: function (val) { setGenReturnProjectId(val); } - }) - ) - ), - React.createElement(Text, { type: 'secondary', style: { fontSize: 12 } }, '选择客户与项目后,将配置预计还车日期并勾选待还车辆。') - ), - - React.createElement(Modal, { - className: 'workbench-gen-task-modal', - title: '配置还车任务', - open: genReturnConfigOpen, - width: 860, - destroyOnClose: true, - onCancel: function () { - setGenReturnConfigOpen(false); - resetGenReturnFlow(); - }, - footer: React.createElement(Space, null, - React.createElement(Button, { - onClick: function () { - setGenReturnConfigOpen(false); - setGenReturnPickOpen(true); - } - }, '上一步'), - React.createElement(Button, { - onClick: function () { - setGenReturnConfigOpen(false); - resetGenReturnFlow(); - } - }, '取消'), - React.createElement(Button, { type: 'primary', onClick: handleGenReturnConfirm }, '确认生成') - ) - }, - selectedGenReturnProject - ? React.createElement('div', { className: 'workbench-gen-task-readonly' }, - React.createElement('div', { className: 'workbench-gen-task-readonly-item' }, React.createElement('strong', null, '项目名称'), selectedGenReturnProject.name), - React.createElement('div', { className: 'workbench-gen-task-readonly-item' }, React.createElement('strong', null, '合同编码'), selectedGenReturnProject.contractCode), - React.createElement('div', { className: 'workbench-gen-task-readonly-item' }, React.createElement('strong', null, '客户名称'), selectedGenReturnProject.customerName), - React.createElement('div', { className: 'workbench-gen-task-readonly-item' }, React.createElement('strong', null, '交车区域'), selectedGenReturnProject.deliveryRegion) - ) - : null, - React.createElement('div', { style: { marginBottom: 12, maxWidth: 320 } }, - renderGenTaskLabel('预计还车日期', true), - React.createElement(DatePicker, { - style: { width: '100%' }, - value: genReturnDate, - onChange: function (val) { setGenReturnDate(val); }, - placeholder: '请选择预计还车日期' - }) - ), - React.createElement('div', { className: 'workbench-gen-task-table-hint' }, - '勾选已交车、待还车辆;仅展示可发起还车流程的车辆。' - ), - React.createElement(Table, { - size: 'small', - rowKey: 'key', - columns: genReturnVehicleColumns, - dataSource: genReturnVehicleList, - rowSelection: genReturnRowSelection, - pagination: false, - scroll: { x: 620, y: 280 }, - locale: { emptyText: '当前项目暂无已交车车辆' } - }) - ) - ) - ); -}; - -export default Component; diff --git a/src/prototypes/payment-records/annotation-source.json b/src/prototypes/payment-records/annotation-source.json index aa58e23..7f3e28b 100644 --- a/src/prototypes/payment-records/annotation-source.json +++ b/src/prototypes/payment-records/annotation-source.json @@ -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" } ] } diff --git a/src/prototypes/payment-records/index.tsx b/src/prototypes/payment-records/index.tsx index 80a07e5..18c9212 100644 --- a/src/prototypes/payment-records/index.tsx +++ b/src/prototypes/payment-records/index.tsx @@ -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); + }} > 批量导入 diff --git a/src/prototypes/self-operated-business-ledger/.spec/requirements-prd.md b/src/prototypes/self-operated-business-ledger/.spec/requirements-prd.md index a0df00c..07a0bfa 100644 --- a/src/prototypes/self-operated-business-ledger/.spec/requirements-prd.md +++ b/src/prototypes/self-operated-business-ledger/.spec/requirements-prd.md @@ -1,7 +1,9 @@ # 物流业务明细 · 产品需求说明(PRD) > 参照《2026年业务二部运营台账总表》物流业务明细子表;行内编辑与导入为原型 Toast 演示。 -> **界面设计规范**与租赁业务明细、租赁合同列表统一,见 `src/prototypes/ledger-shared/DESIGN.md`(`vm-page ldb-page`)。 +> **界面设计规范**与租赁业务明细、租赁合同列表统一,见 `src/prototypes/ledger-shared/DESIGN.md`(`vm-page ldb-page`)。 +> **主路径升级**:调度任务办结自动生成明细行;导入降为补录。规则全文见 [ledger-auto-gen.md](./ledger-auto-gen.md)。 +> 假设:[assumptions.md](../../../resources/self-operated-logistics/assumptions.md)。 --- @@ -11,8 +13,9 @@ |---|---| | 模块名称 | 台账数据 — 物流业务明细 | | 目标用户 | 业务服务组操作人、主管 | -| 核心任务 | 记录出车营收与成本、自动汇总盈亏、导入导出与删改 | +| 核心任务 | 承接调度自动出车明细、补录导入、自动汇总盈亏 | | 页面结构 | 筛选区 + KPI 统计 + 工具栏 + 宽表 + 分页 | +| 上游 | 自营合同 → 轻量调度任务办结 | --- @@ -73,3 +76,22 @@ **总成本** = 氢费 + 人工报销费用 + ETC费用 + 电费 + 薪资 + 车辆费用。 **盈亏** = 金额 − 总成本;参照 A 股配色:**正数红色、负数绿色**。 + +--- + +## 6. 调度办结自动生成(摘要) + +| 项 | 说明 | +|---|---| +| 业务目的 | 办结出车任务后自动落明细,减少手工导入 | +| 触发 | `self-operated-dispatch-task` 办结成功(须车辆已交车) | +| 粒度 | 一任务一行;备注前缀 `[调度自动·任务号]` | +| 成本一期 | 营收侧自动;氢费/ETC 等默认 0,可补录 | +| 数据源 | Bridge `ledger-auto` + 种子/导入行合并展示 | +| 规格全文 | [ledger-auto-gen.md](./ledger-auto-gen.md) | + +### 验收重点(自动生成) + +- [ ] 调度办结后本页刷新可见新行(同浏览器 localStorage) +- [ ] 自动行备注含任务号;金额/盈亏公式正确 +- [ ] 导入仍可用,不覆盖不同 id 的自动行 diff --git a/src/prototypes/self-operated-business-ledger/annotation-source.json b/src/prototypes/self-operated-business-ledger/annotation-source.json index da52e33..67f2517 100644 --- a/src/prototypes/self-operated-business-ledger/annotation-source.json +++ b/src/prototypes/self-operated-business-ledger/annotation-source.json @@ -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---" } ] } diff --git a/src/prototypes/self-operated-business-ledger/index.tsx b/src/prototypes/self-operated-business-ledger/index.tsx index 82a3062..2ad4d72 100644 --- a/src/prototypes/self-operated-business-ledger/index.tsx +++ b/src/prototypes/self-operated-business-ledger/index.tsx @@ -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(seedRows as SelfOperatedLedgerRow[]); + const [baseRecords, setBaseRecords] = useState( + () => seedRows as SelfOperatedLedgerRow[], + ); + const [records, setRecords] = useState(() => loadMergedRecords()); + + useEffect(() => { + const sync = () => setRecords(mergeLedgerWithAutoRows(baseRecords) as SelfOperatedLedgerRow[]); + sync(); + return subscribeSelfOperatedBridge(sync); + }, [baseRecords]); const [pendingFilters, setPendingFilters] = useState(EMPTY_SELF_FILTERS); const [appliedFilters, setAppliedFilters] = useState(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() { +

+ 主路径:调度任务办结自动生成明细(备注含「调度自动」);批量导入为补录。可先打开「调度任务」办结种子单 DD-2026-0001 演示。 +

+
@@ -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}(原型)`); }} diff --git a/src/prototypes/self-operated-business-ledger/scripts/build-annotation-source.mjs b/src/prototypes/self-operated-business-ledger/scripts/build-annotation-source.mjs index 8f6c834..401b251 100644 --- a/src/prototypes/self-operated-business-ledger/scripts/build-annotation-source.mjs +++ b/src/prototypes/self-operated-business-ledger/scripts/build-annotation-source.mjs @@ -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}`, diff --git a/src/prototypes/self-operated-business-ledger/types.ts b/src/prototypes/self-operated-business-ledger/types.ts index ccb2f25..3be5ef3 100644 --- a/src/prototypes/self-operated-business-ledger/types.ts +++ b/src/prototypes/self-operated-business-ledger/types.ts @@ -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 { diff --git a/src/prototypes/vehicle-h2-fee-ledger/.spec/PRD.md b/src/prototypes/vehicle-h2-fee-ledger/.spec/PRD.md index 4f5d770..254ae02 100644 --- a/src/prototypes/vehicle-h2-fee-ledger/.spec/PRD.md +++ b/src/prototypes/vehicle-h2-fee-ledger/.spec/PRD.md @@ -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)。 **系统带出字段(不可手工维护):** 收票日期、加氢站付款状态、开票日期、客户收款状态、开票公司;选择/变更客户等信息后由系统自动刷新,单元格为浅灰底只读样式。 diff --git a/src/prototypes/vehicle-h2-fee-ledger/.spec/verify-reconcile.md b/src/prototypes/vehicle-h2-fee-ledger/.spec/verify-reconcile.md index 9f332c8..dbd7f98 100644 --- a/src/prototypes/vehicle-h2-fee-ledger/.spec/verify-reconcile.md +++ b/src/prototypes/vehicle-h2-fee-ledger/.spec/verify-reconcile.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` 等 | --- diff --git a/src/prototypes/vehicle-h2-fee-ledger/H2LedgerPage.jsx b/src/prototypes/vehicle-h2-fee-ledger/H2LedgerPage.jsx index 84dbe88..234d4c9 100644 --- a/src/prototypes/vehicle-h2-fee-ledger/H2LedgerPage.jsx +++ b/src/prototypes/vehicle-h2-fee-ledger/H2LedgerPage.jsx @@ -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 } diff --git a/src/prototypes/vehicle-h2-fee-ledger/annotation-source.json b/src/prototypes/vehicle-h2-fee-ledger/annotation-source.json index 98b76f2..aba5f77 100644 --- a/src/prototypes/vehicle-h2-fee-ledger/annotation-source.json +++ b/src/prototypes/vehicle-h2-fee-ledger/annotation-source.json @@ -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": [] }, { diff --git a/src/prototypes/vehicle-h2-fee-ledger/index.tsx b/src/prototypes/vehicle-h2-fee-ledger/index.tsx index 26290bd..c92db25 100644 --- a/src/prototypes/vehicle-h2-fee-ledger/index.tsx +++ b/src/prototypes/vehicle-h2-fee-ledger/index.tsx @@ -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'; diff --git a/src/prototypes/vehicle-management/components/OnLeaseFleetSection.tsx b/src/prototypes/vehicle-management/components/OnLeaseFleetSection.tsx index 49180bf..da5c647 100644 --- a/src/prototypes/vehicle-management/components/OnLeaseFleetSection.tsx +++ b/src/prototypes/vehicle-management/components/OnLeaseFleetSection.tsx @@ -59,14 +59,10 @@ function FleetModelCard({ ); } -function FleetOverviewModal({ - open, - onClose, +function FleetShareBody({ segmentCounts, onOpenDetail, }: { - open: boolean; - onClose: () => void; segmentCounts: Record; onOpenDetail: (item: OnLeaseFleetBucket) => void; }) { @@ -87,6 +83,58 @@ function FleetOverviewModal({ ? Math.max(...summary.map((item) => item.count)) : 1; + return ( +
+
+ {ON_LEASE_FLEET_SEGMENTS.map((segment) => ( + + ))} +
+ {summary.length ? ( +
+ {summary.map((item) => ( + + ))} +
+ ) : ( +
+

该业务线暂无在租车辆

+ +
+ )} +
+ ); +} + +function FleetOverviewModal({ + open, + onClose, + segmentCounts, + onOpenDetail, +}: { + open: boolean; + onClose: () => void; + segmentCounts: Record; + onOpenDetail: (item: OnLeaseFleetBucket) => void; +}) { if (!open) return null; return ( @@ -102,51 +150,13 @@ function FleetOverviewModal({

在租车型占比

-

- 当前筛选共 {totalOnLease} 辆在租,展示各品牌型号数量及占当前筛选总数的占比 -

+

按业务线筛选,展示各品牌型号数量及占当前筛选总数的占比

-
-
- {ON_LEASE_FLEET_SEGMENTS.map((segment) => ( - - ))} -
- {summary.length ? ( -
- {summary.map((item) => ( - - ))} -
- ) : ( -
-

该业务线暂无在租车辆

- -
- )} -
+
@@ -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(null); + const [segmentFilter, setSegmentFilter] = useState('全部'); + + const summary = useMemo( + () => buildOnLeaseFleetSummary(fleetSourceList, segmentFilter), + [fleetSourceList, segmentFilter], + ); + const totalOnLease = useMemo( + () => summary.reduce((sum, item) => sum + item.count, 0), + [summary], + ); + + if (segmentCounts.全部 <= 0) { + return ( +

+ {emptyText || '暂无在租车辆,无法查看车型占比'} +

+ ); + } + + return ( + <> +
+
+

在租车型占比

+

+ 当前筛选共 {totalOnLease} 辆在租 +

+
+
+ {ON_LEASE_FLEET_SEGMENTS.map((segment) => ( + + ))} +
+ {summary.length ? ( +
+ {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 ( + + ); + })} +
+ ) : ( +
+

该业务线暂无在租车辆

+ +
+ )} +
+ setDetailItem(null)} + /> + + ); +} + function FleetDetailModal({ open, item, diff --git a/src/prototypes/vehicle-management/data/vehicles.json b/src/prototypes/vehicle-management/data/vehicles.json index d50449a..c2ef8e7 100644 --- a/src/prototypes/vehicle-management/data/vehicles.json +++ b/src/prototypes/vehicle-management/data/vehicles.json @@ -17,14 +17,14 @@ "vehicleType": "轻型厢式货车", "brand": "现代", "model": "帕力安牌4.5吨冷链车", - "customer": "-", - "department": "-", - "manager": "-", - "contractNo": "-", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", "operateStatus": "可运营", "vehicleStatus": "未备车", "licenseStatus": "正常", - "insuranceStatus": "正常", + "insuranceStatus": "临期", "mileage": "0.00", "location": "广东省广州市", "gpsTime": "2026-06-23 14:07:17", @@ -61,10 +61,10 @@ "vehicleType": "轻型厢式货车", "brand": "现代", "model": "帕力安牌4.5吨冷链车", - "customer": "-", - "department": "-", - "manager": "-", - "contractNo": "-", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", "operateStatus": "可运营", "vehicleStatus": "异动中", "licenseStatus": "正常", @@ -112,7 +112,7 @@ "operateStatus": "租赁", "vehicleStatus": "已交车", "licenseStatus": "正常", - "insuranceStatus": "异常", + "insuranceStatus": "临期", "mileage": "25468.00", "location": "广东省广州市", "gpsTime": "2026-06-23 14:17:34", @@ -156,7 +156,7 @@ "operateStatus": "租赁", "vehicleStatus": "已交车", "licenseStatus": "正常", - "insuranceStatus": "异常", + "insuranceStatus": "正常", "mileage": "6210.00", "location": "上海市奉贤区", "gpsTime": "2026-06-23 14:17:35", @@ -199,8 +199,8 @@ "contractNo": "LNZLHT2025110402", "operateStatus": "租赁", "vehicleStatus": "已交车", - "licenseStatus": "异常", - "insuranceStatus": "异常", + "licenseStatus": "正常", + "insuranceStatus": "正常", "mileage": "31233.00", "location": "河南省开封市", "gpsTime": "2026-06-23 14:07:16", @@ -287,7 +287,7 @@ "contractNo": "JXGW-GC-23-ZL-611", "operateStatus": "租赁", "vehicleStatus": "已交车", - "licenseStatus": "正常", + "licenseStatus": "异常", "insuranceStatus": "异常", "mileage": "202.00", "location": "浙江省嘉兴市", @@ -325,14 +325,14 @@ "vehicleType": "重型半挂牵引车", "brand": "飞驰", "model": "49吨牵引车头", - "customer": "-", - "department": "-", - "manager": "-", - "contractNo": "-", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", "operateStatus": "可运营", "vehicleStatus": "未备车", "licenseStatus": "正常", - "insuranceStatus": "异常", + "insuranceStatus": "正常", "mileage": "127012.00", "location": "浙江省嘉兴市", "gpsTime": "2026-06-23 14:17:35", @@ -376,7 +376,7 @@ "operateStatus": "租赁", "vehicleStatus": "已交车", "licenseStatus": "正常", - "insuranceStatus": "异常", + "insuranceStatus": "正常", "mileage": "256.00", "location": "浙江省嘉兴市", "gpsTime": "2026-06-23 14:17:35", @@ -413,10 +413,10 @@ "vehicleType": "重型厢式货车", "brand": "现代", "model": "帕力安牌18吨双飞翼货车", - "customer": "嘉兴玲利供应链科技有限公司", - "department": "业务一部", - "manager": "王磊", - "contractNo": "LNZLHT2025112001-010", + "customer": "嘉兴市南湖区大桥镇雪观运输装卸服务站", + "department": "业务二部", + "manager": "刘念念", + "contractNo": "LNZLHT20251010001", "operateStatus": "租赁", "vehicleStatus": "已交车", "licenseStatus": "正常", @@ -463,7 +463,7 @@ "contractNo": "LNZLHT20251111001", "operateStatus": "租赁", "vehicleStatus": "已交车", - "licenseStatus": "异常", + "licenseStatus": "正常", "insuranceStatus": "正常", "mileage": "97556.00", "location": "浙江省嘉兴市", @@ -501,10 +501,10 @@ "vehicleType": "重型厢式货车", "brand": "现代", "model": "帕力安牌18吨双飞翼货车", - "customer": "上海心坦物流有限公司", + "customer": "上海畅霸实业有限公司", "department": "业务五部", "manager": "秦挺", - "contractNo": "LNZLHT2026032301-042", + "contractNo": "LNZLHT2026012701-042", "operateStatus": "租赁", "vehicleStatus": "已交车", "licenseStatus": "正常", @@ -545,10 +545,10 @@ "vehicleType": "轻型厢式货车", "brand": "现代", "model": "4.5吨货车", - "customer": "-", - "department": "-", - "manager": "-", - "contractNo": "-", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", "operateStatus": "待运营", "vehicleStatus": "未备车", "licenseStatus": "异常", @@ -590,12 +590,12 @@ "vehicleType": "轻型厢式货车", "brand": "现代", "model": "4.5吨货车", - "customer": "杭州冠泽物流有限公司", - "department": "业务二部", - "manager": "董剑煜", - "contractNo": "LNZLHT 20260423002", - "operateStatus": "租赁", - "vehicleStatus": "已交车", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", + "operateStatus": "可运营", + "vehicleStatus": "未备车", "licenseStatus": "正常", "insuranceStatus": "正常", "mileage": "2973.00", @@ -638,7 +638,7 @@ "department": "业务二部", "manager": "谯云", "contractNo": "20251127001", - "operateStatus": "物流", + "operateStatus": "自营", "vehicleStatus": "已交车", "licenseStatus": "正常", "insuranceStatus": "正常", @@ -678,12 +678,12 @@ "vehicleType": "重型半挂牵引车", "brand": "宇通", "model": "49吨牵引车头", - "customer": "-", - "department": "-", - "manager": "-", - "contractNo": "-", - "operateStatus": "待运营", - "vehicleStatus": "未备车", + "customer": "重庆丰坤达供应链有限公司", + "department": "业务一部", + "manager": "陈高伟", + "contractNo": "LNZLHT20260706001", + "operateStatus": "租赁", + "vehicleStatus": "已交车", "licenseStatus": "正常", "insuranceStatus": "异常", "mileage": "11578.00", @@ -722,12 +722,12 @@ "vehicleType": "重型半挂牵引车", "brand": "宇通", "model": "49吨牵引车头", - "customer": "嘉兴玲利供应链科技有限公司", - "department": "业务一部", - "manager": "陈高伟", - "contractNo": "LNZLHT20251201001", - "operateStatus": "租赁", - "vehicleStatus": "已交车", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", + "operateStatus": "待运营", + "vehicleStatus": "未备车", "licenseStatus": "正常", "insuranceStatus": "异常", "mileage": "45195.00", @@ -810,10 +810,10 @@ "vehicleType": "轻型厢式货车", "brand": "跃进", "model": "4.5吨冷链车", - "customer": "-", - "department": "-", - "manager": "-", - "contractNo": "-", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", "operateStatus": "待运营", "vehicleStatus": "未备车", "licenseStatus": "正常", @@ -854,12 +854,12 @@ "vehicleType": "轻型厢式货车", "brand": "跃进", "model": "4.5吨冷链车", - "customer": "-", - "department": "-", - "manager": "-", - "contractNo": "-", + "customer": "", + "department": "", + "manager": "", + "contractNo": "LNZLHTSH2023071301", "operateStatus": "待运营", - "vehicleStatus": "未备车", + "vehicleStatus": "已备车", "licenseStatus": "正常", "insuranceStatus": "异常", "mileage": "4596.00", @@ -898,12 +898,12 @@ "vehicleType": "轻型厢式货车", "brand": "跃进", "model": "4.5吨冷链车", - "customer": "上海浦江特种气体有限公司", - "department": "业务三部", - "manager": "金可鹏", - "contractNo": "LNZLHT2025031301", - "operateStatus": "租赁", - "vehicleStatus": "已交车", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", + "operateStatus": "可运营", + "vehicleStatus": "未备车", "licenseStatus": "正常", "insuranceStatus": "正常", "mileage": "52614.00", @@ -942,10 +942,10 @@ "vehicleType": "重型厢式货车", "brand": "飞驰", "model": "18吨厢式货车", - "customer": "-", - "department": "-", - "manager": "-", - "contractNo": "-", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", "operateStatus": "待运营", "vehicleStatus": "未备车", "licenseStatus": "正常", @@ -986,10 +986,10 @@ "vehicleType": "重型厢式货车", "brand": "飞驰", "model": "18吨厢式货车", - "customer": "-", - "department": "-", - "manager": "-", - "contractNo": "-", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", "operateStatus": "待运营", "vehicleStatus": "未备车", "licenseStatus": "正常", @@ -1036,7 +1036,7 @@ "contractNo": "LNZLHT20251010001", "operateStatus": "租赁", "vehicleStatus": "已交车", - "licenseStatus": "异常", + "licenseStatus": "正常", "insuranceStatus": "正常", "mileage": "72849.00", "location": "浙江省嘉兴市", @@ -1074,14 +1074,14 @@ "vehicleType": "重型厢式货车", "brand": "楚风", "model": "18吨厢式货车", - "customer": "-", - "department": "-", - "manager": "-", - "contractNo": "-", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", "operateStatus": "可运营", "vehicleStatus": "未备车", "licenseStatus": "正常", - "insuranceStatus": "正常", + "insuranceStatus": "临期", "mileage": "0.00", "location": "北京市大兴区", "gpsTime": "2026-06-05 18:17:42", @@ -1118,14 +1118,14 @@ "vehicleType": "重型厢式货车", "brand": "楚风", "model": "18吨厢式货车", - "customer": "-", - "department": "-", - "manager": "-", - "contractNo": "-", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", "operateStatus": "可运营", "vehicleStatus": "未备车", "licenseStatus": "正常", - "insuranceStatus": "正常", + "insuranceStatus": "临期", "mileage": "0.00", "location": "北京市大兴区", "gpsTime": "2026-06-04 17:31:15", @@ -1162,14 +1162,14 @@ "vehicleType": "重型厢式货车", "brand": "楚风", "model": "18吨厢式货车", - "customer": "-", - "department": "-", - "manager": "-", - "contractNo": "-", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", "operateStatus": "可运营", "vehicleStatus": "未备车", "licenseStatus": "正常", - "insuranceStatus": "正常", + "insuranceStatus": "临期", "mileage": "0.00", "location": "北京市大兴区", "gpsTime": "2026-06-01 17:11:51", @@ -1206,14 +1206,14 @@ "vehicleType": "重型集装箱半挂车", "brand": "通华", "model": "重型集装箱半挂车", - "customer": "羚牛氢能-物流中心", - "department": "运营中心", - "manager": "陈伟", - "contractNo": "LNZL-ZY-202403-028", - "operateStatus": "物流", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", + "operateStatus": "自营", "vehicleStatus": "已交车", "licenseStatus": "正常", - "insuranceStatus": "正常", + "insuranceStatus": "异常", "mileage": "-", "location": "浙江省", "gpsTime": "-", @@ -1250,14 +1250,14 @@ "vehicleType": "重型集装箱半挂车", "brand": "通华", "model": "重型集装箱半挂车", - "customer": "羚牛氢能-物流中心", - "department": "运营中心", - "manager": "陈伟", - "contractNo": "LNZL-ZY-202403-029", - "operateStatus": "物流", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", + "operateStatus": "自营", "vehicleStatus": "已交车", - "licenseStatus": "异常", - "insuranceStatus": "正常", + "licenseStatus": "正常", + "insuranceStatus": "异常", "mileage": "-", "location": "浙江省", "gpsTime": "-", @@ -1294,14 +1294,14 @@ "vehicleType": "重型集装箱半挂车", "brand": "通华", "model": "重型集装箱半挂车", - "customer": "羚牛氢能-物流中心", - "department": "运营中心", - "manager": "陈伟", - "contractNo": "LNZL-ZY-202403-030", - "operateStatus": "物流", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", + "operateStatus": "自营", "vehicleStatus": "已交车", "licenseStatus": "正常", - "insuranceStatus": "正常", + "insuranceStatus": "异常", "mileage": "-", "location": "浙江省", "gpsTime": "-", @@ -1345,7 +1345,7 @@ "operateStatus": "租赁", "vehicleStatus": "已交车", "licenseStatus": "正常", - "insuranceStatus": "正常", + "insuranceStatus": "异常", "mileage": "-", "location": "四川省", "gpsTime": "-", @@ -1382,14 +1382,14 @@ "vehicleType": "重型半挂牵引车", "brand": "楚风", "model": "49吨牵引车头", - "customer": "-", - "department": "-", - "manager": "-", - "contractNo": "-", - "operateStatus": "可运营", - "vehicleStatus": "未备车", + "customer": "四川国腾供应链管理有限公司", + "department": "业务五部", + "manager": "范军军", + "contractNo": "LNZLHT2026063001-042", + "operateStatus": "租赁", + "vehicleStatus": "已交车", "licenseStatus": "正常", - "insuranceStatus": "正常", + "insuranceStatus": "异常", "mileage": "-", "location": "四川省", "gpsTime": "-", @@ -1420,7 +1420,7 @@ "ownership": "海珀特科技(北京)有限公司", "scrapDate": "-", "ratingTime": "-", - "operateCompany": "-", + "operateCompany": "", "vehicleSource": "外租", "leaseCompany": "海珀特科技(北京)有限公司", "vehicleType": "重型半挂牵引车", @@ -1433,7 +1433,7 @@ "operateStatus": "租赁", "vehicleStatus": "已交车", "licenseStatus": "正常", - "insuranceStatus": "-", + "insuranceStatus": "异常", "mileage": "-", "location": "-", "gpsTime": "-", @@ -1475,7 +1475,7 @@ "operateStatus": "租赁", "vehicleStatus": "已交车", "licenseStatus": "正常", - "insuranceStatus": "正常", + "insuranceStatus": "异常", "mileage": "-", "location": "浙江省", "gpsTime": "-", @@ -1519,7 +1519,7 @@ "operateStatus": "租赁", "vehicleStatus": "已交车", "licenseStatus": "正常", - "insuranceStatus": "正常", + "insuranceStatus": "异常", "mileage": "-", "location": "浙江省", "gpsTime": "-", @@ -1556,14 +1556,14 @@ "vehicleType": "小型普通客车", "brand": "腾势", "model": "公务用车/小客车", - "customer": "羚牛公务车专用", - "department": "-", - "manager": "-", - "contractNo": "LNZL-GW-202501-036", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", "operateStatus": "租赁", "vehicleStatus": "已交车", - "licenseStatus": "异常", - "insuranceStatus": "正常", + "licenseStatus": "正常", + "insuranceStatus": "异常", "mileage": "-", "location": "广东省", "gpsTime": "-", @@ -1600,13 +1600,13 @@ "vehicleType": "小型普通客车", "brand": "腾势", "model": "公务用车/小客车", - "customer": "羚牛公务车专用", - "department": "-", - "manager": "-", - "contractNo": "LNZL-GW-202501-037", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", "operateStatus": "租赁", "vehicleStatus": "已交车", - "licenseStatus": "正常", + "licenseStatus": "异常", "insuranceStatus": "正常", "mileage": "-", "location": "上海市", @@ -1645,13 +1645,13 @@ "brand": "其他", "model": "公务用车/小客车", "customer": "羚牛公务车专用", - "department": "-", - "manager": "-", + "department": "", + "manager": "", "contractNo": "LNGWC20240105", - "operateStatus": "物流", + "operateStatus": "自营", "vehicleStatus": "已交车", "licenseStatus": "正常", - "insuranceStatus": "正常", + "insuranceStatus": "异常", "mileage": "-", "location": "浙江省", "gpsTime": "-", @@ -1688,14 +1688,14 @@ "vehicleType": "小型普通客车", "brand": "其他", "model": "公务用车/小客车", - "customer": "羚牛公务车专用", - "department": "-", - "manager": "-", - "contractNo": "LNZL-GW-202502-039", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", "operateStatus": "租赁", "vehicleStatus": "已交车", "licenseStatus": "正常", - "insuranceStatus": "正常", + "insuranceStatus": "异常", "mileage": "-", "location": "浙江省", "gpsTime": "-", @@ -1732,14 +1732,14 @@ "vehicleType": "重型集装箱半挂车", "brand": "明威", "model": "重型集装箱半挂车", - "customer": "嘉兴玲利供应链科技有限公司", - "department": "业务一部", - "manager": "陈高伟", - "contractNo": "LNZLHT20251021002", - "operateStatus": "租赁", - "vehicleStatus": "已交车", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", + "operateStatus": "待运营", + "vehicleStatus": "未备车", "licenseStatus": "正常", - "insuranceStatus": "正常", + "insuranceStatus": "异常", "mileage": "-", "location": "浙江省", "gpsTime": "-", @@ -1776,14 +1776,14 @@ "vehicleType": "重型集装箱半挂车", "brand": "明威", "model": "重型集装箱半挂车", - "customer": "嘉兴玲利供应链科技有限公司", - "department": "业务一部", - "manager": "陈高伟", - "contractNo": "LNZLHT20250928001", - "operateStatus": "租赁", - "vehicleStatus": "已交车", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", + "operateStatus": "待运营", + "vehicleStatus": "未备车", "licenseStatus": "正常", - "insuranceStatus": "正常", + "insuranceStatus": "异常", "mileage": "-", "location": "浙江省", "gpsTime": "-", @@ -1827,7 +1827,7 @@ "operateStatus": "租赁", "vehicleStatus": "已交车", "licenseStatus": "正常", - "insuranceStatus": "正常", + "insuranceStatus": "异常", "mileage": "-", "location": "浙江省", "gpsTime": "-", @@ -1864,14 +1864,14 @@ "vehicleType": "重型平板半挂车", "brand": "万风", "model": "重型平板半挂车", - "customer": "-", - "department": "-", - "manager": "-", - "contractNo": "-", - "operateStatus": "可运营", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", + "operateStatus": "待运营", "vehicleStatus": "未备车", "licenseStatus": "正常", - "insuranceStatus": "正常", + "insuranceStatus": "异常", "mileage": "-", "location": "浙江省", "gpsTime": "-", @@ -1908,14 +1908,14 @@ "vehicleType": "叉车", "brand": "其他", "model": "氢能叉车", - "customer": "-", - "department": "-", - "manager": "-", - "contractNo": "-", - "operateStatus": "可运营", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", + "operateStatus": "待运营", "vehicleStatus": "未备车", - "licenseStatus": "正常", - "insuranceStatus": "正常", + "licenseStatus": "异常", + "insuranceStatus": "异常", "mileage": "-", "location": "浙江省", "gpsTime": "-", @@ -1952,14 +1952,14 @@ "vehicleType": "重型厢式货车", "brand": "东风", "model": "挂靠油车", - "customer": "-", - "department": "-", - "manager": "-", - "contractNo": "-", - "operateStatus": "可运营", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", + "operateStatus": "待运营", "vehicleStatus": "未备车", "licenseStatus": "正常", - "insuranceStatus": "正常", + "insuranceStatus": "异常", "mileage": "-", "location": "上海市", "gpsTime": "-", @@ -1996,14 +1996,14 @@ "vehicleType": "小型普通客车", "brand": "远程牌", "model": "公务用车/小客车", - "customer": "羚牛公务车专用", - "department": "-", - "manager": "-", - "contractNo": "LNZL-GW-202502-046", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", "operateStatus": "租赁", "vehicleStatus": "已交车", "licenseStatus": "正常", - "insuranceStatus": "正常", + "insuranceStatus": "异常", "mileage": "-", "location": "浙江省", "gpsTime": "-", @@ -2045,9 +2045,9 @@ "manager": "刘念念", "contractNo": "20251017", "operateStatus": "租赁", - "vehicleStatus": "已交车", + "vehicleStatus": "待还车", "licenseStatus": "正常", - "insuranceStatus": "正常", + "insuranceStatus": "异常", "mileage": "458272.00", "location": "浙江省嘉兴市", "gpsTime": "2026-06-23 14:17:34", @@ -2084,14 +2084,14 @@ "vehicleType": "轻型厢式货车", "brand": "现代", "model": "帕力安牌4.5吨冷链车", - "customer": "广州长运冷链服务有限公司", - "department": "业务三部", - "manager": "金可鹏", - "contractNo": "LNZLHT2025112201", - "operateStatus": "租赁", - "vehicleStatus": "已交车", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", + "operateStatus": "可运营", + "vehicleStatus": "未备车", "licenseStatus": "正常", - "insuranceStatus": "正常", + "insuranceStatus": "临期", "mileage": "1596.00", "location": "广东省广州市", "gpsTime": "2026-06-23 14:17:35", @@ -2172,10 +2172,10 @@ "vehicleType": "重型半挂牵引车", "brand": "飞驰", "model": "49吨牵引车头", - "customer": "-", - "department": "-", - "manager": "-", - "contractNo": "-", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", "operateStatus": "可运营", "vehicleStatus": "未备车", "licenseStatus": "正常", @@ -2260,10 +2260,10 @@ "vehicleType": "轻型厢式货车", "brand": "现代", "model": "4.5吨货车", - "customer": "-", - "department": "-", - "manager": "-", - "contractNo": "-", + "customer": "", + "department": "", + "manager": "", + "contractNo": "LNZLHT2026040201-042", "operateStatus": "可运营", "vehicleStatus": "未备车", "licenseStatus": "正常", @@ -2348,10 +2348,10 @@ "vehicleType": "轻型厢式货车", "brand": "跃进", "model": "4.5吨冷链车", - "customer": "-", - "department": "-", - "manager": "-", - "contractNo": "-", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", "operateStatus": "待运营", "vehicleStatus": "未备车", "licenseStatus": "正常", @@ -2398,7 +2398,7 @@ "contractNo": "LNZLHT20251216001", "operateStatus": "租赁", "vehicleStatus": "已交车", - "licenseStatus": "异常", + "licenseStatus": "正常", "insuranceStatus": "正常", "mileage": "104217.00", "location": "浙江省嘉兴市", @@ -2436,14 +2436,14 @@ "vehicleType": "重型厢式货车", "brand": "楚风", "model": "18吨厢式货车", - "customer": "-", - "department": "-", - "manager": "-", - "contractNo": "-", - "operateStatus": "可运营", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", + "operateStatus": "待运营", "vehicleStatus": "未备车", "licenseStatus": "正常", - "insuranceStatus": "正常", + "insuranceStatus": "异常", "mileage": "0.00", "location": "北京市大兴区", "gpsTime": "2026-06-05 12:38:03", @@ -2480,14 +2480,14 @@ "vehicleType": "重型集装箱半挂车", "brand": "通华", "model": "重型集装箱半挂车", - "customer": "羚牛氢能-物流中心", - "department": "运营中心", - "manager": "陈伟", - "contractNo": "LNZL-ZY-202403-057", - "operateStatus": "物流", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", + "operateStatus": "自营", "vehicleStatus": "已交车", "licenseStatus": "正常", - "insuranceStatus": "正常", + "insuranceStatus": "异常", "mileage": "-", "location": "浙江省", "gpsTime": "-", @@ -2528,10 +2528,10 @@ "department": "业务二部", "manager": "董剑煜", "contractNo": "20260316001", - "operateStatus": "物流", + "operateStatus": "自营", "vehicleStatus": "已交车", "licenseStatus": "正常", - "insuranceStatus": "正常", + "insuranceStatus": "临期", "mileage": "26010.00", "location": "江苏省苏州市", "gpsTime": "2026-06-23 14:17:35", @@ -2656,11 +2656,11 @@ "vehicleType": "重型厢式货车", "brand": "现代", "model": "帕力安牌18吨双飞翼货车", - "customer": "嘉兴智奇供应链管理有限公司", + "customer": "苏州望亭远方物流有限公司", "department": "业务二部", "manager": "刘念念", - "contractNo": "20260105", - "operateStatus": "物流", + "contractNo": "LNZLHT 20260225001", + "operateStatus": "租赁", "vehicleStatus": "已交车", "licenseStatus": "正常", "insuranceStatus": "正常", @@ -2700,13 +2700,13 @@ "vehicleType": "轻型厢式货车", "brand": "现代", "model": "4.5吨货车", - "customer": "-", - "department": "-", - "manager": "-", - "contractNo": "-", - "operateStatus": "待运营", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", + "operateStatus": "可运营", "vehicleStatus": "未备车", - "licenseStatus": "异常", + "licenseStatus": "正常", "insuranceStatus": "正常", "mileage": "25335.00", "location": "浙江省嘉兴市", @@ -2744,10 +2744,10 @@ "vehicleType": "重型半挂牵引车", "brand": "宇通", "model": "49吨牵引车头", - "customer": "-", - "department": "-", - "manager": "-", - "contractNo": "-", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", "operateStatus": "待运营", "vehicleStatus": "未备车", "licenseStatus": "正常", @@ -2788,10 +2788,10 @@ "vehicleType": "轻型厢式货车", "brand": "跃进", "model": "4.5吨冷链车", - "customer": "-", - "department": "-", - "manager": "-", - "contractNo": "-", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", "operateStatus": "待运营", "vehicleStatus": "未备车", "licenseStatus": "正常", @@ -2838,7 +2838,7 @@ "contractNo": "LNZLHT 20241205001", "operateStatus": "租赁", "vehicleStatus": "已交车", - "licenseStatus": "异常", + "licenseStatus": "正常", "insuranceStatus": "正常", "mileage": "94145.00", "location": "浙江省嘉兴市", @@ -2883,7 +2883,7 @@ "operateStatus": "租赁", "vehicleStatus": "已交车", "licenseStatus": "正常", - "insuranceStatus": "正常", + "insuranceStatus": "临期", "mileage": "66023.00", "location": "四川省德阳市", "gpsTime": "2026-06-23 14:17:45", @@ -2920,14 +2920,14 @@ "vehicleType": "重型集装箱半挂车", "brand": "通华", "model": "重型集装箱半挂车", - "customer": "羚牛氢能-物流中心", - "department": "运营中心", - "manager": "陈伟", - "contractNo": "LNZL-ZY-202403-067", - "operateStatus": "物流", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", + "operateStatus": "自营", "vehicleStatus": "已交车", "licenseStatus": "正常", - "insuranceStatus": "正常", + "insuranceStatus": "异常", "mileage": "-", "location": "浙江省", "gpsTime": "-", @@ -2968,7 +2968,7 @@ "department": "业务二部", "manager": "尚建华", "contractNo": "20250801001", - "operateStatus": "物流", + "operateStatus": "自营", "vehicleStatus": "已交车", "licenseStatus": "正常", "insuranceStatus": "正常", @@ -3096,12 +3096,12 @@ "vehicleType": "重型厢式货车", "brand": "现代", "model": "帕力安牌18吨双飞翼货车", - "customer": "嘉兴智奇供应链管理有限公司", - "department": "业务二部", - "manager": "刘念念", - "contractNo": "LNZLHT 20260413001", - "operateStatus": "租赁", - "vehicleStatus": "已交车", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", + "operateStatus": "可运营", + "vehicleStatus": "未备车", "licenseStatus": "正常", "insuranceStatus": "正常", "mileage": "8893.00", @@ -3140,13 +3140,13 @@ "vehicleType": "轻型厢式货车", "brand": "现代", "model": "4.5吨货车", - "customer": "广东氢沣科技有限公司", + "customer": "佛山市昌盛骏业供应链管理有限公司", "department": "业务三部", "manager": "吴纬涛", - "contractNo": "LNZLHT2026040201-042", + "contractNo": "LNZLHT2026052603-042", "operateStatus": "租赁", "vehicleStatus": "已交车", - "licenseStatus": "异常", + "licenseStatus": "正常", "insuranceStatus": "正常", "mileage": "15560.00", "location": "广东省佛山市", @@ -3184,10 +3184,10 @@ "vehicleType": "重型半挂牵引车", "brand": "宇通", "model": "49吨牵引车头", - "customer": "-", - "department": "-", - "manager": "-", - "contractNo": "-", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", "operateStatus": "待运营", "vehicleStatus": "未备车", "licenseStatus": "正常", @@ -3228,10 +3228,10 @@ "vehicleType": "轻型厢式货车", "brand": "跃进", "model": "4.5吨冷链车", - "customer": "-", - "department": "-", - "manager": "-", - "contractNo": "-", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", "operateStatus": "待运营", "vehicleStatus": "未备车", "licenseStatus": "正常", @@ -3278,7 +3278,7 @@ "contractNo": "LNZLHT 20251026001", "operateStatus": "租赁", "vehicleStatus": "已交车", - "licenseStatus": "异常", + "licenseStatus": "正常", "insuranceStatus": "正常", "mileage": "116749.00", "location": "浙江省嘉兴市", @@ -3316,14 +3316,14 @@ "vehicleType": "重型厢式货车", "brand": "楚风", "model": "18吨厢式货车", - "customer": "-", - "department": "-", - "manager": "-", - "contractNo": "-", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", "operateStatus": "可运营", "vehicleStatus": "未备车", "licenseStatus": "正常", - "insuranceStatus": "正常", + "insuranceStatus": "临期", "mileage": "0.00", "location": "北京市大兴区", "gpsTime": "2026-06-03 13:52:34", @@ -3360,14 +3360,14 @@ "vehicleType": "重型集装箱半挂车", "brand": "通华", "model": "重型集装箱半挂车", - "customer": "羚牛氢能-物流中心", - "department": "运营中心", - "manager": "陈伟", - "contractNo": "LNZL-ZY-202403-077", - "operateStatus": "物流", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", + "operateStatus": "自营", "vehicleStatus": "已交车", "licenseStatus": "正常", - "insuranceStatus": "正常", + "insuranceStatus": "异常", "mileage": "-", "location": "浙江省", "gpsTime": "-", @@ -3580,13 +3580,13 @@ "vehicleType": "轻型厢式货车", "brand": "现代", "model": "4.5吨货车", - "customer": "-", - "department": "-", - "manager": "-", - "contractNo": "-", - "operateStatus": "待运营", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", + "operateStatus": "可运营", "vehicleStatus": "未备车", - "licenseStatus": "异常", + "licenseStatus": "正常", "insuranceStatus": "正常", "mileage": "0.00", "location": "浙江省嘉兴市", @@ -3668,10 +3668,10 @@ "vehicleType": "轻型厢式货车", "brand": "跃进", "model": "4.5吨冷链车", - "customer": "-", - "department": "-", - "manager": "-", - "contractNo": "-", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", "operateStatus": "待运营", "vehicleStatus": "未备车", "licenseStatus": "正常", @@ -3712,10 +3712,10 @@ "vehicleType": "重型厢式货车", "brand": "飞驰", "model": "18吨厢式货车", - "customer": "-", - "department": "-", - "manager": "-", - "contractNo": "-", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", "operateStatus": "待运营", "vehicleStatus": "未备车", "licenseStatus": "正常", @@ -3756,10 +3756,10 @@ "vehicleType": "重型厢式货车", "brand": "楚风", "model": "18吨厢式货车", - "customer": "-", - "department": "-", - "manager": "-", - "contractNo": "-", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", "operateStatus": "待运营", "vehicleStatus": "未备车", "licenseStatus": "正常", @@ -3800,14 +3800,14 @@ "vehicleType": "重型集装箱半挂车", "brand": "通华", "model": "重型集装箱半挂车", - "customer": "嘉兴市鼎义物流有限公司", - "department": "-", - "manager": "-", - "contractNo": "LNZL-ZY-202506-087", - "operateStatus": "物流", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", + "operateStatus": "自营", "vehicleStatus": "已交车", "licenseStatus": "正常", - "insuranceStatus": "正常", + "insuranceStatus": "异常", "mileage": "-", "location": "浙江省", "gpsTime": "-", @@ -3845,14 +3845,14 @@ "vehicleType": "轻型厢式货车", "brand": "现代", "model": "帕力安牌4.5吨冷链车", - "customer": "-", - "department": "-", - "manager": "-", - "contractNo": "-", - "operateStatus": "可运营", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", + "operateStatus": "待运营", "vehicleStatus": "未备车", "licenseStatus": "正常", - "insuranceStatus": "正常", + "insuranceStatus": "异常", "mileage": "0.00", "location": "广东省广州市", "gpsTime": "2026-06-23 14:17:35", @@ -3977,10 +3977,10 @@ "vehicleType": "重型厢式货车", "brand": "现代", "model": "帕力安牌18吨双飞翼货车", - "customer": "-", - "department": "-", - "manager": "-", - "contractNo": "-", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", "operateStatus": "可运营", "vehicleStatus": "未备车", "licenseStatus": "正常", @@ -4021,10 +4021,10 @@ "vehicleType": "轻型厢式货车", "brand": "现代", "model": "4.5吨货车", - "customer": "-", - "department": "-", - "manager": "-", - "contractNo": "-", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", "operateStatus": "可运营", "vehicleStatus": "未备车", "licenseStatus": "正常", @@ -4065,10 +4065,10 @@ "vehicleType": "重型半挂牵引车", "brand": "宇通", "model": "49吨牵引车头", - "customer": "-", - "department": "-", - "manager": "-", - "contractNo": "-", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", "operateStatus": "待运营", "vehicleStatus": "未备车", "licenseStatus": "正常", @@ -4109,10 +4109,10 @@ "vehicleType": "轻型厢式货车", "brand": "跃进", "model": "4.5吨冷链车", - "customer": "-", - "department": "-", - "manager": "-", - "contractNo": "-", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", "operateStatus": "待运营", "vehicleStatus": "未备车", "licenseStatus": "正常", @@ -4153,10 +4153,10 @@ "vehicleType": "重型厢式货车", "brand": "飞驰", "model": "18吨厢式货车", - "customer": "-", - "department": "-", - "manager": "-", - "contractNo": "-", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", "operateStatus": "待运营", "vehicleStatus": "未备车", "licenseStatus": "正常", @@ -4197,14 +4197,14 @@ "vehicleType": "重型厢式货车", "brand": "楚风", "model": "18吨厢式货车", - "customer": "-", - "department": "-", - "manager": "-", - "contractNo": "-", + "customer": "", + "department": "", + "manager": "", + "contractNo": "", "operateStatus": "可运营", "vehicleStatus": "未备车", "licenseStatus": "正常", - "insuranceStatus": "正常", + "insuranceStatus": "临期", "mileage": "0.00", "location": "北京市大兴区", "gpsTime": "2026-06-23 11:41:31", @@ -4245,8 +4245,8 @@ "department": "业务二部", "manager": "刘念念", "contractNo": "20250815001", - "operateStatus": "物流", - "vehicleStatus": "已交车", + "operateStatus": "自营", + "vehicleStatus": "待还车", "licenseStatus": "正常", "insuranceStatus": "正常", "mileage": "465.00", @@ -4333,7 +4333,7 @@ "department": "业务二部", "manager": "尚建华", "contractNo": "JXLN-23-019", - "operateStatus": "物流", + "operateStatus": "自营", "vehicleStatus": "已交车", "licenseStatus": "正常", "insuranceStatus": "正常", diff --git a/src/resources/oneos-web-legacy/台账数据/车辆氢费明细-需求文档.md b/src/resources/oneos-web-legacy/台账数据/车辆氢费明细-需求文档.md index 82c47d3..f3c5bf6 100644 --- a/src/resources/oneos-web-legacy/台账数据/车辆氢费明细-需求文档.md +++ b/src/resources/oneos-web-legacy/台账数据/车辆氢费明细-需求文档.md @@ -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(业务版) | diff --git a/vite-plugins/clientPreviewPlugin.ts b/vite-plugins/clientPreviewPlugin.ts index b8234ca..66b52ff 100644 --- a/vite-plugins/clientPreviewPlugin.ts +++ b/vite-plugins/clientPreviewPlugin.ts @@ -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);