迭代 ONE-OS 多原型:提车应收款收款/合同页增强,租赁明细标注同步,导航注册表更新;新增 Git 协作技能并忽略 .local 临时目录。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
王冕
2026-07-13 09:31:28 +08:00
parent 9a4a4a4f6a
commit 962e83597f
33 changed files with 5328 additions and 5863 deletions

View File

@@ -34,7 +34,10 @@
"bucket": "model-lnoneos",
"prefix": "",
"baseUrl": "https://prototype.lnoneos.com",
"endpoint": "https://model-lnoneos.oss-cn-hangzhou.aliyuncs.com"
"endpoint": "https://model-lnoneos.oss-cn-hangzhou.aliyuncs.com",
"pathAliases": {
"oneos-prototype-nav": "oneos-prototype-nav"
}
},
"githubPages": {
"repository": "",

View File

@@ -1,78 +1,73 @@
{
"version": 1,
"updatedAt": "2026-07-11T03:51:46.250Z",
"updatedAt": "2026-07-12T17:06:54.186Z",
"prototypes": [
{
"id": "item-prototypes-task-work-order",
"kind": "item",
"title": "task work order",
"itemKey": "prototypes/task-work-order"
},
"id": "folder-1783875936186-27raza",
"kind": "folder",
"title": "旧ONEOS",
"children": [
{
"id": "item-prototypes-lease-business-line-overview",
"id": "item:prototypes:oneos-web-business",
"kind": "item",
"title": "lease business line overview",
"itemKey": "prototypes/lease-business-line-overview"
},
{
"id": "item-prototypes-oneos-web-business",
"kind": "item",
"title": "oneos web business",
"title": "业务管理",
"itemKey": "prototypes/oneos-web-business"
},
{
"id": "item-prototypes-oneos-web-contract-template",
"id": "item:prototypes:oneos-web-contract-template",
"kind": "item",
"title": "oneos web contract template",
"title": "合同模板管理",
"itemKey": "prototypes/oneos-web-contract-template"
},
{
"id": "item-prototypes-oneos-web-data-analysis",
"id": "item:prototypes:oneos-web-data-analysis",
"kind": "item",
"title": "oneos web data analysis",
"title": "数据分析",
"itemKey": "prototypes/oneos-web-data-analysis"
},
{
"id": "item-prototypes-oneos-web-finance",
"id": "item:prototypes:oneos-web-finance",
"kind": "item",
"title": "oneos web finance",
"title": "财务管理(合包)",
"itemKey": "prototypes/oneos-web-finance"
},
{
"id": "item-prototypes-oneos-web-h2-station",
"id": "item:prototypes:oneos-web-h2-station",
"kind": "item",
"title": "oneos web h2 station",
"title": "加氢站管理",
"itemKey": "prototypes/oneos-web-h2-station"
},
{
"id": "item-prototypes-oneos-web-help-center",
"id": "item:prototypes:oneos-web-help-center",
"kind": "item",
"title": "oneos web help center",
"title": "帮助中心",
"itemKey": "prototypes/oneos-web-help-center"
},
{
"id": "item-prototypes-oneos-web-lease-contract",
"id": "item:prototypes:oneos-web-lease-contract",
"kind": "item",
"title": "oneos web lease contract",
"title": "车辆租赁合同",
"itemKey": "prototypes/oneos-web-lease-contract"
},
{
"id": "item-prototypes-oneos-web-ledger-data",
"id": "item:prototypes:oneos-web-ledger-data",
"kind": "item",
"title": "oneos web ledger data",
"title": "台账数据",
"itemKey": "prototypes/oneos-web-ledger-data"
},
{
"id": "item-prototypes-oneos-web-ops",
"id": "item:prototypes:oneos-web-ops",
"kind": "item",
"title": "oneos web ops",
"title": "运维管理",
"itemKey": "prototypes/oneos-web-ops"
},
{
"id": "item-prototypes-oneos-web-procurement",
"id": "item:prototypes:oneos-web-procurement",
"kind": "item",
"title": "oneos web procurement",
"title": "采购管理",
"itemKey": "prototypes/oneos-web-procurement"
}
]
},
{
"id": "item:prototypes:oneos-prototype-nav",
@@ -241,6 +236,25 @@
"itemKey": "prototypes/vehicle-return-settlement-v2"
}
]
},
{
"id": "folder-1783875880945-4r6tq1",
"kind": "folder",
"title": "任务工单",
"children": [
{
"id": "item:prototypes:task-work-order",
"kind": "item",
"title": "任务工单",
"itemKey": "prototypes/task-work-order"
}
]
},
{
"id": "item:prototypes:lease-business-line-overview",
"kind": "item",
"title": "业务条线说明",
"itemKey": "prototypes/lease-business-line-overview"
}
]
}

View File

@@ -0,0 +1,162 @@
---
name: git-repo-beginner-guide
description: "面向不熟悉 Git 的产品经理、设计师、运营、文档作者等非技术用户,引导 GitHub/GitLab/Gitee/Bitbucket/自建 Git 平台的仓库初始化、授权、克隆、提交同步、冲突处理、版本恢复和协作交接。"
---
# Git 仓库新手引导
## Overview
帮助非技术用户安全、低焦虑地使用 Git 仓库协作。默认由 AI 代做可执行操作,并把必要概念翻译成用户能理解的决策点。
## 核心原则
- 先问目标,不先教术语。用户通常要的是“保存一版”“发给团队”“拿到项目”“回到昨天”,不是学习 Git。
- 先保护现场。任何会丢弃、覆盖、重写历史、删除远程内容的动作,都先解释影响并确认。
- 默认代办。能由 AI 检查、执行、验证的,不要让用户自己照抄命令;用户只负责授权、选择平台、确认风险。
- 渐进解释。只有当概念会影响用户决策时才解释例如远程仓库、提交、同步、冲突。分支、rebase、stash 等先不主动引入。
- 平台中立。不要默认 GitHub先识别或询问 GitHub、GitLab、Gitee、Bitbucket、自建 Git、公司内网平台等。
- 用生活化比喻,但保留必要名词。第一次出现可写“提交 commit给当前文件拍一张可回退的快照”。
- 先小步闭环。每次只完成一个可验证状态:已登录、已连接远程、已保存本地版本、已同步到远程、已恢复文件。
## 交互流程
### 1. 先识别用户处境
先用自然语言复述用户目标,然后做最少量澄清。
常见处境:
- “我有一个本地文件夹,想放到代码平台上” → 初始化/连接远程/首次同步
- “别人给我一个链接,我要拿下来” → 克隆/权限/打开项目
- “我改完了,想保存并发给别人” → 查看改动/提交/推送
- “我怕改坏,想留个版本” → 本地提交或备份分支,按复杂度选择
- “提示要登录/没有权限” → 平台授权引导
- “提示冲突/推不上去” → 解释有人也改了同一处,先保护本地改动再合并
- “我要回到之前” → 明确是恢复文件、撤销未提交改动、还是回到某个历史版本
如果上下文已经足够,不要继续追问。直接说“我先帮你检查当前状态”。
### 2. 明确 AI 能帮什么
在开始前用一句话降低用户负担:
> 我可以帮你检查当前文件状态、创建版本记录、连接远程仓库、同步到平台;你只需要在需要网页登录授权时确认账号,或者告诉我目标仓库链接。
避免说“你运行以下命令”。除非当前环境无法代办,才给用户步骤。
### 3. 检查当前状态并翻译结果
检查本地是否是仓库、是否有远程地址、当前有无未保存改动、是否已登录相关平台。回复用户时不要直接堆 `git status` 原文,先给可行动摘要:
- “这个文件夹还不是仓库,我可以把它变成一个可记录版本的文件夹。”
- “它已经连接到远程平台,地址是 X。”
- “现在有 6 个文件改动了,还没有保存成版本。”
- “远程平台需要登录授权,接下来会打开/提供平台自己的登录流程。”
需要展示命令输出时,只摘关键含义。
### 4. 授权引导要平台灵活
先识别远程 URL 或用户指定平台,再选择授权方式:
- GitHub优先 `gh auth status` / `gh auth login`;必要时解释浏览器 OAuth、SSH、token 的差异。
- GitLab优先平台 CLI 或 HTTPS token/SSH自建 GitLab 注意域名和公司 SSO。
- Gitee常见为 HTTPS 凭证、私人令牌或 SSH key不要套用 GitHub 文案。
- Bitbucket注意 workspace、app password、SSH。
- 自建平台:先问/识别平台地址,按 HTTPS token、SSH key、公司 SSO 三类引导。
授权沟通规范:
- 不要求用户把密码、token、私钥粘贴到聊天里。
- 需要 token 时,引导用户在平台页面生成,并说明只保存到系统凭据/CLI不在聊天中泄露。
- 需要 SSH key 时,先检查是否已有可用 key没有再创建并指导用户把公钥加到平台。
- 遇到 SSO/组织权限时,告诉用户“这是账号权限问题,不是你的文件坏了”。
## 复杂度控制
### 不主动引入分支
如果用户只想把个人文件夹放到远程、保存一版、拉取项目或同步改动,不主动讲分支。
只有在以下情况才引入分支:
- 用户明确要多人协作或不想影响主版本
- 当前仓库已有分支策略或保护规则
- 需要在不动主线的情况下尝试修改
- 提交到主分支被拒绝,需要通过 PR/MR
第一次解释分支时保持短句:
> 分支可以理解成一份独立草稿。你可以在草稿里改,确认没问题后再合到正式版本。
不要一开始讲 rebase、cherry-pick、worktree、detached HEAD。
### 冲突处理面向结果
冲突不是“报错”,而是“同一处内容有两份修改,系统不知道该选哪份”。
处理前先说明:
- 你的本地改动还在
- 远程也有别人/另一台设备的改动
- 我会先帮你看冲突位置,再让你决定保留哪部分或合并两边
如果用户不懂代码,优先生成“冲突内容对照摘要”,不要让用户读冲突标记。
### 版本恢复先问恢复粒度
用户说“还原/回退/恢复”时,先判断是哪一种:
- 只撤销还没保存成版本的本地修改
- 找回某个被删文件
- 查看之前某个版本
- 把整个项目回到某个历史版本
- 远程也要同步回退
远程回退、强推、删除历史前必须明确风险并取得确认。对非技术用户可说:
> 这会改变团队平台上看到的历史版本,可能影响别人。我先给你一个不改远程历史的恢复方案。
## 推荐话术
### 初始化/首次同步
> 我先帮你确认这个文件夹是不是已经有版本记录。如果没有,我会把它变成一个仓库,然后再连接你选择的平台。这个过程不会改变文件内容,只是增加一套版本记录。
### 提交/保存版本
> 现在可以把这次改动保存成一个版本记录。提交信息我会写成人能看懂的一句话,方便以后知道这一版改了什么。
### 推送/同步
> 本地版本已经保存好了。下一步是同步到远程平台,这样团队或另一台电脑才能看到。
### 授权
> 这里需要你用平台账号授权。我不会让你把密码发给我;我们走平台自己的登录流程,完成后我再继续。
### 不确定平台
> 你用的是 GitHub、GitLab、Gitee、Bitbucket还是公司自己的代码平台如果你有仓库链接直接发链接最省事。
## 操作规范
- 执行前先看 `git status`、远程地址和当前分支/默认线索;不要盲目提交或推送。
- 提交前给用户一个简短变更摘要,确认是否包含敏感文件、临时文件、大文件、设计源文件。
- 提交信息用用户能理解的语言,但保留常见规范。示例:`docs: update product requirements draft``feat: add onboarding flow mockup`
- 不把 `.env`、密钥、token、账号导出文件、个人隐私文件提交到仓库发现时先提醒并处理忽略规则。
- 不擅自执行 `git reset --hard``git clean -fd``git push --force`、删除远程分支、覆盖远程历史。
- 如果工作区已有别人未说明的改动,不要回滚;先说明发现了哪些现有改动,并只处理用户当前目标相关内容。
- 对新手用户优先给“下一步我来做什么”和“你需要确认什么”,少给完整命令列表。
## 交付要求
完成后用简短摘要告诉用户:
- 当前状态:本地已保存/已连接远程/已同步/仍需授权/遇到冲突
- 做了什么:初始化、克隆、提交、推送、恢复、创建 issue/PR/MR 等
- 远程位置:仓库链接或远程地址(如果可用)
- 用户下一步:打开链接确认、邀请协作者、继续修改、等待权限、选择冲突保留内容
如果没有完成,说明卡在“权限、网络、平台限制、冲突选择、缺少仓库链接”中的哪一类,并给一个最短可继续步骤。

View File

@@ -0,0 +1,4 @@
interface:
display_name: "Git 仓库新手引导"
short_description: "用非技术用户听得懂的方式引导 Git/GitHub/GitLab/Gitee 仓库协作。"
default_prompt: "帮我用面向非技术用户的方式完成仓库初始化、授权、提交、同步或版本恢复。"

View File

@@ -0,0 +1,162 @@
---
name: git-repo-beginner-guide
description: "面向不熟悉 Git 的产品经理、设计师、运营、文档作者等非技术用户,引导 GitHub/GitLab/Gitee/Bitbucket/自建 Git 平台的仓库初始化、授权、克隆、提交同步、冲突处理、版本恢复和协作交接。"
---
# Git 仓库新手引导
## Overview
帮助非技术用户安全、低焦虑地使用 Git 仓库协作。默认由 AI 代做可执行操作,并把必要概念翻译成用户能理解的决策点。
## 核心原则
- 先问目标,不先教术语。用户通常要的是“保存一版”“发给团队”“拿到项目”“回到昨天”,不是学习 Git。
- 先保护现场。任何会丢弃、覆盖、重写历史、删除远程内容的动作,都先解释影响并确认。
- 默认代办。能由 AI 检查、执行、验证的,不要让用户自己照抄命令;用户只负责授权、选择平台、确认风险。
- 渐进解释。只有当概念会影响用户决策时才解释例如远程仓库、提交、同步、冲突。分支、rebase、stash 等先不主动引入。
- 平台中立。不要默认 GitHub先识别或询问 GitHub、GitLab、Gitee、Bitbucket、自建 Git、公司内网平台等。
- 用生活化比喻,但保留必要名词。第一次出现可写“提交 commit给当前文件拍一张可回退的快照”。
- 先小步闭环。每次只完成一个可验证状态:已登录、已连接远程、已保存本地版本、已同步到远程、已恢复文件。
## 交互流程
### 1. 先识别用户处境
先用自然语言复述用户目标,然后做最少量澄清。
常见处境:
- “我有一个本地文件夹,想放到代码平台上” → 初始化/连接远程/首次同步
- “别人给我一个链接,我要拿下来” → 克隆/权限/打开项目
- “我改完了,想保存并发给别人” → 查看改动/提交/推送
- “我怕改坏,想留个版本” → 本地提交或备份分支,按复杂度选择
- “提示要登录/没有权限” → 平台授权引导
- “提示冲突/推不上去” → 解释有人也改了同一处,先保护本地改动再合并
- “我要回到之前” → 明确是恢复文件、撤销未提交改动、还是回到某个历史版本
如果上下文已经足够,不要继续追问。直接说“我先帮你检查当前状态”。
### 2. 明确 AI 能帮什么
在开始前用一句话降低用户负担:
> 我可以帮你检查当前文件状态、创建版本记录、连接远程仓库、同步到平台;你只需要在需要网页登录授权时确认账号,或者告诉我目标仓库链接。
避免说“你运行以下命令”。除非当前环境无法代办,才给用户步骤。
### 3. 检查当前状态并翻译结果
检查本地是否是仓库、是否有远程地址、当前有无未保存改动、是否已登录相关平台。回复用户时不要直接堆 `git status` 原文,先给可行动摘要:
- “这个文件夹还不是仓库,我可以把它变成一个可记录版本的文件夹。”
- “它已经连接到远程平台,地址是 X。”
- “现在有 6 个文件改动了,还没有保存成版本。”
- “远程平台需要登录授权,接下来会打开/提供平台自己的登录流程。”
需要展示命令输出时,只摘关键含义。
### 4. 授权引导要平台灵活
先识别远程 URL 或用户指定平台,再选择授权方式:
- GitHub优先 `gh auth status` / `gh auth login`;必要时解释浏览器 OAuth、SSH、token 的差异。
- GitLab优先平台 CLI 或 HTTPS token/SSH自建 GitLab 注意域名和公司 SSO。
- Gitee常见为 HTTPS 凭证、私人令牌或 SSH key不要套用 GitHub 文案。
- Bitbucket注意 workspace、app password、SSH。
- 自建平台:先问/识别平台地址,按 HTTPS token、SSH key、公司 SSO 三类引导。
授权沟通规范:
- 不要求用户把密码、token、私钥粘贴到聊天里。
- 需要 token 时,引导用户在平台页面生成,并说明只保存到系统凭据/CLI不在聊天中泄露。
- 需要 SSH key 时,先检查是否已有可用 key没有再创建并指导用户把公钥加到平台。
- 遇到 SSO/组织权限时,告诉用户“这是账号权限问题,不是你的文件坏了”。
## 复杂度控制
### 不主动引入分支
如果用户只想把个人文件夹放到远程、保存一版、拉取项目或同步改动,不主动讲分支。
只有在以下情况才引入分支:
- 用户明确要多人协作或不想影响主版本
- 当前仓库已有分支策略或保护规则
- 需要在不动主线的情况下尝试修改
- 提交到主分支被拒绝,需要通过 PR/MR
第一次解释分支时保持短句:
> 分支可以理解成一份独立草稿。你可以在草稿里改,确认没问题后再合到正式版本。
不要一开始讲 rebase、cherry-pick、worktree、detached HEAD。
### 冲突处理面向结果
冲突不是“报错”,而是“同一处内容有两份修改,系统不知道该选哪份”。
处理前先说明:
- 你的本地改动还在
- 远程也有别人/另一台设备的改动
- 我会先帮你看冲突位置,再让你决定保留哪部分或合并两边
如果用户不懂代码,优先生成“冲突内容对照摘要”,不要让用户读冲突标记。
### 版本恢复先问恢复粒度
用户说“还原/回退/恢复”时,先判断是哪一种:
- 只撤销还没保存成版本的本地修改
- 找回某个被删文件
- 查看之前某个版本
- 把整个项目回到某个历史版本
- 远程也要同步回退
远程回退、强推、删除历史前必须明确风险并取得确认。对非技术用户可说:
> 这会改变团队平台上看到的历史版本,可能影响别人。我先给你一个不改远程历史的恢复方案。
## 推荐话术
### 初始化/首次同步
> 我先帮你确认这个文件夹是不是已经有版本记录。如果没有,我会把它变成一个仓库,然后再连接你选择的平台。这个过程不会改变文件内容,只是增加一套版本记录。
### 提交/保存版本
> 现在可以把这次改动保存成一个版本记录。提交信息我会写成人能看懂的一句话,方便以后知道这一版改了什么。
### 推送/同步
> 本地版本已经保存好了。下一步是同步到远程平台,这样团队或另一台电脑才能看到。
### 授权
> 这里需要你用平台账号授权。我不会让你把密码发给我;我们走平台自己的登录流程,完成后我再继续。
### 不确定平台
> 你用的是 GitHub、GitLab、Gitee、Bitbucket还是公司自己的代码平台如果你有仓库链接直接发链接最省事。
## 操作规范
- 执行前先看 `git status`、远程地址和当前分支/默认线索;不要盲目提交或推送。
- 提交前给用户一个简短变更摘要,确认是否包含敏感文件、临时文件、大文件、设计源文件。
- 提交信息用用户能理解的语言,但保留常见规范。示例:`docs: update product requirements draft``feat: add onboarding flow mockup`
- 不把 `.env`、密钥、token、账号导出文件、个人隐私文件提交到仓库发现时先提醒并处理忽略规则。
- 不擅自执行 `git reset --hard``git clean -fd``git push --force`、删除远程分支、覆盖远程历史。
- 如果工作区已有别人未说明的改动,不要回滚;先说明发现了哪些现有改动,并只处理用户当前目标相关内容。
- 对新手用户优先给“下一步我来做什么”和“你需要确认什么”,少给完整命令列表。
## 交付要求
完成后用简短摘要告诉用户:
- 当前状态:本地已保存/已连接远程/已同步/仍需授权/遇到冲突
- 做了什么:初始化、克隆、提交、推送、恢复、创建 issue/PR/MR 等
- 远程位置:仓库链接或远程地址(如果可用)
- 用户下一步:打开链接确认、邀请协作者、继续修改、等待权限、选择冲突保留内容
如果没有完成,说明卡在“权限、网络、平台限制、冲突选择、缺少仓库链接”中的哪一类,并给一个最短可继续步骤。

1
.gitignore vendored
View File

@@ -133,6 +133,7 @@ storybook-static/
# Temporary folders
tmp/
temp/
.local/
# Git version management temporary files
.git-versions/

1
package-lock.json generated
View File

@@ -10,6 +10,7 @@
"dependencies": {
"@axhub/annotation": "^1.0.10",
"docx-preview": "^0.3.7",
"jszip": "^3.10.1",
"lucide-react": "^0.562.0",
"mammoth": "^1.12.0",
"mermaid": "^11.16.0",

View File

@@ -12,6 +12,7 @@
"project-nav:sync": "node scripts/sync-project-prototype-directory.mjs",
"nav:sync": "node scripts/sync-prototype-nav-registry.mjs",
"nav:enrich-git": "node scripts/enrich-prototype-registry-from-git.mjs",
"release:changelog": "node scripts/apply-release-changelog.mjs",
"nav:xll-sync": "node scripts/sync-xll-nav-menu.mjs",
"oneos-web:sync": "node scripts/sync-oneos-web-prototypes.mjs && node scripts/scan-entries.js && npm run metadata:sync",
"build": "node scripts/sync-vendor-if-present.mjs && node scripts/scan-entries.js && node scripts/build-all.js",
@@ -50,6 +51,7 @@
"dependencies": {
"@axhub/annotation": "^1.0.10",
"docx-preview": "^0.3.7",
"jszip": "^3.10.1",
"lucide-react": "^0.562.0",
"mammoth": "^1.12.0",
"mermaid": "^11.16.0",

View File

@@ -0,0 +1,293 @@
/**
* 将版本发布的功能变更说明写入 prototype-registry.json覆盖 commit 131b963 对应条目)。
*
* 用法node scripts/apply-release-changelog.mjs
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.resolve(__dirname, '..');
const REGISTRY_PATH = path.join(projectRoot, 'src/prototypes/oneos-prototype-nav/prototype-registry.json');
const RELEASE_COMMIT = '131b963';
const RELEASE_DATE = '2026-07-12';
const RELEASE_TIME = '22:45';
const MAX_RECENT = 40;
/** @type {Record<string, { summary: string, details: string[] }>} */
const RELEASE_NOTES = {
'oneos-prototype-nav': {
summary: '统一标注壳与操作规范,租赁明细/合同/提车应收款增强,新增任务工单,同步合包页面',
details: [
'【平台】全项目接入 PrototypeAnnotationHostPRD 迁入右侧标注目录;列表统一 OperationActions 与工具栏图标规范',
'【租赁业务明细】收款状态列、47 列宽表、氢费钻取、字段校验、变更记录、导入导出与主管解锁',
'【租赁合同】KPI/状态收敛、查看页、线上/线下签署、交付变更、附加费用与提车应收联动',
'【提车应收款】独立原型上线finance 合包仅保留租赁账单;工作台快捷入口改跳独立原型',
'【任务工单】新增业管发起端:合同/独立双入口、六类任务类型与督办催办',
'【合包】business/ops/finance/ledger-data/lease-contract/h2-station 等移除面包屑并标准化操作区',
'【下线】移除 login/requirements/vehicle-asset 合包;侧栏新增任务工单与提车应收款',
'【发布】导航页固定发布至 /oneos-prototype-nav/index.html',
],
},
'lease-business-detail': {
summary: '租赁业务明细大改版收款状态、47 列宽表、字段校验与变更记录',
details: [
'新增收款状态列(未收款/部分收款/已结清);月份、部门+业务员合并展示;表尾 23 项金额汇总',
'氢费可钻取 Popover编辑/删除/变更记录;公式列(应收合计、未收、盈亏、氢费等)自动重算',
'客户/提车日期/租金等与车辆/客户/合同交叉校验,支持一键替换建议值',
'批量导入 36 列手工字段、导出 47 列;部分成功 + 错误日志;已对账锁定与主管解锁',
],
},
'lease-contract-management': {
summary: '租赁合同增强KPI/状态收敛、查看页、签署方式与提车应收联动',
details: [
'列表 KPI全部/草稿/进行中/审批中/已终止;合同状态收敛;在租车队 KPI 迁至车辆管理',
'筛选:合同模板类型 + 标准合同名称二级联动;统一流程入口(新增/续签/转正式/增车等)',
'新增只读查看页;线上电子签章 / 线下人工上传主动终止与交付安排变更Cascader/TBD',
'附加费用拆分已有/新增服务项;红线条款 + 新增条款检测;先付后用/先用后付',
'提交审批与交付计划变更同步提车应收 bridge',
],
},
'lease-business-ledger': {
summary: '租赁业务台账:操作区统一为 OperationActions标注壳切换',
details: [
'行操作:编辑 / 操作记录 / 保存(编辑态)',
'关联收款、事故/维保/收据链接弹窗保留',
'接入 PrototypeAnnotationHost移除内嵌原型导航目录',
],
},
'self-operated-business-ledger': {
summary: '物流业务明细:更名、列对齐 Excel、批量导入与自动计算',
details: [
'模块更名:物流业务台账 → 物流业务明细',
'列对齐 Excel 1.2:薪资、电费、日社保/挂车/停车/轮胎、线路计价等;系统车型列',
'新增批量导入:模板下载、错误日志、导入后自动计算金额/总成本/盈亏',
'移除行勾选与批量删除',
],
},
'business-dept-ledger': {
summary: '业务部台账:分组表头与表格可读性增强',
details: [
'统一边框 token、分组表头双层样式业务板块 + 业绩/成本/盈亏)',
'冻结列滚动修复;金额列防裁切;盈亏钻取链接样式调整',
],
},
'vehicle-management': {
summary: '车辆管理:在租车队 KPI 迁入、车型分布弹窗与列表样式优化',
details: [
'在租车队 KPI 从租赁合同列表迁入;按品牌型号统计在租数量',
'车型分布弹窗:品牌筛选、车型卡片、占比进度条、车辆明细',
'详情 Tab 与列表筛选/导入/KPI 操作区统一;表格样式优化',
],
},
'customer-management': {
summary: '客户管理OperationActions 统一操作列,移除面包屑',
details: [
'列表操作:详情 / 编辑 / 管理标签 / 删除',
'风险标签、批量打标等业务逻辑不变',
],
},
'contract-template-management': {
summary: '合同模板:启用/停用、先付后用/先用后付变量与操作区重构',
details: [
'行操作:编辑 + 更多(启用/停用、版本日志、删除);启用中不可编辑/删除',
'模板变量新增 paymentMethod付款周期与氢费结算文案更新',
],
},
'insurance-procurement': {
summary: '保险采购OperationActions 统一操作列',
details: [
'保单列表:编辑 / 预览 / 下载 / 更多',
'标注壳切换',
],
},
'vehicle-pickup-receivable': {
summary: '提车应收款独立原型上线:自动生成/手动办理与合同双向联动',
details: [
'收款情况列:租车数 / 已提车Popover 钻取)',
'合同提交审批即生成主表;有交付时间则提前 2 天生成应收',
'手动办理勾选品牌型号与交付时间15 日前/后分档计费规则',
'与租赁合同 bridge 联动finance 合包剥离;工作台入口改跳本原型',
],
},
'task-work-order': {
summary: '任务工单新原型:业管发起端(合同/独立双入口)',
details: [
'任务类型:里程履约、维保履约、政策兑现、付款/交付节点、条款跟进、通用临时',
'列表 KPI全部/我发布的/我督办的 + 状态视角;详情含进度、反馈、催办',
'执行人不可见合同原文与金额(第一版仅业管端)',
],
},
'payment-records': {
summary: '收款记录OperationActions 与关联账单操作',
details: [
'操作列标准化;表格新增「关联账单」',
'标注壳切换',
],
},
'supplier-management': {
summary: '供应商管理:工具栏与操作区图标标准化',
details: [
'新增供应商按钮图标统一;删除收入 OperationActions',
],
},
'oneos-web-business': {
summary: '业务管理合包18 页移除面包屑OperationActions 统一',
details: [
'保险采购、交车任务、客户/供应商、租赁账单、ETC 管理等页操作区标准化',
],
},
'oneos-web-ops': {
summary: '运维管理合包45 页操作列与工具栏按钮标准化',
details: [
'备件/调拨/交还车/故障/异动/证件/型号参数等页 OperationActions 与 data-vm-icon 图标',
],
},
'oneos-web-finance': {
summary: '财务管理合包:提车应收款剥离,仅保留租赁账单',
details: [
'提车应收款 6 页移除;合包默认页改为租赁账单',
'提车流程由 vehicle-pickup-receivable 独立原型承接',
],
},
'oneos-web-ledger-data': {
summary: '台账数据合包5 页操作区标准化',
details: [
'保险分摊、氢费、维修明细、氢费采购端汇总、租赁业务台账等页 OperationActions',
],
},
'oneos-web-lease-contract': {
summary: '租赁合同合包8 子页同步签署/流程字段与 OperationActions',
details: [
'新增、续签、转正式、三方变更、附加费用等子页操作区标准化',
'修复移除面包屑后的 JSX 括号语法',
],
},
'oneos-web-data-analysis': {
summary: '数据分析合包:业务部台账列宽与盈亏着色优化',
details: [
'9 页操作区标准化;业务部台账列宽加大;盈亏负值着色',
],
},
'oneos-web-h2-station': {
summary: '加氢站合包:站点信息编辑提升为主操作',
details: [
'加氢订单/记录/站点信息 3 页 OperationActions站点编辑不再藏在「更多」',
],
},
'oneos-web-workbench': {
summary: '工作台:提车应收款快捷入口改跳独立原型',
details: [
'快捷入口指向 /prototypes/vehicle-pickup-receivable',
'protoNav 支持直接 location 跳转原型 URL',
],
},
'oneos-web-contract-template': {
summary: '合同模板合包:操作区与独立原型对齐',
details: ['模板管理页 OperationActions 统一'],
},
'oneos-web-procurement': {
summary: '采购管理合包:三方退租车页操作区标准化',
details: ['退租申请与管理页 OperationActions'],
},
'oneos-web-h2-station-site': {
summary: '站点信息:标注目录清理',
details: ['移除内嵌原型导航节点;标注壳切换'],
},
'lease-business-line-overview': {
summary: '业务条线说明:标注目录微调',
details: ['条线数据更新;移除内嵌导航节点'],
},
'vehicle-h2-fee-ledger': {
summary: '车辆氢费明细:标注壳与操作区统一',
details: ['OperationActions标注目录规范化'],
},
'vehicle-maintenance-ledger': {
summary: '车辆维修明细:标注壳与操作区统一',
details: ['OperationActions标注目录规范化'],
},
'vehicle-return-settlement-v2': {
summary: '还车应结算 V2OperationActions 统一为详情主操作',
details: ['列表操作列标准化'],
},
};
function readJson(filePath) {
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
}
function writeJson(filePath, value) {
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
}
function applyReleaseNotes(registry) {
let updated = 0;
for (const [prototypeId, notes] of Object.entries(RELEASE_NOTES)) {
const record = registry.prototypes?.[prototypeId];
if (!record) continue;
const changelog = Array.isArray(record.changelog) ? [...record.changelog] : [];
let targetIndex = changelog.findIndex((entry) => entry.commit === RELEASE_COMMIT);
if (targetIndex < 0 && changelog.length > 0) targetIndex = 0;
const base = targetIndex >= 0 ? changelog[targetIndex] : {
version: record.version,
date: RELEASE_DATE,
time: RELEASE_TIME,
files: record.trackedFiles?.slice(0, 12) || [],
};
const nextEntry = {
...base,
date: RELEASE_DATE,
time: RELEASE_TIME,
summary: notes.summary,
details: notes.details,
commit: RELEASE_COMMIT,
};
if (targetIndex >= 0) {
changelog[targetIndex] = nextEntry;
} else {
changelog.unshift(nextEntry);
}
record.changelog = changelog.slice(0, 30);
record.lastUpdated = `${RELEASE_DATE}T14:45:00.000Z`;
updated += 1;
}
const recent = [];
for (const [prototypeId, notes] of Object.entries(RELEASE_NOTES)) {
const record = registry.prototypes?.[prototypeId];
if (!record) continue;
const entry = record.changelog?.[0];
if (!entry) continue;
recent.push({
prototypeId,
title: record.title || prototypeId,
version: entry.version || record.version,
date: entry.date || RELEASE_DATE,
time: entry.time || RELEASE_TIME,
summary: notes.summary,
details: notes.details,
files: entry.files || [],
});
}
recent.sort((a, b) => {
const ta = new Date(`${a.date}T${a.time}:00+08:00`).getTime();
const tb = new Date(`${b.date}T${b.time}:00+08:00`).getTime();
return tb - ta;
});
registry.recentUpdates = recent.slice(0, MAX_RECENT);
registry.updatedAt = `${RELEASE_DATE}T14:45:00.000Z`;
return updated;
}
const registry = readJson(REGISTRY_PATH);
const count = applyReleaseNotes(registry);
writeJson(REGISTRY_PATH, registry);
console.log(`完成:已写入 ${count} 个原型的版本发布变更说明commit ${RELEASE_COMMIT}`);

View File

@@ -54,6 +54,7 @@ function readS3Config() {
}
return {
...s3,
pathAliases: s3.pathAliases && typeof s3.pathAliases === 'object' ? s3.pathAliases : {},
includeSource: raw?.cloudPublishing?.publishSettings?.includeSource !== false,
};
}
@@ -63,7 +64,10 @@ function toPublishPath(resource) {
return String(raw).replace(/\\/gu, '/').replace(/\/index\.tsx?$/iu, '');
}
function resolveObjectPrefix(entry) {
function resolveObjectPrefix(entry, pathAliases = {}) {
const alias = pathAliases[entry.id];
if (alias) return String(alias).replace(/^\/+|\/+$/gu, '');
const normalized = entry.path.replace(/^\/+|\/+$/gu, '');
const prototypeMatch = normalized.match(/^(?:src\/)?prototypes\/(.+)$/u);
if (prototypeMatch?.[1]) return `prototypes/${prototypeMatch[1]}`;
@@ -97,7 +101,7 @@ function guessContentType(filePath) {
return map[ext] || 'application/octet-stream';
}
async function collectPublishEntries(adminOrigin, projectId, groupFilter) {
async function collectPublishEntries(adminOrigin, projectId, groupFilter, pathAliases = {}) {
const res = await fetch(`${adminOrigin}/api/projects/${encodeURIComponent(projectId)}/resources`);
if (!res.ok) {
throw new Error(`GET /api/projects/${projectId}/resources failed: ${res.status}`);
@@ -114,7 +118,7 @@ async function collectPublishEntries(adminOrigin, projectId, groupFilter) {
id: item.id,
title: item.title || item.id,
path: publishPath,
prefix: resolveObjectPrefix({ path: publishPath, id: item.id }),
prefix: resolveObjectPrefix({ path: publishPath, id: item.id }, pathAliases),
});
}
}
@@ -223,7 +227,7 @@ async function main() {
const adminOrigin = resolveAdminOrigin(args.adminOrigin);
const s3Config = readS3Config();
const includeSource = args.includeSource && s3Config.includeSource;
const entries = await collectPublishEntries(adminOrigin, args.projectId, args.group);
const entries = await collectPublishEntries(adminOrigin, args.projectId, args.group, s3Config.pathAliases);
console.log(`Admin: ${adminOrigin}`);
console.log(`Bucket: ${s3Config.bucket}`);

View File

@@ -117,11 +117,16 @@ function cleanupPrototypeDirectory(prototypeId, projectRoot) {
return true;
}
function filterNavMenuPrototypes(sidebarItems) {
return sidebarItems.filter((node) => node?.kind === 'folder' && node?.title === 'OneOS');
}
function syncPrototypeNavMenu(projectRoot, sidebarItems) {
const navMenuPath = path.join(projectRoot, 'src/prototypes/oneos-prototype-nav/nav-menu.json');
if (!fs.existsSync(path.dirname(navMenuPath))) return false;
writeJson(navMenuPath, { prototypes: sidebarItems });
console.log('[sync] oneos-prototype-nav: 已更新 nav-menu.json');
const filtered = filterNavMenuPrototypes(sidebarItems);
writeJson(navMenuPath, { prototypes: filtered });
console.log(`[sync] oneos-prototype-nav: 已更新 nav-menu.json${filtered.length} 个分区)`);
return true;
}

View File

@@ -167,6 +167,17 @@ function diffChangedFiles(previousFiles, currentFiles) {
return currentFiles.filter((file) => !prevSet.has(file));
}
/** 仅标注/批注文件变动时不自动递增版本(避免 dev 保存刷屏) */
function isAnnotationOnlyChange(changedFiles) {
if (!changedFiles.length) return false;
return changedFiles.every((file) => (
file === 'annotation-source.json'
|| file.endsWith('/annotation-source.json')
|| file.includes('prototype-comments')
|| (file.includes('.spec/') && file.endsWith('.md') && !file.includes('requirements-prd'))
));
}
function bumpVersion(revision) {
return `v1.${revision}`;
}
@@ -211,6 +222,14 @@ function upsertPrototypeRecord(registry, prototypeId, title, contentHash, files,
}
const changedFiles = diffChangedFiles(existing.trackedFiles, files);
if (!options.note && isAnnotationOnlyChange(changedFiles.length ? changedFiles : files)) {
existing.title = title;
existing.contentHash = contentHash;
existing.trackedFiles = files;
registry.prototypes[prototypeId] = existing;
return { changed: false, entry: null };
}
const summary = summarizeChange(changedFiles.length ? changedFiles : files, options.note);
const nextRevision = (existing.revision || 0) + 1;
const version = bumpVersion(nextRevision);

View File

@@ -1,7 +1,7 @@
/**
* docx-preview 高保真 Word 渲染(预览区)
* 使用动态 import避免阻塞原型入口模块加载。
*/
import { renderAsync } from 'docx-preview';
var DEFAULT_RENDER_OPTIONS = {
className: 'ct-docx-preview',
@@ -18,6 +18,11 @@ var DEFAULT_RENDER_OPTIONS = {
export async function renderDocxPreview(container, arrayBuffer, options) {
if (!container || !arrayBuffer) return;
container.innerHTML = '';
var mod = await import('docx-preview');
var renderAsync = mod.renderAsync;
if (typeof renderAsync !== 'function') {
throw new Error('docx-preview 模块未正确加载');
}
await renderAsync(
arrayBuffer,
container,

View File

@@ -2,6 +2,7 @@
* Word (.doc / .docx) → HTML 导入
*/
import mammoth from 'mammoth';
import JSZip from 'jszip';
var MAMMOTH_STYLE_MAP = [
"p[style-name='Title'] => h1.ct-word-doc-title:fresh",
@@ -94,7 +95,6 @@ export function extractInlineStylesFromImportedHtml(html) {
export async function extractDocxThemeStyles(arrayBuffer) {
try {
var JSZip = (await import('jszip')).default;
var zip = await JSZip.loadAsync(arrayBuffer);
var stylesFile = zip.file('word/styles.xml');
if (!stylesFile) return '';

View File

@@ -13,14 +13,14 @@
| 预览地址 | `/prototypes/lease-business-detail` |
| 目标用户 | 业务二部业管 / 运营人员 |
| 核心任务 | 按 Excel 样表维护租赁车辆收入明细、查询汇总、批量导入导出 |
| 页面结构 | 筛选区 + 7 项 KPI + 工具栏 + 47 列宽表(表尾 23 项汇总)+ 分页 |
| 页面结构 | 筛选区 + 7 项 KPI + 工具栏 + 48 列宽表(表尾 23 项汇总)+ 分页 |
| 设计基底 | `vm-page` + `ldb-page` + `lc-page`(见 `ledger-shared/DESIGN.md` |
### 与「租赁业务台账」的分工
| 模块 | 定位 |
|---|---|
| **租赁业务明细**(本模块) | 对齐业管 Excel 运营台账宽表手工列导入维护6 个公式列自动计算;**不**做收款关联与账单状态 |
| **租赁业务明细**(本模块) | 对齐业管 Excel 运营台账宽表手工列导入维护6 个公式列自动计算;收款状态按实收/应收展示 |
| **租赁业务台账** | 账单维度维护、关联收款记录、收款状态(未收/部分/已收)、归档与角色数据范围 |
下游 **业务部台账 / 业务汇总分析** 中「租赁业绩」数据来源为本模块明细(见 `oneos-web-data-analysis/05-业务部台账`)。
@@ -28,15 +28,14 @@
### 不做范围(当前原型)
- 「租赁业务盈亏月度汇总」独立 TabLegacy 数据分析页有,本原型待规划)
- 行内编辑、删除、服务端持久化
- 服务端持久化、员工/主管数据权限隔离(主管预览:`?role=supervisor`
- 关联收款记录自动回写实收
- 员工/主管数据权限隔离
---
## 2. 筛选区
默认展示 4 项(一行),「更多筛选」展开车牌多选。
默认展示 5 项(一行),「更多筛选」展开车牌多选。
| 筛选项 | 说明 |
|---|---|
@@ -44,6 +43,7 @@
| 业务部门 | 精确匹配 |
| 业务员 | 精确匹配 |
| 客户名称 | 模糊包含 |
| 状态 | 已结清 / 部分收款 / 未收款(按实收与应收合计计算) |
| 车牌号码 | 多选精确匹配(更多筛选) |
查询按钮触发筛选KPI 与表尾汇总基于**全量筛选结果**(非当前页)。
@@ -70,28 +70,36 @@
| 按钮 | 行为 |
|---|---|
| 批量导入 | 下载模板(手工录入)→ 上传 Excel → 公式列自动计算 |
| 批量导出 | 导出当前筛选结果(完整 47 列,含公式列结果) |
| 批量导入 | 下载模板(36 列手工录入)→ 上传 Excel → 公式列自动计算 |
| 批量导出 | 导出当前筛选结果(完整 48 列,含公式列结果) |
导入必填:**年份、月份、车牌号码**。
### 行内操作
编辑、删除、变更日志(操作列);已锁定记录仅主管可改(`?role=supervisor`)。
---
## 5. 明细列表47 列)
## 5. 明细列表48 列)
表头顺序严格对齐 Excel「2.租赁车辆收入明细表」
年份、月份、业务部门、业务员、付款日期、车牌号码、车型、客户名称、增值服务、享免政策、提车日期、合同生效日期、合同到期日期、起始日期、退车日期、平均天数、押金、合同标的租金、应收合计、应收租金、月度收入、月度租金、维保包干收入、保险上浮费、运维费收入、其他收入、里程减免金额、其他减免金额、实收金额、未收、开票日期、实际付款日期、付款方式、车辆标准成本、车辆实际成本、保险费、氢费、运维费成本、居间费、其他、总成本、盈亏、GPS里程、其他减免金额明细内容、资产归属、签约公司、备注。
表头顺序对齐 Excel「2.租赁车辆收入明细表」,并在月份列左侧增加「状态」列。
### 交互
- 横向滚动;左冻结:**份 + 车牌号码**
- 横向滚动;左冻结:**状态 + 月份 + 车牌号码**
- 仅**日期类列**表头支持正序/倒序排序
- 公式列(`calculated`)浅灰底色,表示系统计算、导入模板不含
- 表尾**汇总行**23 个金额列 SUBTOTAL列显示「汇总」
- 公式列(`calculated`)浅灰底色,导入模板不含
- 表尾**汇总行**23 个金额列 SUBTOTAL月份列显示「汇总」
- 未收 > 0 单元格标红;盈亏正负着色
- 底部分页10 / 20 / 50 / 100
### 收款状态(状态列)
| 状态 | 判定规则 |
|---|---|
| 未收款 | 实收金额 = 0 或未录入 |
| 部分收款 | 实收金额 > 0 且 < 应收合计 |
| 已结清 | 实收金额 ≥ 应收合计 |
### 表尾汇总23 项)
押金、合同标的租金、应收合计、应收租金、月度收入、月度租金、维保包干收入、保险上浮费、运维费(收入)、其他收入、里程减免金额、其他减免金额、实收金额、未收、车辆标准成本、车辆实际成本、保险费、氢费、运维费(成本)、居间费、其他、总成本、盈亏。
@@ -104,21 +112,15 @@
| 字段 | 计算方式 |
|---|---|
| **应收合计** | 应收租金 + 维保包干收入 + 保险上浮费 + 运维费(收入) + 其他收入 + 里程减免 + 其他减免 |
| **月度收入** | 月度租金 + 同上调整项 |
| **应收合计** | 维保包干收入 + 保险上浮费 + 运维费收入+ 其他收入 + 里程减免 + 其他减免 |
| **月度收入** | 月度租金 + 维保包干收入 + 保险上浮费 + 运维费(收入)+ 其他收入 + 里程减免 + 其他减免 |
| **未收** | 应收合计 实收金额 |
| **车辆实际成本** | 车辆标准成本 × 平均天数 |
| **总成本** | 车辆实际成本 + 保险费 + 氢费 + 运维费(成本) + 居间费 + 其他 |
| **总成本** | 车辆实际成本 + 保险费 + 氢费 + 运维费成本+ 居间费 + 其他 |
| **盈亏** | 月度收入 总成本 |
减免金额在 Excel 中以负数录入;导入时填负数即可。
### 导入模板
- **含**41 列手工录入字段(不含上述 6 列)
- **不含**:应收合计、月度收入、未收、车辆实际成本、总成本、盈亏
- 导入后调用 `applyDetailCalculations` 统一重算
---
## 7. 样例数据
@@ -137,21 +139,22 @@
| 标注源 | `annotation-source.json` |
| 列定义 | `components/tableColumns.ts` |
| 筛选 / KPI 汇总 | `utils/detail.ts` |
| 收款状态 | `utils/payment-status.ts` |
| 公式计算 | `utils/calc-detail.ts` |
| 导入导出 | `utils/batch-io.ts` |
| 变更日志 | `utils/change-log.ts` |
| 类型 | `types.ts` |
| 组件 | `components/FilterPanel.tsx``DetailKpiRow.tsx``DetailTable.tsx``DetailImportModal.tsx` |
| 组件 | `FilterPanel.tsx``DetailKpiRow.tsx``DetailTable.tsx``DetailImportModal.tsx``DetailEditModal.tsx``DetailChangeLogModal.tsx` |
---
## 9. 验收重点
1. 表头 47 列顺序与 Excel 样表一致;双「运维费」列位置正确
1. 筛选区含「状态」;状态列展示未收款 / 部分收款 / 已结清
2. 筛选后 KPI、表尾汇总与明细行合计一致
3. 下载导入模板无 6 个公式列;导入后公式列自动计算且与 Excel 同口径
4. 导出为完整 47 列
5. 横向滚动与左冻结(年份、车牌)正常
6. 未收 > 0 标红;盈亏正负着色
3. 左冻结:状态 + 月份 + 车牌号码
4. 氢费非零可点击明细;编辑/删除/变更日志可用
5. 未收 > 0 标红;盈亏正负着色
---
@@ -162,4 +165,3 @@
- P1盈亏月度汇总 Tab
- P1与租赁业务台账 / 收款模块集成口径
- P2导入重复键年+月+车牌)处理策略
- P2导入行级校验与实收 ≤ 应收校验

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,148 @@
/**
* 将 PRD、markdownMap 与标注节点说明同步到 annotation-source.json 的 directory.nodes
*/
import { readFileSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const root = join(__dirname, '..');
const jsonPath = join(root, 'annotation-source.json');
const prdPath = join(root, '.spec/requirements-prd.md');
const doc = JSON.parse(readFileSync(jsonPath, 'utf8'));
const prd = readFileSync(prdPath, 'utf8');
const mm = doc.markdownMap;
doc.markdownMap['lbd-doc-overview'] = `## 1. 模块定位
| 项 | 说明 |
|---|---|
| 模块名称 | 台账数据 — 租赁业务明细 |
| 目标用户 | 业务二部业管 / 运营人员 |
| 核心任务 | 按 Excel 样表维护租赁车辆收入明细、查询汇总、批量导入导出 |
| 页面结构 | 筛选区 + 7 项 KPI + 工具栏 + 48 列宽表(表尾 23 项汇总)+ 分页 |
| 设计基底 | vm-page + ldb-page + lc-page |
对齐 Excel《2026年业务二部运营台账总表》「2.租赁车辆收入明细表」。
---`;
function markdownDoc(id, title) {
return {
type: 'markdown',
id,
title,
markdown: mm[id.replace('lbd-doc-', '')] ?? mm[id] ?? doc.data.nodes.find((n) => n.id === id.replace('lbd-doc-', ''))?.annotationText ?? '',
};
}
/** 目录 markdown 节点:优先 markdownMap 中带 lbd-doc- 前缀的键,否则用标注节点 id */
function nodeDoc(nodeId, title, docId = `lbd-doc-${nodeId}`) {
const markdown = mm[nodeId] ?? mm[docId] ?? '';
return { type: 'markdown', id: docId, title, markdown };
}
doc.directory.nodes = [
{
type: 'folder',
id: 'lbd-doc-root',
title: '租赁业务明细说明',
defaultExpanded: true,
children: [
{
type: 'markdown',
id: 'lbd-doc-overview',
title: '模块总览',
markdown: doc.markdownMap['lbd-doc-overview'],
},
{
type: 'link',
id: 'lbd-link-self',
title: '租赁业务明细原型',
href: '/prototypes/lease-business-detail',
target: 'self',
},
{
type: 'markdown',
id: 'lbd-doc-prd',
title: 'PRD 全文',
markdown: prd,
markdownPath: 'src/prototypes/lease-business-detail/.spec/requirements-prd.md',
},
{
type: 'folder',
id: 'lbd-doc-page-annotations',
title: '页面标注',
defaultExpanded: true,
children: [
nodeDoc('lbd-filter', '筛选条件'),
nodeDoc('lbd-kpi', 'KPI 汇总'),
nodeDoc('lbd-toolbar', '批量导入导出'),
nodeDoc('lbd-table', '明细列表'),
nodeDoc('lbd-changelog-modal', '变更日志'),
],
},
{
type: 'folder',
id: 'lbd-doc-field-annotations',
title: '字段标注',
defaultExpanded: false,
children: [
nodeDoc('lbd-col-status', '状态'),
nodeDoc('lbd-col-receivable-total', '应收合计'),
nodeDoc('lbd-col-outstanding', '未收'),
nodeDoc('lbd-col-vehicle-actual-cost', '车辆实际成本'),
nodeDoc('lbd-col-total-cost', '总成本'),
nodeDoc('lbd-col-profit-loss', '盈亏'),
nodeDoc('lbd-col-hydrogen-fee', '氢费'),
],
},
{
type: 'folder',
id: 'lbd-doc-specs',
title: '规格说明',
defaultExpanded: false,
children: [
{
type: 'markdown',
id: 'lbd-doc-formulas',
title: '公式列',
markdown: mm['lbd-doc-formulas'],
},
{
type: 'markdown',
id: 'lbd-doc-boundary',
title: '模块边界',
markdown: mm['lbd-doc-boundary'],
},
{
type: 'markdown',
id: 'lbd-doc-source',
title: '源码入口',
markdown: `## 8. 源码入口
| 类型 | 路径 |
|---|---|
| 主入口 | \`index.tsx\` |
| 标注源 | \`annotation-source.json\` |
| 列定义 | \`components/tableColumns.ts\` |
| 筛选 / KPI 汇总 | \`utils/detail.ts\` |
| 收款状态 | \`utils/payment-status.ts\` |
| 公式计算 | \`utils/calc-detail.ts\` |
| 导入导出 | \`utils/batch-io.ts\` |
| 变更日志 | \`utils/change-log.ts\` |
| 类型 | \`types.ts\` |
| 组件 | \`FilterPanel.tsx\`\`DetailKpiRow.tsx\`\`DetailTable.tsx\`\`DetailImportModal.tsx\`\`DetailEditModal.tsx\`\`DetailChangeLogModal.tsx\` |
---`,
},
],
},
],
},
];
doc.data.updatedAt = Date.now();
writeFileSync(jsonPath, `${JSON.stringify(doc, null, 2)}\n`, 'utf8');
console.log('Synced directory.nodes with PRD and', doc.data.nodes.length, 'annotation nodes');

View File

@@ -23,27 +23,45 @@ import {
const CURRENT_PROTOTYPE_ID = 'oneos-prototype-nav';
const PUBLISHED_CLOUD_HOSTS = new Set(['prototype.lnoneos.com']);
/** 导航页左侧 DIRECTORY 仅展示这些一级分区(小羚羚由 xll-nav-menu.json 单独注入) */
const NAV_SIDEBAR_SECTION_TITLES = new Set(['OneOS']);
/** 与 axhub.config.json cloudPublishing.s3.pathAliases 保持一致 */
const CLOUD_PUBLISH_PATH_ALIASES: Record<string, string> = {
'oneos-prototype-nav': 'oneos-prototype-nav',
};
function cloudPrototypeSegment(prototypeId: string): string {
return CLOUD_PUBLISH_PATH_ALIASES[prototypeId] || `prototypes/${prototypeId}`;
}
/** 对象存储静态发布站点(无 Make 本地 dev server 路由) */
export function isPublishedCloudHost(): boolean {
if (typeof window === 'undefined') return false;
const { hostname, pathname } = window.location;
if (PUBLISHED_CLOUD_HOSTS.has(hostname)) return true;
// export-html 发布页:/{prototype}/index.html
return /\/index\.html$/u.test(pathname) && !pathname.startsWith('/prototypes/');
// export-html 发布页:/{prototype}/index.html 或 /prototypes/{prototype}/index.html
return /\/index\.html$/u.test(pathname);
}
export function resolvePrototypeHref(prototypeId: string): string {
if (typeof window === 'undefined') return `/prototypes/${prototypeId}`;
if (isPublishedCloudHost()) {
return `${window.location.origin}/prototypes/${prototypeId}/index.html`;
return `${window.location.origin}/${cloudPrototypeSegment(prototypeId)}/index.html`;
}
return `/prototypes/${prototypeId}`;
}
function normalizeAbsoluteHref(href: string): string {
const matches = href.match(/https?:\/\/[^\s"']+/gu);
if (matches && matches.length > 1) return matches[0];
return href;
}
/** 卡片/弹窗打开时按当前环境解析链接,避免静态包内 href 写死 */
export function linkHref(link: NavLinkItem): string {
if (link.external || /^https?:\/\//u.test(link.href)) return link.href;
if (link.external) return normalizeAbsoluteHref(link.href);
if (/^https?:\/\//u.test(link.href)) return normalizeAbsoluteHref(link.href);
if (link.href.includes('#page=')) return link.href;
return resolvePrototypeHref(link.prototypeId);
}
@@ -242,6 +260,7 @@ function transformSection(node: SidebarNode): NavSection | null {
export function loadNavSections(): NavSection[] {
const prototypes = (navMenu as { prototypes?: SidebarNode[] }).prototypes || [];
const oneosSections = prototypes
.filter((node) => node.kind === 'folder' && NAV_SIDEBAR_SECTION_TITLES.has(String(node.title || '')))
.map((node) => transformSection(node))
.filter((section): section is NavSection => Boolean(section));
const xllSection = loadXllNavSection();

View File

@@ -1,83 +1,5 @@
{
"prototypes": [
{
"id": "item-prototypes-task-work-order",
"kind": "item",
"title": "task work order",
"itemKey": "prototypes/task-work-order"
},
{
"id": "item-prototypes-lease-business-line-overview",
"kind": "item",
"title": "lease business line overview",
"itemKey": "prototypes/lease-business-line-overview"
},
{
"id": "item-prototypes-oneos-web-business",
"kind": "item",
"title": "oneos web business",
"itemKey": "prototypes/oneos-web-business"
},
{
"id": "item-prototypes-oneos-web-contract-template",
"kind": "item",
"title": "oneos web contract template",
"itemKey": "prototypes/oneos-web-contract-template"
},
{
"id": "item-prototypes-oneos-web-data-analysis",
"kind": "item",
"title": "oneos web data analysis",
"itemKey": "prototypes/oneos-web-data-analysis"
},
{
"id": "item-prototypes-oneos-web-finance",
"kind": "item",
"title": "oneos web finance",
"itemKey": "prototypes/oneos-web-finance"
},
{
"id": "item-prototypes-oneos-web-h2-station",
"kind": "item",
"title": "oneos web h2 station",
"itemKey": "prototypes/oneos-web-h2-station"
},
{
"id": "item-prototypes-oneos-web-help-center",
"kind": "item",
"title": "oneos web help center",
"itemKey": "prototypes/oneos-web-help-center"
},
{
"id": "item-prototypes-oneos-web-lease-contract",
"kind": "item",
"title": "oneos web lease contract",
"itemKey": "prototypes/oneos-web-lease-contract"
},
{
"id": "item-prototypes-oneos-web-ledger-data",
"kind": "item",
"title": "oneos web ledger data",
"itemKey": "prototypes/oneos-web-ledger-data"
},
{
"id": "item-prototypes-oneos-web-ops",
"kind": "item",
"title": "oneos web ops",
"itemKey": "prototypes/oneos-web-ops"
},
{
"id": "item-prototypes-oneos-web-procurement",
"kind": "item",
"title": "oneos web procurement",
"itemKey": "prototypes/oneos-web-procurement"
},
{
"id": "item:prototypes:oneos-prototype-nav",
"kind": "item",
"title": "原型导航",
"itemKey": "prototypes/oneos-prototype-nav"
},
{
"id": "folder-1782874576229-r8atbu",
"kind": "folder",

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -1,21 +1,19 @@
import PickupReceivableViewPage from '../oneos-web-finance/pages/05-提车应收款-查看.jsx';
import PickupReceivableInvoicePage from '../oneos-web-finance/pages/06-提车应收款-开票信息.jsx';
import PickupReceivableAuditPage from '../oneos-web-finance/pages/07-提车应收款-审核.jsx';
import PickupReceivableCollectPage from '../oneos-web-finance/pages/08-提车应收款-提车收款单.jsx';
import PickupCollectEditPage from '../oneos-web-finance/pages/04-提车收款单-编辑.jsx';
import VehiclePickupReceivableList from './VehiclePickupReceivableList.jsx';
import VehiclePickupReceivableCollectPage from './VehiclePickupReceivableCollectPage.jsx';
import VehiclePickupReceivableContractView from './VehiclePickupReceivableContractView.jsx';
import {
toCollectPageVehicles,
appendPickupCollectChild,
buildPickupSubmitCcMessage,
applyPickupDateToVehicles,
buildBatchPickupScheduleSummary,
describePickupRule,
buildCollectHandleContext,
buildContractViewContext,
canShowHandle,
} from './pickup-receivable-bridge.js';
var DEFAULT_PICKUP_DATE = '2026-03-12';
/** @typedef {'list' | 'collect' | 'view' | 'edit' | 'invoice' | 'audit'} PickupView */
/** @typedef {'list' | 'collect' | 'contractView' | 'view' | 'edit' | 'invoice' | 'audit'} PickupView */
const Component = function (props) {
var useState = React.useState;
@@ -26,7 +24,11 @@ const Component = function (props) {
useEffect(function () {
if (props && props.onPageViewChange) {
var pageId = view === 'collect' ? 'collect' : 'list';
var pageId = view === 'collect'
? 'collect'
: view === 'contractView'
? 'contractView'
: 'list';
props.onPageViewChange(pageId);
}
}, [view, props]);
@@ -39,6 +41,14 @@ const Component = function (props) {
var handleContext = handleContextState[0];
var setHandleContext = handleContextState[1];
var viewContextState = useState(null);
var viewContext = viewContextState[0];
var setViewContext = viewContextState[1];
var contractViewRecordState = useState(null);
var contractViewRecord = contractViewRecordState[0];
var setContractViewRecord = contractViewRecordState[1];
useEffect(function () {
window.__receivableBack = function () {
setView('list');
@@ -65,11 +75,6 @@ const Component = function (props) {
if (window.__receivableBack) window.__receivableBack();
};
window.__pickupBuildSubmitCcMessage = buildPickupSubmitCcMessage;
window.__pickupCalc = {
applyPickupDateToVehicles: applyPickupDateToVehicles,
buildBatchPickupScheduleSummary: buildBatchPickupScheduleSummary,
describePickupRule: describePickupRule,
};
window.__pickupHandleSubmit = function (payload) {
var ctx = window.__pickupHandleContext;
if (ctx && ctx.contractCode) {
@@ -85,9 +90,9 @@ const Component = function (props) {
delete window.__receivableToAudit;
delete window.__receivableSubmit;
delete window.__pickupHandleContext;
delete window.__pickupViewContext;
delete window.__pickupHandleSubmit;
delete window.__pickupBuildSubmitCcMessage;
delete window.__pickupCalc;
};
}, [setView, bumpListRefresh]);
@@ -96,11 +101,26 @@ const Component = function (props) {
if (view === 'collect' && handleContext) {
window.__pickupHandleContext = handleContext;
}
if (view === 'contractView' && viewContext) {
window.__pickupViewContext = viewContext;
}
},
[view, handleContext],
[view, handleContext, viewContext],
);
if (view === 'collect') return React.createElement(PickupReceivableCollectPage);
if (view === 'collect') return React.createElement(VehiclePickupReceivableCollectPage);
if (view === 'contractView') {
return React.createElement(VehiclePickupReceivableContractView, {
canHandle: contractViewRecord ? canShowHandle(contractViewRecord) : false,
onGoHandle: function () {
if (!contractViewRecord) return;
var ctx = buildCollectHandleContext(contractViewRecord);
setHandleContext(ctx);
window.__pickupHandleContext = ctx;
setView('collect');
},
});
}
if (view === 'view') return React.createElement(PickupReceivableViewPage);
if (view === 'edit') return React.createElement(PickupCollectEditPage);
if (view === 'invoice') return React.createElement(PickupReceivableInvoicePage);
@@ -109,28 +129,22 @@ const Component = function (props) {
return React.createElement(VehiclePickupReceivableList, {
refreshToken: listRefreshToken,
onGoHandle: function (record) {
var months = record.paymentCycleMonths || 1;
var pickupDate = record.lastPickupDate || DEFAULT_PICKUP_DATE;
var ctx = {
contractCode: record.contractCode,
contractType: record.contractType,
projectName: record.projectName,
customerName: record.customerName,
paymentMethod: record.paymentMethod || '预付',
paymentCycle: months + '个月',
paymentCycleMonths: months,
contractStart: record.contractEffectiveDate,
contractEnd: record.contractEndDate || '',
businessDept: record.businessDept,
businessPerson: record.businessPerson,
pickupDate: pickupDate,
vehicles: toCollectPageVehicles(record, pickupDate),
};
var ctx = buildCollectHandleContext(record);
setHandleContext(ctx);
window.__pickupHandleContext = ctx;
setView('collect');
},
onGoView: function () {
onGoContractView: function (record) {
setContractViewRecord(record);
var ctx = buildContractViewContext(record);
setViewContext(ctx);
window.__pickupViewContext = ctx;
setView('contractView');
},
onGoView: function (record, parent) {
if (parent) {
window.__pickupViewContext = { mode: 'batch', batch: record, parent: parent };
}
setView('view');
},
onGoEdit: function () {

View File

@@ -0,0 +1,814 @@
import {
fmtMoneyYuan,
} from './data/pickup-receivable-list-data.js';
import {
buildBatchPickupScheduleSummary,
describePickupRule,
buildCollectLinePreview,
buildCollectSubmitPayload,
buildCollectOrderTableRows,
resolveCollectPlateLabel,
PLATE_ACTUAL_DELIVERY_LABEL,
} from './pickup-receivable-bridge.js';
function displayText(value) {
var text = value == null ? '' : String(value).trim();
return text || '—';
}
function initLineQuantities(lines) {
var map = {};
(lines || []).forEach(function (line) {
map[line.lineKey] = 0;
});
return map;
}
function formatBrandModelLabel(brand, model) {
return [brand, model]
.filter(function (part) {
return part && String(part).trim() && part !== '-';
})
.join(' · ') || '—';
}
function DeliveryFieldIconLocation() {
return React.createElement(
'svg',
{
className: 'vpr-collect-delivery-field__icon',
width: 16,
height: 16,
viewBox: '0 0 24 24',
fill: 'none',
stroke: 'currentColor',
strokeWidth: 2,
strokeLinecap: 'round',
strokeLinejoin: 'round',
'aria-hidden': true,
},
React.createElement('path', { d: 'M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z' }),
React.createElement('circle', { cx: '12', cy: '10', r: '3' }),
);
}
function DeliveryFieldIconCalendar() {
return React.createElement(
'svg',
{
className: 'vpr-collect-delivery-field__icon',
width: 16,
height: 16,
viewBox: '0 0 24 24',
fill: 'none',
stroke: 'currentColor',
strokeWidth: 2,
strokeLinecap: 'round',
strokeLinejoin: 'round',
'aria-hidden': true,
},
React.createElement('rect', { x: '3', y: '4', width: '18', height: '18', rx: '2' }),
React.createElement('line', { x1: '16', y1: '2', x2: '16', y2: '6' }),
React.createElement('line', { x1: '8', y1: '2', x2: '8', y2: '6' }),
React.createElement('line', { x1: '3', y1: '10', x2: '21', y2: '10' }),
);
}
const Component = function () {
var useState = React.useState;
var useMemo = React.useMemo;
var useEffect = React.useEffect;
var antd = window.antd;
var Card = antd.Card;
var Button = antd.Button;
var Input = antd.Input;
var InputNumber = antd.InputNumber;
var DatePicker = antd.DatePicker;
var Modal = antd.Modal;
var message = antd.message;
var dayjs = window.dayjs;
var handleCtx = typeof window !== 'undefined' ? window.__pickupHandleContext : null;
var project = handleCtx || {};
var orderLines = project.leaseOrderLines || [];
var paymentCycleMonths = project.paymentCycleMonths || 1;
var deliveryLocationState = useState(project.defaultDeliveryLocation || '');
var deliveryLocation = deliveryLocationState[0];
var setDeliveryLocation = deliveryLocationState[1];
var deliveryDateState = useState(project.defaultPickupDate || '');
var deliveryDate = deliveryDateState[0];
var setDeliveryDate = deliveryDateState[1];
var lineQuantitiesState = useState(function () {
return initLineQuantities(orderLines);
});
var lineQuantities = lineQuantitiesState[0];
var setLineQuantities = lineQuantitiesState[1];
var dirtyState = useState(false);
var dirty = dirtyState[0];
var setDirty = dirtyState[1];
var submitAttemptState = useState(false);
var submitAttempted = submitAttemptState[0];
var setSubmitAttempted = submitAttemptState[1];
var submitConfirmState = useState(false);
var submitConfirmVisible = submitConfirmState[0];
var setSubmitConfirmVisible = submitConfirmState[1];
useEffect(function () {
setLineQuantities(initLineQuantities(orderLines));
}, [project.contractCode]);
var selectedCount = useMemo(function () {
return (orderLines || []).reduce(function (sum, line) {
var qty = Math.min(
line.availableCount,
Math.max(0, parseInt(lineQuantities[line.lineKey], 10) || 0),
);
return sum + qty;
}, 0);
}, [orderLines, lineQuantities]);
var calcRows = useMemo(function () {
var rows = [];
(orderLines || []).forEach(function (line) {
var qty = Math.min(
line.availableCount,
Math.max(0, parseInt(lineQuantities[line.lineKey], 10) || 0),
);
for (var i = 0; i < qty; i += 1) {
rows.push({
selected: true,
receivableCompleted: false,
monthRent: line.monthRent,
serviceFeeTotal: line.serviceFeeTotal,
deposit: line.deposit,
receivableDeposit: line.deposit,
receivableService: line.serviceFeeTotal,
});
}
});
return rows;
}, [orderLines, lineQuantities]);
var batchSummary = useMemo(function () {
if (!deliveryDate || !calcRows.length) return null;
return buildBatchPickupScheduleSummary(calcRows, deliveryDate, paymentCycleMonths);
}, [calcRows, deliveryDate, paymentCycleMonths]);
var amountBreakdown = useMemo(function () {
if (!batchSummary) {
return { rent: 0, deposit: 0, service: 0, total: 0 };
}
return {
rent: (batchSummary.beforePickup.rent || 0) + (batchSummary.month25.rent || 0),
deposit: batchSummary.beforePickup.deposit || 0,
service: (batchSummary.beforePickup.service || 0) + (batchSummary.month25.service || 0),
total: batchSummary.totalReceivable || 0,
};
}, [batchSummary]);
var orderTableRows = useMemo(function () {
return buildCollectOrderTableRows(orderLines);
}, [orderLines]);
var ruleText = useMemo(function () {
return describePickupRule(batchSummary && batchSummary.rule);
}, [batchSummary]);
var formReady = Boolean(deliveryDate && String(deliveryLocation || '').trim() && selectedCount > 0);
var submitHint = useMemo(function () {
if (!deliveryDate) return '请先设置车辆交付时间';
if (!String(deliveryLocation || '').trim()) return '请先填写车辆交付地点';
if (!selectedCount) return '请至少选择 1 辆本次交车数量';
return '';
}, [deliveryDate, deliveryLocation, selectedCount]);
function markDirty() {
setDirty(true);
}
function handleBack() {
if (dirty) {
Modal.confirm({
title: '确认离开',
content: '当前办理信息尚未提交,确定返回列表吗?',
okText: '返回列表',
cancelText: '继续编辑',
onOk: function () {
if (window.__receivableBack) window.__receivableBack();
},
});
return;
}
if (window.__receivableBack) window.__receivableBack();
}
function fillLineQuantity(lineKey, quantity) {
setLineQuantities(function (prev) {
var next = Object.assign({}, prev);
next[lineKey] = quantity;
return next;
});
markDirty();
}
function fillAllPending() {
setLineQuantities(function () {
var next = {};
(orderLines || []).forEach(function (line) {
next[line.lineKey] = line.availableCount;
});
return next;
});
markDirty();
}
function clearAllQuantities() {
setLineQuantities(initLineQuantities(orderLines));
markDirty();
}
function handleSubmit() {
setSubmitAttempted(true);
if (!deliveryDate) {
message.warning('请设置车辆交付时间');
return;
}
if (!String(deliveryLocation || '').trim()) {
message.warning('请填写车辆交付(交还)地点');
return;
}
if (!selectedCount) {
message.warning('请至少选择 1 辆本次交车数量');
return;
}
setSubmitConfirmVisible(true);
}
function confirmSubmit() {
var payload = buildCollectSubmitPayload(project, {
deliveryDate: deliveryDate,
deliveryLocation: deliveryLocation,
lineQuantities: lineQuantities,
});
if (window.__pickupHandleSubmit) window.__pickupHandleSubmit(payload);
if (window.__pickupBuildSubmitCcMessage) {
message.success(window.__pickupBuildSubmitCcMessage());
} else {
message.success('已提交办理并生成提车应收款单据');
}
setSubmitConfirmVisible(false);
setDirty(false);
if (window.__receivableBack) window.__receivableBack();
}
function renderContractStat(label, value, options) {
var opts = options || {};
var valueClass = 'vm-detail-stat-value';
if (opts.mono) valueClass += ' mono tabular-nums';
return React.createElement(
'div',
{ className: 'vm-detail-stat', key: label },
React.createElement(
'div',
{ className: 'vm-detail-stat-main' },
React.createElement('span', { className: 'vm-detail-stat-label' }, label),
React.createElement(
'span',
{ className: valueClass, title: displayText(value) },
displayText(value),
),
),
);
}
function renderCollectPlateCell(vehicle) {
var label = resolveCollectPlateLabel(vehicle);
var isPending = label === PLATE_ACTUAL_DELIVERY_LABEL;
return React.createElement(
'span',
{
className: 'vpr-pickup-subtable__plate-no tabular-nums'
+ (isPending ? ' vpr-pickup-subtable__plate-no--pending' : ''),
title: label,
},
label,
);
}
function renderCollectModelGroupCell(line) {
var label = formatBrandModelLabel(line.brand, line.model);
return React.createElement(
'div',
{ className: 'vpr-collect-td--model', title: label },
label,
);
}
function renderKpiStat(label, value, highlight) {
return React.createElement(
'div',
{
className: 'vm-detail-stat vpr-collect-kpi-stat'
+ (highlight ? ' vpr-collect-kpi-stat--active' : ''),
key: label,
},
React.createElement(
'div',
{ className: 'vm-detail-stat-main' },
React.createElement('span', { className: 'vm-detail-stat-label' }, label),
React.createElement(
'div',
{ className: 'vm-detail-stat-value vpr-collect-kpi-stat__value tabular-nums' },
value,
),
),
);
}
var businessMeta = [project.businessDept, project.businessPerson]
.filter(function (part) {
return part && String(part).trim();
})
.join(' · ');
var contractStats = [
renderContractStat('签约公司', project.signingCompany),
renderContractStat('付款方式', project.paymentMethodLabel || project.paymentMethod),
renderContractStat('付款周期', project.paymentCycleLabel || project.paymentCycle),
renderContractStat('氢费支付方式', project.hydrogenPaymentMethodLabel || project.hydrogenPaymentMethod),
];
if (project.hydrogenPrepayAmount != null) {
contractStats.push(renderContractStat('预付款金额', fmtMoneyYuan(project.hydrogenPrepayAmount), { mono: true }));
}
return React.createElement(
'div',
{ className: 'vm-page lc-page vpr-page vpr-collect-page' },
React.createElement(
'header',
{
className: 'vm-detail-topbar vpr-collect-topbar',
'data-annotation-id': 'vpr-collect-topbar',
},
React.createElement(
'button',
{
type: 'button',
className: 'vm-btn vm-btn-back',
onClick: handleBack,
},
'返回列表',
),
),
React.createElement(
'main',
{ className: 'vpr-collect-main' },
React.createElement(
'section',
{
className: 'vm-detail-card vpr-collect-contract-card',
'data-annotation-id': 'vpr-collect-project',
},
React.createElement(
'div',
{ className: 'vm-detail-hero vpr-collect-contract-hero' },
React.createElement(
'div',
null,
React.createElement(
'div',
{ className: 'vm-detail-plate', title: displayText(project.customerName) },
displayText(project.customerName),
),
React.createElement(
'p',
{ className: 'vm-detail-meta mono tabular-nums' },
displayText(project.contractCode),
),
React.createElement(
'p',
{ className: 'vm-detail-meta', title: displayText(project.projectName) },
displayText(project.projectName),
),
),
React.createElement(
'div',
{ className: 'vm-detail-aside' },
React.createElement(
'p',
{ className: 'vpr-collect-contract-hero__aside-main', title: displayText(project.signingCompany) },
displayText(project.signingCompany),
),
businessMeta
? React.createElement('p', { className: 'vm-detail-meta' }, businessMeta)
: null,
React.createElement(
'p',
{ className: 'vm-detail-meta' },
displayText(project.contractType || '正式合同'),
),
),
),
React.createElement(
'div',
{ className: 'vm-detail-stats vpr-collect-contract-stats' },
contractStats,
),
),
React.createElement(
'div',
{
className: 'vm-detail-stats vpr-collect-summary-stats',
'data-annotation-id': 'vpr-collect-kpi',
},
renderKpiStat('已选车辆数', selectedCount + ' 辆', selectedCount > 0),
renderKpiStat('应收总额', fmtMoneyYuan(amountBreakdown.total), amountBreakdown.total > 0),
renderKpiStat('租金总额', fmtMoneyYuan(amountBreakdown.rent), amountBreakdown.rent > 0),
renderKpiStat('保证金总额', fmtMoneyYuan(amountBreakdown.deposit), amountBreakdown.deposit > 0),
renderKpiStat('服务项总额', fmtMoneyYuan(amountBreakdown.service), amountBreakdown.service > 0),
),
React.createElement(
Card,
{
className: 'vpr-collect-card vpr-collect-card--order',
title: '提车应收款',
'data-annotation-id': 'vpr-collect-form',
extra: orderLines.length
? React.createElement(
'span',
{ className: 'vpr-collect-card__extra tabular-nums' },
orderLines.length,
' 个待办型号',
)
: null,
},
React.createElement(
'div',
{ className: 'vpr-collect-order-section' },
React.createElement(
'section',
{ className: 'vpr-collect-delivery-panel', 'aria-label': '交付信息' },
React.createElement(
'div',
{ className: 'vpr-collect-delivery-panel__head' },
React.createElement('h3', { className: 'vpr-collect-delivery-panel__title' }, '交付信息'),
React.createElement(
'span',
{ className: 'vpr-collect-delivery-panel__hint' },
'根据交付时间,自动计算提车应收款金额',
),
),
React.createElement(
'div',
{ className: 'vpr-collect-delivery-panel__grid' },
React.createElement(
'label',
{
className:
'vpr-collect-delivery-field'
+ (submitAttempted && !String(deliveryLocation || '').trim() ? ' vpr-collect-delivery-field--invalid' : ''),
},
React.createElement(
'span',
{ className: 'vpr-collect-delivery-field__label' },
React.createElement(DeliveryFieldIconLocation),
'车辆交付(交还)地点',
React.createElement('span', { className: 'vpr-collect-form-item__required', 'aria-hidden': true }, ' *'),
),
React.createElement(Input, {
value: deliveryLocation,
placeholder: '请输入本次交车地点',
className: 'vpr-collect-delivery-field__control',
status: submitAttempted && !String(deliveryLocation || '').trim() ? 'error' : undefined,
onChange: function (e) {
setDeliveryLocation(e.target.value);
markDirty();
},
}),
submitAttempted && !String(deliveryLocation || '').trim()
? React.createElement('span', { className: 'vpr-collect-form-item__error', role: 'alert' }, '请填写交付地点')
: null,
),
React.createElement(
'label',
{
className:
'vpr-collect-delivery-field'
+ (submitAttempted && !deliveryDate ? ' vpr-collect-delivery-field--invalid' : ''),
},
React.createElement(
'span',
{ className: 'vpr-collect-delivery-field__label' },
React.createElement(DeliveryFieldIconCalendar),
'车辆交付时间',
React.createElement('span', { className: 'vpr-collect-form-item__required', 'aria-hidden': true }, ' *'),
),
React.createElement(DatePicker, {
className: 'vpr-collect-delivery-field__control vpr-collect-date-picker',
style: { width: '100%' },
placeholder: '请选择交付日期',
status: submitAttempted && !deliveryDate ? 'error' : undefined,
value: deliveryDate && dayjs ? dayjs(deliveryDate) : null,
onChange: function (_date, dateString) {
setDeliveryDate(dateString || '');
markDirty();
},
}),
submitAttempted && !deliveryDate
? React.createElement('span', { className: 'vpr-collect-form-item__error', role: 'alert' }, '请选择交付时间')
: null,
),
),
),
React.createElement(
'div',
{ className: 'vpr-page-callout vpr-page-callout--rule vpr-collect-order-callout', role: 'note' },
ruleText,
),
React.createElement(
'div',
{ className: 'vpr-collect-table-toolbar' },
React.createElement(
'div',
{ className: 'vpr-collect-table-toolbar__left' },
React.createElement(
'span',
{ className: 'vpr-collect-table-toolbar__badge tabular-nums' },
'待办 ',
orderLines.length,
' 行',
),
React.createElement('span', { className: 'vpr-collect-table-toolbar__hint' }, '填写本次交车数后自动计算应收'),
),
orderLines.length
? React.createElement(
'div',
{ className: 'vpr-collect-table-toolbar__actions' },
React.createElement(Button, { size: 'small', type: 'primary', ghost: true, onClick: fillAllPending }, '填入全部待办'),
React.createElement(Button, { size: 'small', onClick: clearAllQuantities }, '清空数量'),
)
: null,
),
React.createElement(
'div',
{ className: 'vpr-collect-table-wrap', 'data-annotation-id': 'vpr-collect-table' },
orderLines.length
? React.createElement(
'table',
{ className: 'vpr-collect-table' },
React.createElement(
'thead',
null,
React.createElement(
'tr',
null,
React.createElement('th', null, '品牌型号'),
React.createElement('th', { className: 'vpr-collect-th--plate' }, '车牌号'),
React.createElement('th', { className: 'vpr-collect-th--num' }, '未交车辆数'),
React.createElement('th', { className: 'vpr-collect-th--num' }, '本次交车数'),
React.createElement('th', { className: 'vpr-collect-th--num' }, '租金总额'),
React.createElement('th', { className: 'vpr-collect-th--num' }, '保证金总额'),
React.createElement('th', { className: 'vpr-collect-th--num' }, '服务项总额'),
React.createElement('th', { className: 'vpr-collect-th--num' }, '应收总额'),
),
),
React.createElement(
'tbody',
null,
orderTableRows.map(function (row) {
var line = row.line;
var preview = buildCollectLinePreview(
line,
lineQuantities[line.lineKey],
deliveryDate,
paymentCycleMonths,
);
var rowClass = preview.quantity ? 'vpr-collect-row--selected' : 'vpr-collect-row--idle';
var cells = [];
if (row.isFirstInModelGroup) {
cells.push(
React.createElement(
'td',
{
key: 'model',
rowSpan: row.modelGroupRowSpan,
className: 'vpr-collect-td--model-cell',
},
renderCollectModelGroupCell(line),
),
);
}
cells.push(
React.createElement(
'td',
{ key: 'plateNo', className: 'vpr-pickup-subtable__plate-cell' },
renderCollectPlateCell(row.vehicle),
),
);
if (row.isFirstInModelGroup) {
cells.push(
React.createElement(
'td',
{
key: 'pendingCount',
rowSpan: row.modelGroupRowSpan,
className: 'vpr-collect-td--num vpr-collect-td--action tabular-nums',
title: '点击填入该行全部未交车辆',
onClick: function () {
fillLineQuantity(line.lineKey, line.availableCount);
},
},
line.availableCount,
),
React.createElement(
'td',
{
key: 'pickupQty',
rowSpan: row.modelGroupRowSpan,
className: 'vpr-collect-td--num',
},
React.createElement(InputNumber, {
min: 0,
max: line.availableCount,
value: lineQuantities[line.lineKey] || 0,
className: 'vpr-collect-qty-input',
onChange: function (value) {
setLineQuantities(function (prev) {
var next = Object.assign({}, prev);
next[line.lineKey] = value == null ? 0 : value;
return next;
});
markDirty();
},
}),
),
React.createElement(
'td',
{
key: 'rentTotal',
rowSpan: row.modelGroupRowSpan,
className: 'vpr-collect-td--num tabular-nums',
},
fmtMoneyYuan(preview.rentTotal),
),
React.createElement(
'td',
{
key: 'depositTotal',
rowSpan: row.modelGroupRowSpan,
className: 'vpr-collect-td--num tabular-nums',
},
fmtMoneyYuan(preview.depositTotal),
),
React.createElement(
'td',
{
key: 'serviceTotal',
rowSpan: row.modelGroupRowSpan,
className: 'vpr-collect-td--num tabular-nums',
},
fmtMoneyYuan(preview.serviceTotal),
),
React.createElement(
'td',
{
key: 'lineTotal',
rowSpan: row.modelGroupRowSpan,
className: 'vpr-collect-td--num tabular-nums',
},
fmtMoneyYuan(preview.lineTotal),
),
);
}
return React.createElement(
'tr',
{ key: row.rowKey, className: rowClass },
cells,
);
}),
),
)
: React.createElement(
'div',
{ className: 'vpr-collect-table-toolbar vpr-collect-table-toolbar--warn vpr-page-empty' },
React.createElement('p', { className: 'vpr-page-empty__title' }, '暂无可办理的租赁订单明细'),
React.createElement('p', { className: 'vpr-page-empty__desc' }, '本合同车辆可能已全部办理,请返回列表确认。'),
),
),
batchSummary && batchSummary.rule === 'on-or-before-15' && batchSummary.month25.amount > 0
? React.createElement(
'div',
{ className: 'vpr-collect-batch-panel' },
React.createElement('div', { className: 'vpr-collect-batch-panel__title' }, '当月 25 日应付(下周期租金及服务费)'),
React.createElement(
'div',
{ className: 'vpr-collect-batch-panel__amount tabular-nums' },
fmtMoneyYuan(batchSummary.month25.amount),
),
)
: null,
),
),
),
React.createElement(
'footer',
{ className: 'vpr-collect-footer', 'data-annotation-id': 'vpr-collect-footer' },
React.createElement(
'div',
{ className: 'vpr-collect-footer__left' },
dirty
? React.createElement('span', { className: 'vpr-collect-draft-badge' }, '未提交')
: null,
React.createElement(
'div',
{ className: 'vpr-collect-footer__summary' + (formReady ? '' : ' vpr-collect-footer__summary--warn') },
formReady
? React.createElement(
React.Fragment,
null,
'已选 ',
React.createElement('strong', { className: 'tabular-nums' }, String(selectedCount)),
' 辆 · 应收合计 ',
React.createElement(
'strong',
{ className: 'tabular-nums' },
fmtMoneyYuan(amountBreakdown.total),
),
)
: React.createElement('span', null, submitHint || '请完善交付信息与交车数量'),
),
),
React.createElement(
'div',
{ className: 'vpr-collect-footer__actions' },
React.createElement(Button, { onClick: handleBack }, '取消'),
React.createElement(
Button,
{
type: 'primary',
className: 'vm-btn-primary',
disabled: !formReady,
onClick: handleSubmit,
title: formReady ? '提交并生成提车应收款' : submitHint,
},
'提交生成',
),
),
),
React.createElement(
Modal,
{
title: '确认提交办理',
open: submitConfirmVisible,
okText: '确认提交',
cancelText: '再检查一下',
onOk: confirmSubmit,
onCancel: function () {
setSubmitConfirmVisible(false);
},
},
React.createElement(
'div',
{ className: 'vpr-submit-confirm' },
React.createElement(
'p',
null,
'将为 ',
React.createElement('strong', null, String(selectedCount)),
' 辆车生成本次提车应收款。',
),
React.createElement(
'ul',
{ className: 'vpr-submit-confirm__list' },
React.createElement('li', null, '交付时间:', React.createElement('strong', null, deliveryDate || '—')),
React.createElement('li', null, '交付地点:', React.createElement('strong', null, deliveryLocation || '—')),
React.createElement(
'li',
null,
'应收合计:',
React.createElement(
'strong',
{ className: 'tabular-nums' },
fmtMoneyYuan(amountBreakdown.total),
),
),
),
),
),
);
};
export default Component;

View File

@@ -0,0 +1,342 @@
import { StatusTag } from '../vehicle-management/components/StatusTag';
import {
fmtMoneyYuan,
normalizeAuditStatus,
auditStatusTone,
} from './data/pickup-receivable-list-data.js';
function displayText(value) {
var text = value == null ? '' : String(value).trim();
return text || '—';
}
function renderInfoField(label, value) {
return React.createElement(
'div',
{ className: 'vpr-collect-field', key: label },
React.createElement('div', { className: 'vpr-collect-field__label' }, label),
React.createElement(
'div',
{ className: 'vpr-collect-field__value', title: displayText(value) },
displayText(value),
),
);
}
const Component = function (props) {
var useMemo = React.useMemo;
var antd = window.antd;
var Button = antd.Button;
var Card = antd.Card;
var ctx = typeof window !== 'undefined' ? window.__pickupViewContext : null;
var project = ctx || {};
var publishedRecords = project.publishedRecords || [];
var pendingVehicles = project.pendingVehicles || [];
var onGoHandle = props && props.onGoHandle;
var canHandle = props && props.canHandle;
var summary = useMemo(function () {
var receivableTotal = 0;
var receivedTotal = 0;
publishedRecords.forEach(function (row) {
receivableTotal += parseFloat(row.receivableTotal) || 0;
receivedTotal += parseFloat(row.receivedTotal) || 0;
});
return {
publishedCount: publishedRecords.length,
pendingCount: pendingVehicles.length,
receivableTotal: receivableTotal,
receivedTotal: receivedTotal,
};
}, [publishedRecords, pendingVehicles]);
var showHandle = canHandle && pendingVehicles.length > 0 && onGoHandle;
function handleBack() {
if (window.__receivableBack) window.__receivableBack();
}
function handleGoHandle() {
if (onGoHandle) onGoHandle();
}
return React.createElement(
'div',
{ className: 'vm-page lc-page vpr-page vpr-collect-page vpr-contract-view-page' },
React.createElement(
'header',
{ className: 'vpr-collect-topbar', 'data-annotation-id': 'vpr-contract-view-topbar' },
React.createElement(
'button',
{
type: 'button',
className: 'vm-btn vm-btn-back',
onClick: handleBack,
},
'返回列表',
),
),
showHandle
? React.createElement(
'div',
{ className: 'vpr-contract-view-actions' },
React.createElement(
Button,
{
type: 'primary',
className: 'vm-btn-primary',
onClick: handleGoHandle,
},
'办理未发布车辆',
),
)
: null,
React.createElement(
'div',
{ className: 'vpr-page-kpi-row', 'data-annotation-id': 'vpr-contract-view-kpi' },
React.createElement(
'div',
{ className: 'vpr-collect-kpi-card' },
React.createElement('div', { className: 'vpr-collect-kpi-card__label' }, '已发布单据'),
React.createElement(
'div',
{ className: 'vpr-collect-kpi-card__value tabular-nums' },
String(summary.publishedCount),
React.createElement('span', { className: 'vpr-page-kpi-unit' }, ' 条'),
),
),
React.createElement(
'div',
{ className: 'vpr-collect-kpi-card' },
React.createElement('div', { className: 'vpr-collect-kpi-card__label' }, '未发布车辆'),
React.createElement(
'div',
{
className:
'vpr-collect-kpi-card__value tabular-nums'
+ (summary.pendingCount ? ' vpr-page-kpi-value--warn' : ''),
},
String(summary.pendingCount),
React.createElement('span', { className: 'vpr-page-kpi-unit' }, ' 辆'),
),
),
React.createElement(
'div',
{ className: 'vpr-collect-kpi-card' },
React.createElement('div', { className: 'vpr-collect-kpi-card__label' }, '已发布合计应收'),
React.createElement(
'div',
{ className: 'vpr-collect-kpi-card__value tabular-nums' },
fmtMoneyYuan(summary.receivableTotal),
),
),
React.createElement(
'div',
{ className: 'vpr-collect-kpi-card vpr-collect-kpi-card--primary' },
React.createElement('div', { className: 'vpr-collect-kpi-card__label' }, '已发布合计实收'),
React.createElement(
'div',
{ className: 'vpr-collect-kpi-card__value tabular-nums' },
fmtMoneyYuan(summary.receivedTotal),
),
React.createElement('div', { className: 'vpr-collect-kpi-card__hint' }, '由收款记录导入反写'),
),
),
React.createElement(
Card,
{
className: 'vpr-collect-card vpr-collect-card--primary',
title: '合同项目信息',
},
React.createElement(
'div',
{ className: 'vpr-collect-info-grid' },
renderInfoField('客户名称', project.customerName),
renderInfoField('合同编码', project.contractCode),
renderInfoField('项目名称', project.projectName),
renderInfoField('付款周期', project.paymentCycle),
renderInfoField('业务部门', project.businessDept),
renderInfoField('业务负责人', project.businessPerson),
renderInfoField('租车数', project.contractVehicleCount),
renderInfoField('已提车数', project.pickedUpVehicleCount),
),
React.createElement(
'div',
{ className: 'vpr-page-callout', role: 'note' },
React.createElement('strong', null, '说明:'),
'本页汇总本合同全部已发布与未发布提车应收款。未发布车辆可点击上方「办理未发布车辆」进入办理流程。',
),
),
React.createElement(
'section',
{ className: 'vpr-contract-view-section' },
React.createElement(
'div',
{ className: 'vpr-contract-view-section__head' },
React.createElement('h2', { className: 'vpr-contract-view-section__title' }, '已发布提车应收款'),
React.createElement(
'span',
{ className: 'vpr-contract-view-section__count tabular-nums' },
summary.publishedCount + ' 条',
),
),
publishedRecords.length
? React.createElement(
'div',
{ className: 'vpr-pickup-table-wrap vpr-page-table-wrap' },
React.createElement(
'table',
{ className: 'vpr-pickup-native-table vpr-contract-view-table' },
React.createElement(
'thead',
null,
React.createElement(
'tr',
null,
React.createElement('th', null, '提车收款单编码'),
React.createElement('th', null, '发布状态'),
React.createElement('th', null, '单据状态'),
React.createElement('th', null, '交付时间'),
React.createElement('th', { className: 'vpr-th--num' }, '车辆数'),
React.createElement('th', { className: 'vpr-th--num' }, '合计应收'),
React.createElement('th', { className: 'vpr-th--num' }, '合计实收'),
React.createElement('th', null, '生成时间'),
),
),
React.createElement(
'tbody',
null,
publishedRecords.map(function (row) {
var auditLabel = normalizeAuditStatus(row.auditStatus);
return React.createElement(
'tr',
{ key: row.collectCode || row.seq, className: 'vpr-page-table-row' },
React.createElement('td', { className: 'tabular-nums' }, displayText(row.collectCode)),
React.createElement(
'td',
null,
React.createElement(StatusTag, {
label: row.publishStatus,
tone: 'green',
title: row.publishStatus,
}),
),
React.createElement(
'td',
null,
React.createElement(StatusTag, {
label: auditLabel === '已驳回' ? '审批驳回' : auditLabel,
tone: auditStatusTone(row.auditStatus),
title: auditLabel,
}),
),
React.createElement('td', { className: 'tabular-nums' }, displayText(row.pickupDate)),
React.createElement('td', { className: 'vpr-td--num tabular-nums' }, String(row.vehicleCount || 0)),
React.createElement('td', { className: 'vpr-money vpr-td--num tabular-nums' }, fmtMoneyYuan(row.receivableTotal)),
React.createElement('td', { className: 'vpr-money vpr-td--num tabular-nums' }, fmtMoneyYuan(row.receivedTotal)),
React.createElement('td', { className: 'tabular-nums' }, displayText(row.chargeTime)),
);
}),
),
),
)
: React.createElement(
'div',
{ className: 'vpr-contract-view-empty vpr-page-empty' },
React.createElement('p', { className: 'vpr-page-empty__title' }, '暂无已发布记录'),
React.createElement('p', { className: 'vpr-page-empty__desc' }, '办理提交后将在此展示已生成的提车应收款单据。'),
),
),
React.createElement(
'section',
{ className: 'vpr-contract-view-section' },
React.createElement(
'div',
{ className: 'vpr-contract-view-section__head' },
React.createElement('h2', { className: 'vpr-contract-view-section__title' }, '未发布提车应收款'),
React.createElement(
'span',
{ className: 'vpr-contract-view-section__count tabular-nums' },
summary.pendingCount + ' 辆',
),
),
pendingVehicles.length
? React.createElement(
'div',
{ className: 'vpr-pickup-table-wrap vpr-page-table-wrap' },
React.createElement(
'table',
{ className: 'vpr-pickup-native-table vpr-contract-view-table' },
React.createElement(
'thead',
null,
React.createElement(
'tr',
null,
React.createElement('th', { className: 'vpr-th--num' }, '序号'),
React.createElement('th', null, '发布状态'),
React.createElement('th', null, '品牌'),
React.createElement('th', null, '型号'),
React.createElement('th', null, '车牌号'),
React.createElement('th', null, '说明'),
),
),
React.createElement(
'tbody',
null,
pendingVehicles.map(function (vehicle) {
return React.createElement(
'tr',
{
key: (vehicle.seq || '') + '-' + (vehicle.plateNo || '') + '-' + (vehicle.model || ''),
className: 'vpr-page-table-row',
},
React.createElement('td', { className: 'vpr-td--num tabular-nums' }, vehicle.seq),
React.createElement(
'td',
null,
React.createElement(StatusTag, {
label: vehicle.publishStatus,
tone: 'amber',
title: vehicle.publishStatus,
}),
),
React.createElement('td', null, displayText(vehicle.brand)),
React.createElement('td', null, displayText(vehicle.model)),
React.createElement('td', { className: 'tabular-nums' }, displayText(vehicle.plateNo)),
React.createElement('td', { className: 'vpr-page-table-muted' }, '尚未办理,可发起提车应收款办理'),
);
}),
),
),
)
: React.createElement(
'div',
{ className: 'vpr-contract-view-empty vpr-page-empty vpr-page-empty--success' },
React.createElement('p', { className: 'vpr-page-empty__title' }, '本合同车辆均已发布'),
React.createElement('p', { className: 'vpr-page-empty__desc' }, '所有车辆均已生成提车应收款,无需再次办理。'),
),
),
showHandle
? React.createElement(
'div',
{ className: 'vpr-contract-view-footer-cta' },
React.createElement(
Button,
{
type: 'primary',
size: 'large',
className: 'vm-btn-primary',
onClick: handleGoHandle,
},
'办理 ',
summary.pendingCount,
' 辆未发布车辆',
),
)
: null,
);
};
export default Component;

View File

@@ -32,23 +32,12 @@ var ONEOS_ANT_TABLE_GLOBAL_FIX = [
var VPR_LIST_CHROME_OFFSET = 420;
var VPR_MAIN_TABLE_COL_RATIOS = [13, 7, 9.5, 6, 6, 6, 7.5, 6, 6, 6, 7.5, 6];
var VPR_MAIN_TABLE_COL_RATIO_SUM = VPR_MAIN_TABLE_COL_RATIOS.reduce(function (sum, ratio) {
return sum + ratio;
}, 0);
/** 主表列宽pxscroll.x 为各列之和,表头/表体横向同步滚动 */
var VPR_MAIN_TABLE_COLUMN_WIDTHS = [240, 120, 160, 100, 100, 100, 120, 100, 100, 100, 120, 140];
function buildVprMainTableColumnWidths(containerWidth) {
var safeWidth = Math.max(960, Math.floor(containerWidth) || 960);
var widths = VPR_MAIN_TABLE_COL_RATIOS.map(function (ratio) {
return Math.max(52, Math.floor(safeWidth * ratio / VPR_MAIN_TABLE_COL_RATIO_SUM));
});
var sum = widths.reduce(function (acc, width) {
return acc + width;
var VPR_MAIN_TABLE_SCROLL_X = VPR_MAIN_TABLE_COLUMN_WIDTHS.reduce(function (sum, width) {
return sum + width;
}, 0);
var delta = safeWidth - sum;
if (delta) widths[widths.length - 1] += delta;
return { widths: widths, scrollX: safeWidth };
}
var KPI_ICONS = {
all: React.createElement(
@@ -68,7 +57,7 @@ var KPI_ICONS = {
React.createElement('path', { d: 'M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z' }),
React.createElement('path', { d: 'M14 2v4a2 2 0 0 0 2 2h4' }),
),
pending: React.createElement(
notPickedUp: React.createElement(
'svg',
{
xmlns: 'http://www.w3.org/2000/svg',
@@ -85,7 +74,7 @@ var KPI_ICONS = {
React.createElement('circle', { cx: 12, cy: 12, r: 10 }),
React.createElement('polyline', { points: '12 6 12 12 16 14' }),
),
inReview: React.createElement(
partialPickedUp: React.createElement(
'svg',
{
xmlns: 'http://www.w3.org/2000/svg',
@@ -102,7 +91,7 @@ var KPI_ICONS = {
React.createElement('path', { d: 'M21 12a9 9 0 1 1-3-6.7' }),
React.createElement('polyline', { points: '21 3 21 9 15 9' }),
),
approved: React.createElement(
completed: React.createElement(
'svg',
{
xmlns: 'http://www.w3.org/2000/svg',
@@ -118,7 +107,7 @@ var KPI_ICONS = {
},
React.createElement('path', { d: 'M20 6 9 17l-5-5' }),
),
rejected: React.createElement(
refunded: React.createElement(
'svg',
{
xmlns: 'http://www.w3.org/2000/svg',
@@ -132,28 +121,8 @@ var KPI_ICONS = {
strokeLinejoin: 'round',
'aria-hidden': true,
},
React.createElement('circle', { cx: 12, cy: 12, r: 10 }),
React.createElement('path', { d: 'm15 9-6 6' }),
React.createElement('path', { d: 'm9 9 6 6' }),
),
generated: React.createElement(
'svg',
{
xmlns: 'http://www.w3.org/2000/svg',
width: 18,
height: 18,
viewBox: '0 0 24 24',
fill: 'none',
stroke: 'currentColor',
strokeWidth: 2,
strokeLinecap: 'round',
strokeLinejoin: 'round',
'aria-hidden': true,
},
React.createElement('path', { d: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z' }),
React.createElement('polyline', { points: '14 2 14 8 20 8' }),
React.createElement('line', { x1: 12, x2: 12, y1: 18, y2: 12 }),
React.createElement('line', { x1: 9, x2: 15, y1: 15, y2: 15 }),
React.createElement('path', { d: 'M3 7v6h6' }),
React.createElement('path', { d: 'M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13' }),
),
};
@@ -262,6 +231,7 @@ const Component = function (props) {
contractCode: undefined,
projectName: undefined,
customerName: undefined,
plateNo: undefined,
businessDept: undefined,
businessPerson: undefined,
});
@@ -272,6 +242,7 @@ const Component = function (props) {
contractCode: undefined,
projectName: undefined,
customerName: undefined,
plateNo: undefined,
businessDept: undefined,
businessPerson: undefined,
});
@@ -291,31 +262,9 @@ const Component = function (props) {
[props && props.refreshToken],
);
var tableColumnWidths = VPR_MAIN_TABLE_COLUMN_WIDTHS;
var tableScrollX = VPR_MAIN_TABLE_SCROLL_X;
var tableWrapRef = React.useRef(null);
var tableLayoutState = React.useState(function () {
return buildVprMainTableColumnWidths(1200);
});
var tableColumnWidths = tableLayoutState[0].widths;
var tableScrollX = tableLayoutState[0].scrollX;
var setTableLayout = tableLayoutState[1];
useEffect(function () {
var el = tableWrapRef.current;
if (!el || typeof ResizeObserver === 'undefined') return undefined;
var syncWidth = function () {
var nextWidth = el.clientWidth;
if (!nextWidth) return;
setTableLayout(buildVprMainTableColumnWidths(nextWidth));
};
syncWidth();
var observer = new ResizeObserver(function () {
syncWidth();
});
observer.observe(el);
return function () {
observer.disconnect();
};
}, []);
var contractPopoverKeyState = useState(null);
var contractPopoverKey = contractPopoverKeyState[0];
@@ -346,8 +295,8 @@ const Component = function (props) {
}, [mainList, appliedFilters]);
var kpiCounts = useMemo(function () {
return countKpiFromList(filteredByQuery, kpiTab);
}, [filteredByQuery, kpiTab]);
return countKpiFromList(filteredByQuery);
}, [filteredByQuery]);
var filteredData = useMemo(function () {
return filterByKpiTab(filteredByQuery, kpiTab);
@@ -364,6 +313,45 @@ const Component = function (props) {
.slice(start, start + pageSize);
}, [filteredData, safePage, pageSize]);
useEffect(
function () {
var root = tableWrapRef.current;
if (!root) return undefined;
var body = root.querySelector('.ant-table-body');
var header = root.querySelector('.ant-table-header');
if (!body || !header) return undefined;
function applyLayoutSync() {
var scrollbarWidth = Math.max(0, body.offsetWidth - body.clientWidth);
header.style.paddingRight = scrollbarWidth ? scrollbarWidth + 'px' : '';
header.style.boxSizing = 'border-box';
header.scrollLeft = body.scrollLeft;
}
function onBodyScroll() {
header.scrollLeft = body.scrollLeft;
}
applyLayoutSync();
body.addEventListener('scroll', onBodyScroll, { passive: true });
var resizeObserver =
typeof ResizeObserver !== 'undefined'
? new ResizeObserver(function () {
applyLayoutSync();
})
: null;
if (resizeObserver) resizeObserver.observe(body);
return function () {
body.removeEventListener('scroll', onBodyScroll);
if (resizeObserver) resizeObserver.disconnect();
header.style.paddingRight = '';
header.style.boxSizing = '';
};
},
[pagedList, safePage, pageSize, tableScrollX],
);
function handleQuery() {
setAppliedFilters(Object.assign({}, filters));
setPage(1);
@@ -374,6 +362,7 @@ const Component = function (props) {
contractCode: undefined,
projectName: undefined,
customerName: undefined,
plateNo: undefined,
businessDept: undefined,
businessPerson: undefined,
};
@@ -388,6 +377,11 @@ const Component = function (props) {
else message.info('跳转至办理页(原型)');
}
function goContractView(record) {
if (props && typeof props.onGoContractView === 'function') props.onGoContractView(record);
else message.info('查看合同提车应收款(原型):' + (record.contractCode || ''));
}
function goRefund(record) {
message.success(
'已发起退款流程(原型):' + (record.contractCode || '') + ' · 抄送财务复核',
@@ -422,7 +416,49 @@ const Component = function (props) {
if (!vehicle) return '';
return [String(vehicle.brand || '').trim(), String(vehicle.model || '').trim()]
.filter(Boolean)
.join(' ');
.join('::');
}
var PLATE_ACTUAL_DELIVERY_LABEL = '以实际交付为准';
function formatBrandModelLabel(brand, model) {
return [brand, model]
.filter(function (part) {
return part && String(part).trim() && part !== '-';
})
.join(' · ') || '—';
}
function findLeaseOrderVehicle(parent, deliveryVehicle) {
if (!parent || !deliveryVehicle) return null;
return (parent.leaseOrderVehicles || []).find(function (lv) {
if (deliveryVehicle.seq != null && lv.seq === deliveryVehicle.seq) return true;
var deliveryPlate = String(deliveryVehicle.plateNo || '').trim();
var leasePlate = String(lv.plateNo || '').trim();
return deliveryPlate && leasePlate && deliveryPlate === leasePlate;
}) || null;
}
function isLeaseSpecificVehicle(leaseVehicle) {
if (!leaseVehicle) return false;
if (leaseVehicle.plateMode === 'specific') return true;
if (leaseVehicle.plateMode === 'actual') return false;
var leasePlate = String(leaseVehicle.plateNo || '').trim();
return Boolean(leasePlate && leasePlate !== PLATE_ACTUAL_DELIVERY_LABEL && leasePlate !== '-');
}
function resolveSubTablePlateLabel(deliveryVehicle, parent) {
if (!deliveryVehicle) return '—';
var leaseVehicle = findLeaseOrderVehicle(parent, deliveryVehicle);
var deliveryPlate = String(deliveryVehicle.plateNo || '').trim();
if (isLeaseSpecificVehicle(leaseVehicle)) {
var contractPlate = leaseVehicle ? String(leaseVehicle.plateNo || '').trim() : '';
return contractPlate || deliveryPlate || '—';
}
if (deliveryPlate && deliveryPlate !== PLATE_ACTUAL_DELIVERY_LABEL) {
return deliveryPlate;
}
return PLATE_ACTUAL_DELIVERY_LABEL;
}
function buildPickupSubModelGroupRows(parent) {
@@ -434,9 +470,12 @@ const Component = function (props) {
batch: batch,
parent: parent,
modelGroup: null,
vehicle: null,
batchSeq: batch.seq,
isFirstInBatch: true,
batchRowSpan: 1,
isFirstInModelGroup: true,
modelGroupRowSpan: 1,
});
return;
}
@@ -455,14 +494,24 @@ const Component = function (props) {
}
groupMap[key].vehicles.push(vehicle);
});
groupOrder.forEach(function (key, idx) {
var batchVehicleCount = vehicles.length;
var vehicleRowIndex = 0;
groupOrder.forEach(function (key) {
var modelGroup = groupMap[key];
var groupVehicles = modelGroup.vehicles;
groupVehicles.forEach(function (vehicle, vehicleIndex) {
rows.push({
batch: batch,
parent: parent,
modelGroup: groupMap[key],
modelGroup: modelGroup,
vehicle: vehicle,
batchSeq: batch.seq,
isFirstInBatch: idx === 0,
batchRowSpan: groupOrder.length,
isFirstInBatch: vehicleRowIndex === 0,
batchRowSpan: batchVehicleCount,
isFirstInModelGroup: vehicleIndex === 0,
modelGroupRowSpan: groupVehicles.length,
});
vehicleRowIndex += 1;
});
});
});
@@ -471,34 +520,25 @@ const Component = function (props) {
function renderModelGroupCell(modelGroup) {
if (!modelGroup) return '—';
var label = [modelGroup.brand, modelGroup.model].filter(Boolean).join(' ');
var plates = (modelGroup.vehicles || [])
.map(function (vehicle) {
return String(vehicle.plateNo || '').trim();
})
.filter(Boolean);
var label = formatBrandModelLabel(modelGroup.brand, modelGroup.model);
return React.createElement(
'div',
{ className: 'vpr-pickup-subtable__model-group' },
React.createElement(
'div',
{ className: 'vpr-pickup-subtable__model-group-name', title: label || '—' },
label || '—',
),
plates.length
? React.createElement(
'div',
{ className: 'vpr-pickup-subtable__model-group-name', title: label },
label,
);
}
function renderPlateNoCell(vehicle, parent) {
var label = resolveSubTablePlateLabel(vehicle, parent);
var isPending = label === PLATE_ACTUAL_DELIVERY_LABEL;
return React.createElement(
'span',
{
className: 'vpr-pickup-subtable__model-group-plates tabular-nums',
title: plates.join(''),
className: 'vpr-pickup-subtable__plate-no tabular-nums'
+ (isPending ? ' vpr-pickup-subtable__plate-no--pending' : ''),
title: label,
},
plates.join('、'),
)
: React.createElement(
'div',
{ className: 'vpr-pickup-subtable__model-group-plates is-empty' },
'—',
),
label,
);
}
@@ -592,42 +632,10 @@ const Component = function (props) {
});
}
function handleProjectInfoClick(record, e) {
if (e) e.stopPropagation();
if (canShowHandle(record)) {
goHandle(record);
return;
}
message.info('当前合同暂无可办理的提车应收款');
}
function renderProjectInfoCell(_, record) {
var clickable = canShowHandle(record);
var hasSubRows = Boolean(record && record.children && record.children.length);
return React.createElement(
'div',
{ className: 'lc-project-info-merged-cell' },
hasSubRows ? renderRowExpandIcon(record) : React.createElement('span', { className: 'vpr-row-expand-spacer', 'aria-hidden': true }),
React.createElement(
'div',
{
className: 'lc-project-info-cell' + (clickable ? ' vpr-project-info-cell--clickable' : ''),
role: clickable ? 'button' : undefined,
tabIndex: clickable ? 0 : undefined,
title: clickable ? '点击进入办理' : displayText(record.customerName),
onClick: function (e) {
handleProjectInfoClick(record, e);
},
onKeyDown: clickable
? function (e) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleProjectInfoClick(record, e);
}
}
: undefined,
},
React.createElement(
var stackContent = React.createElement(
'div',
{ className: 'vm-stack' },
React.createElement(
@@ -645,6 +653,33 @@ const Component = function (props) {
{ className: 'sub', title: displayText(record.projectName) },
displayText(record.projectName),
),
);
return React.createElement(
'div',
{ className: 'lc-project-info-merged-cell' },
hasSubRows ? renderRowExpandIcon(record) : React.createElement('span', { className: 'vpr-row-expand-spacer', 'aria-hidden': true }),
React.createElement(
'div',
{ className: 'lc-project-info-cell' },
clickable
? React.createElement(
'button',
{
type: 'button',
className: 'lc-project-info-btn',
title: '点击进入办理',
onClick: function (e) {
e.stopPropagation();
goHandle(record);
},
},
stackContent,
)
: React.createElement(
'div',
{ className: 'lc-project-info-cell__static' },
stackContent,
),
),
);
@@ -1124,35 +1159,22 @@ const Component = function (props) {
function renderMainActionCell(_, record) {
var showHandle = canShowHandle(record);
if (!showHandle) {
return React.createElement('span', { className: 'vm-operation-actions vm-operation-actions--empty' }, '—');
}
return React.createElement(
'div',
{
className: 'vm-row-actions vm-operation-actions',
className: 'vm-row-actions vm-operation-actions vpr-main-action-cell',
onClick: function (e) {
e.stopPropagation();
},
},
React.createElement(
'button',
{
type: 'button',
className: 'vm-op-action',
'aria-label': '办理',
onClick: function (e) {
e.stopPropagation();
goHandle(record);
},
},
React.createElement(ClipboardCheck, {
className: 'vm-op-action__icon',
'aria-hidden': true,
strokeWidth: 1.75,
renderPrimaryOpButton('查看', Eye, function () {
goContractView(record);
}),
React.createElement('span', { className: 'vm-op-action__label' }, '办理'),
),
showHandle
? renderPrimaryOpButton('办理', ClipboardCheck, function () {
goHandle(record);
})
: null,
);
}
@@ -1194,7 +1216,8 @@ const Component = function (props) {
React.createElement('th', { style: { width: 88 } }, '单据状态'),
React.createElement('th', { style: { width: 108 } }, '交付时间'),
React.createElement('th', { style: { width: 120 } }, '交付地点'),
React.createElement('th', { style: { width: 180 } }, '品牌型号'),
React.createElement('th', { style: { width: 160 } }, '品牌型号'),
React.createElement('th', { style: { width: 120 } }, '车牌号'),
React.createElement('th', { style: { width: 64 } }, '数量'),
React.createElement('th', { style: { width: 104 } }, '合计应收'),
React.createElement('th', { style: { width: 104 } }, '合计实收'),
@@ -1207,11 +1230,14 @@ const Component = function (props) {
modelGroupRows.map(function (row) {
var batch = row.batch;
var modelGroup = row.modelGroup;
var sampleVehicle = modelGroup && modelGroup.vehicles.length ? modelGroup.vehicles[0] : null;
var vehicle = row.vehicle;
var sampleVehicle = vehicle || (modelGroup && modelGroup.vehicles.length ? modelGroup.vehicles[0] : null);
var auditStatus = normalizeAuditStatus(batch.auditStatus);
var auditLabel = auditStatus === '已驳回' ? '审批驳回' : (auditStatus || '—');
var batchTotals = getBatchAmountTotals(batch);
var rowKey = (parent.contractCode || '') + '-' + batch.seq + '-' + (modelGroup ? modelGroup.key : 'empty');
var rowKey = (parent.contractCode || '') + '-' + batch.seq + '-'
+ (vehicle ? (vehicle.seq != null ? vehicle.seq : buildVehicleModelKey(vehicle)) : 'empty')
+ '-' + (row.isFirstInModelGroup ? '0' : '1');
var cells = [];
if (row.isFirstInBatch) {
@@ -1265,8 +1291,26 @@ const Component = function (props) {
);
}
if (row.isFirstInModelGroup) {
cells.push(
React.createElement('td', { key: 'vehicleInfo' }, renderModelGroupCell(modelGroup)),
React.createElement(
'td',
{
key: 'vehicleInfo',
rowSpan: row.modelGroupRowSpan,
className: 'vpr-pickup-subtable__model-group-cell',
},
renderModelGroupCell(modelGroup),
),
);
}
cells.push(
React.createElement(
'td',
{ key: 'plateNo', className: 'vpr-pickup-subtable__plate-cell' },
vehicle ? renderPlateNoCell(vehicle, parent) : '—',
),
);
if (row.isFirstInBatch) {
@@ -1361,6 +1405,7 @@ const Component = function (props) {
title: '项目信息',
key: 'projectInfo',
width: colWidths[0],
fixed: 'left',
ellipsis: true,
className: 'lc-project-info-col vpr-col-project',
onHeaderCell: function () {
@@ -1505,12 +1550,13 @@ const Component = function (props) {
title: '操作',
key: 'action',
width: colWidths[11],
fixed: 'right',
className: 'vpr-col-action',
render: renderMainActionCell,
},
];
},
[expandedRowKeys, contractPopoverKey, mainList, tableColumnWidths],
[expandedRowKeys, contractPopoverKey, mainList],
);
var filterItems = [
@@ -1571,6 +1617,25 @@ const Component = function (props) {
},
}),
),
React.createElement(
'label',
{ key: 'plateNo', className: 'vm-filter-field' },
React.createElement('span', null, '车牌号'),
React.createElement(Select, {
value: filters.plateNo,
options: filterOptions.plateNo,
allowClear: true,
placeholder: '请选择或输入车牌号',
showSearch: true,
filterOption: filterOption,
className: 'lc-filter-select',
onChange: function (v) {
setFilters(function (p) {
return Object.assign({}, p, { plateNo: v });
});
},
}),
),
React.createElement(
'label',
{ key: 'businessDept', className: 'vm-filter-field' },
@@ -1580,6 +1645,8 @@ const Component = function (props) {
options: filterOptions.businessDept,
allowClear: true,
placeholder: '请选择业务部门',
showSearch: true,
filterOption: filterOption,
className: 'lc-filter-select',
onChange: function (v) {
setFilters(function (p) {
@@ -1672,7 +1739,7 @@ const Component = function (props) {
{
className: 'vm-kpi-row',
role: 'group',
'aria-label': '提车收款统计',
'aria-label': '提车收款统计',
'data-annotation-id': 'vpr-kpi',
},
PICKUP_KPI_CARDS.map(function (card) {

View File

@@ -17,8 +17,8 @@
"fingerprint": "vpr-filter",
"path": []
},
"aiPrompt": "合同编码、项目名称、客户名称、业务部门、业务负责人筛选。",
"annotationText": "默认首行 4 项;超过 1 行时其余项收入「更多筛选」。支持重置与查询,交互对齐租赁合同列表页。",
"aiPrompt": "合同编码、项目名称、客户名称、车牌号、业务部门、业务负责人筛选。",
"annotationText": "默认首行 4 项:合同编码、项目名称、客户名称、车牌号(可输入模糊搜索);业务部门、业务负责人收入「更多筛选」。",
"hasMarkdown": true,
"color": "#2563eb",
"images": [],
@@ -28,15 +28,15 @@
{
"id": "vpr-kpi",
"index": 2,
"title": "提车收款 KPI",
"title": "提车收款 KPI",
"pageId": "list",
"locator": {
"selectors": ["[data-annotation-id=\"vpr-kpi\"]"],
"fingerprint": "vpr-kpi",
"path": []
},
"aiPrompt": "全部 / 待审批 / 审批中 / 审批通过 / 已驳回统计卡片。",
"annotationText": "点击 KPI 卡片筛选子表提车收款;计数基于当前筛选结果内的明细条数。",
"aiPrompt": "全部 / 未提车 / 部分提车 / 已完成 / 已退款统计卡片。",
"annotationText": "点击 KPI 卡片按合同提车收款闭环进度筛选列表;计数当前筛选结果内的合同条数。",
"hasMarkdown": true,
"color": "#0f766e",
"images": [],
@@ -90,7 +90,7 @@
"path": []
},
"aiPrompt": "主表按合同聚合;展开子表为提车收款单明细。",
"annotationText": "主表操作列外显「办理」;租车数等于已提车数时不显示办理。退款仅在子表操作。展开子表按提车批次展示车辆清单,子表操作外显查看、关联收款记录,合同审批驳回时可退款。",
"annotationText": "主表操作列外显「查看」「办理」:查看可浏览本合同全部已发布与未发布提车应收款;办理仅针对尚未操作的车辆。租车数等于已提车数时不显示办理。退款仅在子表操作。",
"hasMarkdown": true,
"color": "#32a06e",
"images": [],
@@ -107,8 +107,8 @@
"fingerprint": "vpr-collect-topbar",
"path": []
},
"aiPrompt": "办理提车收款单的返回与标题区。",
"annotationText": "返回列表;展示合同编码、项目名称;有未保存修改时显示「未保存」徽章。",
"aiPrompt": "办理页顶栏,仅返回列表。",
"annotationText": "仅保留「返回列表」;与表单页一致。未保存修改时底栏显示「未提交」。",
"hasMarkdown": true,
"color": "#334155",
"images": [],
@@ -118,15 +118,15 @@
{
"id": "vpr-collect-project",
"index": 7,
"title": "项目信息",
"title": "合同信息",
"pageId": "collect",
"locator": {
"selectors": ["[data-annotation-id=\"vpr-collect-project\"]"],
"fingerprint": "vpr-collect-project",
"path": []
},
"aiPrompt": "合同与项目只读信息卡片。",
"annotationText": "从办理上下文带出合同编码、客户、付款方式/周期、业务部门等;默认展开,可收起。",
"aiPrompt": "合同只读信息卡片,字段与租赁合同模块关联。",
"annotationText": "参照车辆资产详情页 hero客户名称主标题合同编码与项目名称右侧签约公司、业务部门·负责人、合同类型下方 stats 展示付款方式周期、氢费等。",
"hasMarkdown": true,
"color": "#64748b",
"images": [],
@@ -136,15 +136,15 @@
{
"id": "vpr-collect-form",
"index": 8,
"title": "提车日期与计费规则",
"title": "提车应收款表单",
"pageId": "collect",
"locator": {
"selectors": ["[data-annotation-id=\"vpr-collect-form\"]"],
"fingerprint": "vpr-collect-form",
"path": []
},
"aiPrompt": "提车日期、计费规则说明与应收付款节点。",
"annotationText": "提车日期必填,变更后重算各车应收;展示提车前 2 日内应付与当月 25 日应付节点摘要。",
"aiPrompt": "交付信息、计费规则与租赁订单明细表。",
"annotationText": "交付信息提示「根据交付时间,自动计算提车应收款金额」;明细表含租赁车辆数、未交车辆数及租金/保证金/服务项/应收总额列。",
"hasMarkdown": true,
"color": "#2563eb",
"images": [],
@@ -161,8 +161,8 @@
"fingerprint": "vpr-collect-kpi",
"path": []
},
"aiPrompt": "应收款总额、提车前应付、应开票总额。",
"annotationText": "基于已勾选车辆汇总;前两项卡片可点击查看明细 Popover。",
"aiPrompt": "已选车辆数、应收/租金/保证金/服务项总额。",
"annotationText": "五列 vm-detail-stats已选车辆数、应收总额、租金总额、保证金总额、服务项总额填写交车数与交付时间后联动更新。",
"hasMarkdown": true,
"color": "#0f766e",
"images": [],
@@ -179,8 +179,8 @@
"fingerprint": "vpr-collect-table",
"path": []
},
"aiPrompt": "勾选车辆、应收/实收金额与氢费预付款。",
"annotationText": "点击行切换勾选;已完成车辆不可再选。支持全选/取消全选;实收由收款记录导入反写。",
"aiPrompt": "租赁车辆数、未交车辆数、交车数及租金/保证金/服务项/应收总额。",
"annotationText": "按品牌型号汇总;车牌号与列表子表规则一致;未交车辆数可点击填入;金额列按交付时间与本次交车数自动拆分。",
"hasMarkdown": true,
"color": "#32a06e",
"images": [],
@@ -208,17 +208,17 @@
]
},
"markdownMap": {
"vpr-filter": "# 筛选条件\n\n合同编码、项目名称、客户名称、业务部门、业务负责人等。\n\n| 操作 | 行为 |\n|---|---|\n| 查询 | 应用筛选并重算 KPI |\n| 重置 | 清空条件 |\n| 更多筛选 | 展开其余筛选项 |\n\n交互对齐租赁合同列表页。",
"vpr-kpi": "# 提车收款 KPI\n\n全部 / 待审批 / 审批中 / 审批通过 / 已驳回。\n\n点击卡片筛选子表提车收款单明细;计数基于当前筛选结果内的条数。",
"vpr-filter": "# 筛选条件\n\n| 字段 | 说明 |\n|---|---|\n| 合同编码 | 可输入模糊搜索的下拉选择 |\n| 项目名称 | 可输入模糊搜索的下拉选择 |\n| 客户名称 | 可输入模糊搜索的下拉选择 |\n| 车牌号 | 首行第 4 项;可输入模糊搜索,匹配租赁订单或已提车明细中的车牌 |\n| 业务部门 | 「更多筛选」中展示 |\n| 业务负责人 | 「更多筛选」中展示 |\n\n| 操作 | 行为 |\n|---|---|\n| 查询 | 应用筛选并重算 KPI |\n| 重置 | 清空条件 |\n| 更多筛选 | 展开业务部门、业务负责人 |\n\n交互对齐租赁合同列表页。",
"vpr-kpi": "# 提车收款 KPI\n\n| 卡片 | 说明 |\n|---|---|\n| 全部 | 显示全部租赁合同对应的提车应收款信息 |\n| 未提车 | 没有任何车辆完成「提车应收款 + 收款记录关联」闭环 |\n| 部分提车 | 仅部分车辆完成闭环 |\n| 已完成 | 全部车辆均完成闭环 |\n| 已退款 | 合同审批终止且已完成退款与付款记录关联 |\n\n点击卡片筛选主表合同;计数基于当前筛选结果内的合同条数。",
"vpr-col-pickup": "# 收款情况\n\n展示 **租车数** 与 **已提车**,交互对齐租赁合同列表。\n\n| 操作 | 行为 |\n|---|---|\n| 点击 **租车数** | Popover 展示品牌、型号及对应车辆数汇总表 |\n| 点击 **已提车** | 展开/收起本合同提车应收款子表(已提车 ≥ 0 均可点击) |\n\n## 提车应收款生成逻辑\n\n### A. 自动生成\n\n1. 租赁合同 **提交审批** 时立即生成主表任务。\n2. 合同标注 **交付时间** 时,在交付日 **前 2 天** 自动生成应收数据。\n\n### B. 手动办理\n\n未约定交付时间时点击操作列 **办理**,勾选车辆并设置交付信息。\n\n## 计算规则\n\n以 **车辆交付时间** 为依据;当月 ≤15 日与 >15 日应付节点不同。详见目录「收款情况与生成规则」。",
"vpr-toolbar": "# 列表工具栏\n\n| 按钮 | 说明 |\n|---|---|\n| 导出 | 导出当前筛选结果 |\n\n总条数由底部分页展示不在工具栏重复计数。",
"vpr-table": "# 主从嵌套列表\n\n**主表**:按合同聚合,展示收款情况、应收/实收金额等;操作列外显 **办理**(租车数 = 已提车数时不显示)。\n\n**子表**:按 **提车批次 + 品牌型号分组** 展示。\n\n- **序号**:一个序号对应一次提车办理批次\n- **同一批次多辆车**:交付时间、交付地点、数量、合计应收/实收与操作列按批次合并\n- **品牌型号**:同批次内相同品牌型号合并为一行,并在该单元格内列出全部车牌号;多个品牌型号各占一行\n- **子表操作**(外显):查看、关联收款记录;合同 **审批驳回** 时显示退款\n\n**数据来源**:租赁合同提交审核时同步生成主记录;办理提交后追加子表;实收由收款记录导入反写。",
"vpr-collect-topbar": "# 办理页顶栏\n\n- **返回**:回到列表(有未保存修改时二次确认)\n- **标题**:办理提车收款单\n- **副标题**合同编码 · 项目名称\n- **未保存**:表单有改动且未保存时显示徽章",
"vpr-collect-project": "# 项目信息\n\n只读卡片,默认展开:\n\n提车收款单编码、合同编码/类型、项目名称、客户名称、付款方式与周期、合同起止时间、业务部门与负责人。",
"vpr-collect-form": "# 提车日期与计费\n\n- **提车日期**(必填):变更后重算各车应收与 KPI\n- **计费规则**:按合同付款周期自动生成说明文案\n- **应收付款节点**:展示提车前 2 日内应付、当月 25 日应付(如有)\n\n实收金额由「收款记录」导入后反写办理页只录应收。",
"vpr-collect-kpi": "# 办理页 KPI\n\n| 指标 | 说明 |\n|---|---|\n| 应收总额 | 已选车辆应收合计;可点击查看明细 |\n| 提车前应付 | 提车前须到账金额;可点击查看明细 |\n| 应开票总额 | 按实收租金与服务费计 |",
"vpr-collect-table": "# 提车车辆明细\n\n- 勾选列:点击行或复选框切换;已完成车辆标记「已完成」且不可选\n- 工具栏:全选/取消全选;未选时警告提示\n- 列:车牌、应收租金/保证金/服务费、实收、氢费应收等\n- **氢费预付款**:合同氢费支付方式相关只读/录入区\n\n金额展示使用等宽数字与千分位格式。",
"vpr-collect-footer": "# 底栏操作\n\n| 按钮 | 行为 |\n|---|---|\n| 取消 | 返回列表,未保存时确认 |\n| 保存 | 保存草稿,清除「未保存」状态 |\n| 提交审核 | 至少勾选 1 辆车;确认后生成子表单据并抄送财务 |\n\n底栏汇总展示提车前应付与已选车辆数。"
"vpr-table": "# 主从嵌套列表\n\n**主表**:按合同聚合,展示收款情况、应收/实收金额等。\n\n**主表操作列**\n- **查看**:浏览本合同全部 **已发布** 与 **未发布** 提车应收款记录(只读)\n- **办理**:仅对 **尚未操作** 的未发布车辆发起办理;租车数 = 已提车数时不显示\n\n**项目信息列**:可点击进入办理的客户/合同/项目文案使用 **主题色** 链接样式;左侧展开图标同样使用主题色,提示可展开查看子表。\n\n**子表**:按 **提车批次 + 品牌型号分组** 展示。\n\n- **序号**:一个序号对应一次提车办理批次\n- **同一批次多辆车**:交付时间、交付地点、数量、合计应收/实收与操作列按批次合并\n- **品牌型号**格式为「品牌·型号」;同批次内相同品牌型号合并单元格\n- **车牌号**:独立列;合同为「以实际交付为准」时先显示该文案,交车后显示实际车牌;「选择特定车辆」时显示合同车牌\n- **同一品牌型号多辆车**:品牌型号合并,车牌号各占一行\n- **子表操作**(外显):查看、关联收款记录;合同 **审批驳回** 时显示退款\n\n**数据来源**:租赁合同提交审核时同步生成主记录;办理提交后追加子表;实收由收款记录导入反写。",
"vpr-collect-topbar": "# 办理页顶栏\n\n- **返回列表**:回到列表(有未提交修改时二次确认)\n- 与租赁合同等表单页一致,顶栏仅保留返回按钮\n- 合同编码项目名称见下方「合同信息」模块;有未保存修改时在底栏显示「未提交」",
"vpr-collect-project": "# 合同信息\n\n参照车辆资产详情页上方展示(`vm-detail-card` + `vm-detail-hero` + `vm-detail-stats`\n\n- **Hero 左侧**:客户名称(主标题)、合同编码、项目名称\n- **Hero 右侧**:签约公司、业务部门·负责人、合同类型\n- **Stats 行**:签约公司、付款方式、付款周期、氢费支付方式;氢费为「预付款」时追加预付款金额",
"vpr-collect-form": "# 提车应收款\n\n## 交付信息\n\n两列对齐面板地点 + 时间,均必填);提示文案:**根据交付时间,自动计算提车应收款金额**。\n\n## 租赁订单明细表\n\n| 列 | 说明 |\n|---|---|\n| 品牌型号 | 格式「品牌·型号」;同型号多车合并单元格 |\n| 车牌号 | 与列表子表一致:「选择特定车辆」显示合同车牌;「以实际交付为准」显示对应文案 |\n| 未交车辆数 | 尚未办理提车的数量;点击可填入本次交车数 |\n| 本次交车数 | 本次办理数量,不超过未交车辆数 |\n| 租金总额 | 按交付时间与本次交车数计算的租金合计 |\n| 保证金总额 | 按交付时间与本次交车数计算的保证金合计 |\n| 服务项总额 | 按交付时间与本次交车数计算的服务费合计 |\n| 应收总额 | 租金总额 + 保证金总额 + 服务项总额 |\n\n## 计算规则\n\n- 提车日 ≤15 日:提车前 2 日内付保证金+当月剩余租金及服务费;当月 25 日付下周期\n- 提车日 >15 日:提车前 2 日内付保证金+当月剩余+下周期",
"vpr-collect-kpi": "# 办理汇总\n\n五列 `vm-detail-stats` 统计条:\n\n| 指标 | 说明 |\n|---|---|\n| 已选车辆数 | 各品牌型号交车数合计 |\n| 应收总额 | 按交付时间自动计算并求和 |\n| 租金总额 | 提车前 + 当月 25 日节点租金合计 |\n| 保证金总额 | 提车前应付保证金 |\n| 服务项总额 | 提车前 + 当月 25 日节点服务费计 |",
"vpr-collect-table": "# 租赁订单明细\n\n按品牌/型号汇总;车牌号列与列表子表一致(特定车辆显示车牌、以实际交付为准显示对应文案,同型号多车合并品牌型号单元格);含未交车辆数、本次交车数及租金/保证金/服务项/应收总额。",
"vpr-collect-footer": "# 底栏操作\n\n| 按钮 | 行为 |\n|---|---|\n| 取消 | 返回列表,未提交时确认 |\n| 提交生成 | 至少 1 辆车且填写交付时间/地点;确认后生成子表并抄送财务 |"
},
"assetMap": {},
"directory": {
@@ -283,7 +283,7 @@
"type": "markdown",
"id": "vpr-doc-collect-flow",
"title": "办理流程",
"markdown": "## 办理流程\n\n1. 列表点击「办理」进入\n2. 确认项目信息与提车日期\n3. 勾选本次提车车辆(默认预选未完成车辆\n4. 核对 KPI 与明细表应收金额\n5. **保存** 草稿或 **提交审核**\n\n提交生成子表单据,抄送财务;返回列表可看到新子表记录。"
"markdown": "## 办理流程\n\n1. 列表点击 **办理** 进入办理页\n2. 确认顶部 **合同信息**(参照车辆资产详情页样式\n3. 查看 **办理汇总** KPI已选车辆数、应收/租金/保证金/服务项总额)\n4. 在 **提车应收款** 卡片填写交付地点与时间,并设置各品牌型号本次交车数量\n5. 系统自动按提车应收款规则计算应收\n6. **提交生成** 后写入子表并抄送财务"
},
{
"type": "markdown",

View File

@@ -10,7 +10,9 @@ import {
formatDeliveryLocation,
dedupePickupListByContractCode,
pickSeedIdentityFields,
kpiKeyFromPickupProgress,
} from '../pickup-receivable-bridge.js';
import { formatMoneyYuan as formatMoneyYuanCore } from '../../../common/format-number.js';
/**
* 模拟「租赁合同提交 / 交付日前 2 天」自动生成的提车应收款子单
@@ -194,13 +196,17 @@ export const PICKUP_RECEIVABLE_LIST_SEED = [
contractType: '正式合同',
projectName: '嘉兴氢能示范项目',
customerName: '嘉兴某某物流有限公司',
signingCompany: '嘉兴羚牛氢能科技有限公司',
businessDept: '业务1部',
businessPerson: '张经理',
contractEffectiveDate: '2025-01-15',
contractApprovalStatus: '审批通过',
contractStatus: '合同进行中',
paymentCycleMonths: 6,
paymentMethod: '预付',
paymentMethod: 'advance',
hydrogenBearer: '客户',
hydrogenPaymentMethod: 'prepay',
hydrogenPrepay: 5000,
createdFromLeaseSubmit: true,
leaseSubmitAt: '2025-01-08 11:20',
deliveryDate: '2025-03-01',
@@ -245,13 +251,18 @@ export const PICKUP_RECEIVABLE_LIST_SEED = [
contractType: '正式合同',
projectName: '杭州城配租赁项目',
customerName: '杭州某某租赁有限公司',
signingCompany: '上海羚牛氢能科技有限公司',
businessDept: '业务3部',
businessPerson: '王专员',
contractEffectiveDate: '2025-02-10',
contractApprovalStatus: '审批驳回',
contractStatus: '已结束',
pickupRefundCompleted: true,
refundPaymentLinked: true,
paymentCycleMonths: 1,
paymentMethod: '后付',
paymentMethod: 'postpay',
hydrogenBearer: '客户',
hydrogenPaymentMethod: 'month',
createdFromLeaseSubmit: true,
leaseSubmitAt: '2025-02-11 09:30',
allVehiclesReceivableCompleted: false,
@@ -376,16 +387,33 @@ export const PICKUP_RECEIVABLE_LIST_SEED = [
];
export const PICKUP_KPI_CARDS = [
{ key: 'all', title: '全部', desc: '当前筛选范围内全部提车应收款单据' },
{ key: 'generated', title: '已生成', desc: '办理提交后已生成、待财务跟进的提车应收款单据' },
{ key: 'pending', title: '待审批', desc: '已保存但未提交审批的提车应收款单据' },
{ key: 'inReview', title: '审批中', desc: '审批流程进行中的提车应收款单据' },
{ key: 'approved', title: '审批通过', desc: '已完成最终审批的提车应收款单据' },
{ key: 'rejected', title: '已驳回', desc: '审批被驳回、可重新编辑提交的提车应收款单据' },
{
key: 'all',
title: '全部',
desc: '显示全部租赁合同对应的提车应收款信息',
},
{
key: 'notPickedUp',
title: '未提车',
desc: '显示租赁合同中没有完成任何车辆提车应收款并关联收款记录闭环的数据',
},
{
key: 'partialPickedUp',
title: '部分提车',
desc: '显示租赁合同中只完成部分车辆提车应收款并关联收款记录闭环的数据',
},
{
key: 'completed',
title: '已完成',
desc: '显示租赁合同中全部车辆都已完成提车应收款并关联收款记录闭环的数据',
},
{
key: 'refunded',
title: '已退款',
desc: '显示租赁合同已被审批终止,且点击退款并完成付款记录关联的数据',
},
];
import { formatMoneyYuan as formatMoneyYuanCore } from '../../../common/format-number.js';
export function buildCollectCode(contractCode, seq) {
const suffix = `TC${String(seq != null ? seq : '').padStart(4, '0')}`;
return `${String(contractCode || '')}${suffix}`;
@@ -455,6 +483,7 @@ export function buildFilterOptions(list) {
const codes = [];
const projects = [];
const customers = [];
const plateNos = [];
const depts = [];
const persons = [];
(list || []).forEach((r) => {
@@ -463,50 +492,74 @@ export function buildFilterOptions(list) {
if (r.customerName && !customers.includes(r.customerName)) customers.push(r.customerName);
if (r.businessDept && !depts.includes(r.businessDept)) depts.push(r.businessDept);
if (r.businessPerson && !persons.includes(r.businessPerson)) persons.push(r.businessPerson);
(r.leaseOrderVehicles || []).forEach((v) => {
var plate = String(v.plateNo || '').trim();
if (plate && !plateNos.includes(plate)) plateNos.push(plate);
});
(r.children || []).forEach((child) => {
(child.deliveryVehicles || []).forEach((v) => {
var childPlate = String(v.plateNo || '').trim();
if (childPlate && !plateNos.includes(childPlate)) plateNos.push(childPlate);
});
});
});
plateNos.sort();
return {
contractCode: codes.map((v) => ({ value: v, label: v })),
projectName: projects.map((v) => ({ value: v, label: v })),
customerName: customers.map((v) => ({ value: v, label: v })),
plateNo: plateNos.map((v) => ({ value: v, label: v })),
businessDept: depts.map((v) => ({ value: v, label: v })),
businessPerson: persons.map((v) => ({ value: v, label: v })),
};
}
function recordMatchesPlateNo(record, plateNo) {
var target = String(plateNo || '').trim();
if (!target) return true;
var matched = (record.leaseOrderVehicles || []).some(function (v) {
return String(v.plateNo || '').trim() === target;
});
if (matched) return true;
return (record.children || []).some(function (child) {
return (child.deliveryVehicles || []).some(function (v) {
return String(v.plateNo || '').trim() === target;
});
});
}
export function filterMainList(list, filters) {
let rows = (list || []).filter(isValidPickupMainRecord);
if (filters.contractCode) rows = rows.filter((r) => r.contractCode === filters.contractCode);
if (filters.projectName) rows = rows.filter((r) => r.projectName === filters.projectName);
if (filters.customerName) rows = rows.filter((r) => r.customerName === filters.customerName);
if (filters.plateNo) rows = rows.filter((r) => recordMatchesPlateNo(r, filters.plateNo));
if (filters.businessDept) rows = rows.filter((r) => r.businessDept === filters.businessDept);
if (filters.businessPerson) rows = rows.filter((r) => r.businessPerson === filters.businessPerson);
return rows;
}
export function countKpiFromList(list, kpiTab) {
const counts = { all: 0, generated: 0, pending: 0, inReview: 0, approved: 0, rejected: 0 };
(list || []).forEach((parent) => {
(parent.children || []).forEach((child) => {
export function countKpiFromList(list) {
const counts = {
all: 0,
notPickedUp: 0,
partialPickedUp: 0,
completed: 0,
refunded: 0,
};
(list || []).forEach(function (parent) {
counts.all += 1;
const key = kpiKeyFromAuditStatus(child.auditStatus);
if (key) counts[key] += 1;
});
const key = kpiKeyFromPickupProgress(parent);
if (key && counts[key] != null) counts[key] += 1;
});
return counts;
}
export function filterByKpiTab(list, kpiTab) {
if (!kpiTab || kpiTab === 'all') return list;
return (list || [])
.map((parent) => {
const children = (parent.children || []).filter((child) => {
const key = kpiKeyFromAuditStatus(child.auditStatus);
return key === kpiTab;
return (list || []).filter(function (parent) {
return kpiKeyFromPickupProgress(parent) === kpiTab;
});
if (!children.length) return null;
return { ...parent, children };
})
.filter(Boolean);
}
function mergeSeedWithStored(seed, stored) {

View File

@@ -8,6 +8,11 @@ import {
buildBatchPickupScheduleSummary,
describePickupRule,
} from './pickup-receivable-calc.js';
import {
formatHydrogenPaymentLabel,
formatPaymentMethodLabel,
formatPaymentPeriodLabel,
} from '../lease-contract-management/lease-contract-list-data.js';
export {
buildVehiclePickupSchedule,
@@ -85,6 +90,10 @@ export function pickSeedIdentityFields(seedRow) {
contractStatus: seedRow.contractStatus,
paymentCycleMonths: seedRow.paymentCycleMonths,
paymentMethod: seedRow.paymentMethod,
signingCompany: seedRow.signingCompany,
hydrogenBearer: seedRow.hydrogenBearer,
hydrogenPaymentMethod: seedRow.hydrogenPaymentMethod,
hydrogenPrepay: seedRow.hydrogenPrepay,
deliveryDate: seedRow.deliveryDate,
deliveryDateTbd: seedRow.deliveryDateTbd,
deliveryRegion: seedRow.deliveryRegion,
@@ -327,6 +336,61 @@ export function countPickedUpVehicles(record) {
return collectPickedVehicleKeys(record.children).size;
}
/** 子单是否已完成收款闭环(实收 ≥ 应收) */
export function isBatchPaymentClosed(child) {
var status = String(child && child.auditStatus || '');
if (status === '已驳回' || status === '审批驳回') return false;
var received = parseFloat(
child && child.financeReceived != null ? child.financeReceived : child && child.actualTotal,
) || 0;
var receivable = parseFloat(child && child.receivableTotal) || 0;
if (receivable <= 0) return false;
return received + 0.009 >= receivable;
}
/** 已完成提车应收款并关联收款记录闭环的车辆 */
export function collectClosedLoopVehicleKeys(children) {
var keys = new Set();
(children || []).forEach(function (child) {
if (!isBatchPaymentClosed(child)) return;
(child.deliveryVehicles || []).forEach(function (v) {
keys.add(vehicleMatchKey(v));
});
});
return keys;
}
export function countClosedLoopVehicles(record) {
return collectClosedLoopVehicleKeys(record && record.children).size;
}
export function isContractApprovalTerminated(record) {
var approval = String(record && record.contractApprovalStatus || record && record.leaseApprovalStatus || '').trim();
var status = String(record && record.contractStatus || '').trim();
return approval === '审批驳回'
|| approval === '已驳回'
|| approval === '审批终止'
|| status === '已终止'
|| status === '已结束';
}
export function isPickupRefundCompleted(record) {
return Boolean(record && (record.pickupRefundCompleted || record.refundPaymentLinked));
}
/** 列表 KPI按合同提车与收款闭环进度分类 */
export function kpiKeyFromPickupProgress(record) {
if (isContractApprovalTerminated(record) && isPickupRefundCompleted(record)) {
return 'refunded';
}
var total = record.contractVehicleCount || (record.leaseOrderVehicles || []).length;
if (!total) return 'notPickedUp';
var closed = countClosedLoopVehicles(record);
if (closed <= 0) return 'notPickedUp';
if (closed >= total) return 'completed';
return 'partialPickedUp';
}
export function calcReceivedTotal(children) {
return (children || []).reduce(function (sum, child) {
var status = String(child.auditStatus || '');
@@ -727,6 +791,363 @@ export function toCollectPageVehicles(record, pickupDate) {
});
}
export function buildVehicleModelKey(vehicle) {
if (!vehicle) return '';
return [String(vehicle.brand || '').trim(), String(vehicle.model || '').trim()]
.filter(Boolean)
.join(' ');
}
export var PLATE_ACTUAL_DELIVERY_LABEL = '以实际交付为准';
export function isLeaseSpecificVehicle(leaseVehicle) {
if (!leaseVehicle) return false;
if (leaseVehicle.plateMode === 'specific') return true;
if (leaseVehicle.plateMode === 'actual') return false;
var leasePlate = String(leaseVehicle.plateNo || '').trim();
return Boolean(leasePlate && leasePlate !== PLATE_ACTUAL_DELIVERY_LABEL && leasePlate !== '-');
}
/** 办理页表格:按租赁合同规则解析车牌展示文案 */
export function resolveCollectPlateLabel(leaseVehicle) {
if (!leaseVehicle) return '—';
if (isLeaseSpecificVehicle(leaseVehicle)) {
var contractPlate = String(leaseVehicle.plateNo || '').trim();
return contractPlate || '—';
}
return PLATE_ACTUAL_DELIVERY_LABEL;
}
export function buildCollectOrderTableRows(orderLines) {
var rows = [];
(orderLines || []).forEach(function (line) {
var displayVehicles = (line.leaseVehicles && line.leaseVehicles.length)
? line.leaseVehicles
: (line.vehicles && line.vehicles.length)
? line.vehicles
: [{ seq: 0, brand: line.brand, model: line.model, plateNo: '' }];
var rowSpan = displayVehicles.length;
displayVehicles.forEach(function (vehicle, index) {
rows.push({
line: line,
vehicle: vehicle,
rowKey: line.lineKey + '::' + (vehicle.seq != null ? vehicle.seq : index),
isFirstInModelGroup: index === 0,
modelGroupRowSpan: rowSpan,
});
});
});
return rows;
}
export function buildLeaseOrderLinesForCollect(record) {
var enriched = enrichPickupRecord(Object.assign({}, record));
var pending = getPendingPickupVehicles(enriched);
var totalCountMap = {};
var leaseVehiclesByKey = {};
(enriched.leaseOrderVehicles || []).forEach(function (vehicle, index) {
var totalKey = buildVehicleModelKey(vehicle) || '__line-' + index;
totalCountMap[totalKey] = (totalCountMap[totalKey] || 0) + 1;
if (!leaseVehiclesByKey[totalKey]) leaseVehiclesByKey[totalKey] = [];
leaseVehiclesByKey[totalKey].push(vehicle);
});
var groupMap = {};
var order = [];
pending.forEach(function (vehicle, index) {
var key = buildVehicleModelKey(vehicle) || '__line-' + index;
if (!groupMap[key]) {
groupMap[key] = {
lineKey: key,
brand: vehicle.brand || '',
model: vehicle.model || '',
monthRent: vehicle.monthRent || 0,
deposit: vehicle.deposit || 0,
serviceFeeTotal: vehicle.serviceFeeTotal || 0,
vehicles: [],
leaseVehicles: leaseVehiclesByKey[key] || [],
leasedCount: totalCountMap[key] || 0,
availableCount: 0,
};
order.push(key);
}
groupMap[key].vehicles.push(vehicle);
groupMap[key].availableCount += 1;
});
return order.map(function (key) {
var line = groupMap[key];
if (!line.leasedCount) line.leasedCount = line.availableCount;
return line;
});
}
function normalizePaymentMethodCode(method) {
var text = String(method || '').trim();
if (text === '预付' || text === '先付后用') return 'advance';
if (text === '后付' || text === '先用后付') return 'postpay';
return text;
}
function normalizeHydrogenPaymentCode(method) {
var text = String(method || '').trim();
if (text === '预付' || text === '预付款') return 'prepay';
if (text === '月付款' || text === '按月结算') return 'month';
if (text === '自行结算' || text === '自行解决') return 'self';
return text;
}
function isHydrogenPrepayMethod(method) {
var code = normalizeHydrogenPaymentCode(method);
return code === 'prepay';
}
/** 氢费支付方式展示(与租赁合同选项对齐) */
export function resolveHydrogenPaymentLabel(record) {
var code = normalizeHydrogenPaymentCode(record && record.hydrogenPaymentMethod);
if (code) return formatHydrogenPaymentLabel(code);
var bearer = String((record && record.hydrogenBearer) || '').trim();
if (bearer === '我方') return '我方承担';
return '—';
}
/** 氢气预付款金额;仅氢费支付方式为「预付款」时有值 */
export function resolveHydrogenPrepayAmount(record) {
if (!isHydrogenPrepayMethod(record && record.hydrogenPaymentMethod)) return null;
var raw = record.hydrogenPrepay != null ? record.hydrogenPrepay : record.hydrogenPrepayAmount;
if (raw == null || raw === '') return null;
var amount = parseFloat(raw);
return Number.isFinite(amount) ? amount : null;
}
function applyLeaseContractFieldDefaults(record) {
var next = Object.assign({}, record);
if (!next.signingCompany) next.signingCompany = '嘉兴羚牛氢能科技有限公司';
if (!next.hydrogenBearer) next.hydrogenBearer = '客户';
next.paymentMethod = normalizePaymentMethodCode(next.paymentMethod || 'advance');
next.hydrogenPaymentMethod = normalizeHydrogenPaymentCode(
next.hydrogenPaymentMethod || (next.hydrogenBearer === '客户' ? 'prepay' : ''),
);
if (next.hydrogenPrepay == null && isHydrogenPrepayMethod(next.hydrogenPaymentMethod)) {
next.hydrogenPrepay = 5000;
}
return next;
}
/** 列表「办理」页上下文:合同主信息 + 附件1 租赁订单可办明细 */
export function buildCollectHandleContext(record) {
var enriched = applyLeaseContractFieldDefaults(enrichPickupRecord(Object.assign({}, record)));
var months = enriched.paymentCycleMonths || 1;
var paymentMethodCode = normalizePaymentMethodCode(enriched.paymentMethod);
var hydrogenMethodCode = normalizeHydrogenPaymentCode(enriched.hydrogenPaymentMethod);
return {
contractCode: enriched.contractCode,
contractType: enriched.contractType || '正式合同',
projectName: enriched.projectName,
customerName: enriched.customerName,
signingCompany: enriched.signingCompany || '',
paymentMethod: paymentMethodCode,
paymentMethodLabel: formatPaymentMethodLabel(paymentMethodCode),
paymentCycleMonths: months,
paymentCycle: formatPaymentPeriodLabel(months),
paymentCycleLabel: formatPaymentPeriodLabel(months),
hydrogenBearer: enriched.hydrogenBearer || '',
hydrogenPaymentMethod: hydrogenMethodCode,
hydrogenPaymentMethodLabel: formatHydrogenPaymentLabel(hydrogenMethodCode),
hydrogenPrepayAmount: resolveHydrogenPrepayAmount(enriched),
contractStart: enriched.contractEffectiveDate || '',
contractEnd: enriched.contractEndDate || '',
businessDept: enriched.businessDept || '',
businessPerson: enriched.businessPerson || '',
contractStatus: enriched.contractStatus || '',
contractApprovalStatus: enriched.contractApprovalStatus || '',
defaultDeliveryLocation: formatDeliveryLocation(enriched),
defaultPickupDate: enriched.lastPickupDate || enriched.deliveryDate || '',
leaseOrderLines: buildLeaseOrderLinesForCollect(enriched),
};
}
function vehicleScheduleParts(schedule) {
if (!schedule) return { rent: 0, deposit: 0, service: 0, total: 0 };
var before = schedule.items && schedule.items[0] ? schedule.items[0] : {};
var month25 = schedule.items && schedule.items[1] ? schedule.items[1] : {};
var rent = (before.rent || 0) + (month25.rent || 0);
var service = (before.service || 0) + (month25.service || 0);
var deposit = before.deposit || 0;
return {
rent: rent,
deposit: deposit,
service: service,
total: schedule.totalReceivable || rent + deposit + service,
};
}
export function buildCollectLinePreview(line, quantity, pickupDate, paymentCycleMonths) {
var qty = Math.min(line.availableCount, Math.max(0, parseInt(quantity, 10) || 0));
if (!qty || !pickupDate) {
return {
quantity: qty,
unitTotal: 0,
lineTotal: 0,
rentTotal: 0,
depositTotal: 0,
serviceTotal: 0,
schedule: null,
};
}
var schedule = buildVehiclePickupSchedule({
monthRent: line.monthRent,
monthServiceFee: line.serviceFeeTotal,
deposit: line.deposit,
pickupDate: pickupDate,
paymentCycleMonths: paymentCycleMonths,
});
var parts = vehicleScheduleParts(schedule);
return {
quantity: qty,
unitTotal: parts.total,
lineTotal: parts.total * qty,
rentTotal: parts.rent * qty,
depositTotal: parts.deposit * qty,
serviceTotal: parts.service * qty,
schedule: schedule,
};
}
export function buildCollectSubmitPayload(handleContext, form) {
var months = handleContext.paymentCycleMonths || 1;
var pickupDate = form.deliveryDate;
var deliveryLocation = form.deliveryLocation;
var deliveryVehicles = [];
var calcRows = [];
(handleContext.leaseOrderLines || []).forEach(function (line) {
var qty = Math.min(
line.availableCount,
Math.max(0, parseInt(form.lineQuantities[line.lineKey], 10) || 0),
);
line.vehicles.slice(0, qty).forEach(function (vehicle) {
var schedule = buildVehiclePickupSchedule({
monthRent: line.monthRent,
monthServiceFee: line.serviceFeeTotal,
deposit: line.deposit,
pickupDate: pickupDate,
paymentCycleMonths: months,
});
var parts = vehicleScheduleParts(schedule);
var nextVehicle = Object.assign({}, vehicle, {
deliveryTime: pickupDate,
deliveryLocation: deliveryLocation,
receivableSchedule: schedule,
receivableTotal: parts.total,
receivableRent: parts.rent,
receivableDeposit: parts.deposit,
receivableService: parts.service,
receivedTotal: 0,
});
deliveryVehicles.push(nextVehicle);
calcRows.push({
selected: true,
receivableCompleted: false,
monthRent: line.monthRent,
serviceFeeTotal: line.serviceFeeTotal,
deposit: line.deposit,
receivableDeposit: line.deposit,
receivableService: line.serviceFeeTotal,
receivableSchedule: schedule,
});
});
});
var batchSchedule = buildBatchPickupScheduleSummary(calcRows, pickupDate, months);
var breakdown = breakdownFromBatchSummary(batchSchedule);
return {
pickupDate: pickupDate,
deliveryLocation: deliveryLocation,
deliveryVehicles: deliveryVehicles,
receivableSchedule: batchSchedule,
receivableRent: breakdown.rent,
receivableDeposit: breakdown.deposit,
receivableService: breakdown.service,
receivableTotal: batchSchedule.totalReceivable,
actualTotal: 0,
receivedRent: 0,
receivedDeposit: 0,
receivedService: 0,
creator: '当前用户',
};
}
export function getPendingPickupVehicles(record) {
if (!record) return [];
var pickedKeys = collectPickedVehicleKeys(record.children);
return (record.leaseOrderVehicles || []).filter(function (vehicle) {
return !pickedKeys.has(vehicleMatchKey(vehicle));
});
}
function buildPickupCollectCode(contractCode, seq) {
return String(contractCode || '') + 'TC' + String(seq != null ? seq : '').padStart(4, '0');
}
function summarizePublishedPickupRecord(child, contractCode) {
if (!child) return null;
var receivableTotal = parseFloat(child.receivableTotal);
if (Number.isNaN(receivableTotal)) {
receivableTotal = (child.deliveryVehicles || []).reduce(function (sum, vehicle) {
return sum + (parseFloat(vehicle.receivableTotal) || 0);
}, 0);
}
var receivedTotal = parseFloat(
child.financeReceived != null ? child.financeReceived : child.actualTotal,
) || 0;
return {
seq: child.seq,
collectCode: buildPickupCollectCode(contractCode, child.seq),
auditStatus: child.auditStatus || '已生成',
publishStatus: '已发布',
pickupDate: child.pickupDate || '—',
deliveryLocation: child.deliveryLocation || '—',
vehicleCount: (child.deliveryVehicles || []).length || child.deliveryCount || 0,
receivableTotal: receivableTotal,
receivedTotal: receivedTotal,
chargeTime: child.chargeTime || '—',
creator: child.creator || '—',
};
}
/** 列表「查看」页上下文:已发布子单 + 未发布待办理车辆 */
export function buildContractViewContext(record) {
var enriched = enrichPickupRecord(Object.assign({}, record));
return {
mode: 'contract',
contractCode: enriched.contractCode,
contractType: enriched.contractType,
projectName: enriched.projectName,
customerName: enriched.customerName,
paymentMethod: enriched.paymentMethod,
paymentCycleMonths: enriched.paymentCycleMonths,
paymentCycle: (enriched.paymentCycleMonths || 1) + '个月',
contractStart: enriched.contractEffectiveDate,
contractEnd: enriched.contractEndDate || '',
businessDept: enriched.businessDept,
businessPerson: enriched.businessPerson,
contractVehicleCount: enriched.contractVehicleCount,
pickedUpVehicleCount: enriched.pickedUpVehicleCount,
publishedRecords: (enriched.children || []).map(function (child) {
return summarizePublishedPickupRecord(child, enriched.contractCode);
}).filter(Boolean),
pendingVehicles: getPendingPickupVehicles(enriched).map(function (vehicle, index) {
return {
seq: vehicle.seq != null ? vehicle.seq : index + 1,
brand: vehicle.brand,
model: vehicle.model,
plateNo: vehicle.plateNo || '—',
publishStatus: '未发布',
};
}),
};
}
export function applyPickupDateToVehicles(vehicles, pickupDate, paymentCycleMonths) {
return (vehicles || []).map(function (row) {
if (row.receivableCompleted) return row;

File diff suppressed because it is too large Load Diff

View File

@@ -100,6 +100,9 @@ export default defineConfig(({ command }) => {
optimizeDeps: {
include: [
'lucide-react',
'mammoth',
'docx-preview',
'jszip',
],
},