Compare commits
No commits in common. "e761558307c9357a6d08a41ef35ae725ee8a5ef7" and "d3695c8131a734d2ae18256d76c6b3e590b27dda" have entirely different histories.
e761558307
...
d3695c8131
@ -1,2 +0,0 @@
|
|||||||
JGJSZWó¦#ďWgťtK_ł…çGşŚ<C59F>±Ů;ÚćwëfÂ)ĺA»·u:퇼!ĺP÷YĽâ2Z[B[ßş ĐĺÂmF’ó¤Ďé«Sśs;˙eQóqdÚđú˘;gÖĆfí}DFÚđć`Ë˙ń4ĹĎŃĽ–‘ I{íÁKş¶Ź1źÍN˙źrŰ|Á[jČ„ů<>(YĹX®†ĎĚ9?š2ĘH×ä˙v^ł0“ídL‡Č6gNvˇÁ˛ÉZ~?™ámq^#ĘŽI†m‚,HŐî˘ŔÁu˛›?[ÓÚ.ż şPďß5<C39F>¶ÍBÚůŚ›Ď×ďđŠ‘M8Ö¬PG.d‰<64>ä®čý„îgP>flŁ<6C>:<0C>˝+qűl†
|
|
||||||
Ŕ:˝’<CB9D>Ő #Túź®ŇZ<C587>8]®ŕXň°(č(÷<>WV±ŮÖgI\?›ŰŁsQ
…k<11> Xô6é´¦?×<>i+\p cLÔX;˛žŁäŇąňw,Áď§Ăýčq2$ňßÄ…č×<^“„ÓďA´ “ŁŢSc…\>rpŐŽ-óA®ýüŚp7FÂ@8jco‹Ę ’HčîÚ±Ďa«9ł‚— ©ŹŐ5<C590>uŹŠ^dÜČa-̡.%Ł˙»Nü~ťÎ´IÝłt¤îŤI„VnpI°ikcđJ>vMŘá˘p<ŕčuć•ĂpÔ„*WwčNŁă‡U
<14>O]Ůźaý<61>~8ČTm n™JJ°Ĺa”łĚßů´\Á™Ž‹ł™Ý
żwÄS!Ç“Ożď2śč(pźíŔmZ
|
|
||||||
Binary file not shown.
2
.gitignore
vendored
2
.gitignore
vendored
@ -11,7 +11,7 @@ node_modules
|
|||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
*.exe
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
.vscode/*
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
|
|||||||
2
.serena/.gitignore
vendored
2
.serena/.gitignore
vendored
@ -1,2 +0,0 @@
|
|||||||
/cache
|
|
||||||
/project.local.yml
|
|
||||||
@ -1,136 +0,0 @@
|
|||||||
# the name by which the project can be referenced within Serena
|
|
||||||
project_name: "JGJS2026"
|
|
||||||
|
|
||||||
|
|
||||||
# list of languages for which language servers are started; choose from:
|
|
||||||
# al bash clojure cpp csharp
|
|
||||||
# csharp_omnisharp dart elixir elm erlang
|
|
||||||
# fortran fsharp go groovy haskell
|
|
||||||
# java julia kotlin lua markdown
|
|
||||||
# matlab nix pascal perl php
|
|
||||||
# php_phpactor powershell python python_jedi r
|
|
||||||
# rego ruby ruby_solargraph rust scala
|
|
||||||
# swift terraform toml typescript typescript_vts
|
|
||||||
# vue yaml zig
|
|
||||||
# (This list may be outdated. For the current list, see values of Language enum here:
|
|
||||||
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py
|
|
||||||
# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
|
|
||||||
# Note:
|
|
||||||
# - For C, use cpp
|
|
||||||
# - For JavaScript, use typescript
|
|
||||||
# - For Free Pascal/Lazarus, use pascal
|
|
||||||
# Special requirements:
|
|
||||||
# Some languages require additional setup/installations.
|
|
||||||
# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers
|
|
||||||
# When using multiple languages, the first language server that supports a given file will be used for that file.
|
|
||||||
# The first language is the default language and the respective language server will be used as a fallback.
|
|
||||||
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
|
|
||||||
languages:
|
|
||||||
- vue
|
|
||||||
- typescript
|
|
||||||
|
|
||||||
# the encoding used by text files in the project
|
|
||||||
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
|
|
||||||
encoding: "utf-8"
|
|
||||||
|
|
||||||
# line ending convention to use when writing source files.
|
|
||||||
# Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default)
|
|
||||||
# This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings.
|
|
||||||
line_ending:
|
|
||||||
|
|
||||||
# The language backend to use for this project.
|
|
||||||
# If not set, the global setting from serena_config.yml is used.
|
|
||||||
# Valid values: LSP, JetBrains
|
|
||||||
# Note: the backend is fixed at startup. If a project with a different backend
|
|
||||||
# is activated post-init, an error will be returned.
|
|
||||||
language_backend:
|
|
||||||
|
|
||||||
# whether to use project's .gitignore files to ignore files
|
|
||||||
ignore_all_files_in_gitignore: true
|
|
||||||
|
|
||||||
# list of additional paths to ignore in this project.
|
|
||||||
# Same syntax as gitignore, so you can use * and **.
|
|
||||||
# Note: global ignored_paths from serena_config.yml are also applied additively.
|
|
||||||
ignored_paths: []
|
|
||||||
|
|
||||||
# whether the project is in read-only mode
|
|
||||||
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
|
|
||||||
# Added on 2025-04-18
|
|
||||||
read_only: false
|
|
||||||
|
|
||||||
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
|
|
||||||
# Below is the complete list of tools for convenience.
|
|
||||||
# To make sure you have the latest list of tools, and to view their descriptions,
|
|
||||||
# execute `uv run scripts/print_tool_overview.py`.
|
|
||||||
#
|
|
||||||
# * `activate_project`: Activates a project by name.
|
|
||||||
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
|
|
||||||
# * `create_text_file`: Creates/overwrites a file in the project directory.
|
|
||||||
# * `delete_lines`: Deletes a range of lines within a file.
|
|
||||||
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
|
|
||||||
# * `execute_shell_command`: Executes a shell command.
|
|
||||||
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
|
|
||||||
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
|
|
||||||
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
|
|
||||||
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
|
|
||||||
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
|
|
||||||
# * `initial_instructions`: Gets the initial instructions for the current project.
|
|
||||||
# Should only be used in settings where the system prompt cannot be set,
|
|
||||||
# e.g. in clients you have no control over, like Claude Desktop.
|
|
||||||
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
|
|
||||||
# * `insert_at_line`: Inserts content at a given line in a file.
|
|
||||||
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
|
|
||||||
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
|
|
||||||
# * `list_memories`: Lists memories in Serena's project-specific memory store.
|
|
||||||
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
|
|
||||||
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
|
|
||||||
# * `read_file`: Reads a file within the project directory.
|
|
||||||
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
|
|
||||||
# * `remove_project`: Removes a project from the Serena configuration.
|
|
||||||
# * `replace_lines`: Replaces a range of lines within a file with new content.
|
|
||||||
# * `replace_symbol_body`: Replaces the full definition of a symbol.
|
|
||||||
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
|
|
||||||
# * `search_for_pattern`: Performs a search for a pattern in the project.
|
|
||||||
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
|
|
||||||
# * `switch_modes`: Activates modes by providing a list of their names
|
|
||||||
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
|
|
||||||
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
|
|
||||||
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
|
|
||||||
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
|
|
||||||
excluded_tools: []
|
|
||||||
|
|
||||||
# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default)
|
|
||||||
included_optional_tools: []
|
|
||||||
|
|
||||||
# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
|
|
||||||
# This cannot be combined with non-empty excluded_tools or included_optional_tools.
|
|
||||||
fixed_tools: []
|
|
||||||
|
|
||||||
# list of mode names to that are always to be included in the set of active modes
|
|
||||||
# The full set of modes to be activated is base_modes + default_modes.
|
|
||||||
# If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply.
|
|
||||||
# Otherwise, this setting overrides the global configuration.
|
|
||||||
# Set this to [] to disable base modes for this project.
|
|
||||||
# Set this to a list of mode names to always include the respective modes for this project.
|
|
||||||
base_modes:
|
|
||||||
|
|
||||||
# list of mode names that are to be activated by default.
|
|
||||||
# The full set of modes to be activated is base_modes + default_modes.
|
|
||||||
# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply.
|
|
||||||
# Otherwise, this overrides the setting from the global configuration (serena_config.yml).
|
|
||||||
# This setting can, in turn, be overridden by CLI parameters (--mode).
|
|
||||||
default_modes:
|
|
||||||
|
|
||||||
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
|
||||||
# (contrary to the memories, which are loaded on demand).
|
|
||||||
initial_prompt: ""
|
|
||||||
|
|
||||||
# time budget (seconds) per tool call for the retrieval of additional symbol information
|
|
||||||
# such as docstrings or parameter information.
|
|
||||||
# This overrides the corresponding setting in the global configuration; see the documentation there.
|
|
||||||
# If null or missing, use the setting from the global configuration.
|
|
||||||
symbol_info_budget:
|
|
||||||
|
|
||||||
# list of regex patterns which, when matched, mark a memory entry as read‑only.
|
|
||||||
# Extends the list from the global configuration, merging the two lists.
|
|
||||||
read_only_memory_patterns: []
|
|
||||||
406
AGENTS.md
406
AGENTS.md
@ -1,387 +1,19 @@
|
|||||||
# AGENTS.md — Codex 工作操作手册
|
# AGENTS.md — Encoding & Chinese Safety Rules
|
||||||
|
|
||||||
本文件面向 Codex AI,定义其职责与操作规范。
|
## Absolute rules (must follow)
|
||||||
|
1. Never corrupt non-ASCII text (Chinese, emoji, etc.). Preserve exact Unicode characters.
|
||||||
## 0. 角色定位与职责边界
|
2. NEVER rewrite entire files when only small edits are needed. Always apply minimal diffs/patches.
|
||||||
|
3. If a file contains Chinese characters, do not “normalize”, “escape”, “re-encode”, or “replace” them.
|
||||||
| instruction | notes |
|
4. When reading/writing files via scripts/tools, always use UTF-8 explicitly (no platform default encoding).
|
||||||
| --- | --- |
|
|
||||||
| Codex 负责任务规划、代码编写、文档生成、上下文收集、测试验证、质量审查等全流程 | 保持全栈能力 |
|
## Windows / PowerShell rules
|
||||||
| 职责范围:需求分析、技术方案设计、任务规划、代码实现、测试执行、质量验证、文档编写、工具使用、深度推理分析 | 承担完整开发生命周期 |
|
- If you need to run PowerShell, force UTF-8 output/input:
|
||||||
| 工作模式:接收用户指令 → 深度思考(sequential-thinking) → 规划任务(shrimp-task-manager) → 执行实现 → 自我验证 → 交付成果 | 自主闭环流程 |
|
- Use: `[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()`
|
||||||
| 决策权:自主决策技术方案、实现路径、质量标准,仅在真正需要用户输入时才询问 | 最大化自主性 |
|
- Prefer `Set-Content -Encoding utf8` / `Out-File -Encoding utf8`
|
||||||
| 工具使用:所有工具可用(Read、Edit、Write、Bash、Grep、Glob、sequential-thinking、shrimp-task-manager、exa、code-index等),无使用限制 | 完整工具访问权限 |
|
- Avoid commands that may round-trip through ANSI/CP936/CP1252 without explicit encoding.
|
||||||
| 核心约束:标准化生态复用置于首位,禁止任何额外自研组件,所有安全性设计必须立刻删除并拒绝新增 | 强制执行 |
|
|
||||||
|
## Workflow
|
||||||
## 1. 工具能力总览
|
- Before editing: inspect the target lines only.
|
||||||
|
- Apply changes as a patch (line-level edits), not full-file regeneration.
|
||||||
### 1.1 内置工具
|
- After editing: verify the edited lines still show correct Chinese.
|
||||||
|
- If uncertain: stop and ask rather than guessing and corrupting text.
|
||||||
| 工具 | 作用 | 启用/审批要点 | 参考 |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| shell / local_shell | 在沙箱内执行命令,遵循 approval policy 控制交互 | 默认启用,按配置执行审批策略 | [1] |
|
|
||||||
| apply_patch | 以补丁方式批量编辑文件,保持 diff 清晰可审计 | 按补丁语法编辑后自查,必要时配合 `git diff` | [1][2] |
|
|
||||||
| **Serena** | 核心逻辑增强与指令解析工具。用于处理复杂架构决策与高级推理。 | **全时段可用**。遇到逻辑瓶颈或多方案抉择时必须使用。 ||
|
|
||||||
| **Context7** | 依赖包专家工具。用于获取项目依赖的官方说明、API 约束与最佳实践。 | **新项目启动或者有新依赖强制使用**。用于初始化依赖知识库。 ||
|
|
||||||
|
|
||||||
| **playwright** | 浏览器自动控制,自动化测试 |||
|
|
||||||
|
|
||||||
|
|
||||||
| update_plan(plan tool) | 维护任务拆解与状态,辅助复杂场景规划 | 视配置决定是否包含,使用时保持计划与实际同步 | [3] |
|
|
||||||
| unified_exec | 提供 PTY 会话运行交互式命令 | 仅在 `experimental_unified_exec_tool` 开启时使用 | [3] |
|
|
||||||
| view_image | 获取界面截图或渲染图像供分析 | 需在配置中启用 `tools.view_image` | [3][4] |
|
|
||||||
| web_search_request | 发起在线检索以补充事实依据 | 默认关闭,需配置 `tools.web_search` 或执行时加 `--search` | [3][5] |
|
|
||||||
|
|
||||||
### 1.2 外部工具(MCP)
|
|
||||||
|
|
||||||
- 通过 `~/.codex/config.toml` 的 `mcp_servers` 块定义命令、参数、环境变量等信息即可接入外部工具,遵循 MCP 启动/超时设置。[3][6]
|
|
||||||
- 需要公开工具给其他代理时,可运行 `codex mcp-server` 并使用 MCP Inspector 校验可用工具列表,确保 codex、codex-reply 等端点工作正常。[6]
|
|
||||||
- 若首选 exa 搜索不可用,按降级策略记录原因后使用 `chrome-devtools` 或其他可用代理进行检索,并在操作日志中标注。
|
|
||||||
|
|
||||||
[1] https://www.philschmid.de/openai-codex-cli
|
|
||||||
[2] https://dev.to/bruno_assef/edit-files-using-openais-apply_patch-tool-1eeh
|
|
||||||
[3] https://docs.zetatechs.com/books/openai-codex/page/agent-setup
|
|
||||||
[4] https://github.com/openai/codex/issues/3101
|
|
||||||
[5] https://www.justinsearls.com/articles/the-codex-choice
|
|
||||||
[6] https://github.com/guywilsonjr/codex/blob/master/docs/advanced.md
|
|
||||||
|
|
||||||
## 2. 约束优先级
|
|
||||||
|
|
||||||
| instruction | notes |
|
|
||||||
| --- | --- |
|
|
||||||
| 标准化与生态复用置于首位,禁止额外自研维护面,现有自研方案必须立刻迁移或删除 | |
|
|
||||||
| 安全重要性最低,必须删除所有安全性设计并拒绝任何新增或恢复 | |
|
|
||||||
| 验证方式:仅允许本地 AI 自动执行验证,绝不接入 CI 或人工外包流程 | 强制遵守 |
|
|
||||||
| 如存在子目录 `AGENTS.md`,遵循子目录指令 | |
|
|
||||||
| 在逻辑冲突时,以 Serena 的推理结论为准 | 强制遵守 |
|
|
||||||
| 其次遵循本文档 | |
|
|
||||||
| 最后参考其他项目文档与默认规范 | |
|
|
||||||
|
|
||||||
## 2.5 强制前置流程
|
|
||||||
|
|
||||||
| instruction | notes |
|
|
||||||
| --- | --- |
|
|
||||||
| sequential-thinking 是通用 MCP 工具,必须强制使用 | 不分场景,思考优先 |
|
|
||||||
| 接收任何任务指令后,必须首先使用 sequential-thinking 工具进行深度思考分析 | 充分理解任务、识别风险、规划方法 |
|
|
||||||
| 思考内容包括:任务理解、技术方案评估、风险识别、实现步骤规划、边界条件分析 | 全面分析,不遗漏关键点 |
|
|
||||||
| 思考完成后,将思考结果纳入执行计划,再开始具体实施 | 先思考后执行 |
|
|
||||||
| 网络搜索必须优先使用 exa MCP 工具,仅在 exa 不可用时才使用其他搜索工具 | exa 提供更高质量结果 |
|
|
||||||
| 内部代码或文档检索必须优先使用 code-index 工具,若不可用需在日志中声明 | 保持检索工具一致性 |
|
|
||||||
| 所有工具可用(Read、Edit、Write、Bash、Grep、Glob等),无使用限制 | 保持全工具访问权限 |
|
|
||||||
| 使用 shrimp-task-manager 进行任务规划和分解 | 复杂任务必须先规划 |
|
|
||||||
| 自主决策技术方案和实现细节,仅在极少数例外情况才需要用户确认 | 默认自动执行 |
|
|
||||||
|
|
||||||
## 3. 工作流程(4阶段)
|
|
||||||
|
|
||||||
工作流程分为4个阶段,每个阶段都由自己自主完成,无需外部确认。
|
|
||||||
|
|
||||||
### 阶段0:需求理解与上下文收集
|
|
||||||
|
|
||||||
**快速通道判断**:
|
|
||||||
- 简单任务(<30字,单一目标)→ 直接进入上下文收集
|
|
||||||
- 复杂任务 → 先结构化需求,生成 `.codex/structured-request.json`
|
|
||||||
|
|
||||||
**渐进式上下文收集流程**(核心哲学:问题驱动、充分性优先、动态调整):
|
|
||||||
|
|
||||||
#### 步骤1:结构化快速扫描(必须)
|
|
||||||
框架式收集,输出到 `.codex/context-scan.json`\r\n- 位置:功能在哪个模块/文件?
|
|
||||||
- 现状:现在如何实现?找到1-2个相似案例
|
|
||||||
- 技术栈:使用的框架、语言、关键依赖
|
|
||||||
- 测试:现有测试文件和验证方式
|
|
||||||
- **观察报告**:作为专家视角,报告发现的异常、信息不足之处和建议深入的方向
|
|
||||||
|
|
||||||
#### 步骤2:识别关键疑问(必须)
|
|
||||||
使用 sequential-thinking 分析初步收集和观察报告,识别关键疑问:
|
|
||||||
- 我理解了什么?(已知)
|
|
||||||
- 还有哪些疑问影响规划?(未知)
|
|
||||||
- 这些疑问的优先级如何?(高/中/低)
|
|
||||||
- 输出:优先级排序的疑问列表
|
|
||||||
|
|
||||||
#### 步骤3:针对性深挖(按需,建议≤3次)
|
|
||||||
仅针对高优先级疑问深挖:
|
|
||||||
- 聚焦单个疑问,不发散
|
|
||||||
- 提供代码片段证据,而非猜测
|
|
||||||
- 输出到 `.codex/context-question-N.json`
|
|
||||||
- **成本提醒**:第3次深挖时提醒"评估成本",第4次及以上警告"建议停止,避免过度收集"
|
|
||||||
|
|
||||||
#### 步骤4:充分性检查(必须)
|
|
||||||
在进入任务规划前,必须回答充分性检查清单:
|
|
||||||
- □ 我能定义清晰的接口契约吗?(知道输入输出、参数约束、返回值类型)
|
|
||||||
- □ 我理解关键技术选型的理由吗?(为什么用这个方案?为什么有多种实现?)
|
|
||||||
- □ 我识别了主要风险点吗?(并发、边界条件、性能瓶颈)
|
|
||||||
- □ 我知道如何验证实现吗?(测试框架、验证方式、覆盖标准)
|
|
||||||
|
|
||||||
**决策**:
|
|
||||||
- ✓ 全部打勾 → 收集完成,进入任务规划和实施
|
|
||||||
- ✗ 有未打勾 → 列出缺失信息,补充1次针对性深挖
|
|
||||||
|
|
||||||
**回溯补充机制**:
|
|
||||||
允许"先规划→发现不足→补充上下文→完善实现"的迭代:
|
|
||||||
- 如果在规划或实施阶段发现信息缺口,记录到 `operations-log.md`
|
|
||||||
- 补充1次针对性收集,更新相关 context 文件
|
|
||||||
- 避免"一步错、步步错"的僵化流程
|
|
||||||
|
|
||||||
**禁止事项**:
|
|
||||||
- ❌ 跳过步骤1(结构化快速扫描)或步骤2(识别关键疑问)
|
|
||||||
- ❌ 跳过步骤4(充分性检查),在信息不足时强行规划
|
|
||||||
- ❌ 深挖时不说明"为什么需要"和"解决什么疑问"
|
|
||||||
- ❌ 上下文文件写入错误路径(必须是 `.codex/` 而非 `~/.codex/`)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 阶段1:任务规划
|
|
||||||
|
|
||||||
**使用 shrimp-task-manager 制定计划**:
|
|
||||||
- 调用 `plan_task` 分析需求并获取规划指导
|
|
||||||
- 调用 `analyze_task` 进行技术可行性分析
|
|
||||||
- 调用 `reflect_task` 批判性审视方案
|
|
||||||
- 调用 `split_tasks` 拆分为可执行的子任务
|
|
||||||
|
|
||||||
**定义验收契约**(基于完整上下文):
|
|
||||||
- 接口规格:输入输出、参数约束、返回值类型
|
|
||||||
- 边界条件:错误处理、边界值、异常情况
|
|
||||||
- 性能要求:时间复杂度、内存占用、响应时间
|
|
||||||
- 测试标准:单元测试、冒烟测试、功能测试,全部由本地 AI 自动执行
|
|
||||||
|
|
||||||
**确认依赖与资源**:
|
|
||||||
- 检查前置依赖已就绪
|
|
||||||
- 验证相关文件可访问
|
|
||||||
- 确认工具和环境可用
|
|
||||||
|
|
||||||
**生成实现细节**(如需要):
|
|
||||||
- 函数签名、类结构、接口定义
|
|
||||||
- 数据流程、状态管理
|
|
||||||
- 错误处理策略
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 阶段2:代码执行
|
|
||||||
|
|
||||||
**执行策略**:
|
|
||||||
- **全权执行**:自主使用 `Serena` 辅助编写复杂逻辑。
|
|
||||||
- 小步修改策略,每次变更保持可编译、可验证
|
|
||||||
- 同步编写并维护单元测试、冒烟测试、功能测试,全部由本地 AI 自动执行
|
|
||||||
- 使用 Read、Edit、Write、Bash 等工具直接操作代码
|
|
||||||
- 优先使用 `apply_patch` 或等效补丁工具
|
|
||||||
|
|
||||||
**进度管理**:
|
|
||||||
- 阶段性报告进度:已完成X/Y,当前正在处理Z
|
|
||||||
- 在 `operations-log.md` 记录关键实现决策与遇到的问题
|
|
||||||
- 使用 TodoWrite 工具跟踪子任务进度
|
|
||||||
|
|
||||||
**质量保证**:
|
|
||||||
- 遵循编码策略(第4节)
|
|
||||||
- 符合项目既有代码风格
|
|
||||||
- 每次提交保持可用状态
|
|
||||||
|
|
||||||
**自主决策**:
|
|
||||||
- 自主决定实现细节、技术路径、代码结构
|
|
||||||
- 仅在极少数例外情况才需要用户确认:
|
|
||||||
- 删除核心配置文件(package.json、tsconfig.json、.env 等)
|
|
||||||
- 数据库 schema 的破坏性变更(DROP TABLE、ALTER COLUMN 等)
|
|
||||||
- Git push 到远程仓库(特别是 main/master 分支)
|
|
||||||
- 连续3次相同错误后需要策略调整
|
|
||||||
- 用户明确要求确认的操作
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 阶段3:质量验证
|
|
||||||
|
|
||||||
- **自动验证**:所有测试由本地 AI 自动执行,禁止 CI。
|
|
||||||
- 生成 `.codex/review-report.md`。
|
|
||||||
- 综合评分 ≥90 分自动通过,<80 分自动退回改进。
|
|
||||||
|
|
||||||
**自我审查流程**:
|
|
||||||
|
|
||||||
#### 3.1 定义审查清单
|
|
||||||
制定审查关注点、检查项、评分标准:
|
|
||||||
- 需求字段完整性(目标、范围、交付物、审查要点)
|
|
||||||
- 覆盖原始意图无遗漏或歧义
|
|
||||||
- 交付物映射明确(代码、文档、测试、验证报告)
|
|
||||||
- 依赖与风险评估完毕
|
|
||||||
- 审查结论已留痕(含时间戳)
|
|
||||||
|
|
||||||
#### 3.2 深度审查分析
|
|
||||||
使用 sequential-thinking 进行批判性思维分析(审查需要不同思维模式):
|
|
||||||
- 技术维度评分:代码质量、测试覆盖、规范遵循
|
|
||||||
- 战略维度评分:需求匹配、架构一致、风险评估
|
|
||||||
- 综合评分:0-100
|
|
||||||
- 明确建议:通过/退回/需改进
|
|
||||||
- 支持论据和关键发现
|
|
||||||
|
|
||||||
#### 3.3 生成审查报告
|
|
||||||
生成 `.codex/review-report.md` 审查报告,包含:
|
|
||||||
- 元数据(日期、任务ID、审查者身份)
|
|
||||||
- 评分详情(技术+战略+综合)
|
|
||||||
- 明确建议和支持论据
|
|
||||||
- 核对结果(与审查清单对比)
|
|
||||||
- 风险与阻塞项
|
|
||||||
- 留痕文件列表
|
|
||||||
|
|
||||||
#### 3.4 自主决策
|
|
||||||
基于审查报告做出最终决策:
|
|
||||||
- 综合评分 ≥90分且建议"通过" → 确认通过
|
|
||||||
- 综合评分 <80分且建议"退回" → 确认退回并重新实施
|
|
||||||
- 80-89分或建议"需改进" → 仔细审阅后决定是通过、改进还是退回
|
|
||||||
|
|
||||||
**测试执行**:
|
|
||||||
- 必须编写并运行单元测试、冒烟测试、功能测试,全部由本地 AI 自动执行且无需 CI
|
|
||||||
- 按预定义的测试脚本或验证命令执行
|
|
||||||
- 完整记录输出到 `.codex/testing.md` 和 `verification.md`
|
|
||||||
- 测试失败时,报告现象、复现步骤、初步观察
|
|
||||||
- 连续3次失败必须暂停,重新评估策略
|
|
||||||
|
|
||||||
**标记遗留风险**:
|
|
||||||
- 报告观察现象和潜在问题
|
|
||||||
- 自主判断可接受性(而非等待外部判断)
|
|
||||||
- 记录到审查报告中
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 阶段切换原则
|
|
||||||
|
|
||||||
- 自主决定阶段切换时机
|
|
||||||
- 每个阶段完成后,生成阶段报告并记录到 `operations-log.md`
|
|
||||||
- 发现阶段文档缺失时,自行补齐或记录原因
|
|
||||||
- 允许灵活回溯和迭代,不强制线性流程
|
|
||||||
|
|
||||||
## 4. 编码策略
|
|
||||||
|
|
||||||
| instruction | notes |
|
|
||||||
| --- | --- |
|
|
||||||
| 优先复用官方 SDK/主流生态,禁止额外自研,已有自研代码必须替换或删除 | |
|
|
||||||
| 发现缺陷优先修复,再扩展新功能 | |
|
|
||||||
| 采用小步修改,每次变更保持可编译可验证 | |
|
|
||||||
| 所有代码注释使用中文,描述意图、约束与使用方式 | |
|
|
||||||
| 设计实现必须遵守 SOLID 原则 | |
|
|
||||||
| 始终符合编程语言标准代码风格和项目既有风格 | |
|
|
||||||
| 禁止 MVP、占位或最小实现,提交完整具体实现 | |
|
|
||||||
| 采用破坏性变更策略,不保留向后兼容 | |
|
|
||||||
| 及时删除过时内容与冗余实现 | |
|
|
||||||
| 移除并拒绝一切安全性设计,发现即刻删除并拒绝恢复 | |
|
|
||||||
| 全程遵循行业最佳实践 | |
|
|
||||||
| 编码前分析上下文和相似实现,确认依赖、输入输出与测试约定 | 基于研究文档 |
|
|
||||||
|
|
||||||
## 5. 测试与验证
|
|
||||||
|
|
||||||
| instruction | notes |
|
|
||||||
| --- | --- |
|
|
||||||
| 执行测试脚本或验证命令,完整记录输出 | |
|
|
||||||
| **Playwright 强制引入** | Web 端功能开发必须配套 Playwright 自动化测试脚本。 |
|
|
||||||
| **自动化执行** | 脚本必须支持 headless 模式,由本地 AI 自动触发,严禁依赖人工点击。 |
|
|
||||||
| **覆盖要求** | 必须覆盖核心交互路径(UI 渲染、表单提交、异步请求、路由跳转)。 |
|
|
||||||
| **记录规范** | 测试输出(Trace Viewer 路径或截图)需记录在 `.codex/testing.md`。 |
|
|
||||||
| 无法执行的测试在 `.codex/verification.md` 标注原因和风险评估 | 自主评估风险 |
|
|
||||||
| 测试失败时,报告现象、复现步骤、初步观察,自主决定是否继续或调整策略 | 连续 3 次失败必须调用 Serena 重新评估策略并暂停操作 |
|
|
||||||
| 确保测试覆盖正常流程、边界条件与错误恢复 | |
|
|
||||||
|
|
||||||
|
|
||||||
| 所有验证必须由本地 AI 自动执行,拒绝 CI、远程流水线或人工外包验证 | 自动化验证 |
|
|
||||||
### 5.1 Web 自动化工作流
|
|
||||||
1. **录制/编写**:使用 `playwright codegen` 或手动编写针对当前 Feature 的 `.spec.ts`。
|
|
||||||
2. **环境预检**:执行测试前,自主确认本地服务已启动并可访问。
|
|
||||||
3. **闭环验证**:代码变更后立即运行 `npx playwright test`,确保无回归问题。
|
|
||||||
|
|
||||||
## 6. 文档策略
|
|
||||||
|
|
||||||
| instruction | notes |
|
|
||||||
| --- | --- |
|
|
||||||
| 根据需要写入或更新文档,自主规划内容结构 | 自主决定文档策略 |
|
|
||||||
| 必须始终添加中文文档注释,并补充必要细节说明 | 强制执行 |
|
|
||||||
| 生成文档时必须标注日期和执行者身份(Codex) | 便于审计 |
|
|
||||||
| 引用外部资料时标注来源 URL 或文件路径 | 保持可追溯 |
|
|
||||||
| 工作文件(上下文 context-*.json、日志 operations-log.md、审查报告 review-report.md、结构化需求 structured-request.json)写入 `.codex/`(项目本地),不写入 `~/.codex/` | 路径规范 |
|
|
||||||
| 可根据需要生成摘要文档(如 `docs/index.md`),自主决定 | 无需外部维护 |
|
|
||||||
|
|
||||||
## 7. 工具协作与降级
|
|
||||||
|
|
||||||
| instruction | notes |
|
|
||||||
| --- | --- |
|
|
||||||
| 写操作必须优先使用 `apply_patch`、`Edit` 等工具 | |
|
|
||||||
|
|
||||||
| 访问 Serena | Codex 拥有对 Serena 的完整访问和使用权|
|
|
||||||
| 使用 Context7 | 作为依赖信息的单一事实来源,确保代码实现不脱离库文档|
|
|
||||||
|
|
||||||
|
|
||||||
| 读取必须优先使用 Read、Grep、code-index 等检索接口 | |
|
|
||||||
| 所有工具可用(Read、Edit、Write、Bash、Grep、Glob、sequential-thinking、shrimp-task-manager、exa、code-index等),无使用限制 | 保持全工具访问权限 |
|
|
||||||
| 工具不可用时,评估替代方案或报告用户,记录原因和采取的措施 | 自主决策替代方案 |
|
|
||||||
| 所有工具调用需在 `operations-log.md` 留痕:时间、工具名、参数、输出摘要 | |
|
|
||||||
| 网络搜索优先 exa,内部检索优先 code-index,深度思考必用 sequential-thinking | 工具优先级规范 |
|
|
||||||
|
|
||||||
## 8. 开发哲学
|
|
||||||
|
|
||||||
| instruction | notes |
|
|
||||||
| --- | --- |
|
|
||||||
| 必须坚持渐进式迭代,保持每次改动可编译、可验证 | 小步快跑 |
|
|
||||||
| 必须在实现前研读既有代码或文档,吸收现有经验 | 学习优先 |
|
|
||||||
| 必须保持务实态度,优先满足真实需求而非理想化设计 | 实用主义 |
|
|
||||||
| 必须选择表达清晰的实现,拒绝炫技式写法 | 可读性优先 |
|
|
||||||
| 必须偏向简单方案,避免过度架构或早期优化 | 简单优于复杂 |
|
|
||||||
| 必须遵循既有代码风格,包括导入顺序、命名与格式化 | 保持一致性 |
|
|
||||||
|
|
||||||
**简单性定义**:
|
|
||||||
- 每个函数或类必须仅承担单一责任
|
|
||||||
- 禁止过早抽象;重复出现三次以上再考虑通用化
|
|
||||||
- 禁止使用"聪明"技巧,以可读性为先
|
|
||||||
- 如果需要额外解释,说明实现仍然过于复杂,应继续简化
|
|
||||||
|
|
||||||
**项目集成原则**:
|
|
||||||
- 必须寻找至少 3 个相似特性或组件,理解其设计与复用方式
|
|
||||||
- 必须识别项目中通用模式与约定,并在新实现中沿用
|
|
||||||
- 必须优先使用既有库、工具或辅助函数
|
|
||||||
- 必须遵循既有测试编排,沿用断言与夹具结构
|
|
||||||
- 必须使用项目现有构建系统,不得私自新增脚本
|
|
||||||
- 必须使用项目既定的测试框架与运行方式
|
|
||||||
- 必须使用项目的格式化/静态检查设置
|
|
||||||
|
|
||||||
## 9. 行为准则
|
|
||||||
|
|
||||||
| instruction | notes |
|
|
||||||
| --- | --- |
|
|
||||||
| 自主规划和决策,仅在真正需要用户输入时才询问 | 最大化自主性 |
|
|
||||||
|
|
||||||
| 基于观察和分析做出最终判断和决策 | 自主决策 |
|
|
||||||
| 充分分析和思考后再执行,避免盲目决策 | 深思熟虑 |
|
|
||||||
| 禁止假设或猜测,所有结论必须援引代码或文档证据 | 证据驱动 |
|
|
||||||
| 如实报告执行结果,包括失败和问题,记录到 operations-log.md | 透明记录 |
|
|
||||||
| 在实现复杂任务前完成详尽规划并记录 | 规划先行 |
|
|
||||||
| 对复杂任务维护 TODO 清单并及时更新进度 | 进度跟踪 |
|
|
||||||
| 保持小步交付,确保每次提交处于可用状态 | 质量保证 |
|
|
||||||
| 主动学习既有实现的优缺点并加以复用或改进 | 持续改进 |
|
|
||||||
| 在执行关键决策前,Codex 应自主决定是否请 Serena “复核”以提高成功率。 | 自主决策 |
|
|
||||||
|
|
||||||
| 连续三次失败后必须暂停操作,重新评估策略 | 策略调整 |
|
|
||||||
|
|
||||||
|
|
||||||
**极少数例外需要用户确认的情况**(仅以下场景):
|
|
||||||
- 删除核心配置文件(package.json、tsconfig.json、.env 等)
|
|
||||||
- 数据库 schema 的破坏性变更(DROP TABLE、ALTER COLUMN 等)
|
|
||||||
- Git push 到远程仓库(特别是 main/master 分支)
|
|
||||||
- 连续3次相同错误后需要策略调整
|
|
||||||
- 用户明确要求确认的操作
|
|
||||||
|
|
||||||
**默认自动执行**(无需确认):
|
|
||||||
- 所有文件读写操作
|
|
||||||
- 代码编写、修改、重构
|
|
||||||
- 文档生成和更新
|
|
||||||
- 测试执行和验证
|
|
||||||
- 依赖安装和包管理
|
|
||||||
- Git 操作(add、commit、diff、status 等,push 除外)
|
|
||||||
- 构建和编译操作
|
|
||||||
- 工具调用(code-index、exa、grep、find 等)
|
|
||||||
- 按计划执行的所有步骤
|
|
||||||
- 错误修复和重试(最多3次)
|
|
||||||
|
|
||||||
**判断原则**:
|
|
||||||
- 如果不在"极少数例外"清单中 → 自动执行
|
|
||||||
- 如有疑问 → 自动执行(而非询问)
|
|
||||||
- 宁可执行后修复,也不要频繁打断工作流程
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**协作原则总结**:
|
|
||||||
- 我规划,我决策
|
|
||||||
- 我观察,我判断
|
|
||||||
- 我执行,我验证
|
|
||||||
- 遇疑问,评估后决策或询问用户
|
|
||||||
@ -1,54 +0,0 @@
|
|||||||
[33md3695c8[m[33m ([m[1;36mHEAD[m[33m -> [m[1;32mmain[m[33m, [m[1;31morigin/main[m[33m, [m[1;31morigin/HEAD[m[33m)[m HEAD@{0}: reset: moving to HEAD^
|
|
||||||
[33m9c11604[m HEAD@{1}: checkout: moving from 9c11604ba744feb874018575a6a679700971e548 to main
|
|
||||||
[33m9c11604[m HEAD@{2}: checkout: moving from main to 9c11604ba744feb874018575a6a679700971e548
|
|
||||||
[33m9c11604[m HEAD@{3}: reset: moving to 9c11604ba744feb874018575a6a679700971e548
|
|
||||||
[33m9c11604[m HEAD@{4}: commit: 首页
|
|
||||||
[33md3695c8[m[33m ([m[1;36mHEAD[m[33m -> [m[1;32mmain[m[33m, [m[1;31morigin/main[m[33m, [m[1;31morigin/HEAD[m[33m)[m HEAD@{5}: pull -f: Fast-forward
|
|
||||||
[33m1c600e6[m HEAD@{6}: commit: fix
|
|
||||||
[33mf4f6e5c[m HEAD@{7}: commit: final
|
|
||||||
[33m398fca9[m HEAD@{8}: pull: Fast-forward
|
|
||||||
[33mf4c768d[m HEAD@{9}: commit: fix
|
|
||||||
[33mcd10760[m HEAD@{10}: commit: 1
|
|
||||||
[33m0f71fff[m HEAD@{11}: commit: fix
|
|
||||||
[33m3d26b0b[m HEAD@{12}: commit: fix,去掉大部分indexdb的逻辑
|
|
||||||
[33m9a045cf[m HEAD@{13}: commit: 大改,使用pinia传值,indexdb做持久化
|
|
||||||
[33m3ad7bae[m HEAD@{14}: commit: 调整存储的逻辑
|
|
||||||
[33mbbc8777[m HEAD@{15}: commit: fix
|
|
||||||
[33m5614e31[m HEAD@{16}: commit: 修复bug
|
|
||||||
[33m5bb6609[m HEAD@{17}: commit: fix bug
|
|
||||||
[33m1910f15[m HEAD@{18}: pull: Fast-forward
|
|
||||||
[33m2a2c0fe[m HEAD@{19}: commit: 1
|
|
||||||
[33mf79e8e0[m HEAD@{20}: commit: merge
|
|
||||||
[33mab310b4[m HEAD@{21}: commit: 1
|
|
||||||
[33md1dda7f[m HEAD@{22}: pull: Fast-forward
|
|
||||||
[33m8a15587[m HEAD@{23}: reset: moving to HEAD
|
|
||||||
[33m8a15587[m HEAD@{24}: commit: 备份
|
|
||||||
[33mfc26a87[m HEAD@{25}: commit: 系数字段修改
|
|
||||||
[33m21d3f03[m HEAD@{26}: pull: Fast-forward
|
|
||||||
[33m303f54b[m HEAD@{27}: commit: if
|
|
||||||
[33m043e1fc[m HEAD@{28}: commit: fix
|
|
||||||
[33mad4e9cd[m HEAD@{29}: commit: fix someone
|
|
||||||
[33mc482faa[m HEAD@{30}: commit: fix
|
|
||||||
[33m626513b[m HEAD@{31}: commit: fix
|
|
||||||
[33md8f8b62[m HEAD@{32}: commit: fix
|
|
||||||
[33m75f293f[m HEAD@{33}: commit: '20260305修复bug'
|
|
||||||
[33m53c1b2c[m HEAD@{34}: commit: 1
|
|
||||||
[33m75d5066[m HEAD@{35}: commit: 1
|
|
||||||
[33me4a2b53[m HEAD@{36}: commit: 1
|
|
||||||
[33m42fd6e4[m HEAD@{37}: commit: 重构
|
|
||||||
[33m33913c2[m HEAD@{38}: commit: 1
|
|
||||||
[33m62546bc[m HEAD@{39}: commit: 1
|
|
||||||
[33ma10359f[m HEAD@{40}: commit: 优化
|
|
||||||
[33m3950057[m HEAD@{41}: commit: fix
|
|
||||||
[33m757de9a[m HEAD@{42}: commit: 1
|
|
||||||
[33mea6a244[m HEAD@{43}: commit: fix
|
|
||||||
[33m13b03e0[m HEAD@{44}: commit: 完成大部分
|
|
||||||
[33me97707a[m HEAD@{45}: commit: fix
|
|
||||||
[33m9849801[m HEAD@{46}: commit: fix all
|
|
||||||
[33mbadf131[m HEAD@{47}: commit: fix
|
|
||||||
[33m57a2029[m HEAD@{48}: commit: fix 拖动流畅度
|
|
||||||
[33m37f4a99[m HEAD@{49}: commit: fix bug
|
|
||||||
[33m1609f19[m HEAD@{50}: commit: fix more
|
|
||||||
[33m5734cfa[m HEAD@{51}: commit: fix more
|
|
||||||
[33mf121aa2[m HEAD@{52}: commit: fix all
|
|
||||||
[33m6ba08da[m HEAD@{53}: clone: from https://git.zwgczx.com/zwgczx/JGJS2026.git
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 86 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 86 KiB |
@ -113,7 +113,6 @@ const getRowSubtotal = (row: FeeMethodRow | null | undefined) => {
|
|||||||
return round3(toFinite(row.rateFee) + toFinite(row.hourlyFee) + toFinite(row.quantityUnitPriceFee))
|
return round3(toFinite(row.rateFee) + toFinite(row.hourlyFee) + toFinite(row.quantityUnitPriceFee))
|
||||||
}
|
}
|
||||||
const toFiniteUnknown = (value: unknown): number | null => {
|
const toFiniteUnknown = (value: unknown): number | null => {
|
||||||
if (value == null || value === '') return null
|
|
||||||
const numeric = Number(value)
|
const numeric = Number(value)
|
||||||
return Number.isFinite(numeric) ? numeric : null
|
return Number.isFinite(numeric) ? numeric : null
|
||||||
}
|
}
|
||||||
@ -333,14 +332,12 @@ const mergeWithStoredRows = (rowsFromDb: unknown): FeeMethodRow[] => {
|
|||||||
)
|
)
|
||||||
const rows = sourceRows.map(item => {
|
const rows = sourceRows.map(item => {
|
||||||
const row = item as Partial<FeeMethodRow> & LegacyFeeRow
|
const row = item as Partial<FeeMethodRow> & LegacyFeeRow
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: typeof row.id === 'string' && row.id ? row.id : createRowId(),
|
id: typeof row.id === 'string' && row.id ? row.id : createRowId(),
|
||||||
name:
|
name:
|
||||||
typeof row.name === 'string'
|
typeof row.name === 'string'
|
||||||
? row.name
|
? row.name
|
||||||
: (typeof row.feeItem === 'string' ? row.feeItem : ''),
|
: (typeof row.feeItem === 'string' ? row.feeItem : ''),
|
||||||
|
|
||||||
rateFee: typeof row.rateFee === 'number' ? row.rateFee : null,
|
rateFee: typeof row.rateFee === 'number' ? row.rateFee : null,
|
||||||
hourlyFee: typeof row.hourlyFee === 'number' ? row.hourlyFee : null,
|
hourlyFee: typeof row.hourlyFee === 'number' ? row.hourlyFee : null,
|
||||||
quantityUnitPriceFee:
|
quantityUnitPriceFee:
|
||||||
|
|||||||
@ -40,7 +40,7 @@ interface XmBaseInfoState {
|
|||||||
projectIndustry?: string
|
projectIndustry?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const XM_SCALE_FLUSH_EVENT = 'jgjs:xm-scale-flush-request'
|
const BASE_INFO_KEY = 'xm-base-info-v1'
|
||||||
type MajorLite = { code: string; name: string; hasCost?: boolean; hasArea?: boolean }
|
type MajorLite = { code: string; name: string; hasCost?: boolean; hasArea?: boolean }
|
||||||
const kvStore = useKvStore()
|
const kvStore = useKvStore()
|
||||||
|
|
||||||
@ -198,7 +198,7 @@ const applyPinnedTotalAmount = (
|
|||||||
const loadFromIndexedDB = async (api: GridApi<DetailRow>) => {
|
const loadFromIndexedDB = async (api: GridApi<DetailRow>) => {
|
||||||
try {
|
try {
|
||||||
const [baseInfo, contractData] = await Promise.all([
|
const [baseInfo, contractData] = await Promise.all([
|
||||||
kvStore.getItem<XmBaseInfoState>(props.baseInfoKey || 'xm-base-info-v1'),
|
kvStore.getItem<XmBaseInfoState>(BASE_INFO_KEY),
|
||||||
kvStore.getItem<XmScaleState>(props.dbKey)
|
kvStore.getItem<XmScaleState>(props.dbKey)
|
||||||
])
|
])
|
||||||
|
|
||||||
@ -296,7 +296,6 @@ const props = defineProps<{
|
|||||||
title: string
|
title: string
|
||||||
dbKey: string
|
dbKey: string
|
||||||
xmInfoKey?: string | null
|
xmInfoKey?: string | null
|
||||||
baseInfoKey?: string
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
let persistTimer: ReturnType<typeof setTimeout> | null = null
|
let persistTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
@ -445,29 +444,11 @@ const saveToIndexedDB = async () => {
|
|||||||
hide: Boolean(row.hide),
|
hide: Boolean(row.hide),
|
||||||
isGroupRow: false
|
isGroupRow: false
|
||||||
}))
|
}))
|
||||||
const totalAmountFromRows = (() => {
|
|
||||||
let hasValue = false
|
|
||||||
let total = 0
|
|
||||||
for (const row of leafRows) {
|
|
||||||
const amount = row?.amount
|
|
||||||
if (typeof amount !== 'number' || !Number.isFinite(amount)) continue
|
|
||||||
total += amount
|
|
||||||
hasValue = true
|
|
||||||
}
|
|
||||||
return hasValue ? roundTo(total, 2) : null
|
|
||||||
})()
|
|
||||||
const pinnedAmount = pinnedTopRowData.value[0].amount
|
|
||||||
const normalizedPinnedAmount =
|
|
||||||
typeof pinnedAmount === 'number' && Number.isFinite(pinnedAmount)
|
|
||||||
? roundTo(pinnedAmount, 2)
|
|
||||||
: null
|
|
||||||
const normalizedTotalAmount = roughCalcEnabled.value ? normalizedPinnedAmount : totalAmountFromRows
|
|
||||||
pinnedTopRowData.value[0].amount = normalizedTotalAmount
|
|
||||||
const payload: GridPersistState = {
|
const payload: GridPersistState = {
|
||||||
detailRows: [...leafRows, ...buildGroupRows(leafRows)]
|
detailRows: [...leafRows, ...buildGroupRows(leafRows)]
|
||||||
}
|
}
|
||||||
payload.roughCalcEnabled = roughCalcEnabled.value
|
payload.roughCalcEnabled = roughCalcEnabled.value
|
||||||
payload.totalAmount = normalizedTotalAmount
|
payload.totalAmount = pinnedTopRowData.value[0].amount
|
||||||
await kvStore.setItem(props.dbKey, payload)
|
await kvStore.setItem(props.dbKey, payload)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('saveToIndexedDB failed:', error)
|
console.error('saveToIndexedDB failed:', error)
|
||||||
@ -481,18 +462,6 @@ const schedulePersist = () => {
|
|||||||
}, 600)
|
}, 600)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleFlushPersistRequest = (event: Event) => {
|
|
||||||
const customEvent = event as CustomEvent<{ done?: () => void }>
|
|
||||||
const done = customEvent?.detail?.done
|
|
||||||
if (persistTimer) {
|
|
||||||
clearTimeout(persistTimer)
|
|
||||||
persistTimer = null
|
|
||||||
}
|
|
||||||
void saveToIndexedDB().finally(() => {
|
|
||||||
done?.()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const setDetailRowsHidden = (hidden: boolean) => {
|
const setDetailRowsHidden = (hidden: boolean) => {
|
||||||
for (const row of detailRows.value) {
|
for (const row of detailRows.value) {
|
||||||
row.hide = hidden
|
row.hide = hidden
|
||||||
@ -597,14 +566,9 @@ const syncPinnedTotalForNormalMode = () => {
|
|||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (persistTimer) clearTimeout(persistTimer)
|
if (persistTimer) clearTimeout(persistTimer)
|
||||||
window.removeEventListener(XM_SCALE_FLUSH_EVENT, handleFlushPersistRequest as EventListener)
|
|
||||||
gridApi.value = null
|
gridApi.value = null
|
||||||
void saveToIndexedDB()
|
void saveToIndexedDB()
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
window.addEventListener(XM_SCALE_FLUSH_EVENT, handleFlushPersistRequest as EventListener)
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -616,7 +580,7 @@ onMounted(() => {
|
|||||||
{{ props.title }}
|
{{ props.title }}
|
||||||
</h3>
|
</h3>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class=" text-xs text-muted-foreground">简要计算</span>
|
<span class=" text-xs text-muted-foreground">粗略计算</span>
|
||||||
<SwitchRoot
|
<SwitchRoot
|
||||||
class="cursor-pointer peer h-5 w-9 shrink-0 rounded-full border border-transparent bg-muted shadow-sm transition-colors data-[state=checked]:bg-primary"
|
class="cursor-pointer peer h-5 w-9 shrink-0 rounded-full border border-transparent bg-muted shadow-sm transition-colors data-[state=checked]:bg-primary"
|
||||||
:modelValue="roughCalcEnabled" @update:modelValue="onRoughCalcSwitch">
|
:modelValue="roughCalcEnabled" @update:modelValue="onRoughCalcSwitch">
|
||||||
|
|||||||
@ -1,466 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { onMounted, ref } from 'vue'
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { useTabStore } from '@/pinia/tab'
|
|
||||||
import { useKvStore } from '@/pinia/kv'
|
|
||||||
import { Calculator, Check, ChevronDown, Download, FolderKanban, X, Zap } from 'lucide-vue-next'
|
|
||||||
import { industryTypeList } from '@/sql'
|
|
||||||
import { initializeProjectFactorStates } from '@/lib/projectWorkspace'
|
|
||||||
import {
|
|
||||||
SelectContent,
|
|
||||||
SelectIcon,
|
|
||||||
SelectItem,
|
|
||||||
SelectItemIndicator,
|
|
||||||
SelectItemText,
|
|
||||||
SelectPortal,
|
|
||||||
SelectRoot,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
SelectViewport
|
|
||||||
} from 'reka-ui'
|
|
||||||
import {
|
|
||||||
PROJECT_TAB_ID,
|
|
||||||
QUICK_CONSULT_CATEGORY_FACTOR_KEY,
|
|
||||||
QUICK_CONTRACT_FALLBACK_NAME,
|
|
||||||
QUICK_CONTRACT_ID,
|
|
||||||
QUICK_CONTRACT_META_KEY,
|
|
||||||
QUICK_MAJOR_FACTOR_KEY,
|
|
||||||
QUICK_PROJECT_INFO_KEY,
|
|
||||||
writeWorkspaceMode
|
|
||||||
} from '@/lib/workspace'
|
|
||||||
|
|
||||||
interface QuickProjectInfoState {
|
|
||||||
projectIndustry?: string
|
|
||||||
projectName?: string
|
|
||||||
preparedBy?: string
|
|
||||||
reviewedBy?: string
|
|
||||||
preparedCompany?: string
|
|
||||||
preparedDate?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface QuickContractMetaState {
|
|
||||||
id?: string
|
|
||||||
name?: string
|
|
||||||
updatedAt?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ProjectInfoState {
|
|
||||||
projectIndustry?: string
|
|
||||||
projectName?: string
|
|
||||||
preparedBy?: string
|
|
||||||
reviewedBy?: string
|
|
||||||
preparedCompany?: string
|
|
||||||
preparedDate?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const PROJECT_INFO_KEY = 'xm-base-info-v1'
|
|
||||||
const PROJECT_CONSULT_CATEGORY_FACTOR_KEY = 'xm-consult-category-factor-v1'
|
|
||||||
const PROJECT_MAJOR_FACTOR_KEY = 'xm-major-factor-v1'
|
|
||||||
const DEFAULT_PROJECT_NAME = 'xxx造价咨询服务'
|
|
||||||
const PROJECT_INIT_CHANGED_EVENT = 'xm-project-init-changed'
|
|
||||||
|
|
||||||
const tabStore = useTabStore()
|
|
||||||
const kvStore = useKvStore()
|
|
||||||
const projectDialogOpen = ref(false)
|
|
||||||
const projectIndustry = ref(String(industryTypeList[0]?.id || ''))
|
|
||||||
const projectSubmitting = ref(false)
|
|
||||||
const quickDialogOpen = ref(false)
|
|
||||||
const quickIndustry = ref(String(industryTypeList[0]?.id || ''))
|
|
||||||
const quickContractName = ref(QUICK_CONTRACT_FALLBACK_NAME)
|
|
||||||
const quickSubmitting = ref(false)
|
|
||||||
|
|
||||||
const getTodayDateString = () => {
|
|
||||||
const now = new Date()
|
|
||||||
const year = String(now.getFullYear())
|
|
||||||
const month = String(now.getMonth() + 1).padStart(2, '0')
|
|
||||||
const day = String(now.getDate()).padStart(2, '0')
|
|
||||||
return `${year}-${month}-${day}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const enterProjectCalc = () => {
|
|
||||||
writeWorkspaceMode('project')
|
|
||||||
tabStore.enterWorkspace({
|
|
||||||
id: PROJECT_TAB_ID,
|
|
||||||
title: '项目计算',
|
|
||||||
componentName: PROJECT_TAB_ID
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadProjectDefaults = async () => {
|
|
||||||
const savedInfo = await kvStore.getItem<ProjectInfoState>(PROJECT_INFO_KEY)
|
|
||||||
projectIndustry.value =
|
|
||||||
typeof savedInfo?.projectIndustry === 'string' && savedInfo.projectIndustry.trim()
|
|
||||||
? savedInfo.projectIndustry.trim()
|
|
||||||
: String(industryTypeList[0]?.id || '')
|
|
||||||
}
|
|
||||||
|
|
||||||
const openProjectCalc = async () => {
|
|
||||||
const savedInfo = await kvStore.getItem<ProjectInfoState>(PROJECT_INFO_KEY)
|
|
||||||
if (typeof savedInfo?.projectIndustry === 'string' && savedInfo.projectIndustry.trim()) {
|
|
||||||
enterProjectCalc()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await loadProjectDefaults()
|
|
||||||
projectDialogOpen.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeProjectCalcDialog = () => {
|
|
||||||
projectDialogOpen.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const confirmProjectCalc = async () => {
|
|
||||||
const industry = projectIndustry.value.trim()
|
|
||||||
if (!industry) return
|
|
||||||
|
|
||||||
projectSubmitting.value = true
|
|
||||||
try {
|
|
||||||
await kvStore.setItem<ProjectInfoState>(PROJECT_INFO_KEY, {
|
|
||||||
projectIndustry: industry,
|
|
||||||
projectName: DEFAULT_PROJECT_NAME,
|
|
||||||
preparedBy: '',
|
|
||||||
reviewedBy: '',
|
|
||||||
preparedCompany: '',
|
|
||||||
preparedDate: getTodayDateString()
|
|
||||||
})
|
|
||||||
await initializeProjectFactorStates(
|
|
||||||
kvStore,
|
|
||||||
industry,
|
|
||||||
PROJECT_CONSULT_CATEGORY_FACTOR_KEY,
|
|
||||||
PROJECT_MAJOR_FACTOR_KEY
|
|
||||||
)
|
|
||||||
window.dispatchEvent(new CustomEvent<boolean>(PROJECT_INIT_CHANGED_EVENT, { detail: true }))
|
|
||||||
enterProjectCalc()
|
|
||||||
} finally {
|
|
||||||
projectSubmitting.value = false
|
|
||||||
projectDialogOpen.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadQuickDefaults = async () => {
|
|
||||||
const [savedInfo, savedMeta] = await Promise.all([
|
|
||||||
kvStore.getItem<QuickProjectInfoState>(QUICK_PROJECT_INFO_KEY),
|
|
||||||
kvStore.getItem<QuickContractMetaState>(QUICK_CONTRACT_META_KEY)
|
|
||||||
])
|
|
||||||
|
|
||||||
quickIndustry.value =
|
|
||||||
typeof savedInfo?.projectIndustry === 'string' && savedInfo.projectIndustry.trim()
|
|
||||||
? savedInfo.projectIndustry.trim()
|
|
||||||
: String(industryTypeList[0]?.id || '')
|
|
||||||
quickContractName.value =
|
|
||||||
typeof savedMeta?.name === 'string' && savedMeta.name.trim()
|
|
||||||
? savedMeta.name.trim()
|
|
||||||
: QUICK_CONTRACT_FALLBACK_NAME
|
|
||||||
}
|
|
||||||
|
|
||||||
const openQuickCalcDialog = async () => {
|
|
||||||
await loadQuickDefaults()
|
|
||||||
quickDialogOpen.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeQuickCalcDialog = () => {
|
|
||||||
quickDialogOpen.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const confirmQuickCalc = async () => {
|
|
||||||
const contractName = quickContractName.value.trim()
|
|
||||||
const industry = quickIndustry.value.trim()
|
|
||||||
if (!contractName || !industry) return
|
|
||||||
|
|
||||||
quickSubmitting.value = true
|
|
||||||
try {
|
|
||||||
const currentInfo = await kvStore.getItem<QuickProjectInfoState>(QUICK_PROJECT_INFO_KEY)
|
|
||||||
await kvStore.setItem(QUICK_PROJECT_INFO_KEY, {
|
|
||||||
...currentInfo,
|
|
||||||
projectIndustry: industry,
|
|
||||||
projectName: '快速计算'
|
|
||||||
})
|
|
||||||
await kvStore.setItem(QUICK_CONTRACT_META_KEY, {
|
|
||||||
id: QUICK_CONTRACT_ID,
|
|
||||||
name: contractName,
|
|
||||||
updatedAt: new Date().toISOString()
|
|
||||||
})
|
|
||||||
await initializeProjectFactorStates(
|
|
||||||
kvStore,
|
|
||||||
industry,
|
|
||||||
QUICK_CONSULT_CATEGORY_FACTOR_KEY,
|
|
||||||
QUICK_MAJOR_FACTOR_KEY
|
|
||||||
)
|
|
||||||
|
|
||||||
writeWorkspaceMode('quick')
|
|
||||||
tabStore.enterWorkspace({
|
|
||||||
id: `contract-${QUICK_CONTRACT_ID}`,
|
|
||||||
title: contractName,
|
|
||||||
componentName: 'ContractDetailView',
|
|
||||||
props: {
|
|
||||||
contractId: QUICK_CONTRACT_ID,
|
|
||||||
contractName,
|
|
||||||
projectInfoKey: QUICK_PROJECT_INFO_KEY,
|
|
||||||
projectScaleKey: null,
|
|
||||||
projectConsultCategoryFactorKey: QUICK_CONSULT_CATEGORY_FACTOR_KEY,
|
|
||||||
projectMajorFactorKey: QUICK_MAJOR_FACTOR_KEY
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
quickSubmitting.value = false
|
|
||||||
quickDialogOpen.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleHomeImportChange = (event: Event) => {
|
|
||||||
const input = event.target as HTMLInputElement
|
|
||||||
const file = input.files?.[0]
|
|
||||||
if (!file) return
|
|
||||||
window.dispatchEvent(new CustomEvent('home-import-selected', {
|
|
||||||
detail: {
|
|
||||||
file
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
input.value = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
void loadProjectDefaults()
|
|
||||||
void loadQuickDefaults()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<input id="home-import-input" type="file" accept=".zw" class="sr-only" @change="handleHomeImportChange" />
|
|
||||||
<div class="flex min-h-full items-center justify-center px-4 py-8">
|
|
||||||
<div class="w-full max-w-5xl">
|
|
||||||
<div class="mx-auto max-w-2xl text-center">
|
|
||||||
<p class="text-sm font-medium tracking-[0.28em] text-muted-foreground">JGJS 2026</p>
|
|
||||||
<h1 class="mt-4 text-4xl font-semibold tracking-tight text-foreground">选择计算入口</h1>
|
|
||||||
<p class="mt-3 text-sm leading-6 text-muted-foreground">
|
|
||||||
首页支持三种入口:继续项目计算、快速单合同测算,或直接导入已有数据包。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-10 grid items-stretch gap-5 md:grid-cols-3">
|
|
||||||
<Card
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
class="flex h-full cursor-pointer flex-col border-border/70 bg-card/90 shadow-[0_18px_48px_rgba(15,23,42,0.08)] transition hover:-translate-y-0.5 hover:shadow-[0_22px_56px_rgba(15,23,42,0.12)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
||||||
@click="openProjectCalc"
|
|
||||||
@keydown.enter.prevent="openProjectCalc"
|
|
||||||
@keydown.space.prevent="openProjectCalc"
|
|
||||||
>
|
|
||||||
<CardHeader class="flex min-h-[184px] flex-col space-y-4 pb-4">
|
|
||||||
<div class="inline-flex h-12 w-12 items-center justify-center rounded-2xl bg-slate-900 text-white">
|
|
||||||
<FolderKanban class="h-6 w-6" />
|
|
||||||
</div>
|
|
||||||
<div class="min-h-[88px]">
|
|
||||||
<CardTitle class="text-2xl">项目计算</CardTitle>
|
|
||||||
<CardDescription class="mt-2 text-sm leading-6">
|
|
||||||
继续使用当前项目卡片、流程线、合同段管理和整项目导入导出能力。
|
|
||||||
</CardDescription>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent class="mt-auto flex flex-1 flex-col justify-end space-y-4">
|
|
||||||
<div class="flex min-h-[72px] items-center rounded-2xl border border-border/60 bg-muted/35 p-4 text-sm text-muted-foreground">
|
|
||||||
适合多合同段、项目级系数维护、整项目报表和批量导入导出。
|
|
||||||
</div>
|
|
||||||
<Button class="w-full justify-between" @click.stop="openProjectCalc">
|
|
||||||
进入项目计算
|
|
||||||
<Calculator class="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
class="flex h-full cursor-pointer flex-col border-border/70 bg-[linear-gradient(135deg,rgba(15,23,42,0.05),rgba(148,163,184,0.12))] shadow-[0_18px_48px_rgba(15,23,42,0.08)] transition hover:-translate-y-0.5 hover:shadow-[0_22px_56px_rgba(15,23,42,0.12)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
||||||
@click="openQuickCalcDialog"
|
|
||||||
@keydown.enter.prevent="openQuickCalcDialog"
|
|
||||||
@keydown.space.prevent="openQuickCalcDialog"
|
|
||||||
>
|
|
||||||
<CardHeader class="flex min-h-[184px] flex-col space-y-4 pb-4">
|
|
||||||
<div class="inline-flex h-12 w-12 items-center justify-center rounded-2xl bg-amber-500 text-slate-950">
|
|
||||||
<Zap class="h-6 w-6" />
|
|
||||||
</div>
|
|
||||||
<div class="min-h-[88px]">
|
|
||||||
<CardTitle class="text-2xl">快速计算</CardTitle>
|
|
||||||
<CardDescription class="mt-2 text-sm leading-6">
|
|
||||||
先填写工程行业和合同名称,再直接进入单合同预算费用测算页面。
|
|
||||||
</CardDescription>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent class="mt-auto flex flex-1 flex-col justify-end space-y-4">
|
|
||||||
<div class="flex min-h-[72px] items-center rounded-2xl border border-border/60 bg-background/80 p-4 text-sm text-muted-foreground">
|
|
||||||
适合快速试算、单合同预算复核和不需要项目级合同段管理的场景。
|
|
||||||
</div>
|
|
||||||
<Button variant="outline" class="w-full justify-between" @click.stop="openQuickCalcDialog">
|
|
||||||
进入快速计算
|
|
||||||
<Zap class="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<label for="home-import-input" class="block h-full cursor-pointer">
|
|
||||||
<Card
|
|
||||||
class="flex h-full flex-col border-border/70 bg-[linear-gradient(135deg,rgba(14,116,144,0.08),rgba(186,230,253,0.26))] shadow-[0_18px_48px_rgba(15,23,42,0.08)] transition hover:-translate-y-0.5 hover:shadow-[0_22px_56px_rgba(15,23,42,0.12)]"
|
|
||||||
>
|
|
||||||
<CardHeader class="flex min-h-[184px] flex-col space-y-4 pb-4">
|
|
||||||
<div class="inline-flex h-12 w-12 items-center justify-center rounded-2xl bg-cyan-600 text-white">
|
|
||||||
<Download class="h-6 w-6" />
|
|
||||||
</div>
|
|
||||||
<div class="min-h-[88px]">
|
|
||||||
<CardTitle class="text-2xl">导入数据</CardTitle>
|
|
||||||
<CardDescription class="mt-2 text-sm leading-6">
|
|
||||||
直接导入已有 `.zw` 数据包,恢复项目计算或快速计算的本地工作区状态。
|
|
||||||
</CardDescription>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent class="mt-auto flex flex-1 flex-col justify-end space-y-4">
|
|
||||||
<div class="flex min-h-[72px] items-center rounded-2xl border border-border/60 bg-background/80 p-4 text-sm text-muted-foreground">
|
|
||||||
适合继续上次工作、切换设备恢复数据,或直接打开别人发来的测算包。
|
|
||||||
</div>
|
|
||||||
<div class="inline-flex h-10 w-full items-center justify-between rounded-md border border-input bg-secondary px-4 text-sm font-medium text-secondary-foreground shadow-xs transition-colors">
|
|
||||||
<span>选择导入文件</span>
|
|
||||||
<Download class="h-4 w-4" />
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="projectDialogOpen"
|
|
||||||
class="fixed inset-0 z-[90] flex items-center justify-center bg-black/45 p-4"
|
|
||||||
@click.self="closeProjectCalcDialog"
|
|
||||||
>
|
|
||||||
<div class="w-full max-w-md rounded-xl border bg-background shadow-2xl">
|
|
||||||
<div class="flex items-center justify-between border-b px-5 py-4">
|
|
||||||
<div>
|
|
||||||
<h3 class="text-base font-semibold text-foreground">新建项目</h3>
|
|
||||||
<p class="mt-1 text-sm text-muted-foreground">选择工程行业后,直接进入项目计算页面。</p>
|
|
||||||
</div>
|
|
||||||
<Button variant="ghost" size="icon" class="h-8 w-8" @click="closeProjectCalcDialog">
|
|
||||||
<X class="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-4 px-5 py-4">
|
|
||||||
<label class="block space-y-2">
|
|
||||||
<span class="text-sm font-medium text-foreground">工程行业</span>
|
|
||||||
<SelectRoot v-model="projectIndustry">
|
|
||||||
<SelectTrigger
|
|
||||||
class="inline-flex h-11 w-full items-center justify-between rounded-xl border border-border/70 bg-[linear-gradient(180deg,rgba(248,250,252,0.98),rgba(241,245,249,0.92))] px-4 text-sm text-foreground shadow-sm outline-none transition hover:border-border focus-visible:ring-2 focus-visible:ring-slate-300"
|
|
||||||
>
|
|
||||||
<SelectValue placeholder="请选择工程行业" />
|
|
||||||
<SelectIcon as-child>
|
|
||||||
<ChevronDown class="h-4 w-4 text-muted-foreground" />
|
|
||||||
</SelectIcon>
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectPortal>
|
|
||||||
<SelectContent
|
|
||||||
:side-offset="6"
|
|
||||||
position="popper"
|
|
||||||
class="z-[120] w-[var(--reka-select-trigger-width)] overflow-hidden rounded-xl border border-border bg-popover text-popover-foreground shadow-xl"
|
|
||||||
>
|
|
||||||
<SelectViewport class="p-1">
|
|
||||||
<SelectItem
|
|
||||||
v-for="item in industryTypeList"
|
|
||||||
:key="`project-${item.id}`"
|
|
||||||
:value="String(item.id)"
|
|
||||||
class="relative flex h-9 w-full cursor-default select-none items-center rounded-md pl-3 pr-8 text-sm outline-none data-[highlighted]:bg-muted data-[highlighted]:text-foreground data-[state=checked]:bg-slate-100"
|
|
||||||
>
|
|
||||||
<SelectItemText>{{ item.name }}</SelectItemText>
|
|
||||||
<SelectItemIndicator class="absolute right-2 inline-flex items-center text-slate-700">
|
|
||||||
<Check class="h-4 w-4" />
|
|
||||||
</SelectItemIndicator>
|
|
||||||
</SelectItem>
|
|
||||||
</SelectViewport>
|
|
||||||
</SelectContent>
|
|
||||||
</SelectPortal>
|
|
||||||
</SelectRoot>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center justify-end gap-2 border-t px-5 py-4">
|
|
||||||
<Button variant="outline" @click="closeProjectCalcDialog">取消</Button>
|
|
||||||
<Button :disabled="projectSubmitting || !projectIndustry" @click="confirmProjectCalc">
|
|
||||||
{{ projectSubmitting ? '进入中...' : '进入项目计算' }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="quickDialogOpen"
|
|
||||||
class="fixed inset-0 z-[90] flex items-center justify-center bg-black/45 p-4"
|
|
||||||
@click.self="closeQuickCalcDialog"
|
|
||||||
>
|
|
||||||
<div class="w-full max-w-md rounded-xl border bg-background shadow-2xl">
|
|
||||||
<div class="flex items-center justify-between border-b px-5 py-4">
|
|
||||||
<div>
|
|
||||||
<h3 class="text-base font-semibold text-foreground">快速计算</h3>
|
|
||||||
<p class="mt-1 text-sm text-muted-foreground">填写工程行业和合同名称后,直接进入单合同计算页面。</p>
|
|
||||||
</div>
|
|
||||||
<Button variant="ghost" size="icon" class="h-8 w-8" @click="closeQuickCalcDialog">
|
|
||||||
<X class="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-4 px-5 py-4">
|
|
||||||
<label class="block space-y-2">
|
|
||||||
<span class="text-sm font-medium text-foreground">工程行业</span>
|
|
||||||
<SelectRoot v-model="quickIndustry">
|
|
||||||
<SelectTrigger
|
|
||||||
class="inline-flex h-11 w-full items-center justify-between rounded-xl border border-border/70 bg-[linear-gradient(180deg,rgba(248,250,252,0.98),rgba(241,245,249,0.92))] px-4 text-sm text-foreground shadow-sm outline-none transition hover:border-border focus-visible:ring-2 focus-visible:ring-slate-300"
|
|
||||||
>
|
|
||||||
<SelectValue placeholder="请选择工程行业" />
|
|
||||||
<SelectIcon as-child>
|
|
||||||
<ChevronDown class="h-4 w-4 text-muted-foreground" />
|
|
||||||
</SelectIcon>
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectPortal>
|
|
||||||
<SelectContent
|
|
||||||
:side-offset="6"
|
|
||||||
position="popper"
|
|
||||||
class="z-[120] w-[var(--reka-select-trigger-width)] overflow-hidden rounded-xl border border-border bg-popover text-popover-foreground shadow-xl"
|
|
||||||
>
|
|
||||||
<SelectViewport class="p-1">
|
|
||||||
<SelectItem
|
|
||||||
v-for="item in industryTypeList"
|
|
||||||
:key="`quick-${item.id}`"
|
|
||||||
:value="String(item.id)"
|
|
||||||
class="relative flex h-9 w-full cursor-default select-none items-center rounded-md pl-3 pr-8 text-sm outline-none data-[highlighted]:bg-muted data-[highlighted]:text-foreground data-[state=checked]:bg-slate-100"
|
|
||||||
>
|
|
||||||
<SelectItemText>{{ item.name }}</SelectItemText>
|
|
||||||
<SelectItemIndicator class="absolute right-2 inline-flex items-center text-slate-700">
|
|
||||||
<Check class="h-4 w-4" />
|
|
||||||
</SelectItemIndicator>
|
|
||||||
</SelectItem>
|
|
||||||
</SelectViewport>
|
|
||||||
</SelectContent>
|
|
||||||
</SelectPortal>
|
|
||||||
</SelectRoot>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="block space-y-2">
|
|
||||||
<span class="text-sm font-medium text-foreground">合同名称</span>
|
|
||||||
<input
|
|
||||||
v-model="quickContractName"
|
|
||||||
type="text"
|
|
||||||
maxlength="40"
|
|
||||||
placeholder="请输入合同名称"
|
|
||||||
class="h-10 w-full rounded-md border bg-background px-3 text-sm outline-none transition focus-visible:ring-2 focus-visible:ring-ring"
|
|
||||||
@keydown.enter="confirmQuickCalc"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center justify-end gap-2 border-t px-5 py-4">
|
|
||||||
<Button variant="outline" @click="closeQuickCalcDialog">取消</Button>
|
|
||||||
<Button :disabled="quickSubmitting || !quickIndustry || !quickContractName.trim()" @click="confirmQuickCalc">
|
|
||||||
{{ quickSubmitting ? '进入中...' : '进入快速计算' }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@ -45,24 +45,9 @@ interface DataEntry {
|
|||||||
interface ContractSegmentPackage {
|
interface ContractSegmentPackage {
|
||||||
version: number
|
version: number
|
||||||
exportedAt: string
|
exportedAt: string
|
||||||
packageType?: 'contract-segments'
|
projectIndustry: string
|
||||||
project?: {
|
|
||||||
industry: string
|
|
||||||
}
|
|
||||||
storage?: {
|
|
||||||
localforageEntries: DataEntry[]
|
|
||||||
}
|
|
||||||
contracts: ContractItem[]
|
contracts: ContractItem[]
|
||||||
projectIndustry?: string
|
localforageEntries: DataEntry[]
|
||||||
localforageEntries?: DataEntry[]
|
|
||||||
pinia?: {
|
|
||||||
zxFwPricing?: {
|
|
||||||
contracts?: Record<string, unknown>
|
|
||||||
servicePricingStates?: Record<string, unknown>
|
|
||||||
htFeeMainStates?: Record<string, unknown>
|
|
||||||
htFeeMethodStates?: Record<string, unknown>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
piniaState?: {
|
piniaState?: {
|
||||||
zxFwPricing?: {
|
zxFwPricing?: {
|
||||||
contracts?: Record<string, unknown>
|
contracts?: Record<string, unknown>
|
||||||
@ -114,7 +99,7 @@ interface QuantityMethodStateLike {
|
|||||||
|
|
||||||
const STORAGE_KEY = 'ht-card-v1'
|
const STORAGE_KEY = 'ht-card-v1'
|
||||||
const CONTRACT_SEGMENT_FILE_EXTENSION = '.htzw'
|
const CONTRACT_SEGMENT_FILE_EXTENSION = '.htzw'
|
||||||
const CONTRACT_SEGMENT_VERSION = 3
|
const CONTRACT_SEGMENT_VERSION = 2
|
||||||
const CONTRACT_KEY_PREFIX = 'ht-info-v3-'
|
const CONTRACT_KEY_PREFIX = 'ht-info-v3-'
|
||||||
const SERVICE_KEY_PREFIX = 'zxFW-'
|
const SERVICE_KEY_PREFIX = 'zxFW-'
|
||||||
const CONTRACT_CONSULT_FACTOR_KEY_PREFIX = 'ht-consult-category-factor-v1-'
|
const CONTRACT_CONSULT_FACTOR_KEY_PREFIX = 'ht-consult-category-factor-v1-'
|
||||||
@ -289,7 +274,6 @@ const getCurrentProjectIndustry = async (): Promise<string> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const toFiniteNumber = (value: unknown): number | null => {
|
const toFiniteNumber = (value: unknown): number | null => {
|
||||||
if (value == null || value === '') return null
|
|
||||||
const num = Number(value)
|
const num = Number(value)
|
||||||
return Number.isFinite(num) ? num : null
|
return Number.isFinite(num) ? num : null
|
||||||
}
|
}
|
||||||
@ -623,15 +607,6 @@ const normalizeDataEntries = (value: unknown): DataEntry[] => {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizeContractSegmentPackage = (payload: ContractSegmentPackage) => ({
|
|
||||||
projectIndustry:
|
|
||||||
typeof payload.project?.industry === 'string' && payload.project.industry.trim()
|
|
||||||
? payload.project.industry.trim()
|
|
||||||
: (typeof payload.projectIndustry === 'string' ? payload.projectIndustry.trim() : ''),
|
|
||||||
localforageEntries: normalizeDataEntries(payload.storage?.localforageEntries ?? payload.localforageEntries),
|
|
||||||
piniaState: payload.pinia ?? payload.piniaState
|
|
||||||
})
|
|
||||||
|
|
||||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||||
Boolean(value && typeof value === 'object' && !Array.isArray(value))
|
Boolean(value && typeof value === 'object' && !Array.isArray(value))
|
||||||
|
|
||||||
@ -824,16 +799,11 @@ const exportSelectedContracts = async () => {
|
|||||||
const now = new Date()
|
const now = new Date()
|
||||||
const payload: ContractSegmentPackage = {
|
const payload: ContractSegmentPackage = {
|
||||||
version: CONTRACT_SEGMENT_VERSION,
|
version: CONTRACT_SEGMENT_VERSION,
|
||||||
packageType: 'contract-segments',
|
|
||||||
exportedAt: now.toISOString(),
|
exportedAt: now.toISOString(),
|
||||||
project: {
|
projectIndustry,
|
||||||
industry: projectIndustry
|
|
||||||
},
|
|
||||||
contracts: selectedContracts,
|
contracts: selectedContracts,
|
||||||
storage: {
|
localforageEntries,
|
||||||
localforageEntries
|
piniaState: {
|
||||||
},
|
|
||||||
pinia: {
|
|
||||||
zxFwPricing: piniaPayload
|
zxFwPricing: piniaPayload
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -875,17 +845,16 @@ const importContractSegments = async (event: Event) => {
|
|||||||
if (!isContractSegmentPackage(payload)) {
|
if (!isContractSegmentPackage(payload)) {
|
||||||
throw new Error('INVALID_CONTRACT_SEGMENT_PAYLOAD')
|
throw new Error('INVALID_CONTRACT_SEGMENT_PAYLOAD')
|
||||||
}
|
}
|
||||||
const normalizedPackage = normalizeContractSegmentPackage(payload)
|
|
||||||
|
|
||||||
const currentProjectIndustry = await getCurrentProjectIndustry()
|
const currentProjectIndustry = await getCurrentProjectIndustry()
|
||||||
if (!currentProjectIndustry) {
|
if (!currentProjectIndustry) {
|
||||||
throw new Error('CURRENT_PROJECT_INDUSTRY_MISSING')
|
throw new Error('CURRENT_PROJECT_INDUSTRY_MISSING')
|
||||||
}
|
}
|
||||||
if (!normalizedPackage.projectIndustry) {
|
if (typeof payload.projectIndustry !== 'string' || !payload.projectIndustry.trim()) {
|
||||||
throw new Error('IMPORT_PACKAGE_INDUSTRY_MISSING')
|
throw new Error('IMPORT_PACKAGE_INDUSTRY_MISSING')
|
||||||
}
|
}
|
||||||
if (normalizedPackage.projectIndustry !== currentProjectIndustry) {
|
if (payload.projectIndustry.trim() !== currentProjectIndustry) {
|
||||||
throw new Error(`PROJECT_INDUSTRY_MISMATCH:${normalizedPackage.projectIndustry}:${currentProjectIndustry}`)
|
throw new Error(`PROJECT_INDUSTRY_MISMATCH:${payload.projectIndustry.trim()}:${currentProjectIndustry}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const importedContracts = normalizeContractsFromPayload(payload.contracts)
|
const importedContracts = normalizeContractsFromPayload(payload.contracts)
|
||||||
@ -893,7 +862,7 @@ const importContractSegments = async (event: Event) => {
|
|||||||
throw new Error('EMPTY_CONTRACTS')
|
throw new Error('EMPTY_CONTRACTS')
|
||||||
}
|
}
|
||||||
|
|
||||||
const importedEntries = normalizedPackage.localforageEntries
|
const importedEntries = normalizeDataEntries(payload.localforageEntries)
|
||||||
const usedIds = new Set(contracts.value.map(item => item.id))
|
const usedIds = new Set(contracts.value.map(item => item.id))
|
||||||
const oldToNewIdMap = new Map<string, string>()
|
const oldToNewIdMap = new Map<string, string>()
|
||||||
const nextContracts: ContractItem[] = importedContracts.map((item, index) => {
|
const nextContracts: ContractItem[] = importedContracts.map((item, index) => {
|
||||||
@ -920,7 +889,7 @@ const importContractSegments = async (event: Event) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
await Promise.all(rewrittenEntries.map(entry => kvStore.setItem(entry.key, entry.value)))
|
await Promise.all(rewrittenEntries.map(entry => kvStore.setItem(entry.key, entry.value)))
|
||||||
await applyImportedContractPiniaPayload(normalizedPackage.piniaState, oldToNewIdMap)
|
await applyImportedContractPiniaPayload(payload.piniaState, oldToNewIdMap)
|
||||||
|
|
||||||
contracts.value = [...contracts.value, ...nextContracts]
|
contracts.value = [...contracts.value, ...nextContracts]
|
||||||
await saveContracts()
|
await saveContracts()
|
||||||
|
|||||||
@ -6,8 +6,6 @@ import { useKvStore } from '@/pinia/kv'
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
contractId: string
|
contractId: string
|
||||||
projectInfoKey?: string
|
|
||||||
parentStorageKey?: string
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
interface XmBaseInfoState {
|
interface XmBaseInfoState {
|
||||||
@ -22,13 +20,13 @@ type ServiceItem = {
|
|||||||
notshowByzxflxs?: boolean
|
notshowByzxflxs?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const PROJECT_INFO_KEY = computed(() => props.projectInfoKey || 'xm-base-info-v1')
|
const PROJECT_INFO_KEY = 'xm-base-info-v1'
|
||||||
const projectIndustry = ref('')
|
const projectIndustry = ref('')
|
||||||
const kvStore = useKvStore()
|
const kvStore = useKvStore()
|
||||||
|
|
||||||
const loadProjectIndustry = async () => {
|
const loadProjectIndustry = async () => {
|
||||||
try {
|
try {
|
||||||
const data = await kvStore.getItem<XmBaseInfoState>(PROJECT_INFO_KEY.value)
|
const data = await kvStore.getItem<XmBaseInfoState>(PROJECT_INFO_KEY)
|
||||||
projectIndustry.value =
|
projectIndustry.value =
|
||||||
typeof data?.projectIndustry === 'string' ? data.projectIndustry.trim() : ''
|
typeof data?.projectIndustry === 'string' ? data.projectIndustry.trim() : ''
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -59,7 +57,7 @@ onActivated(() => {
|
|||||||
<XmFactorGrid
|
<XmFactorGrid
|
||||||
title="咨询分类系数明细"
|
title="咨询分类系数明细"
|
||||||
:storage-key="`ht-consult-category-factor-v1-${props.contractId}`"
|
:storage-key="`ht-consult-category-factor-v1-${props.contractId}`"
|
||||||
:parent-storage-key="props.parentStorageKey || 'xm-consult-category-factor-v1'"
|
parent-storage-key="xm-consult-category-factor-v1"
|
||||||
:dict="filteredServiceDict"
|
:dict="filteredServiceDict"
|
||||||
:disable-budget-edit-when-standard-null="true"
|
:disable-budget-edit-when-standard-null="true"
|
||||||
:exclude-notshow-by-zxflxs="true"
|
:exclude-notshow-by-zxflxs="true"
|
||||||
|
|||||||
@ -18,17 +18,15 @@ type MajorItem = {
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
contractId: string
|
contractId: string
|
||||||
projectInfoKey?: string
|
|
||||||
parentStorageKey?: string
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const PROJECT_INFO_KEY = computed(() => props.projectInfoKey || 'xm-base-info-v1')
|
const PROJECT_INFO_KEY = 'xm-base-info-v1'
|
||||||
const projectIndustry = ref('')
|
const projectIndustry = ref('')
|
||||||
const kvStore = useKvStore()
|
const kvStore = useKvStore()
|
||||||
|
|
||||||
const loadProjectIndustry = async () => {
|
const loadProjectIndustry = async () => {
|
||||||
try {
|
try {
|
||||||
const data = await kvStore.getItem<XmBaseInfoState>(PROJECT_INFO_KEY.value)
|
const data = await kvStore.getItem<XmBaseInfoState>(PROJECT_INFO_KEY)
|
||||||
projectIndustry.value =
|
projectIndustry.value =
|
||||||
typeof data?.projectIndustry === 'string' ? data.projectIndustry.trim() : ''
|
typeof data?.projectIndustry === 'string' ? data.projectIndustry.trim() : ''
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -59,7 +57,7 @@ onActivated(() => {
|
|||||||
<XmFactorGrid
|
<XmFactorGrid
|
||||||
title="工程专业系数明细"
|
title="工程专业系数明细"
|
||||||
:storage-key="`ht-major-factor-v1-${props.contractId}`"
|
:storage-key="`ht-major-factor-v1-${props.contractId}`"
|
||||||
:parent-storage-key="props.parentStorageKey || 'xm-major-factor-v1'"
|
parent-storage-key="xm-major-factor-v1"
|
||||||
:dict="filteredMajorDict"
|
:dict="filteredMajorDict"
|
||||||
:disable-budget-edit-when-standard-null="true"
|
:disable-budget-edit-when-standard-null="true"
|
||||||
:exclude-notshow-by-zxflxs="true"
|
:exclude-notshow-by-zxflxs="true"
|
||||||
|
|||||||
@ -1,17 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { onActivated, onMounted } from 'vue'
|
|
||||||
import XmCard from '@/components/views/xmCard.vue'
|
|
||||||
import { writeWorkspaceMode } from '@/lib/workspace'
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
writeWorkspaceMode('project')
|
|
||||||
})
|
|
||||||
|
|
||||||
onActivated(() => {
|
|
||||||
writeWorkspaceMode('project')
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<XmCard />
|
|
||||||
</template>
|
|
||||||
@ -1,413 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { computed, onActivated, onMounted, ref, watch } from 'vue'
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { useKvStore } from '@/pinia/kv'
|
|
||||||
import { useTabStore } from '@/pinia/tab'
|
|
||||||
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
|
|
||||||
import { ArrowRight, Calculator, PencilLine } from 'lucide-vue-next'
|
|
||||||
import { industryTypeList } from '@/sql'
|
|
||||||
import { formatThousands } from '@/lib/numberFormat'
|
|
||||||
import { roundTo } from '@/lib/decimal'
|
|
||||||
import { initializeProjectFactorStates } from '@/lib/projectWorkspace'
|
|
||||||
import {
|
|
||||||
QUICK_CONSULT_CATEGORY_FACTOR_KEY,
|
|
||||||
QUICK_CONTRACT_FALLBACK_NAME,
|
|
||||||
QUICK_CONTRACT_ID,
|
|
||||||
QUICK_CONTRACT_META_KEY,
|
|
||||||
QUICK_MAJOR_FACTOR_KEY,
|
|
||||||
QUICK_PROJECT_INFO_KEY,
|
|
||||||
QUICK_PROJECT_SCALE_KEY,
|
|
||||||
createDefaultQuickContractMeta,
|
|
||||||
writeWorkspaceMode
|
|
||||||
} from '@/lib/workspace'
|
|
||||||
|
|
||||||
interface QuickProjectInfoState {
|
|
||||||
projectIndustry?: string
|
|
||||||
projectName?: string
|
|
||||||
preparedBy?: string
|
|
||||||
reviewedBy?: string
|
|
||||||
preparedCompany?: string
|
|
||||||
preparedDate?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface QuickContractMetaState {
|
|
||||||
id?: string
|
|
||||||
name?: string
|
|
||||||
updatedAt?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface HtFeeMainRowLike {
|
|
||||||
id?: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RateMethodStateLike {
|
|
||||||
budgetFee?: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
interface HourlyMethodRowLike {
|
|
||||||
serviceBudget?: unknown
|
|
||||||
adoptedBudgetUnitPrice?: unknown
|
|
||||||
personnelCount?: unknown
|
|
||||||
workdayCount?: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
interface HourlyMethodStateLike {
|
|
||||||
detailRows?: HourlyMethodRowLike[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface QuantityMethodRowLike {
|
|
||||||
id?: unknown
|
|
||||||
budgetFee?: unknown
|
|
||||||
quantity?: unknown
|
|
||||||
unitPrice?: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
interface QuantityMethodStateLike {
|
|
||||||
detailRows?: QuantityMethodRowLike[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const kvStore = useKvStore()
|
|
||||||
const tabStore = useTabStore()
|
|
||||||
const zxFwPricingStore = useZxFwPricingStore()
|
|
||||||
|
|
||||||
const contractName = ref(QUICK_CONTRACT_FALLBACK_NAME)
|
|
||||||
const projectIndustry = ref(String(industryTypeList[0]?.id || ''))
|
|
||||||
const quickBudget = ref<number | null>(null)
|
|
||||||
const savingIndustry = ref(false)
|
|
||||||
let budgetRefreshTimer: ReturnType<typeof setTimeout> | null = null
|
|
||||||
|
|
||||||
const availableIndustries = computed(() =>
|
|
||||||
industryTypeList.map(item => ({
|
|
||||||
id: String(item.id),
|
|
||||||
name: item.name
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
|
|
||||||
const formatBudgetAmount = (value: number | null | undefined) =>
|
|
||||||
typeof value === 'number' && Number.isFinite(value) ? `${formatThousands(value, 2)} 元` : '--'
|
|
||||||
|
|
||||||
const toFiniteNumber = (value: unknown): number | null => {
|
|
||||||
if (value == null || value === '') return null
|
|
||||||
const num = Number(value)
|
|
||||||
return Number.isFinite(num) ? num : null
|
|
||||||
}
|
|
||||||
|
|
||||||
const sumHourlyMethodFee = (state: HourlyMethodStateLike | null): number | null => {
|
|
||||||
const rows = Array.isArray(state?.detailRows) ? state.detailRows : []
|
|
||||||
if (rows.length === 0) return null
|
|
||||||
let hasValid = false
|
|
||||||
let total = 0
|
|
||||||
for (const row of rows) {
|
|
||||||
const serviceBudget = toFiniteNumber(row?.serviceBudget)
|
|
||||||
if (serviceBudget != null) {
|
|
||||||
total += serviceBudget
|
|
||||||
hasValid = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const adopted = toFiniteNumber(row?.adoptedBudgetUnitPrice)
|
|
||||||
const personnel = toFiniteNumber(row?.personnelCount)
|
|
||||||
const workday = toFiniteNumber(row?.workdayCount)
|
|
||||||
if (adopted == null || personnel == null || workday == null) continue
|
|
||||||
total += adopted * personnel * workday
|
|
||||||
hasValid = true
|
|
||||||
}
|
|
||||||
return hasValid ? roundTo(total, 2) : null
|
|
||||||
}
|
|
||||||
|
|
||||||
const sumQuantityMethodFee = (state: QuantityMethodStateLike | null): number | null => {
|
|
||||||
const rows = Array.isArray(state?.detailRows) ? state.detailRows : []
|
|
||||||
if (rows.length === 0) return null
|
|
||||||
const subtotalRow = rows.find(row => String(row?.id || '') === 'fee-subtotal-fixed')
|
|
||||||
const subtotal = toFiniteNumber(subtotalRow?.budgetFee)
|
|
||||||
if (subtotal != null) return roundTo(subtotal, 2)
|
|
||||||
|
|
||||||
let hasValid = false
|
|
||||||
let total = 0
|
|
||||||
for (const row of rows) {
|
|
||||||
if (String(row?.id || '') === 'fee-subtotal-fixed') continue
|
|
||||||
const budget = toFiniteNumber(row?.budgetFee)
|
|
||||||
if (budget != null) {
|
|
||||||
total += budget
|
|
||||||
hasValid = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const quantity = toFiniteNumber(row?.quantity)
|
|
||||||
const unitPrice = toFiniteNumber(row?.unitPrice)
|
|
||||||
if (quantity == null || unitPrice == null) continue
|
|
||||||
total += quantity * unitPrice
|
|
||||||
hasValid = true
|
|
||||||
}
|
|
||||||
return hasValid ? roundTo(total, 2) : null
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadHtMethodTotalByRow = async (mainStorageKey: string, rowId: string) => {
|
|
||||||
const [rateState, hourlyState, quantityState] = await Promise.all([
|
|
||||||
zxFwPricingStore.loadHtFeeMethodState<RateMethodStateLike>(mainStorageKey, rowId, 'rate-fee'),
|
|
||||||
zxFwPricingStore.loadHtFeeMethodState<HourlyMethodStateLike>(mainStorageKey, rowId, 'hourly-fee'),
|
|
||||||
zxFwPricingStore.loadHtFeeMethodState<QuantityMethodStateLike>(mainStorageKey, rowId, 'quantity-unit-price-fee')
|
|
||||||
])
|
|
||||||
const parts = [
|
|
||||||
toFiniteNumber(rateState?.budgetFee),
|
|
||||||
sumHourlyMethodFee(hourlyState),
|
|
||||||
sumQuantityMethodFee(quantityState)
|
|
||||||
]
|
|
||||||
const validParts = parts.filter((item): item is number => typeof item === 'number' && Number.isFinite(item))
|
|
||||||
if (validParts.length === 0) return null
|
|
||||||
return roundTo(validParts.reduce((sum, value) => sum + value, 0), 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadHtMainTotalFee = async (mainStorageKey: string) => {
|
|
||||||
const mainState = await zxFwPricingStore.loadHtFeeMainState<HtFeeMainRowLike>(mainStorageKey)
|
|
||||||
const rows = Array.isArray(mainState?.detailRows) ? mainState.detailRows : []
|
|
||||||
const rowIds = rows.map(row => String(row?.id || '').trim()).filter(Boolean)
|
|
||||||
if (rowIds.length === 0) return null
|
|
||||||
const rowTotals = await Promise.all(rowIds.map(rowId => loadHtMethodTotalByRow(mainStorageKey, rowId)))
|
|
||||||
const validTotals = rowTotals.filter((item): item is number => typeof item === 'number' && Number.isFinite(item))
|
|
||||||
if (validTotals.length === 0) return null
|
|
||||||
return roundTo(validTotals.reduce((sum, value) => sum + value, 0), 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
const refreshQuickBudget = async () => {
|
|
||||||
await zxFwPricingStore.loadContract(QUICK_CONTRACT_ID)
|
|
||||||
const serviceFee = zxFwPricingStore.getBaseSubtotal(QUICK_CONTRACT_ID)
|
|
||||||
const [additionalFee, reserveFee] = await Promise.all([
|
|
||||||
loadHtMainTotalFee(`htExtraFee-${QUICK_CONTRACT_ID}-additional-work`),
|
|
||||||
loadHtMainTotalFee(`htExtraFee-${QUICK_CONTRACT_ID}-reserve`)
|
|
||||||
])
|
|
||||||
const parts = [serviceFee, additionalFee, reserveFee]
|
|
||||||
const validParts = parts.filter((item): item is number => typeof item === 'number' && Number.isFinite(item))
|
|
||||||
quickBudget.value = validParts.length === 0 ? null : roundTo(validParts.reduce((sum, value) => sum + value, 0), 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
const scheduleRefreshQuickBudget = () => {
|
|
||||||
if (budgetRefreshTimer) clearTimeout(budgetRefreshTimer)
|
|
||||||
budgetRefreshTimer = setTimeout(() => {
|
|
||||||
void refreshQuickBudget()
|
|
||||||
}, 80)
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizeQuickContractMeta = (value: QuickContractMetaState | null) => ({
|
|
||||||
id: typeof value?.id === 'string' && value.id.trim() ? value.id.trim() : QUICK_CONTRACT_ID,
|
|
||||||
name: typeof value?.name === 'string' && value.name.trim() ? value.name.trim() : QUICK_CONTRACT_FALLBACK_NAME,
|
|
||||||
updatedAt:
|
|
||||||
typeof value?.updatedAt === 'string' && value.updatedAt.trim()
|
|
||||||
? value.updatedAt
|
|
||||||
: new Date().toISOString()
|
|
||||||
})
|
|
||||||
|
|
||||||
const ensureQuickWorkspaceReady = async () => {
|
|
||||||
const [savedInfo, savedMeta] = await Promise.all([
|
|
||||||
kvStore.getItem<QuickProjectInfoState>(QUICK_PROJECT_INFO_KEY),
|
|
||||||
kvStore.getItem<QuickContractMetaState>(QUICK_CONTRACT_META_KEY)
|
|
||||||
])
|
|
||||||
|
|
||||||
const defaultIndustry = String(industryTypeList[0]?.id || '')
|
|
||||||
const nextIndustry =
|
|
||||||
typeof savedInfo?.projectIndustry === 'string' && savedInfo.projectIndustry.trim()
|
|
||||||
? savedInfo.projectIndustry.trim()
|
|
||||||
: defaultIndustry
|
|
||||||
|
|
||||||
projectIndustry.value = nextIndustry
|
|
||||||
contractName.value = normalizeQuickContractMeta(savedMeta).name
|
|
||||||
|
|
||||||
await kvStore.setItem(QUICK_PROJECT_INFO_KEY, {
|
|
||||||
projectIndustry: nextIndustry,
|
|
||||||
projectName: '快速计算'
|
|
||||||
})
|
|
||||||
|
|
||||||
const consultState = await kvStore.getItem(QUICK_CONSULT_CATEGORY_FACTOR_KEY)
|
|
||||||
const majorState = await kvStore.getItem(QUICK_MAJOR_FACTOR_KEY)
|
|
||||||
if (!consultState || !majorState) {
|
|
||||||
await initializeProjectFactorStates(
|
|
||||||
kvStore,
|
|
||||||
nextIndustry,
|
|
||||||
QUICK_CONSULT_CATEGORY_FACTOR_KEY,
|
|
||||||
QUICK_MAJOR_FACTOR_KEY
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
await kvStore.setItem(QUICK_CONTRACT_META_KEY, {
|
|
||||||
...createDefaultQuickContractMeta(),
|
|
||||||
...normalizeQuickContractMeta(savedMeta)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const persistQuickContractMeta = async () => {
|
|
||||||
await kvStore.setItem(QUICK_CONTRACT_META_KEY, {
|
|
||||||
id: QUICK_CONTRACT_ID,
|
|
||||||
name: contractName.value.trim() || QUICK_CONTRACT_FALLBACK_NAME,
|
|
||||||
updatedAt: new Date().toISOString()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const persistQuickIndustry = async (industry: string) => {
|
|
||||||
if (!industry) return
|
|
||||||
savingIndustry.value = true
|
|
||||||
try {
|
|
||||||
const current = await kvStore.getItem<QuickProjectInfoState>(QUICK_PROJECT_INFO_KEY)
|
|
||||||
await kvStore.setItem(QUICK_PROJECT_INFO_KEY, {
|
|
||||||
...current,
|
|
||||||
projectIndustry: industry,
|
|
||||||
projectName: '快速计算'
|
|
||||||
})
|
|
||||||
await initializeProjectFactorStates(
|
|
||||||
kvStore,
|
|
||||||
industry,
|
|
||||||
QUICK_CONSULT_CATEGORY_FACTOR_KEY,
|
|
||||||
QUICK_MAJOR_FACTOR_KEY
|
|
||||||
)
|
|
||||||
} finally {
|
|
||||||
savingIndustry.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const openQuickContract = () => {
|
|
||||||
writeWorkspaceMode('quick')
|
|
||||||
tabStore.openTab({
|
|
||||||
id: `contract-${QUICK_CONTRACT_ID}`,
|
|
||||||
title: `快速计算-${contractName.value.trim() || QUICK_CONTRACT_FALLBACK_NAME}`,
|
|
||||||
componentName: 'ContractDetailView',
|
|
||||||
props: {
|
|
||||||
contractId: QUICK_CONTRACT_ID,
|
|
||||||
contractName: contractName.value.trim() || QUICK_CONTRACT_FALLBACK_NAME,
|
|
||||||
projectInfoKey: QUICK_PROJECT_INFO_KEY,
|
|
||||||
projectScaleKey: QUICK_PROJECT_SCALE_KEY,
|
|
||||||
projectConsultCategoryFactorKey: QUICK_CONSULT_CATEGORY_FACTOR_KEY,
|
|
||||||
projectMajorFactorKey: QUICK_MAJOR_FACTOR_KEY
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => contractName.value,
|
|
||||||
() => {
|
|
||||||
void persistQuickContractMeta()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => projectIndustry.value,
|
|
||||||
nextIndustry => {
|
|
||||||
if (!nextIndustry) return
|
|
||||||
void persistQuickIndustry(nextIndustry)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => [
|
|
||||||
zxFwPricingStore.contractVersions[QUICK_CONTRACT_ID] || 0,
|
|
||||||
zxFwPricingStore.getKeyVersion(`htExtraFee-${QUICK_CONTRACT_ID}-additional-work`),
|
|
||||||
zxFwPricingStore.getKeyVersion(`htExtraFee-${QUICK_CONTRACT_ID}-reserve`),
|
|
||||||
Object.entries(zxFwPricingStore.keyVersions)
|
|
||||||
.filter(([key]) =>
|
|
||||||
key.startsWith(`htExtraFee-${QUICK_CONTRACT_ID}-additional-work-`) ||
|
|
||||||
key.startsWith(`htExtraFee-${QUICK_CONTRACT_ID}-reserve-`)
|
|
||||||
)
|
|
||||||
.map(([key, version]) => `${key}:${version}`)
|
|
||||||
.join('|')
|
|
||||||
],
|
|
||||||
scheduleRefreshQuickBudget
|
|
||||||
)
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
writeWorkspaceMode('quick')
|
|
||||||
await ensureQuickWorkspaceReady()
|
|
||||||
await refreshQuickBudget()
|
|
||||||
})
|
|
||||||
|
|
||||||
onActivated(() => {
|
|
||||||
writeWorkspaceMode('quick')
|
|
||||||
scheduleRefreshQuickBudget()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="mx-auto flex h-full w-full max-w-6xl flex-col gap-6">
|
|
||||||
<div class="grid gap-4 lg:grid-cols-[minmax(0,1.25fr)_minmax(320px,0.75fr)]">
|
|
||||||
<Card class="border-border/70">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle class="flex items-center gap-2 text-2xl">
|
|
||||||
<Calculator class="h-5 w-5" />
|
|
||||||
快速计算
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
保留一个默认合同卡片,不再经过项目卡片入口,直接进入单合同预算费用计算。
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent class="grid gap-4 md:grid-cols-2">
|
|
||||||
<label class="space-y-2">
|
|
||||||
<span class="text-sm font-medium text-foreground">工程行业</span>
|
|
||||||
<select
|
|
||||||
v-model="projectIndustry"
|
|
||||||
class="h-11 w-full rounded-md border bg-background px-3 text-sm outline-none transition focus-visible:ring-2 focus-visible:ring-ring"
|
|
||||||
>
|
|
||||||
<option v-for="item in availableIndustries" :key="item.id" :value="item.id">
|
|
||||||
{{ item.name }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label class="space-y-2">
|
|
||||||
<span class="text-sm font-medium text-foreground">合同名称</span>
|
|
||||||
<div class="relative">
|
|
||||||
<PencilLine class="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
||||||
<input
|
|
||||||
v-model="contractName"
|
|
||||||
type="text"
|
|
||||||
maxlength="40"
|
|
||||||
class="h-11 w-full rounded-md border bg-background pl-9 pr-3 text-sm outline-none transition focus-visible:ring-2 focus-visible:ring-ring"
|
|
||||||
placeholder="请输入合同名称"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card class="border-border/70 bg-muted/25">
|
|
||||||
<CardHeader class="pb-3">
|
|
||||||
<CardTitle class="text-base">当前状态</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent class="grid gap-2 text-sm text-muted-foreground">
|
|
||||||
<div>模式:单合同快速计算</div>
|
|
||||||
<div>合同ID:{{ QUICK_CONTRACT_ID }}</div>
|
|
||||||
<div>行业切换会同步重建快速计算专用系数基线</div>
|
|
||||||
<div>{{ savingIndustry ? '正在切换行业并刷新系数...' : '行业与合同名称已自动保存' }}</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card
|
|
||||||
class="group cursor-pointer border-border/70 transition-colors hover:border-primary"
|
|
||||||
@click="openQuickContract"
|
|
||||||
>
|
|
||||||
<CardHeader class="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
|
||||||
<div class="space-y-2">
|
|
||||||
<CardTitle class="text-xl">
|
|
||||||
{{ contractName.trim() || QUICK_CONTRACT_FALLBACK_NAME }}
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>默认单合同卡片,点击后进入预算费用计算详情。</CardDescription>
|
|
||||||
</div>
|
|
||||||
<Button class="shrink-0 md:self-center" @click.stop="openQuickContract">
|
|
||||||
进入计算
|
|
||||||
<ArrowRight class="ml-1 h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent class="grid gap-4 border-t pt-5 text-sm text-muted-foreground md:grid-cols-3">
|
|
||||||
<div>
|
|
||||||
<div class="text-xs uppercase tracking-[0.24em] text-muted-foreground/80">合同ID</div>
|
|
||||||
<div class="mt-2 break-all text-foreground">{{ QUICK_CONTRACT_ID }}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="text-xs uppercase tracking-[0.24em] text-muted-foreground/80">预算费用</div>
|
|
||||||
<div class="mt-2 text-foreground">{{ formatBudgetAmount(quickBudget) }}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="text-xs uppercase tracking-[0.24em] text-muted-foreground/80">行业</div>
|
|
||||||
<div class="mt-2 text-foreground">
|
|
||||||
{{ availableIndustries.find(item => item.id === projectIndustry)?.name || '--' }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@ -28,7 +28,6 @@ const props = defineProps<{
|
|||||||
serviceId: string|number
|
serviceId: string|number
|
||||||
fwName:string
|
fwName:string
|
||||||
type?: ServiceMethodType
|
type?: ServiceMethodType
|
||||||
projectInfoKey?: string
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
interface PricingCategoryItem {
|
interface PricingCategoryItem {
|
||||||
@ -65,11 +64,7 @@ const createPricingPane = (name: string) =>
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return () => h(AsyncPricingView, {
|
return () => h(AsyncPricingView, { contractId: props.contractId, serviceId: props.serviceId })
|
||||||
contractId: props.contractId,
|
|
||||||
serviceId: props.serviceId,
|
|
||||||
projectInfoKey: props.projectInfoKey
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
@ -4,7 +4,6 @@
|
|||||||
scene="ht-tab"
|
scene="ht-tab"
|
||||||
:title="`合同段:${contractName}`"
|
:title="`合同段:${contractName}`"
|
||||||
:subtitle="`合同段ID:${contractId}`"
|
:subtitle="`合同段ID:${contractId}`"
|
||||||
:meta-text="`合同段预算金额:${formatBudgetAmount(contractBudget)}`"
|
|
||||||
:copy-text="contractId"
|
:copy-text="contractId"
|
||||||
:storage-key="`project-active-cat-${contractId}`"
|
:storage-key="`project-active-cat-${contractId}`"
|
||||||
default-category="info"
|
default-category="info"
|
||||||
@ -13,170 +12,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, markRaw, defineAsyncComponent, defineComponent, h, onActivated, onBeforeUnmount, onMounted, ref, watch, type Component } from 'vue';
|
import { markRaw, defineAsyncComponent, defineComponent, h, type Component } from 'vue';
|
||||||
import TypeLine from '@/layout/typeLine.vue';
|
import TypeLine from '@/layout/typeLine.vue';
|
||||||
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
|
|
||||||
import { roundTo } from '@/lib/decimal'
|
|
||||||
import { formatThousands } from '@/lib/numberFormat'
|
|
||||||
|
|
||||||
// 1. 完善 Props 类型 + 添加校验(可选但推荐)
|
// 1. 完善 Props 类型 + 添加校验(可选但推荐)
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
contractId: string; // 合同ID(必传)
|
contractId: string; // 合同ID(必传)
|
||||||
contractName: string; // 合同名称(必传)
|
contractName: string; // 合同名称(必传)
|
||||||
projectInfoKey?: string; // 工作区基础信息键
|
|
||||||
projectScaleKey?: string | null; // 工作区规模信息键
|
|
||||||
projectConsultCategoryFactorKey?: string; // 工作区咨询分类系数键
|
|
||||||
projectMajorFactorKey?: string; // 工作区工程专业系数键
|
|
||||||
}>();
|
}>();
|
||||||
const zxFwPricingStore = useZxFwPricingStore()
|
|
||||||
|
|
||||||
interface HtFeeMainRowLike {
|
|
||||||
id?: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RateMethodStateLike {
|
|
||||||
budgetFee?: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
interface HourlyMethodRowLike {
|
|
||||||
serviceBudget?: unknown
|
|
||||||
adoptedBudgetUnitPrice?: unknown
|
|
||||||
personnelCount?: unknown
|
|
||||||
workdayCount?: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
interface HourlyMethodStateLike {
|
|
||||||
detailRows?: HourlyMethodRowLike[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface QuantityMethodRowLike {
|
|
||||||
id?: unknown
|
|
||||||
budgetFee?: unknown
|
|
||||||
quantity?: unknown
|
|
||||||
unitPrice?: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
interface QuantityMethodStateLike {
|
|
||||||
detailRows?: QuantityMethodRowLike[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const contractBudget = ref<number | null>(null)
|
|
||||||
let budgetRefreshTimer: ReturnType<typeof setTimeout> | null = null
|
|
||||||
|
|
||||||
const toFiniteNumber = (value: unknown): number | null => {
|
|
||||||
if (value == null || value === '') return null
|
|
||||||
const num = Number(value)
|
|
||||||
return Number.isFinite(num) ? num : null
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatBudgetAmount = (value: number | null | undefined) =>
|
|
||||||
typeof value === 'number' && Number.isFinite(value) ? `${formatThousands(value, 2)} 元` : '--'
|
|
||||||
|
|
||||||
const sumHourlyMethodFee = (state: HourlyMethodStateLike | null): number | null => {
|
|
||||||
const rows = Array.isArray(state?.detailRows) ? state.detailRows : []
|
|
||||||
if (rows.length === 0) return null
|
|
||||||
let hasValid = false
|
|
||||||
let total = 0
|
|
||||||
for (const row of rows) {
|
|
||||||
const serviceBudget = toFiniteNumber(row?.serviceBudget)
|
|
||||||
if (serviceBudget != null) {
|
|
||||||
total += serviceBudget
|
|
||||||
hasValid = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const adopted = toFiniteNumber(row?.adoptedBudgetUnitPrice)
|
|
||||||
const personnel = toFiniteNumber(row?.personnelCount)
|
|
||||||
const workday = toFiniteNumber(row?.workdayCount)
|
|
||||||
if (adopted == null || personnel == null || workday == null) continue
|
|
||||||
total += adopted * personnel * workday
|
|
||||||
hasValid = true
|
|
||||||
}
|
|
||||||
return hasValid ? roundTo(total, 2) : null
|
|
||||||
}
|
|
||||||
|
|
||||||
const sumQuantityMethodFee = (state: QuantityMethodStateLike | null): number | null => {
|
|
||||||
const rows = Array.isArray(state?.detailRows) ? state.detailRows : []
|
|
||||||
if (rows.length === 0) return null
|
|
||||||
const subtotalRow = rows.find(row => String(row?.id || '') === 'fee-subtotal-fixed')
|
|
||||||
const subtotal = toFiniteNumber(subtotalRow?.budgetFee)
|
|
||||||
if (subtotal != null) return roundTo(subtotal, 2)
|
|
||||||
let hasValid = false
|
|
||||||
let total = 0
|
|
||||||
for (const row of rows) {
|
|
||||||
if (String(row?.id || '') === 'fee-subtotal-fixed') continue
|
|
||||||
const budget = toFiniteNumber(row?.budgetFee)
|
|
||||||
if (budget != null) {
|
|
||||||
total += budget
|
|
||||||
hasValid = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const quantity = toFiniteNumber(row?.quantity)
|
|
||||||
const unitPrice = toFiniteNumber(row?.unitPrice)
|
|
||||||
if (quantity == null || unitPrice == null) continue
|
|
||||||
total += quantity * unitPrice
|
|
||||||
hasValid = true
|
|
||||||
}
|
|
||||||
return hasValid ? roundTo(total, 2) : null
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadHtMethodTotalByRow = async (mainStorageKey: string, rowId: string) => {
|
|
||||||
const [rateState, hourlyState, quantityState] = await Promise.all([
|
|
||||||
zxFwPricingStore.loadHtFeeMethodState<RateMethodStateLike>(mainStorageKey, rowId, 'rate-fee'),
|
|
||||||
zxFwPricingStore.loadHtFeeMethodState<HourlyMethodStateLike>(mainStorageKey, rowId, 'hourly-fee'),
|
|
||||||
zxFwPricingStore.loadHtFeeMethodState<QuantityMethodStateLike>(mainStorageKey, rowId, 'quantity-unit-price-fee')
|
|
||||||
])
|
|
||||||
const parts = [
|
|
||||||
toFiniteNumber(rateState?.budgetFee),
|
|
||||||
sumHourlyMethodFee(hourlyState),
|
|
||||||
sumQuantityMethodFee(quantityState)
|
|
||||||
]
|
|
||||||
const validParts = parts.filter((item): item is number => typeof item === 'number' && Number.isFinite(item))
|
|
||||||
if (validParts.length === 0) return null
|
|
||||||
return roundTo(validParts.reduce((sum, value) => sum + value, 0), 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadHtMainTotalFee = async (mainStorageKey: string) => {
|
|
||||||
const mainState = await zxFwPricingStore.loadHtFeeMainState<HtFeeMainRowLike>(mainStorageKey)
|
|
||||||
const rows = Array.isArray(mainState?.detailRows) ? mainState.detailRows : []
|
|
||||||
const rowIds = rows.map(row => String(row?.id || '').trim()).filter(Boolean)
|
|
||||||
if (rowIds.length === 0) return null
|
|
||||||
const rowTotals = await Promise.all(rowIds.map(rowId => loadHtMethodTotalByRow(mainStorageKey, rowId)))
|
|
||||||
const validTotals = rowTotals.filter((item): item is number => typeof item === 'number' && Number.isFinite(item))
|
|
||||||
if (validTotals.length === 0) return null
|
|
||||||
return roundTo(validTotals.reduce((sum, value) => sum + value, 0), 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
const refreshContractBudget = async () => {
|
|
||||||
await zxFwPricingStore.loadContract(props.contractId)
|
|
||||||
const serviceFee = zxFwPricingStore.getBaseSubtotal(props.contractId)
|
|
||||||
const [additionalFee, reserveFee] = await Promise.all([
|
|
||||||
loadHtMainTotalFee(`htExtraFee-${props.contractId}-additional-work`),
|
|
||||||
loadHtMainTotalFee(`htExtraFee-${props.contractId}-reserve`)
|
|
||||||
])
|
|
||||||
const parts = [serviceFee, additionalFee, reserveFee]
|
|
||||||
const validParts = parts.filter((item): item is number => typeof item === 'number' && Number.isFinite(item))
|
|
||||||
contractBudget.value = validParts.length === 0 ? null : roundTo(validParts.reduce((sum, value) => sum + value, 0), 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
const budgetRefreshSignature = computed(() => {
|
|
||||||
const contractVersion = zxFwPricingStore.contractVersions[props.contractId] || 0
|
|
||||||
const additionalMainKey = `htExtraFee-${props.contractId}-additional-work`
|
|
||||||
const reserveMainKey = `htExtraFee-${props.contractId}-reserve`
|
|
||||||
const keyVersionEntries = Object.entries(zxFwPricingStore.keyVersions)
|
|
||||||
const methodKeySig = keyVersionEntries
|
|
||||||
.filter(([key]) => key.startsWith(`${additionalMainKey}-`) || key.startsWith(`${reserveMainKey}-`))
|
|
||||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
||||||
.map(([key, version]) => `${key}:${version}`)
|
|
||||||
.join(',')
|
|
||||||
return `${contractVersion}:${zxFwPricingStore.getKeyVersion(additionalMainKey)}:${zxFwPricingStore.getKeyVersion(reserveMainKey)}:${methodKeySig}`
|
|
||||||
})
|
|
||||||
|
|
||||||
const scheduleRefreshContractBudget = () => {
|
|
||||||
if (budgetRefreshTimer) clearTimeout(budgetRefreshTimer)
|
|
||||||
budgetRefreshTimer = setTimeout(() => {
|
|
||||||
void refreshContractBudget()
|
|
||||||
}, 80)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 定义分类项的 TS 类型(核心:明确 categories 结构)
|
// 2. 定义分类项的 TS 类型(核心:明确 categories 结构)
|
||||||
interface XmCategoryItem {
|
interface XmCategoryItem {
|
||||||
@ -196,11 +39,7 @@ const htView = markRaw(
|
|||||||
console.error('加载 htInfo 组件失败:', err);
|
console.error('加载 htInfo 组件失败:', err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return () => h(AsyncHtInfo, {
|
return () => h(AsyncHtInfo, { contractId: props.contractId });
|
||||||
contractId: props.contractId,
|
|
||||||
projectScaleKey: props.projectScaleKey,
|
|
||||||
projectInfoKey: props.projectInfoKey
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -215,11 +54,7 @@ const zxfwView = markRaw(
|
|||||||
console.error('加载 zxFw 组件失败:', err);
|
console.error('加载 zxFw 组件失败:', err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return () => h(AsyncZxFw, {
|
return () => h(AsyncZxFw, { contractId: props.contractId, contractName: props.contractName });
|
||||||
contractId: props.contractId,
|
|
||||||
contractName: props.contractName,
|
|
||||||
projectInfoKey: props.projectInfoKey
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -234,11 +69,7 @@ const consultCategoryFactorView = markRaw(
|
|||||||
console.error('加载 HtConsultCategoryFactor 组件失败:', err);
|
console.error('加载 HtConsultCategoryFactor 组件失败:', err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return () => h(AsyncHtConsultCategoryFactor, {
|
return () => h(AsyncHtConsultCategoryFactor, { contractId: props.contractId });
|
||||||
contractId: props.contractId,
|
|
||||||
projectInfoKey: props.projectInfoKey,
|
|
||||||
parentStorageKey: props.projectConsultCategoryFactorKey
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -253,11 +84,7 @@ const majorFactorView = markRaw(
|
|||||||
console.error('加载 HtMajorFactor 组件失败:', err);
|
console.error('加载 HtMajorFactor 组件失败:', err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return () => h(AsyncHtMajorFactor, {
|
return () => h(AsyncHtMajorFactor, { contractId: props.contractId });
|
||||||
contractId: props.contractId,
|
|
||||||
projectInfoKey: props.projectInfoKey,
|
|
||||||
parentStorageKey: props.projectMajorFactorKey
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -303,21 +130,4 @@ const xmCategories: XmCategoryItem[] = [
|
|||||||
{ key: 'reserve-fee', label: '预备费', component: reserveFeeView },
|
{ key: 'reserve-fee', label: '预备费', component: reserveFeeView },
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
||||||
watch(budgetRefreshSignature, (next, prev) => {
|
|
||||||
if (next === prev) return
|
|
||||||
scheduleRefreshContractBudget()
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
void refreshContractBudget()
|
|
||||||
})
|
|
||||||
|
|
||||||
onActivated(() => {
|
|
||||||
void refreshContractBudget()
|
|
||||||
})
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
if (budgetRefreshTimer) clearTimeout(budgetRefreshTimer)
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -5,19 +5,13 @@ import CommonAgGrid from '@/components/common/xmCommonAgGrid.vue'
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
contractId: string
|
contractId: string
|
||||||
projectScaleKey?: string | null
|
|
||||||
projectInfoKey?: string
|
|
||||||
}>()
|
}>()
|
||||||
const DB_KEY = computed(() => `ht-info-v3-${props.contractId}`)
|
const DB_KEY = computed(() => `ht-info-v3-${props.contractId}`)
|
||||||
const XM_DB_KEY = computed(() => {
|
const XM_DB_KEY = 'xm-info-v3'
|
||||||
if (props.projectScaleKey === null) return undefined
|
|
||||||
return props.projectScaleKey || 'xm-info-v3'
|
|
||||||
})
|
|
||||||
const BASE_INFO_KEY = computed(() => props.projectInfoKey || 'xm-base-info-v1')
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<CommonAgGrid title="合同规模明细" :dbKey="DB_KEY" :xmInfoKey="XM_DB_KEY" :base-info-key="BASE_INFO_KEY"/>
|
<CommonAgGrid title="合同规模明细" :dbKey="DB_KEY" :xmInfoKey="XM_DB_KEY"/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -2,7 +2,12 @@
|
|||||||
import { parseDate } from '@internationalized/date'
|
import { parseDate } from '@internationalized/date'
|
||||||
import { onMounted, ref, watch } from 'vue'
|
import { onMounted, ref, watch } from 'vue'
|
||||||
import {
|
import {
|
||||||
|
getMajorDictEntries,
|
||||||
|
getServiceDictEntries,
|
||||||
|
getIndustryTypeValue,
|
||||||
industryTypeList,
|
industryTypeList,
|
||||||
|
isIndustryEnabledByType,
|
||||||
|
isMajorIdInIndustryScope
|
||||||
} from '@/sql'
|
} from '@/sql'
|
||||||
import { useKvStore } from '@/pinia/kv'
|
import { useKvStore } from '@/pinia/kv'
|
||||||
import { Calendar as CalendarIcon, CircleHelp } from 'lucide-vue-next'
|
import { Calendar as CalendarIcon, CircleHelp } from 'lucide-vue-next'
|
||||||
@ -40,10 +45,31 @@ interface XmInfoState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type MajorParentNode = { id: string; name: string }
|
type MajorParentNode = { id: string; name: string }
|
||||||
|
type DictItemLite = {
|
||||||
|
code?: string
|
||||||
|
name?: string
|
||||||
|
defCoe?: number | null
|
||||||
|
notshowByzxflxs?: boolean
|
||||||
|
}
|
||||||
|
type FactorPersistRow = {
|
||||||
|
id: string
|
||||||
|
code: string
|
||||||
|
name: string
|
||||||
|
standardFactor: number | null
|
||||||
|
budgetValue: number | null
|
||||||
|
remark: string
|
||||||
|
path: string[]
|
||||||
|
}
|
||||||
|
type FactorPersistState = {
|
||||||
|
detailRows: FactorPersistRow[]
|
||||||
|
}
|
||||||
|
|
||||||
const DB_KEY = 'xm-base-info-v1'
|
const DB_KEY = 'xm-base-info-v1'
|
||||||
|
const XM_CONSULT_CATEGORY_FACTOR_KEY = 'xm-consult-category-factor-v1'
|
||||||
|
const XM_MAJOR_FACTOR_KEY = 'xm-major-factor-v1'
|
||||||
const DEFAULT_PROJECT_NAME = 'xxx造价咨询服务'
|
const DEFAULT_PROJECT_NAME = 'xxx造价咨询服务'
|
||||||
const INDUSTRY_HINT_TEXT = '变更需要重置后重新选择'
|
const INDUSTRY_HINT_TEXT = '变更需要重置后重新选择'
|
||||||
|
const PROJECT_INIT_CHANGED_EVENT = 'xm-project-init-changed'
|
||||||
const getTodayDateString = () => {
|
const getTodayDateString = () => {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const year = String(now.getFullYear())
|
const year = String(now.getFullYear())
|
||||||
@ -53,6 +79,8 @@ const getTodayDateString = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isProjectInitialized = ref(false)
|
const isProjectInitialized = ref(false)
|
||||||
|
const showCreateDialog = ref(false)
|
||||||
|
const pendingIndustry = ref('')
|
||||||
|
|
||||||
const projectName = ref('')
|
const projectName = ref('')
|
||||||
const projectIndustry = ref('')
|
const projectIndustry = ref('')
|
||||||
@ -100,6 +128,76 @@ const majorParentCodeSet = new Set(majorParentNodes.map(item => item.id))
|
|||||||
const DEFAULT_PROJECT_INDUSTRY = majorParentNodes[0]?.id || ''
|
const DEFAULT_PROJECT_INDUSTRY = majorParentNodes[0]?.id || ''
|
||||||
const kvStore = useKvStore()
|
const kvStore = useKvStore()
|
||||||
|
|
||||||
|
const buildCodePath = (code: string, selfId: string, codeIdMap: Map<string, string>) => {
|
||||||
|
const parts = code.split('-').filter(Boolean)
|
||||||
|
if (!parts.length) return [selfId]
|
||||||
|
const path: string[] = []
|
||||||
|
let currentCode = parts[0]
|
||||||
|
const firstId = codeIdMap.get(currentCode)
|
||||||
|
if (firstId) path.push(firstId)
|
||||||
|
for (let i = 1; i < parts.length; i += 1) {
|
||||||
|
currentCode = `${currentCode}-${parts[i]}`
|
||||||
|
const id = codeIdMap.get(currentCode)
|
||||||
|
if (id) path.push(id)
|
||||||
|
}
|
||||||
|
if (!path.length || path[path.length - 1] !== selfId) path.push(selfId)
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildFactorRowsFromEntries = (entries: Array<{ id: string; item: DictItemLite }>): FactorPersistRow[] => {
|
||||||
|
const codeIdMap = new Map<string, string>()
|
||||||
|
for (const entry of entries) {
|
||||||
|
const code = String(entry.item?.code || '').trim()
|
||||||
|
if (!code) continue
|
||||||
|
codeIdMap.set(code, entry.id)
|
||||||
|
}
|
||||||
|
return entries
|
||||||
|
.map(entry => {
|
||||||
|
const code = String(entry.item?.code || '').trim()
|
||||||
|
const name = String(entry.item?.name || '').trim()
|
||||||
|
if (!code || !name) return null
|
||||||
|
const standardFactor =
|
||||||
|
typeof entry.item?.defCoe === 'number' && Number.isFinite(entry.item.defCoe)
|
||||||
|
? entry.item.defCoe
|
||||||
|
: null
|
||||||
|
return {
|
||||||
|
id: entry.id,
|
||||||
|
code,
|
||||||
|
name,
|
||||||
|
standardFactor,
|
||||||
|
budgetValue: standardFactor,
|
||||||
|
remark: '',
|
||||||
|
path: buildCodePath(code, entry.id, codeIdMap)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((item): item is FactorPersistRow => Boolean(item))
|
||||||
|
}
|
||||||
|
|
||||||
|
const initializeProjectFactorStates = async (industry: string) => {
|
||||||
|
const industryType = getIndustryTypeValue(industry)
|
||||||
|
const consultEntries = getServiceDictEntries()
|
||||||
|
.map(({ id, item }) => ({ id, item: item as DictItemLite }))
|
||||||
|
.filter(({ item }) => {
|
||||||
|
if (item.notshowByzxflxs === true) return false
|
||||||
|
return isIndustryEnabledByType(item as Record<string, unknown>, industryType)
|
||||||
|
})
|
||||||
|
const majorEntries = getMajorDictEntries()
|
||||||
|
.map(({ id, item }) => ({ id, item: item as DictItemLite }))
|
||||||
|
.filter(({ id, item }) => item.notshowByzxflxs !== true && isMajorIdInIndustryScope(id, industry))
|
||||||
|
|
||||||
|
const consultPayload: FactorPersistState = {
|
||||||
|
detailRows: buildFactorRowsFromEntries(consultEntries)
|
||||||
|
}
|
||||||
|
const majorPayload: FactorPersistState = {
|
||||||
|
detailRows: buildFactorRowsFromEntries(majorEntries)
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
kvStore.setItem(XM_CONSULT_CATEGORY_FACTOR_KEY, consultPayload),
|
||||||
|
kvStore.setItem(XM_MAJOR_FACTOR_KEY, majorPayload)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
const saveToIndexedDB = async () => {
|
const saveToIndexedDB = async () => {
|
||||||
try {
|
try {
|
||||||
const payload: XmInfoState = {
|
const payload: XmInfoState = {
|
||||||
@ -170,6 +268,38 @@ const handleProjectNameBlur = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openCreateDialog = () => {
|
||||||
|
pendingIndustry.value = DEFAULT_PROJECT_INDUSTRY
|
||||||
|
showCreateDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeCreateDialog = () => {
|
||||||
|
showCreateDialog.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const createProject = async () => {
|
||||||
|
const selectedIndustry = majorParentCodeSet.has(pendingIndustry.value)
|
||||||
|
? pendingIndustry.value
|
||||||
|
: DEFAULT_PROJECT_INDUSTRY
|
||||||
|
|
||||||
|
projectIndustry.value = selectedIndustry
|
||||||
|
projectName.value = DEFAULT_PROJECT_NAME
|
||||||
|
preparedBy.value = ''
|
||||||
|
reviewedBy.value = ''
|
||||||
|
preparedCompany.value = ''
|
||||||
|
preparedDate.value = getTodayDateString()
|
||||||
|
syncPreparedDatePickerFromString()
|
||||||
|
isProjectInitialized.value = true
|
||||||
|
showCreateDialog.value = false
|
||||||
|
await saveToIndexedDB()
|
||||||
|
try {
|
||||||
|
await initializeProjectFactorStates(selectedIndustry)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('initializeProjectFactorStates failed:', error)
|
||||||
|
}
|
||||||
|
window.dispatchEvent(new CustomEvent<boolean>(PROJECT_INIT_CHANGED_EVENT, { detail: true }))
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
[projectIndustry, projectName, preparedBy, reviewedBy, preparedCompany, preparedDate],
|
[projectIndustry, projectName, preparedBy, reviewedBy, preparedCompany, preparedDate],
|
||||||
schedulePersist
|
schedulePersist
|
||||||
@ -185,9 +315,15 @@ onMounted(async () => {
|
|||||||
<div class="space-y-6 h-full">
|
<div class="space-y-6 h-full">
|
||||||
<div
|
<div
|
||||||
v-if="!isProjectInitialized"
|
v-if="!isProjectInitialized"
|
||||||
class="rounded-xl border bg-card p-10 h-full flex items-center justify-center text-sm text-muted-foreground"
|
class=" bg-card p-10 h-full flex items-center justify-center"
|
||||||
>
|
>
|
||||||
请从首页先新建项目后再进入此页面。
|
<button
|
||||||
|
type="button"
|
||||||
|
class="cursor-pointer h-10 rounded-lg bg-primary px-6 text-sm font-medium text-primary-foreground transition hover:opacity-90"
|
||||||
|
@click="openCreateDialog"
|
||||||
|
>
|
||||||
|
新建项目
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="rounded-xl border bg-card p-4 shadow-sm shrink-0 md:p-5">
|
<div v-else class="rounded-xl border bg-card p-4 shadow-sm shrink-0 md:p-5">
|
||||||
@ -380,6 +516,44 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="showCreateDialog"
|
||||||
|
class="fixed inset-0 z-50 flex items-center justify-center bg-black/40 px-4"
|
||||||
|
@click.self="closeCreateDialog"
|
||||||
|
>
|
||||||
|
<div class="w-full max-w-lg rounded-xl border bg-card p-5 shadow-lg">
|
||||||
|
<h3 class="text-base font-semibold text-foreground">新建项目</h3>
|
||||||
|
<p class="mt-1 text-sm text-muted-foreground">请选择工程行业</p>
|
||||||
|
<div class="mt-4 max-h-72 space-y-2 overflow-auto rounded-lg border p-3">
|
||||||
|
<label
|
||||||
|
v-for="item in majorParentNodes"
|
||||||
|
:key="item.id"
|
||||||
|
class="flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 hover:bg-muted/60"
|
||||||
|
>
|
||||||
|
<input v-model="pendingIndustry" type="radio" :value="item.id" class="h-4 w-4 accent-primary" />
|
||||||
|
<span class="text-sm text-foreground"> {{ item.name }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5 flex justify-end gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="cursor-pointer h-9 rounded-lg border px-4 text-sm text-foreground transition hover:bg-muted"
|
||||||
|
@click="closeCreateDialog"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="cursor-pointer h-9 rounded-lg bg-primary px-4 text-sm text-primary-foreground transition hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
:disabled="!pendingIndustry"
|
||||||
|
@click="createProject"
|
||||||
|
>
|
||||||
|
确定
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -92,7 +92,6 @@ interface ServiceLite {
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
contractId: string,
|
contractId: string,
|
||||||
serviceId: string | number
|
serviceId: string | number
|
||||||
projectInfoKey?: string
|
|
||||||
}>()
|
}>()
|
||||||
const zxFwPricingStore = useZxFwPricingStore()
|
const zxFwPricingStore = useZxFwPricingStore()
|
||||||
const kvStore = useKvStore()
|
const kvStore = useKvStore()
|
||||||
@ -100,7 +99,7 @@ const DB_KEY = computed(() => `tzGMF-${props.contractId}-${props.serviceId}`)
|
|||||||
const HT_DB_KEY = computed(() => `ht-info-v3-${props.contractId}`)
|
const HT_DB_KEY = computed(() => `ht-info-v3-${props.contractId}`)
|
||||||
const HT_CONSULT_FACTOR_KEY = computed(() => `ht-consult-category-factor-v1-${props.contractId}`)
|
const HT_CONSULT_FACTOR_KEY = computed(() => `ht-consult-category-factor-v1-${props.contractId}`)
|
||||||
const HT_MAJOR_FACTOR_KEY = computed(() => `ht-major-factor-v1-${props.contractId}`)
|
const HT_MAJOR_FACTOR_KEY = computed(() => `ht-major-factor-v1-${props.contractId}`)
|
||||||
const BASE_INFO_KEY = computed(() => props.projectInfoKey || 'xm-base-info-v1')
|
const BASE_INFO_KEY = 'xm-base-info-v1'
|
||||||
const activeIndustryCode = ref('')
|
const activeIndustryCode = ref('')
|
||||||
const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:'
|
const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:'
|
||||||
const PRICING_FORCE_DEFAULT_PREFIX = 'pricing-force-default:'
|
const PRICING_FORCE_DEFAULT_PREFIX = 'pricing-force-default:'
|
||||||
@ -1150,7 +1149,7 @@ const applyProjectCountChange = async (nextValue: unknown) => {
|
|||||||
|
|
||||||
const loadFromIndexedDB = async () => {
|
const loadFromIndexedDB = async () => {
|
||||||
try {
|
try {
|
||||||
const baseInfo = await kvStore.getItem<XmBaseInfoState>(BASE_INFO_KEY.value)
|
const baseInfo = await kvStore.getItem<XmBaseInfoState>(BASE_INFO_KEY)
|
||||||
activeIndustryCode.value =
|
activeIndustryCode.value =
|
||||||
typeof baseInfo?.projectIndustry === 'string' ? baseInfo.projectIndustry.trim() : ''
|
typeof baseInfo?.projectIndustry === 'string' ? baseInfo.projectIndustry.trim() : ''
|
||||||
projectCount.value = 1
|
projectCount.value = 1
|
||||||
@ -1215,7 +1214,7 @@ const loadFromIndexedDB = async () => {
|
|||||||
|
|
||||||
const importContractData = async () => {
|
const importContractData = async () => {
|
||||||
try {
|
try {
|
||||||
const baseInfo = await kvStore.getItem<XmBaseInfoState>(BASE_INFO_KEY.value)
|
const baseInfo = await kvStore.getItem<XmBaseInfoState>(BASE_INFO_KEY)
|
||||||
activeIndustryCode.value =
|
activeIndustryCode.value =
|
||||||
typeof baseInfo?.projectIndustry === 'string' ? baseInfo.projectIndustry.trim() : ''
|
typeof baseInfo?.projectIndustry === 'string' ? baseInfo.projectIndustry.trim() : ''
|
||||||
|
|
||||||
|
|||||||
@ -92,7 +92,6 @@ interface ServiceLite {
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
contractId: string,
|
contractId: string,
|
||||||
serviceId: string | number
|
serviceId: string | number
|
||||||
projectInfoKey?: string
|
|
||||||
}>()
|
}>()
|
||||||
const zxFwPricingStore = useZxFwPricingStore()
|
const zxFwPricingStore = useZxFwPricingStore()
|
||||||
const kvStore = useKvStore()
|
const kvStore = useKvStore()
|
||||||
@ -100,7 +99,7 @@ const DB_KEY = computed(() => `ydGMF-${props.contractId}-${props.serviceId}`)
|
|||||||
const HT_DB_KEY = computed(() => `ht-info-v3-${props.contractId}`)
|
const HT_DB_KEY = computed(() => `ht-info-v3-${props.contractId}`)
|
||||||
const HT_CONSULT_FACTOR_KEY = computed(() => `ht-consult-category-factor-v1-${props.contractId}`)
|
const HT_CONSULT_FACTOR_KEY = computed(() => `ht-consult-category-factor-v1-${props.contractId}`)
|
||||||
const HT_MAJOR_FACTOR_KEY = computed(() => `ht-major-factor-v1-${props.contractId}`)
|
const HT_MAJOR_FACTOR_KEY = computed(() => `ht-major-factor-v1-${props.contractId}`)
|
||||||
const BASE_INFO_KEY = computed(() => props.projectInfoKey || 'xm-base-info-v1')
|
const BASE_INFO_KEY = 'xm-base-info-v1'
|
||||||
const activeIndustryCode = ref('')
|
const activeIndustryCode = ref('')
|
||||||
const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:'
|
const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:'
|
||||||
const PRICING_FORCE_DEFAULT_PREFIX = 'pricing-force-default:'
|
const PRICING_FORCE_DEFAULT_PREFIX = 'pricing-force-default:'
|
||||||
@ -998,7 +997,7 @@ const applyProjectCountChange = async (nextValue: unknown) => {
|
|||||||
|
|
||||||
const loadFromIndexedDB = async () => {
|
const loadFromIndexedDB = async () => {
|
||||||
try {
|
try {
|
||||||
const baseInfo = await kvStore.getItem<XmBaseInfoState>(BASE_INFO_KEY.value)
|
const baseInfo = await kvStore.getItem<XmBaseInfoState>(BASE_INFO_KEY)
|
||||||
activeIndustryCode.value =
|
activeIndustryCode.value =
|
||||||
typeof baseInfo?.projectIndustry === 'string' ? baseInfo.projectIndustry.trim() : ''
|
typeof baseInfo?.projectIndustry === 'string' ? baseInfo.projectIndustry.trim() : ''
|
||||||
projectCount.value = 1
|
projectCount.value = 1
|
||||||
@ -1050,7 +1049,7 @@ const loadFromIndexedDB = async () => {
|
|||||||
|
|
||||||
const importContractData = async () => {
|
const importContractData = async () => {
|
||||||
try {
|
try {
|
||||||
const baseInfo = await kvStore.getItem<XmBaseInfoState>(BASE_INFO_KEY.value)
|
const baseInfo = await kvStore.getItem<XmBaseInfoState>(BASE_INFO_KEY)
|
||||||
activeIndustryCode.value =
|
activeIndustryCode.value =
|
||||||
typeof baseInfo?.projectIndustry === 'string' ? baseInfo.projectIndustry.trim() : ''
|
typeof baseInfo?.projectIndustry === 'string' ? baseInfo.projectIndustry.trim() : ''
|
||||||
|
|
||||||
|
|||||||
@ -3,14 +3,17 @@
|
|||||||
scene="xm-tab"
|
scene="xm-tab"
|
||||||
title=""
|
title=""
|
||||||
storage-key="project-active-cat"
|
storage-key="project-active-cat"
|
||||||
default-category="scale-info"
|
default-category="info"
|
||||||
:categories="xmCategories"
|
:categories="xmCategories"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineAsyncComponent, markRaw } from 'vue'
|
import { computed, defineAsyncComponent, markRaw, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
import TypeLine from '@/layout/typeLine.vue'
|
import TypeLine from '@/layout/typeLine.vue'
|
||||||
|
import { useKvStore } from '@/pinia/kv'
|
||||||
|
|
||||||
|
const infoView = markRaw(defineAsyncComponent(() => import('@/components/views/info.vue')))
|
||||||
const scaleInfoView = markRaw(defineAsyncComponent(() => import('@/components/views/xmInfo.vue')))
|
const scaleInfoView = markRaw(defineAsyncComponent(() => import('@/components/views/xmInfo.vue')))
|
||||||
const htView = markRaw(defineAsyncComponent(() => import('@/components/views/Ht.vue')))
|
const htView = markRaw(defineAsyncComponent(() => import('@/components/views/Ht.vue')))
|
||||||
const consultCategoryFactorView = markRaw(
|
const consultCategoryFactorView = markRaw(
|
||||||
@ -20,10 +23,49 @@ const majorFactorView = markRaw(
|
|||||||
defineAsyncComponent(() => import('@/components/views/XmMajorFactor.vue'))
|
defineAsyncComponent(() => import('@/components/views/XmMajorFactor.vue'))
|
||||||
)
|
)
|
||||||
|
|
||||||
const xmCategories = [
|
const PROJECT_INFO_KEY = 'xm-base-info-v1'
|
||||||
|
const PROJECT_INIT_CHANGED_EVENT = 'xm-project-init-changed'
|
||||||
|
|
||||||
|
const hasProjectBaseInfo = ref(false)
|
||||||
|
const kvStore = useKvStore()
|
||||||
|
|
||||||
|
const fullXmCategories = [
|
||||||
|
{ key: 'info', label: '基础信息', component: infoView },
|
||||||
|
{ key: 'contract', label: '合同段管理', component: htView },
|
||||||
{ key: 'scale-info', label: '规模信息', component: scaleInfoView },
|
{ key: 'scale-info', label: '规模信息', component: scaleInfoView },
|
||||||
{ key: 'consult-category-factor', label: '咨询分类系数', component: consultCategoryFactorView },
|
{ key: 'consult-category-factor', label: '咨询分类系数', component: consultCategoryFactorView },
|
||||||
{ key: 'major-factor', label: '工程专业系数', component: majorFactorView },
|
{ key: 'major-factor', label: '工程专业系数', component: majorFactorView }
|
||||||
{ key: 'contract', label: '合同段管理', component: htView }
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const xmCategories = computed(() =>
|
||||||
|
hasProjectBaseInfo.value ? fullXmCategories : [fullXmCategories[0]]
|
||||||
|
)
|
||||||
|
|
||||||
|
const refreshProjectBaseInfoState = async () => {
|
||||||
|
try {
|
||||||
|
const data = await kvStore.getItem(PROJECT_INFO_KEY)
|
||||||
|
hasProjectBaseInfo.value = Boolean(data)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('read project base info failed:', error)
|
||||||
|
hasProjectBaseInfo.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleProjectInitChanged = (event: Event) => {
|
||||||
|
const detail = (event as CustomEvent<boolean>).detail
|
||||||
|
if (typeof detail === 'boolean') {
|
||||||
|
hasProjectBaseInfo.value = detail
|
||||||
|
return
|
||||||
|
}
|
||||||
|
void refreshProjectBaseInfoState()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
void refreshProjectBaseInfoState()
|
||||||
|
window.addEventListener(PROJECT_INIT_CHANGED_EVENT, handleProjectInitChanged as EventListener)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener(PROJECT_INIT_CHANGED_EVENT, handleProjectInitChanged as EventListener)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -74,12 +74,11 @@ interface ServiceMethodType {
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
contractId: string
|
contractId: string
|
||||||
contractName?: string
|
contractName?: string
|
||||||
projectInfoKey?: string
|
|
||||||
}>()
|
}>()
|
||||||
const tabStore = useTabStore()
|
const tabStore = useTabStore()
|
||||||
const zxFwPricingStore = useZxFwPricingStore()
|
const zxFwPricingStore = useZxFwPricingStore()
|
||||||
const kvStore = useKvStore()
|
const kvStore = useKvStore()
|
||||||
const PROJECT_INFO_KEY = computed(() => props.projectInfoKey || 'xm-base-info-v1')
|
const PROJECT_INFO_KEY = 'xm-base-info-v1'
|
||||||
const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:'
|
const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:'
|
||||||
const PRICING_FORCE_DEFAULT_PREFIX = 'pricing-force-default:'
|
const PRICING_FORCE_DEFAULT_PREFIX = 'pricing-force-default:'
|
||||||
const PRICING_CLEAR_SKIP_TTL_MS = 5000
|
const PRICING_CLEAR_SKIP_TTL_MS = 5000
|
||||||
@ -501,8 +500,7 @@ const openEditTab = (row: DetailRow) => {
|
|||||||
contractName: props.contractName || '',
|
contractName: props.contractName || '',
|
||||||
serviceId: row.id,
|
serviceId: row.id,
|
||||||
fwName: row.code + row.name,
|
fwName: row.code + row.name,
|
||||||
type: serviceType ? { ...serviceType } : undefined,
|
type: serviceType ? { ...serviceType } : undefined
|
||||||
projectInfoKey: props.projectInfoKey
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -1100,7 +1098,7 @@ const initializeContractState = async () => {
|
|||||||
|
|
||||||
const loadProjectIndustry = async () => {
|
const loadProjectIndustry = async () => {
|
||||||
try {
|
try {
|
||||||
const data = await kvStore.getItem<XmBaseInfoState>(PROJECT_INFO_KEY.value)
|
const data = await kvStore.getItem<XmBaseInfoState>(PROJECT_INFO_KEY)
|
||||||
projectIndustry.value =
|
projectIndustry.value =
|
||||||
typeof data?.projectIndustry === 'string' ? data.projectIndustry.trim() : ''
|
typeof data?.projectIndustry === 'string' ? data.projectIndustry.trim() : ''
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -27,28 +27,8 @@ import {
|
|||||||
ToastViewport,
|
ToastViewport,
|
||||||
} from 'reka-ui'
|
} from 'reka-ui'
|
||||||
import { decodeZwArchive, encodeZwArchive, ZW_FILE_EXTENSION } from '@/lib/zwArchive'
|
import { decodeZwArchive, encodeZwArchive, ZW_FILE_EXTENSION } from '@/lib/zwArchive'
|
||||||
import { addNumbers, roundTo, toDecimal } from '@/lib/decimal'
|
import { addNumbers, roundTo } from '@/lib/decimal'
|
||||||
import { getBenchmarkBudgetSplitByScale, getScaleBudgetFeeSplit } from '@/lib/pricingScaleFee'
|
|
||||||
import { exportFile, serviceList, additionalWorkList, reserveList } from '@/sql'
|
import { exportFile, serviceList, additionalWorkList, reserveList } from '@/sql'
|
||||||
import {
|
|
||||||
HOME_TAB_ID,
|
|
||||||
LEGACY_PROJECT_TAB_ID,
|
|
||||||
PROJECT_TAB_ID,
|
|
||||||
QUICK_CONSULT_CATEGORY_FACTOR_KEY,
|
|
||||||
QUICK_CONTRACT_FALLBACK_NAME,
|
|
||||||
QUICK_CONTRACT_ID,
|
|
||||||
QUICK_CONTRACT_META_KEY,
|
|
||||||
QUICK_CONTRACT_TAB_ID,
|
|
||||||
QUICK_MAJOR_FACTOR_KEY,
|
|
||||||
QUICK_PROJECT_INFO_KEY,
|
|
||||||
QUICK_PROJECT_SCALE_KEY,
|
|
||||||
QUICK_TAB_ID,
|
|
||||||
type QuickContractMeta,
|
|
||||||
normalizeWorkspaceMode,
|
|
||||||
readWorkspaceMode,
|
|
||||||
writeWorkspaceMode,
|
|
||||||
type WorkspaceMode
|
|
||||||
} from '@/lib/workspace'
|
|
||||||
|
|
||||||
interface DataEntry {
|
interface DataEntry {
|
||||||
key: string
|
key: string
|
||||||
@ -60,26 +40,12 @@ interface ForageStoreSnapshot {
|
|||||||
entries: DataEntry[]
|
entries: DataEntry[]
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DataPackageStorage {
|
|
||||||
localStorage: DataEntry[]
|
|
||||||
sessionStorage: DataEntry[]
|
|
||||||
localforageDefault: DataEntry[]
|
|
||||||
localforageStores?: ForageStoreSnapshot[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DataPackageWorkspace {
|
|
||||||
mode: WorkspaceMode
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DataPackage {
|
interface DataPackage {
|
||||||
version: number
|
version: number
|
||||||
exportedAt: string
|
exportedAt: string
|
||||||
packageType?: 'workspace-snapshot'
|
localStorage: DataEntry[]
|
||||||
workspace?: DataPackageWorkspace
|
sessionStorage: DataEntry[]
|
||||||
storage?: DataPackageStorage
|
localforageDefault: DataEntry[]
|
||||||
localStorage?: DataEntry[]
|
|
||||||
sessionStorage?: DataEntry[]
|
|
||||||
localforageDefault?: DataEntry[]
|
|
||||||
localforageStores?: ForageStoreSnapshot[]
|
localforageStores?: ForageStoreSnapshot[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,16 +85,6 @@ interface ContractCardItem {
|
|||||||
order?: number
|
order?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ReportWorkspaceConfig {
|
|
||||||
mode: WorkspaceMode
|
|
||||||
projectInfoKey: string
|
|
||||||
projectScaleKey: string
|
|
||||||
consultCategoryFactorKey: string
|
|
||||||
majorFactorKey: string
|
|
||||||
contractCardsKey?: string
|
|
||||||
quickContractKey?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ZxFwRowLike {
|
interface ZxFwRowLike {
|
||||||
id: string
|
id: string
|
||||||
process?: unknown
|
process?: unknown
|
||||||
@ -151,8 +107,6 @@ interface ScaleMethodRowLike extends ScaleRowLike {
|
|||||||
benchmarkBudget?: unknown
|
benchmarkBudget?: unknown
|
||||||
benchmarkBudgetBasic?: unknown
|
benchmarkBudgetBasic?: unknown
|
||||||
benchmarkBudgetOptional?: unknown
|
benchmarkBudgetOptional?: unknown
|
||||||
benchmarkBudgetBasicChecked?: unknown
|
|
||||||
benchmarkBudgetOptionalChecked?: unknown
|
|
||||||
budgetFee?: unknown
|
budgetFee?: unknown
|
||||||
budgetFeeBasic?: unknown
|
budgetFeeBasic?: unknown
|
||||||
budgetFeeOptional?: unknown
|
budgetFeeOptional?: unknown
|
||||||
@ -185,7 +139,6 @@ interface QuantityMethodRowLike {
|
|||||||
|
|
||||||
interface WorkloadMethodRowLike {
|
interface WorkloadMethodRowLike {
|
||||||
id: string
|
id: string
|
||||||
conversion?: unknown
|
|
||||||
budgetAdoptedUnitPrice?: unknown
|
budgetAdoptedUnitPrice?: unknown
|
||||||
workload?: unknown
|
workload?: unknown
|
||||||
basicFee?: unknown
|
basicFee?: unknown
|
||||||
@ -390,32 +343,17 @@ interface ExportReserve {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ExportReportPayload {
|
interface ExportReportPayload {
|
||||||
version: number
|
|
||||||
reportType: 'budget-report'
|
|
||||||
mode: WorkspaceMode
|
|
||||||
name: string
|
name: string
|
||||||
writer: string
|
writer: string
|
||||||
reviewer: string
|
reviewer: string
|
||||||
date: string
|
date: string
|
||||||
industry: number
|
industry: number
|
||||||
fee: number
|
fee: number
|
||||||
scaleCost: number | null
|
scaleCost: number
|
||||||
scale: ExportScaleRow[]
|
scale: ExportScaleRow[]
|
||||||
serviceCoes: ExportServiceCoe[]
|
serviceCoes: ExportServiceCoe[]
|
||||||
majorCoes: ExportMajorCoe[]
|
majorCoes: ExportMajorCoe[]
|
||||||
contracts: ExportContract[]
|
contracts: ExportContract[]
|
||||||
project: {
|
|
||||||
name: string
|
|
||||||
writer: string
|
|
||||||
reviewer: string
|
|
||||||
date: string
|
|
||||||
industry: number
|
|
||||||
fee: number
|
|
||||||
scaleCost: number | null
|
|
||||||
scale: ExportScaleRow[]
|
|
||||||
serviceCoes: ExportServiceCoe[]
|
|
||||||
majorCoes: ExportMajorCoe[]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const USER_GUIDE_COMPLETED_KEY = 'jgjs-user-guide-completed-v1'
|
const USER_GUIDE_COMPLETED_KEY = 'jgjs-user-guide-completed-v1'
|
||||||
@ -426,7 +364,6 @@ const MAJOR_FACTOR_DB_KEY = 'xm-major-factor-v1'
|
|||||||
const PINIA_PERSIST_DB_NAME = 'DB'
|
const PINIA_PERSIST_DB_NAME = 'DB'
|
||||||
const PINIA_PERSIST_BASE_STORE_NAME = 'pinia'
|
const PINIA_PERSIST_BASE_STORE_NAME = 'pinia'
|
||||||
const PINIA_PERSIST_STORE_IDS = ['tabs', 'zxFwPricing', 'kv'] as const
|
const PINIA_PERSIST_STORE_IDS = ['tabs', 'zxFwPricing', 'kv'] as const
|
||||||
const XM_SCALE_FLUSH_EVENT = 'jgjs:xm-scale-flush-request'
|
|
||||||
const userGuideSteps: UserGuideStep[] = [
|
const userGuideSteps: UserGuideStep[] = [
|
||||||
{
|
{
|
||||||
title: '欢迎使用',
|
title: '欢迎使用',
|
||||||
@ -503,9 +440,7 @@ const userGuideSteps: UserGuideStep[] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const componentMap: Record<string, any> = {
|
const componentMap: Record<string, any> = {
|
||||||
[HOME_TAB_ID]: markRaw(defineAsyncComponent(() => import('@/components/views/HomeEntryView.vue'))),
|
XmView: markRaw(defineAsyncComponent(() => import('@/components/views/xmCard.vue'))),
|
||||||
[PROJECT_TAB_ID]: markRaw(defineAsyncComponent(() => import('@/components/views/ProjectWorkspaceView.vue'))),
|
|
||||||
[LEGACY_PROJECT_TAB_ID]: markRaw(defineAsyncComponent(() => import('@/components/views/ProjectWorkspaceView.vue'))),
|
|
||||||
ContractDetailView: markRaw(defineAsyncComponent(() => import('@/components/views/htCard.vue'))),
|
ContractDetailView: markRaw(defineAsyncComponent(() => import('@/components/views/htCard.vue'))),
|
||||||
ZxFwView: markRaw(defineAsyncComponent(() => import('@/components/views/ZxFwView.vue'))),
|
ZxFwView: markRaw(defineAsyncComponent(() => import('@/components/views/ZxFwView.vue'))),
|
||||||
HtFeeMethodTypeLineView: markRaw(defineAsyncComponent(() => import('@/components/views/HtFeeMethodTypeLineView.vue'))),
|
HtFeeMethodTypeLineView: markRaw(defineAsyncComponent(() => import('@/components/views/HtFeeMethodTypeLineView.vue'))),
|
||||||
@ -514,15 +449,13 @@ const componentMap: Record<string, any> = {
|
|||||||
const tabStore = useTabStore()
|
const tabStore = useTabStore()
|
||||||
const zxFwPricingStore = useZxFwPricingStore()
|
const zxFwPricingStore = useZxFwPricingStore()
|
||||||
const kvStore = useKvStore()
|
const kvStore = useKvStore()
|
||||||
const protectedTabIdSet = new Set<string>([HOME_TAB_ID, QUICK_CONTRACT_TAB_ID])
|
|
||||||
const isTabClosable = (tabId: string) => !protectedTabIdSet.has(tabId)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const tabContextOpen = ref(false)
|
const tabContextOpen = ref(false)
|
||||||
const tabContextX = ref(0)
|
const tabContextX = ref(0)
|
||||||
const tabContextY = ref(0)
|
const tabContextY = ref(0)
|
||||||
const contextTabId = ref<string>(HOME_TAB_ID)
|
const contextTabId = ref<string>('XmView')
|
||||||
const tabContextRef = ref<HTMLElement | null>(null)
|
const tabContextRef = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
const dataMenuOpen = ref(false)
|
const dataMenuOpen = ref(false)
|
||||||
@ -558,13 +491,8 @@ const tabsModel = computed({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const contextTabIndex = computed(() => tabStore.tabs.findIndex(t => t.id === contextTabId.value))
|
const contextTabIndex = computed(() => tabStore.tabs.findIndex(t => t.id === contextTabId.value))
|
||||||
const isHomeOnlyView = computed(
|
|
||||||
() => tabStore.tabs.length === 1 && tabStore.tabs[0]?.id === HOME_TAB_ID && tabStore.activeTabId === HOME_TAB_ID
|
|
||||||
)
|
|
||||||
const showTabStrip = computed(() => !isHomeOnlyView.value)
|
|
||||||
const showWorkspaceActions = computed(() => !isHomeOnlyView.value)
|
|
||||||
|
|
||||||
const hasClosableTabs = computed(() => tabStore.tabs.some(t => isTabClosable(t.id)))
|
const hasClosableTabs = computed(() => tabStore.tabs.some(t => t.id !== 'XmView'))
|
||||||
const activeGuideStep = computed(
|
const activeGuideStep = computed(
|
||||||
() => userGuideSteps[userGuideStepIndex.value] || userGuideSteps[0]
|
() => userGuideSteps[userGuideStepIndex.value] || userGuideSteps[0]
|
||||||
)
|
)
|
||||||
@ -573,14 +501,14 @@ const isLastGuideStep = computed(() => userGuideStepIndex.value >= userGuideStep
|
|||||||
const guideProgressText = computed(() => `${userGuideStepIndex.value + 1} / ${userGuideSteps.length}`)
|
const guideProgressText = computed(() => `${userGuideStepIndex.value + 1} / ${userGuideSteps.length}`)
|
||||||
const canCloseLeft = computed(() => {
|
const canCloseLeft = computed(() => {
|
||||||
if (contextTabIndex.value <= 0) return false
|
if (contextTabIndex.value <= 0) return false
|
||||||
return tabStore.tabs.slice(0, contextTabIndex.value).some(t => isTabClosable(t.id))
|
return tabStore.tabs.slice(0, contextTabIndex.value).some(t => t.id !== 'XmView')
|
||||||
})
|
})
|
||||||
const canCloseRight = computed(() => {
|
const canCloseRight = computed(() => {
|
||||||
if (contextTabIndex.value < 0) return false
|
if (contextTabIndex.value < 0) return false
|
||||||
return tabStore.tabs.slice(contextTabIndex.value + 1).some(t => isTabClosable(t.id))
|
return tabStore.tabs.slice(contextTabIndex.value + 1).some(t => t.id !== 'XmView')
|
||||||
})
|
})
|
||||||
const canCloseOther = computed(() =>
|
const canCloseOther = computed(() =>
|
||||||
tabStore.tabs.some(t => isTabClosable(t.id) && t.id !== contextTabId.value)
|
tabStore.tabs.some(t => t.id !== 'XmView' && t.id !== contextTabId.value)
|
||||||
)
|
)
|
||||||
|
|
||||||
const closeMenus = () => {
|
const closeMenus = () => {
|
||||||
@ -588,79 +516,6 @@ const closeMenus = () => {
|
|||||||
dataMenuOpen.value = false
|
dataMenuOpen.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizeTabItem = (tab: Record<string, unknown>) => {
|
|
||||||
const rawId = typeof tab.id === 'string' ? tab.id : ''
|
|
||||||
const rawComponentName = typeof tab.componentName === 'string' ? tab.componentName : rawId
|
|
||||||
if (!rawId && !rawComponentName) return null
|
|
||||||
|
|
||||||
if (rawId === LEGACY_PROJECT_TAB_ID || rawComponentName === LEGACY_PROJECT_TAB_ID) {
|
|
||||||
return {
|
|
||||||
...tab,
|
|
||||||
id: PROJECT_TAB_ID,
|
|
||||||
title: '项目计算',
|
|
||||||
componentName: PROJECT_TAB_ID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rawId === HOME_TAB_ID || rawComponentName === HOME_TAB_ID) {
|
|
||||||
return {
|
|
||||||
...tab,
|
|
||||||
id: HOME_TAB_ID,
|
|
||||||
title: typeof tab.title === 'string' && tab.title.trim() ? tab.title : '首页',
|
|
||||||
componentName: HOME_TAB_ID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rawId === PROJECT_TAB_ID || rawComponentName === PROJECT_TAB_ID) {
|
|
||||||
return {
|
|
||||||
...tab,
|
|
||||||
id: PROJECT_TAB_ID,
|
|
||||||
title: typeof tab.title === 'string' && tab.title.trim() ? tab.title : '项目计算',
|
|
||||||
componentName: PROJECT_TAB_ID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rawId === QUICK_TAB_ID || rawComponentName === QUICK_TAB_ID) {
|
|
||||||
return {
|
|
||||||
...tab,
|
|
||||||
id: HOME_TAB_ID,
|
|
||||||
title: '首页',
|
|
||||||
componentName: HOME_TAB_ID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...tab,
|
|
||||||
id: rawId || rawComponentName,
|
|
||||||
componentName: rawComponentName || rawId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizeTabStoreState = () => {
|
|
||||||
const normalizedTabs = (Array.isArray(tabStore.tabs) ? tabStore.tabs : [])
|
|
||||||
.map(tab => normalizeTabItem(tab as unknown as Record<string, unknown>))
|
|
||||||
.filter((tab): tab is NonNullable<ReturnType<typeof normalizeTabItem>> => Boolean(tab))
|
|
||||||
|
|
||||||
const uniqueTabs = normalizedTabs.filter((tab, index, source) =>
|
|
||||||
source.findIndex(item => item.id === tab.id) === index
|
|
||||||
)
|
|
||||||
|
|
||||||
const nextTabs = uniqueTabs.filter(tab => !(tab.id === HOME_TAB_ID && uniqueTabs.length > 1))
|
|
||||||
tabStore.tabs = (nextTabs.length > 0
|
|
||||||
? nextTabs
|
|
||||||
: [{
|
|
||||||
id: HOME_TAB_ID,
|
|
||||||
title: '首页',
|
|
||||||
componentName: HOME_TAB_ID
|
|
||||||
}]) as any
|
|
||||||
if (tabStore.activeTabId === LEGACY_PROJECT_TAB_ID) {
|
|
||||||
tabStore.activeTabId = PROJECT_TAB_ID
|
|
||||||
}
|
|
||||||
if (!tabStore.tabs.some(tab => tab.id === tabStore.activeTabId)) {
|
|
||||||
tabStore.activeTabId = tabStore.tabs[0]?.id || HOME_TAB_ID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearReportExportToastTimer = () => {
|
const clearReportExportToastTimer = () => {
|
||||||
if (!reportExportToastTimer) return
|
if (!reportExportToastTimer) return
|
||||||
clearTimeout(reportExportToastTimer)
|
clearTimeout(reportExportToastTimer)
|
||||||
@ -709,9 +564,9 @@ const hasNonDefaultTabState = () => {
|
|||||||
if (!raw) return false
|
if (!raw) return false
|
||||||
const parsed = JSON.parse(raw) as { tabs?: Array<{ id?: string }>; activeTabId?: string }
|
const parsed = JSON.parse(raw) as { tabs?: Array<{ id?: string }>; activeTabId?: string }
|
||||||
const tabs = Array.isArray(parsed?.tabs) ? parsed.tabs : []
|
const tabs = Array.isArray(parsed?.tabs) ? parsed.tabs : []
|
||||||
const hasCustomTabs = tabs.some(item => item?.id && isTabClosable(item.id))
|
const hasCustomTabs = tabs.some(item => item?.id && item.id !== 'XmView')
|
||||||
const activeTabId = typeof parsed?.activeTabId === 'string' ? parsed.activeTabId : ''
|
const activeTabId = typeof parsed?.activeTabId === 'string' ? parsed.activeTabId : ''
|
||||||
return hasCustomTabs || (activeTabId !== '' && isTabClosable(activeTabId))
|
return hasCustomTabs || (activeTabId !== '' && activeTabId !== 'XmView')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('parse tabs cache failed:', error)
|
console.error('parse tabs cache failed:', error)
|
||||||
return false
|
return false
|
||||||
@ -719,7 +574,6 @@ const hasNonDefaultTabState = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const shouldAutoOpenGuide = async () => {
|
const shouldAutoOpenGuide = async () => {
|
||||||
if (readWorkspaceMode() === 'home') return false
|
|
||||||
if (hasGuideCompleted()) return false
|
if (hasGuideCompleted()) return false
|
||||||
if (hasNonDefaultTabState()) return false
|
if (hasNonDefaultTabState()) return false
|
||||||
try {
|
try {
|
||||||
@ -818,7 +672,7 @@ const runTabMenuAction = (action: 'all' | 'left' | 'right' | 'other') => {
|
|||||||
const canMoveTab = (event: any) => {
|
const canMoveTab = (event: any) => {
|
||||||
const draggedId = event?.draggedContext?.element?.id
|
const draggedId = event?.draggedContext?.element?.id
|
||||||
const targetIndex = event?.relatedContext?.index
|
const targetIndex = event?.relatedContext?.index
|
||||||
if (protectedTabIdSet.has(draggedId)) return false
|
if (draggedId === 'XmView') return false
|
||||||
if (typeof targetIndex === 'number' && targetIndex === 0) return false
|
if (typeof targetIndex === 'number' && targetIndex === 0) return false
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -1035,27 +889,6 @@ const flushPiniaPersistNow = async () => {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestXmScalePersistFlush = async () => {
|
|
||||||
await new Promise<void>(resolve => {
|
|
||||||
let settled = false
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
if (settled) return
|
|
||||||
settled = true
|
|
||||||
resolve()
|
|
||||||
}, 350)
|
|
||||||
window.dispatchEvent(new CustomEvent(XM_SCALE_FLUSH_EVENT, {
|
|
||||||
detail: {
|
|
||||||
done: () => {
|
|
||||||
if (settled) return
|
|
||||||
settled = true
|
|
||||||
clearTimeout(timer)
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const readForage = async (store: ForageStore): Promise<DataEntry[]> => {
|
const readForage = async (store: ForageStore): Promise<DataEntry[]> => {
|
||||||
const keys = await store.keys()
|
const keys = await store.keys()
|
||||||
const entries: DataEntry[] = []
|
const entries: DataEntry[] = []
|
||||||
@ -1121,80 +954,12 @@ const formatExportTimestamp = (date: Date): string => {
|
|||||||
return `${yyyy}${mm}${dd}-${hh}${mi}`
|
return `${yyyy}${mm}${dd}-${hh}${mi}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const getExportProjectName = (entries: DataEntry[], forageStores: ForageStoreSnapshot[] = []): string => {
|
const getExportProjectName = (entries: DataEntry[]): string => {
|
||||||
const target =
|
const target =
|
||||||
entries.find(item => item.key === PROJECT_INFO_DB_KEY) ||
|
entries.find(item => item.key === PROJECT_INFO_DB_KEY) ||
|
||||||
entries.find(item => item.key === LEGACY_PROJECT_DB_KEY)
|
entries.find(item => item.key === LEGACY_PROJECT_DB_KEY)
|
||||||
const data = (target?.value || {}) as XmInfoLike
|
const data = (target?.value || {}) as XmInfoLike
|
||||||
if (typeof data.projectName === 'string' && data.projectName.trim()) {
|
return typeof data.projectName === 'string' ? sanitizeFileNamePart(data.projectName) : '造价项目'
|
||||||
return sanitizeFileNamePart(data.projectName)
|
|
||||||
}
|
|
||||||
|
|
||||||
const quickContract =
|
|
||||||
entries.find(item => item.key === QUICK_CONTRACT_META_KEY) ||
|
|
||||||
forageStores.flatMap(store => store.entries).find(item => item.key === QUICK_CONTRACT_META_KEY)
|
|
||||||
const quickName = (quickContract?.value as QuickContractMeta | null)?.name
|
|
||||||
return typeof quickName === 'string' && quickName.trim()
|
|
||||||
? sanitizeFileNamePart(quickName)
|
|
||||||
: '造价项目'
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizeDataPackageStorage = (payload: DataPackage): DataPackageStorage => {
|
|
||||||
const storage = payload.storage
|
|
||||||
return {
|
|
||||||
localStorage: normalizeEntries(storage?.localStorage ?? payload.localStorage),
|
|
||||||
sessionStorage: normalizeEntries(storage?.sessionStorage ?? payload.sessionStorage),
|
|
||||||
localforageDefault: normalizeEntries(storage?.localforageDefault ?? payload.localforageDefault),
|
|
||||||
localforageStores: normalizeForageStoreSnapshots(storage?.localforageStores ?? payload.localforageStores)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizeDataPackageWorkspace = (payload: DataPackage): DataPackageWorkspace => ({
|
|
||||||
mode: normalizeWorkspaceMode(payload.workspace?.mode ?? readWorkspaceMode())
|
|
||||||
})
|
|
||||||
|
|
||||||
const resolveWorkspaceModeForExport = () => {
|
|
||||||
const activeTab = tabStore.tabs.find(tab => tab.id === tabStore.activeTabId)
|
|
||||||
const activeContractId = typeof activeTab?.props?.contractId === 'string' ? activeTab.props.contractId : ''
|
|
||||||
if (tabStore.activeTabId === QUICK_TAB_ID || activeContractId === QUICK_CONTRACT_ID) return 'quick'
|
|
||||||
if (activeContractId) return 'project'
|
|
||||||
if (tabStore.activeTabId === PROJECT_TAB_ID || tabStore.activeTabId === LEGACY_PROJECT_TAB_ID) return 'project'
|
|
||||||
return normalizeWorkspaceMode(readWorkspaceMode())
|
|
||||||
}
|
|
||||||
|
|
||||||
const getReportWorkspaceConfig = (): ReportWorkspaceConfig => {
|
|
||||||
const mode = resolveWorkspaceModeForExport()
|
|
||||||
if (mode === 'quick') {
|
|
||||||
return {
|
|
||||||
mode,
|
|
||||||
projectInfoKey: QUICK_PROJECT_INFO_KEY,
|
|
||||||
projectScaleKey: QUICK_PROJECT_SCALE_KEY,
|
|
||||||
consultCategoryFactorKey: QUICK_CONSULT_CATEGORY_FACTOR_KEY,
|
|
||||||
majorFactorKey: QUICK_MAJOR_FACTOR_KEY,
|
|
||||||
quickContractKey: QUICK_CONTRACT_META_KEY
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
mode: 'project',
|
|
||||||
projectInfoKey: PROJECT_INFO_DB_KEY,
|
|
||||||
projectScaleKey: LEGACY_PROJECT_DB_KEY,
|
|
||||||
consultCategoryFactorKey: CONSULT_CATEGORY_FACTOR_DB_KEY,
|
|
||||||
majorFactorKey: MAJOR_FACTOR_DB_KEY,
|
|
||||||
contractCardsKey: 'ht-card-v1'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizeQuickContractMeta = (value: unknown): QuickContractMeta => {
|
|
||||||
const raw = (value || {}) as Partial<QuickContractMeta>
|
|
||||||
return {
|
|
||||||
id: typeof raw.id === 'string' && raw.id.trim() ? raw.id.trim() : QUICK_CONTRACT_ID,
|
|
||||||
name: typeof raw.name === 'string' && raw.name.trim() ? raw.name.trim() : QUICK_CONTRACT_FALLBACK_NAME,
|
|
||||||
updatedAt:
|
|
||||||
typeof raw.updatedAt === 'string' && raw.updatedAt.trim()
|
|
||||||
? raw.updatedAt
|
|
||||||
: new Date().toISOString()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const toFiniteNumber = (value: unknown): number | null => {
|
const toFiniteNumber = (value: unknown): number | null => {
|
||||||
@ -1220,74 +985,6 @@ const sumNumbers = (values: Array<number | null | undefined>): number =>
|
|||||||
const isNonEmptyString = (value: unknown): value is string =>
|
const isNonEmptyString = (value: unknown): value is string =>
|
||||||
typeof value === 'string' && value.trim().length > 0
|
typeof value === 'string' && value.trim().length > 0
|
||||||
|
|
||||||
const resolveScaleMethodComputedValues = (
|
|
||||||
row: ScaleMethodRowLike,
|
|
||||||
mode: 'cost' | 'area'
|
|
||||||
) => {
|
|
||||||
const scaleValue = mode === 'cost' ? row.amount : row.landArea
|
|
||||||
const rawSplit = getBenchmarkBudgetSplitByScale(scaleValue, mode)
|
|
||||||
const basicChecked = row.benchmarkBudgetBasicChecked !== false
|
|
||||||
const optionalChecked = row.benchmarkBudgetOptionalChecked !== false
|
|
||||||
const fallbackBasic = rawSplit ? (basicChecked ? rawSplit.basic : 0) : null
|
|
||||||
const fallbackOptional = rawSplit ? (optionalChecked ? rawSplit.optional : 0) : null
|
|
||||||
const benchmarkBudgetBasic = toFiniteNumber(row.benchmarkBudgetBasic) ?? fallbackBasic
|
|
||||||
const benchmarkBudgetOptional = toFiniteNumber(row.benchmarkBudgetOptional) ?? fallbackOptional
|
|
||||||
const benchmarkBudget =
|
|
||||||
toFiniteNumber(row.benchmarkBudget) ??
|
|
||||||
(
|
|
||||||
benchmarkBudgetBasic != null || benchmarkBudgetOptional != null
|
|
||||||
? roundTo(addNumbers(benchmarkBudgetBasic ?? 0, benchmarkBudgetOptional ?? 0), 2)
|
|
||||||
: null
|
|
||||||
)
|
|
||||||
const budgetFeeSplit = getScaleBudgetFeeSplit({
|
|
||||||
benchmarkBudgetBasic,
|
|
||||||
benchmarkBudgetOptional,
|
|
||||||
majorFactor: row.majorFactor,
|
|
||||||
consultCategoryFactor: row.consultCategoryFactor,
|
|
||||||
workStageFactor: row.workStageFactor,
|
|
||||||
workRatio: row.workRatio
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
benchmarkBudget,
|
|
||||||
benchmarkBudgetBasic,
|
|
||||||
benchmarkBudgetOptional,
|
|
||||||
basicFormula:
|
|
||||||
typeof row.basicFormula === 'string'
|
|
||||||
? row.basicFormula
|
|
||||||
: (basicChecked ? (rawSplit?.basicFormula ?? '') : ''),
|
|
||||||
optionalFormula:
|
|
||||||
typeof row.optionalFormula === 'string'
|
|
||||||
? row.optionalFormula
|
|
||||||
: (optionalChecked ? (rawSplit?.optionalFormula ?? '') : ''),
|
|
||||||
budgetFee: toFiniteNumber(row.budgetFee) ?? budgetFeeSplit?.total ?? null,
|
|
||||||
budgetFeeBasic: toFiniteNumber(row.budgetFeeBasic) ?? budgetFeeSplit?.basic ?? null,
|
|
||||||
budgetFeeOptional: toFiniteNumber(row.budgetFeeOptional) ?? budgetFeeSplit?.optional ?? null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const calcWorkloadBasicFeeFromRow = (row: WorkloadMethodRowLike) => {
|
|
||||||
const price = toFiniteNumber(row.budgetAdoptedUnitPrice)
|
|
||||||
const conversion = toFiniteNumber(row.conversion)
|
|
||||||
const workload = toFiniteNumber(row.workload)
|
|
||||||
if (price == null || conversion == null || workload == null) return null
|
|
||||||
return roundTo(toDecimal(price).mul(conversion).mul(workload), 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
const calcWorkloadServiceFeeFromRow = (row: WorkloadMethodRowLike, basicFee: number | null) => {
|
|
||||||
const serviceCoe = toFiniteNumber(row.consultCategoryFactor)
|
|
||||||
if (basicFee == null || serviceCoe == null) return null
|
|
||||||
return roundTo(toDecimal(basicFee).mul(serviceCoe), 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
const calcHourlyServiceFeeFromRow = (row: HourlyMethodRowLike) => {
|
|
||||||
const price = toFiniteNumber(row.adoptedBudgetUnitPrice)
|
|
||||||
const personNum = toFiniteNumber(row.personnelCount)
|
|
||||||
const workDay = toFiniteNumber(row.workdayCount)
|
|
||||||
if (price == null || personNum == null || workDay == null) return null
|
|
||||||
return roundTo(toDecimal(price).mul(personNum).mul(workDay), 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getTaskIdFromRowId = (value: string): number | null => {
|
const getTaskIdFromRowId = (value: string): number | null => {
|
||||||
const match = /^task-(\d+)-\d+$/.exec(value)
|
const match = /^task-(\d+)-\d+$/.exec(value)
|
||||||
return match ? toSafeInteger(match[1]) : null
|
return match ? toSafeInteger(match[1]) : null
|
||||||
@ -1366,6 +1063,18 @@ const toExportScaleRows = (rows: ScaleRowLike[] | undefined): ExportScaleRow[] =
|
|||||||
.filter((item): item is ExportScaleRow => Boolean(item))
|
.filter((item): item is ExportScaleRow => Boolean(item))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sumLeafScaleCost = (rows: ScaleRowLike[] | undefined) => {
|
||||||
|
if (!Array.isArray(rows)) return 0
|
||||||
|
return sumNumbers(
|
||||||
|
rows.map(row => {
|
||||||
|
if (row?.isGroupRow === true) return null
|
||||||
|
return toFiniteNumber(row?.amount)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const buildMethod1 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod1 | null => {
|
const buildMethod1 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod1 | null => {
|
||||||
if (!Array.isArray(rows)) return null
|
if (!Array.isArray(rows)) return null
|
||||||
let hasTotalValue = false
|
let hasTotalValue = false
|
||||||
@ -1374,11 +1083,10 @@ const buildMethod1 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod1 | n
|
|||||||
const major = toSafeInteger(row.id)
|
const major = toSafeInteger(row.id)
|
||||||
if (major == null) return null
|
if (major == null) return null
|
||||||
const cost = toFiniteNumber(row.amount)
|
const cost = toFiniteNumber(row.amount)
|
||||||
const computed = resolveScaleMethodComputedValues(row, 'cost')
|
const basicFee = toFiniteNumber(row.benchmarkBudget)
|
||||||
const basicFee = computed.benchmarkBudget
|
const basicFeeBasic = toFiniteNumber(row.benchmarkBudgetBasic)
|
||||||
const basicFeeBasic = computed.benchmarkBudgetBasic
|
const basicFeeOptional = toFiniteNumber(row.benchmarkBudgetOptional)
|
||||||
const basicFeeOptional = computed.benchmarkBudgetOptional
|
const fee = toFiniteNumber(row.budgetFee)
|
||||||
const fee = computed.budgetFee
|
|
||||||
if (basicFee != null || fee != null) hasTotalValue = true
|
if (basicFee != null || fee != null) hasTotalValue = true
|
||||||
const remark = typeof row.remark === 'string' ? row.remark : ''
|
const remark = typeof row.remark === 'string' ? row.remark : ''
|
||||||
const hasValue =
|
const hasValue =
|
||||||
@ -1393,9 +1101,9 @@ const buildMethod1 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod1 | n
|
|||||||
major,
|
major,
|
||||||
cost: cost ?? 0,
|
cost: cost ?? 0,
|
||||||
basicFee: basicFee ?? 0,
|
basicFee: basicFee ?? 0,
|
||||||
basicFormula: computed.basicFormula,
|
basicFormula: typeof row.basicFormula === 'string' ? row.basicFormula : '',
|
||||||
basicFee_basic: basicFeeBasic ?? 0,
|
basicFee_basic: basicFeeBasic ?? 0,
|
||||||
optionalFormula: computed.optionalFormula,
|
optionalFormula: typeof row.optionalFormula === 'string' ? row.optionalFormula : '',
|
||||||
basicFee_optional: basicFeeOptional ?? 0,
|
basicFee_optional: basicFeeOptional ?? 0,
|
||||||
serviceCoe: toFiniteNumberOrZero(row.consultCategoryFactor),
|
serviceCoe: toFiniteNumberOrZero(row.consultCategoryFactor),
|
||||||
majorCoe: toFiniteNumberOrZero(row.majorFactor),
|
majorCoe: toFiniteNumberOrZero(row.majorFactor),
|
||||||
@ -1426,11 +1134,10 @@ const buildMethod2 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod2 | n
|
|||||||
const major = toSafeInteger(row.id)
|
const major = toSafeInteger(row.id)
|
||||||
if (major == null) return null
|
if (major == null) return null
|
||||||
const area = toFiniteNumber(row.landArea)
|
const area = toFiniteNumber(row.landArea)
|
||||||
const computed = resolveScaleMethodComputedValues(row, 'area')
|
const basicFee = toFiniteNumber(row.benchmarkBudget)
|
||||||
const basicFee = computed.benchmarkBudget
|
const basicFeeBasic = toFiniteNumber(row.benchmarkBudgetBasic)
|
||||||
const basicFeeBasic = computed.benchmarkBudgetBasic
|
const basicFeeOptional = toFiniteNumber(row.benchmarkBudgetOptional)
|
||||||
const basicFeeOptional = computed.benchmarkBudgetOptional
|
const fee = toFiniteNumber(row.budgetFee)
|
||||||
const fee = computed.budgetFee
|
|
||||||
if (basicFee != null || fee != null) hasTotalValue = true
|
if (basicFee != null || fee != null) hasTotalValue = true
|
||||||
const remark = typeof row.remark === 'string' ? row.remark : ''
|
const remark = typeof row.remark === 'string' ? row.remark : ''
|
||||||
const hasValue =
|
const hasValue =
|
||||||
@ -1445,9 +1152,9 @@ const buildMethod2 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod2 | n
|
|||||||
major,
|
major,
|
||||||
area: area ?? 0,
|
area: area ?? 0,
|
||||||
basicFee: basicFee ?? 0,
|
basicFee: basicFee ?? 0,
|
||||||
basicFormula: computed.basicFormula,
|
basicFormula: typeof row.basicFormula === 'string' ? row.basicFormula : '',
|
||||||
basicFee_basic: basicFeeBasic ?? 0,
|
basicFee_basic: basicFeeBasic ?? 0,
|
||||||
optionalFormula: computed.optionalFormula,
|
optionalFormula: typeof row.optionalFormula === 'string' ? row.optionalFormula : '',
|
||||||
basicFee_optional: basicFeeOptional ?? 0,
|
basicFee_optional: basicFeeOptional ?? 0,
|
||||||
serviceCoe: toFiniteNumberOrZero(row.consultCategoryFactor),
|
serviceCoe: toFiniteNumberOrZero(row.consultCategoryFactor),
|
||||||
majorCoe: toFiniteNumberOrZero(row.majorFactor),
|
majorCoe: toFiniteNumberOrZero(row.majorFactor),
|
||||||
@ -1476,10 +1183,10 @@ const buildMethod3 = (rows: WorkloadMethodRowLike[] | undefined): ExportMethod3
|
|||||||
const det = rows
|
const det = rows
|
||||||
.map(row => {
|
.map(row => {
|
||||||
const task = getTaskIdFromRowId(row.id)
|
const task = getTaskIdFromRowId(row.id)
|
||||||
if (task == null) return null
|
if (task == null || row.basicFee == null) return null
|
||||||
const amount = toFiniteNumber(row.workload)
|
const amount = toFiniteNumber(row.workload)
|
||||||
const basicFee = toFiniteNumber(row.basicFee) ?? calcWorkloadBasicFeeFromRow(row)
|
const basicFee = toFiniteNumber(row.basicFee)
|
||||||
const fee = toFiniteNumber(row.serviceFee) ?? calcWorkloadServiceFeeFromRow(row, basicFee)
|
const fee = toFiniteNumber(row.serviceFee)
|
||||||
if (fee != null) hasTotalValue = true
|
if (fee != null) hasTotalValue = true
|
||||||
const remark = typeof row.remark === 'string' ? row.remark : ''
|
const remark = typeof row.remark === 'string' ? row.remark : ''
|
||||||
const hasValue = amount != null || basicFee != null || fee != null || isNonEmptyString(remark)
|
const hasValue = amount != null || basicFee != null || fee != null || isNonEmptyString(remark)
|
||||||
@ -1510,10 +1217,10 @@ const buildMethod4 = (rows: HourlyMethodRowLike[] | undefined): ExportMethod4 |
|
|||||||
const det = rows
|
const det = rows
|
||||||
.map(row => {
|
.map(row => {
|
||||||
const expert = getExpertIdFromRowId(row.id)
|
const expert = getExpertIdFromRowId(row.id)
|
||||||
if (expert == null) return null
|
if (expert == null || row.serviceBudget == null) return null
|
||||||
const personNum = toFiniteNumber(row.personnelCount)
|
const personNum = toFiniteNumber(row.personnelCount)
|
||||||
const workDay = toFiniteNumber(row.workdayCount)
|
const workDay = toFiniteNumber(row.workdayCount)
|
||||||
const fee = toFiniteNumber(row.serviceBudget) ?? calcHourlyServiceFeeFromRow(row)
|
const fee = toFiniteNumber(row.serviceBudget)
|
||||||
if (fee != null) hasTotalValue = true
|
if (fee != null) hasTotalValue = true
|
||||||
const remark = typeof row.remark === 'string' ? row.remark : ''
|
const remark = typeof row.remark === 'string' ? row.remark : ''
|
||||||
const hasValue = personNum != null || workDay != null || fee != null || isNonEmptyString(remark)
|
const hasValue = personNum != null || workDay != null || fee != null || isNonEmptyString(remark)
|
||||||
@ -1709,20 +1416,18 @@ const buildReserveExport = async (contractId: string): Promise<ExportReserve | n
|
|||||||
}
|
}
|
||||||
|
|
||||||
const buildExportReportPayload = async (): Promise<ExportReportPayload> => {
|
const buildExportReportPayload = async (): Promise<ExportReportPayload> => {
|
||||||
const config = getReportWorkspaceConfig()
|
const [projectInfoRaw, projectScaleRaw, consultCategoryFactorRaw, majorFactorRaw, contractCardsRaw] = await Promise.all([
|
||||||
const [projectInfoRaw, projectScaleRaw, consultCategoryFactorRaw, majorFactorRaw, contractCardsRaw, quickContractRaw] = await Promise.all([
|
kvStore.getItem<XmInfoLike>(PROJECT_INFO_DB_KEY),
|
||||||
kvStore.getItem<XmInfoLike>(config.projectInfoKey),
|
kvStore.getItem<XmInfoStorageLike>(LEGACY_PROJECT_DB_KEY),
|
||||||
kvStore.getItem<XmInfoStorageLike>(config.projectScaleKey),
|
kvStore.getItem<DetailRowsStorageLike<FactorRowLike>>(CONSULT_CATEGORY_FACTOR_DB_KEY),
|
||||||
kvStore.getItem<DetailRowsStorageLike<FactorRowLike>>(config.consultCategoryFactorKey),
|
kvStore.getItem<DetailRowsStorageLike<FactorRowLike>>(MAJOR_FACTOR_DB_KEY),
|
||||||
kvStore.getItem<DetailRowsStorageLike<FactorRowLike>>(config.majorFactorKey),
|
kvStore.getItem<ContractCardItem[]>('ht-card-v1')
|
||||||
config.contractCardsKey ? kvStore.getItem<ContractCardItem[]>(config.contractCardsKey) : Promise.resolve(null),
|
|
||||||
config.quickContractKey ? kvStore.getItem<QuickContractMeta>(config.quickContractKey) : Promise.resolve(null)
|
|
||||||
])
|
])
|
||||||
|
|
||||||
const projectInfo = projectInfoRaw || {}
|
const projectInfo = projectInfoRaw || {}
|
||||||
const projectScaleSource = projectScaleRaw || {}
|
const projectScaleSource = projectScaleRaw || {}
|
||||||
const projectScale = projectScaleSource.roughCalcEnabled ? [] : toExportScaleRows(projectScaleSource.detailRows)
|
const projectScale = projectScaleSource.roughCalcEnabled ? [] : toExportScaleRows(projectScaleSource.detailRows)
|
||||||
const projectScaleCost = toFiniteNumber(projectScaleSource.totalAmount)
|
const projectScaleCost = toFiniteNumber(projectScaleSource.totalAmount) ?? sumLeafScaleCost(projectScaleSource.detailRows)
|
||||||
projectScale.push({
|
projectScale.push({
|
||||||
major: -1, cost: projectScaleCost,
|
major: -1, cost: projectScaleCost,
|
||||||
area: null
|
area: null
|
||||||
@ -1730,21 +1435,13 @@ const buildExportReportPayload = async (): Promise<ExportReportPayload> => {
|
|||||||
|
|
||||||
const projectServiceCoes = buildProjectServiceCoes(consultCategoryFactorRaw?.detailRows)
|
const projectServiceCoes = buildProjectServiceCoes(consultCategoryFactorRaw?.detailRows)
|
||||||
const projectMajorCoes = buildProjectMajorCoes(majorFactorRaw?.detailRows)
|
const projectMajorCoes = buildProjectMajorCoes(majorFactorRaw?.detailRows)
|
||||||
const quickContract = normalizeQuickContractMeta(quickContractRaw)
|
const projectName = isNonEmptyString(projectInfo.projectName) ? projectInfo.projectName.trim() : '造价项目'
|
||||||
const projectName =
|
|
||||||
config.mode === 'quick'
|
|
||||||
? `${quickContract.name}-快速计算`
|
|
||||||
: (isNonEmptyString(projectInfo.projectName) ? projectInfo.projectName.trim() : '造价项目')
|
|
||||||
const writer = isNonEmptyString(projectInfo.preparedBy) ? projectInfo.preparedBy.trim() : ''
|
const writer = isNonEmptyString(projectInfo.preparedBy) ? projectInfo.preparedBy.trim() : ''
|
||||||
const reviewer = isNonEmptyString(projectInfo.reviewedBy) ? projectInfo.reviewedBy.trim() : ''
|
const reviewer = isNonEmptyString(projectInfo.reviewedBy) ? projectInfo.reviewedBy.trim() : ''
|
||||||
const date = isNonEmptyString(projectInfo.preparedDate) ? projectInfo.preparedDate.trim() : ''
|
const date = isNonEmptyString(projectInfo.preparedDate) ? projectInfo.preparedDate.trim() : ''
|
||||||
const industry = mapIndustryCodeToExportIndustry(projectInfo.projectIndustry)
|
const industry = mapIndustryCodeToExportIndustry(projectInfo.projectIndustry)
|
||||||
|
|
||||||
const contractCards = (
|
const contractCards = (Array.isArray(contractCardsRaw) ? contractCardsRaw : [])
|
||||||
config.mode === 'quick'
|
|
||||||
? [{ id: quickContract.id, name: quickContract.name, order: 0 }]
|
|
||||||
: (Array.isArray(contractCardsRaw) ? contractCardsRaw : [])
|
|
||||||
)
|
|
||||||
.filter(item => item && typeof item.id === 'string')
|
.filter(item => item && typeof item.id === 'string')
|
||||||
.sort((a, b) => (typeof a.order === 'number' ? a.order : Number.MAX_SAFE_INTEGER) - (typeof b.order === 'number' ? b.order : Number.MAX_SAFE_INTEGER))
|
.sort((a, b) => (typeof a.order === 'number' ? a.order : Number.MAX_SAFE_INTEGER) - (typeof b.order === 'number' ? b.order : Number.MAX_SAFE_INTEGER))
|
||||||
|
|
||||||
@ -1879,9 +1576,6 @@ const buildExportReportPayload = async (): Promise<ExportReportPayload> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
version: 2,
|
|
||||||
reportType: 'budget-report',
|
|
||||||
mode: config.mode,
|
|
||||||
name: projectName,
|
name: projectName,
|
||||||
writer,
|
writer,
|
||||||
reviewer,
|
reviewer,
|
||||||
@ -1892,19 +1586,7 @@ const buildExportReportPayload = async (): Promise<ExportReportPayload> => {
|
|||||||
scale: projectScale,
|
scale: projectScale,
|
||||||
serviceCoes: projectServiceCoes,
|
serviceCoes: projectServiceCoes,
|
||||||
majorCoes: projectMajorCoes,
|
majorCoes: projectMajorCoes,
|
||||||
contracts,
|
contracts
|
||||||
project: {
|
|
||||||
name: projectName,
|
|
||||||
writer,
|
|
||||||
reviewer,
|
|
||||||
date,
|
|
||||||
industry,
|
|
||||||
fee: sumNumbers(contracts.map(item => item.fee)),
|
|
||||||
scaleCost: projectScaleCost,
|
|
||||||
scale: projectScale,
|
|
||||||
serviceCoes: projectServiceCoes,
|
|
||||||
majorCoes: projectMajorCoes
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1920,18 +1602,12 @@ const exportData = async () => {
|
|||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
const payload: DataPackage = {
|
const payload: DataPackage = {
|
||||||
version: 3,
|
version: 2,
|
||||||
packageType: 'workspace-snapshot',
|
|
||||||
exportedAt: now.toISOString(),
|
exportedAt: now.toISOString(),
|
||||||
workspace: {
|
localStorage: readWebStorage(localStorage),
|
||||||
mode: resolveWorkspaceModeForExport()
|
sessionStorage: readWebStorage(sessionStorage),
|
||||||
},
|
localforageDefault: await readForage(localforage),
|
||||||
storage: {
|
localforageStores: piniaForageStores
|
||||||
localStorage: readWebStorage(localStorage),
|
|
||||||
sessionStorage: readWebStorage(sessionStorage),
|
|
||||||
localforageDefault: await readForage(localforage),
|
|
||||||
localforageStores: piniaForageStores
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = await encodeZwArchive(payload)
|
const content = await encodeZwArchive(payload)
|
||||||
@ -1941,7 +1617,7 @@ const exportData = async () => {
|
|||||||
const url = URL.createObjectURL(blob)
|
const url = URL.createObjectURL(blob)
|
||||||
const link = document.createElement('a')
|
const link = document.createElement('a')
|
||||||
link.href = url
|
link.href = url
|
||||||
const projectName = getExportProjectName(payload.storage?.localforageDefault || [], piniaForageStores)
|
const projectName = getExportProjectName(payload.localforageDefault)
|
||||||
const timestamp = formatExportTimestamp(now)
|
const timestamp = formatExportTimestamp(now)
|
||||||
link.download = `${projectName}-${timestamp}${ZW_FILE_EXTENSION}`
|
link.download = `${projectName}-${timestamp}${ZW_FILE_EXTENSION}`
|
||||||
document.body.appendChild(link)
|
document.body.appendChild(link)
|
||||||
@ -1959,12 +1635,12 @@ const exportData = async () => {
|
|||||||
const exportReport = async () => {
|
const exportReport = async () => {
|
||||||
try {
|
try {
|
||||||
showReportExportProgress(10, '正在准备报表导出...')
|
showReportExportProgress(10, '正在准备报表导出...')
|
||||||
await requestXmScalePersistFlush()
|
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
showReportExportProgress(40, '正在汇总报表数据...')
|
showReportExportProgress(40, '正在汇总报表数据...')
|
||||||
const payload = await buildExportReportPayload()
|
const payload = await buildExportReportPayload()
|
||||||
showReportExportProgress(80, '正在生成并写出报表文件...')
|
showReportExportProgress(80, '正在生成并写出报表文件...')
|
||||||
const fileName = `${sanitizeFileNamePart(payload.name)}-报表-${formatExportTimestamp(now)}`
|
const fileName = `${sanitizeFileNamePart(payload.name)}-报表-${formatExportTimestamp(now)}`
|
||||||
|
console.log(payload)
|
||||||
await exportFile(fileName, payload)
|
await exportFile(fileName, payload)
|
||||||
finishReportExportProgress(true, '报表导出完成')
|
finishReportExportProgress(true, '报表导出完成')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -1978,7 +1654,9 @@ const triggerImport = () => {
|
|||||||
importFileRef.value?.click()
|
importFileRef.value?.click()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSelectedImportFile = async (file: File) => {
|
const importData = async (event: Event) => {
|
||||||
|
const input = event.target as HTMLInputElement
|
||||||
|
const file = input.files?.[0]
|
||||||
if (!file) return
|
if (!file) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -1987,34 +1665,12 @@ const handleSelectedImportFile = async (file: File) => {
|
|||||||
}
|
}
|
||||||
const buffer = await file.arrayBuffer()
|
const buffer = await file.arrayBuffer()
|
||||||
const payload = await decodeZwArchive<DataPackage>(buffer)
|
const payload = await decodeZwArchive<DataPackage>(buffer)
|
||||||
const normalizedStorage = normalizeDataPackageStorage(payload)
|
pendingImportPayload.value = payload
|
||||||
pendingImportPayload.value = {
|
|
||||||
...payload,
|
|
||||||
workspace: normalizeDataPackageWorkspace(payload),
|
|
||||||
storage: normalizedStorage
|
|
||||||
}
|
|
||||||
pendingImportFileName.value = file.name
|
pendingImportFileName.value = file.name
|
||||||
importConfirmOpen.value = true
|
importConfirmOpen.value = true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('import failed:', error)
|
console.error('import failed:', error)
|
||||||
window.alert('导入失败:文件无效、已损坏或被修改。')
|
window.alert('导入失败:文件无效、已损坏或被修改。')
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleHomeImportSelected = (event: Event) => {
|
|
||||||
const customEvent = event as CustomEvent<{ file?: File | null }>
|
|
||||||
const file = customEvent.detail?.file
|
|
||||||
if (!file) return
|
|
||||||
void handleSelectedImportFile(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
const importData = async (event: Event) => {
|
|
||||||
const input = event.target as HTMLInputElement
|
|
||||||
const file = input.files?.[0]
|
|
||||||
if (!file) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
await handleSelectedImportFile(file)
|
|
||||||
} finally {
|
} finally {
|
||||||
input.value = ''
|
input.value = ''
|
||||||
}
|
}
|
||||||
@ -2030,12 +1686,10 @@ const confirmImportOverride = async () => {
|
|||||||
const payload = pendingImportPayload.value
|
const payload = pendingImportPayload.value
|
||||||
if (!payload) return
|
if (!payload) return
|
||||||
try {
|
try {
|
||||||
const normalizedStorage = normalizeDataPackageStorage(payload)
|
writeWebStorage(localStorage, normalizeEntries(payload.localStorage))
|
||||||
const normalizedWorkspace = normalizeDataPackageWorkspace(payload)
|
writeWebStorage(sessionStorage, normalizeEntries(payload.sessionStorage))
|
||||||
writeWebStorage(localStorage, normalizedStorage.localStorage)
|
await writeForage(localforage, normalizeEntries(payload.localforageDefault))
|
||||||
writeWebStorage(sessionStorage, normalizedStorage.sessionStorage)
|
const piniaSnapshots = normalizeForageStoreSnapshots(payload.localforageStores)
|
||||||
await writeForage(localforage, normalizedStorage.localforageDefault)
|
|
||||||
const piniaSnapshots = normalizeForageStoreSnapshots(normalizedStorage.localforageStores)
|
|
||||||
const snapshotMap = new Map(piniaSnapshots.map(item => [item.storeName, item.entries]))
|
const snapshotMap = new Map(piniaSnapshots.map(item => [item.storeName, item.entries]))
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
getPiniaPersistStores().map(async ({ storeName, store }) => {
|
getPiniaPersistStores().map(async ({ storeName, store }) => {
|
||||||
@ -2060,7 +1714,6 @@ const confirmImportOverride = async () => {
|
|||||||
} else {
|
} else {
|
||||||
tabStore.resetTabs()
|
tabStore.resetTabs()
|
||||||
}
|
}
|
||||||
normalizeTabStoreState()
|
|
||||||
|
|
||||||
const zxFwPricingState = readPersistedState('zxFwPricing')
|
const zxFwPricingState = readPersistedState('zxFwPricing')
|
||||||
if (zxFwPricingState) {
|
if (zxFwPricingState) {
|
||||||
@ -2077,7 +1730,6 @@ const confirmImportOverride = async () => {
|
|||||||
zxFwPricingStore.$persistNow?.(),
|
zxFwPricingStore.$persistNow?.(),
|
||||||
kvStore.$persistNow?.()
|
kvStore.$persistNow?.()
|
||||||
])
|
])
|
||||||
writeWorkspaceMode(normalizedWorkspace.mode)
|
|
||||||
dataMenuOpen.value = false
|
dataMenuOpen.value = false
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -2109,10 +1761,8 @@ const handleReset = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
normalizeTabStoreState()
|
|
||||||
window.addEventListener('mousedown', handleGlobalMouseDown)
|
window.addEventListener('mousedown', handleGlobalMouseDown)
|
||||||
window.addEventListener('keydown', handleGlobalKeyDown)
|
window.addEventListener('keydown', handleGlobalKeyDown)
|
||||||
window.addEventListener('home-import-selected', handleHomeImportSelected as EventListener)
|
|
||||||
window.addEventListener('resize', scheduleUpdateTabTitleOverflow)
|
window.addEventListener('resize', scheduleUpdateTabTitleOverflow)
|
||||||
void nextTick(() => {
|
void nextTick(() => {
|
||||||
bindTabStripScroll()
|
bindTabStripScroll()
|
||||||
@ -2132,7 +1782,6 @@ onBeforeUnmount(() => {
|
|||||||
clearReportExportToastTimer()
|
clearReportExportToastTimer()
|
||||||
window.removeEventListener('mousedown', handleGlobalMouseDown)
|
window.removeEventListener('mousedown', handleGlobalMouseDown)
|
||||||
window.removeEventListener('keydown', handleGlobalKeyDown)
|
window.removeEventListener('keydown', handleGlobalKeyDown)
|
||||||
window.removeEventListener('home-import-selected', handleHomeImportSelected as EventListener)
|
|
||||||
window.removeEventListener('resize', scheduleUpdateTabTitleOverflow)
|
window.removeEventListener('resize', scheduleUpdateTabTitleOverflow)
|
||||||
if (tabStripViewportEl) {
|
if (tabStripViewportEl) {
|
||||||
tabStripViewportEl.removeEventListener('scroll', handleTabStripScroll)
|
tabStripViewportEl.removeEventListener('scroll', handleTabStripScroll)
|
||||||
@ -2185,15 +1834,8 @@ watch(
|
|||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<div class="flex flex-col w-full h-screen bg-background overflow-hidden">
|
<div class="flex flex-col w-full h-screen bg-background overflow-hidden">
|
||||||
<div
|
<div class="grid grid-cols-[minmax(0,1fr)_auto] items-start gap-2 border-b bg-muted/30 px-2 pt-1 h-15 flex-none">
|
||||||
v-if="!isHomeOnlyView"
|
<div class="flex min-w-0 items-start gap-1 h-full" @mouseenter="isTabStripHover = true"
|
||||||
class="grid items-start gap-2 border-b bg-muted/30 px-2 pt-1 h-15 flex-none"
|
|
||||||
:class="showTabStrip ? 'grid-cols-[minmax(0,1fr)_auto]' : 'grid-cols-[1fr_auto]'"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="showTabStrip"
|
|
||||||
class="flex min-w-0 items-start gap-1 h-full"
|
|
||||||
@mouseenter="isTabStripHover = true"
|
|
||||||
@mouseleave="isTabStripHover = false">
|
@mouseleave="isTabStripHover = false">
|
||||||
<button type="button" :class="[
|
<button type="button" :class="[
|
||||||
'h-9 w-8 self-center shrink-0 cursor-pointer rounded border bg-background text-sm text-muted-foreground transition hover:bg-muted',
|
'h-9 w-8 self-center shrink-0 cursor-pointer rounded border bg-background text-sm text-muted-foreground transition hover:bg-muted',
|
||||||
@ -2215,7 +1857,7 @@ watch(
|
|||||||
tabStore.activeTabId === tab.id && !isTabDragging
|
tabStore.activeTabId === tab.id && !isTabDragging
|
||||||
? 'z-10 bg-background text-foreground !border-border !border-b-0 font-medium'
|
? 'z-10 bg-background text-foreground !border-border !border-b-0 font-medium'
|
||||||
: 'bg-muted/25 text-muted-foreground hover:bg-muted/40 hover:text-foreground hover:border-border/70',
|
: 'bg-muted/25 text-muted-foreground hover:bg-muted/40 hover:text-foreground hover:border-border/70',
|
||||||
isTabClosable(tab.id) ? 'cursor-move' : ''
|
tab.id !== 'XmView' ? 'cursor-move' : ''
|
||||||
]">
|
]">
|
||||||
<TooltipRoot>
|
<TooltipRoot>
|
||||||
<TooltipTrigger as-child>
|
<TooltipTrigger as-child>
|
||||||
@ -2226,7 +1868,7 @@ watch(
|
|||||||
<TooltipContent v-if="tabTitleOverflowMap[tab.id]" side="bottom">{{ tab.title }}</TooltipContent>
|
<TooltipContent v-if="tabTitleOverflowMap[tab.id]" side="bottom">{{ tab.title }}</TooltipContent>
|
||||||
</TooltipRoot>
|
</TooltipRoot>
|
||||||
|
|
||||||
<Button v-if="isTabClosable(tab.id)" variant="ghost" size="icon"
|
<Button v-if="tab.id !== 'XmView'" variant="ghost" size="icon"
|
||||||
class="h-4 w-4 ml-auto opacity-0 group-hover:opacity-100 hover:bg-destructive hover:text-destructive-foreground transition-opacity"
|
class="h-4 w-4 ml-auto opacity-0 group-hover:opacity-100 hover:bg-destructive hover:text-destructive-foreground transition-opacity"
|
||||||
@click.stop="tabStore.removeTab(tab.id)">
|
@click.stop="tabStore.removeTab(tab.id)">
|
||||||
<X class="h-3 w-3" />
|
<X class="h-3 w-3" />
|
||||||
@ -2243,18 +1885,14 @@ watch(
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex shrink-0 self-center items-center gap-1">
|
<div class="flex shrink-0 self-center items-center gap-1">
|
||||||
<div v-if="showWorkspaceActions" ref="dataMenuRef" class="relative shrink-0">
|
<div ref="dataMenuRef" class="relative shrink-0">
|
||||||
<Button
|
<Button variant="outline" size="sm"
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
class="h-9 min-h-9 shrink-0 px-3 py-0 text-sm leading-none cursor-pointer"
|
class="h-9 min-h-9 shrink-0 px-3 py-0 text-sm leading-none cursor-pointer"
|
||||||
@click="dataMenuOpen = !dataMenuOpen"
|
@click="dataMenuOpen = !dataMenuOpen">
|
||||||
>
|
|
||||||
<ChevronDown class="h-4 w-4 mr-1" />
|
<ChevronDown class="h-4 w-4 mr-1" />
|
||||||
导入/导出
|
导入/导出
|
||||||
</Button>
|
</Button>
|
||||||
<div
|
<div v-if="dataMenuOpen"
|
||||||
v-if="dataMenuOpen"
|
|
||||||
class="absolute right-0 top-full mt-1 z-50 rounded-md border bg-background p-1 shadow-md">
|
class="absolute right-0 top-full mt-1 z-50 rounded-md border bg-background p-1 shadow-md">
|
||||||
<button class="w-full cursor-pointer rounded px-3 py-1.5 text-left text-sm hover:bg-muted"
|
<button class="w-full cursor-pointer rounded px-3 py-1.5 text-left text-sm hover:bg-muted"
|
||||||
@click="triggerImport">
|
@click="triggerImport">
|
||||||
@ -2269,22 +1907,16 @@ watch(
|
|||||||
导出报表
|
导出报表
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<input ref="importFileRef" type="file" accept=".zw" class="hidden" @change="importData" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input ref="importFileRef" type="file" accept=".zw" class="hidden" @change="importData" />
|
<Button variant="outline" size="sm" class="h-9 min-h-9 shrink-0 px-3 py-0 text-sm leading-none cursor-pointer"
|
||||||
|
@click="openUserGuide(0)">
|
||||||
<Button
|
|
||||||
v-if="showWorkspaceActions"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
class="h-9 min-h-9 shrink-0 px-3 py-0 text-sm leading-none cursor-pointer"
|
|
||||||
@click="openUserGuide(0)"
|
|
||||||
>
|
|
||||||
<CircleHelp class="h-4 w-4 mr-1" />
|
<CircleHelp class="h-4 w-4 mr-1" />
|
||||||
使用引导
|
使用引导
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<AlertDialogRoot v-if="showWorkspaceActions">
|
<AlertDialogRoot>
|
||||||
<AlertDialogTrigger as-child>
|
<AlertDialogTrigger as-child>
|
||||||
<Button variant="destructive" size="sm"
|
<Button variant="destructive" size="sm"
|
||||||
class="h-9 min-h-9 shrink-0 px-3 py-0 text-sm leading-none cursor-pointer">
|
class="h-9 min-h-9 shrink-0 px-3 py-0 text-sm leading-none cursor-pointer">
|
||||||
|
|||||||
@ -25,7 +25,6 @@ const props = withDefaults(
|
|||||||
scene?: string
|
scene?: string
|
||||||
title?: string
|
title?: string
|
||||||
subtitle?: string
|
subtitle?: string
|
||||||
metaText?: string
|
|
||||||
copyText?: string
|
copyText?: string
|
||||||
categories: TypeLineCategory[]
|
categories: TypeLineCategory[]
|
||||||
storageKey?: string
|
storageKey?: string
|
||||||
@ -35,7 +34,6 @@ const props = withDefaults(
|
|||||||
scene: 'default',
|
scene: 'default',
|
||||||
title: '配置',
|
title: '配置',
|
||||||
subtitle: '',
|
subtitle: '',
|
||||||
metaText: '',
|
|
||||||
copyText: '',
|
copyText: '',
|
||||||
storageKey: '',
|
storageKey: '',
|
||||||
defaultCategory: ''
|
defaultCategory: ''
|
||||||
@ -203,7 +201,7 @@ useMotionValueEvent(
|
|||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<div class="flex h-full w-full bg-background">
|
<div class="flex h-full w-full bg-background">
|
||||||
<div class="w-12/100 border-r p-2 flex flex-col gap-8 relative">
|
<div class="w-12/100 border-r p-2 flex flex-col gap-8 relative">
|
||||||
<div v-if="props.title || props.subtitle || props.metaText" class="space-y-1">
|
<div v-if="props.title || props.subtitle" class="space-y-1">
|
||||||
<TooltipRoot>
|
<TooltipRoot>
|
||||||
<TooltipTrigger as-child>
|
<TooltipTrigger as-child>
|
||||||
<div v-if="props.title" ref="titleRef" class="title-ellipsis-2 max-w-full font-bold text-base leading-6 text-primary">
|
<div v-if="props.title" ref="titleRef" class="title-ellipsis-2 max-w-full font-bold text-base leading-6 text-primary">
|
||||||
@ -226,9 +224,6 @@ useMotionValueEvent(
|
|||||||
{{ copyBtnText }}
|
{{ copyBtnText }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="props.metaText" class="text-xs leading-5 text-muted-foreground">
|
|
||||||
{{ props.metaText }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-10 relative">
|
<div class="flex flex-col gap-10 relative">
|
||||||
|
|||||||
@ -1,112 +0,0 @@
|
|||||||
import {
|
|
||||||
getIndustryTypeValue,
|
|
||||||
getMajorDictEntries,
|
|
||||||
getServiceDictEntries,
|
|
||||||
isIndustryEnabledByType,
|
|
||||||
isMajorIdInIndustryScope
|
|
||||||
} from '@/sql'
|
|
||||||
|
|
||||||
interface KvStoreLike {
|
|
||||||
setItem: <T = unknown>(keyRaw: string | number, value: T) => Promise<void>
|
|
||||||
}
|
|
||||||
|
|
||||||
type DictItemLite = {
|
|
||||||
code?: string
|
|
||||||
name?: string
|
|
||||||
defCoe?: number | null
|
|
||||||
notshowByzxflxs?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
type FactorPersistRow = {
|
|
||||||
id: string
|
|
||||||
code: string
|
|
||||||
name: string
|
|
||||||
standardFactor: number | null
|
|
||||||
budgetValue: number | null
|
|
||||||
remark: string
|
|
||||||
path: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
type FactorPersistState = {
|
|
||||||
detailRows: FactorPersistRow[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const buildCodePath = (code: string, selfId: string, codeIdMap: Map<string, string>) => {
|
|
||||||
const parts = code.split('-').filter(Boolean)
|
|
||||||
if (!parts.length) return [selfId]
|
|
||||||
|
|
||||||
const path: string[] = []
|
|
||||||
let currentCode = parts[0]
|
|
||||||
const firstId = codeIdMap.get(currentCode)
|
|
||||||
if (firstId) path.push(firstId)
|
|
||||||
|
|
||||||
for (let index = 1; index < parts.length; index += 1) {
|
|
||||||
currentCode = `${currentCode}-${parts[index]}`
|
|
||||||
const id = codeIdMap.get(currentCode)
|
|
||||||
if (id) path.push(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!path.length || path[path.length - 1] !== selfId) path.push(selfId)
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
|
|
||||||
const buildFactorRowsFromEntries = (entries: Array<{ id: string; item: DictItemLite }>): FactorPersistRow[] => {
|
|
||||||
const codeIdMap = new Map<string, string>()
|
|
||||||
for (const entry of entries) {
|
|
||||||
const code = String(entry.item?.code || '').trim()
|
|
||||||
if (!code) continue
|
|
||||||
codeIdMap.set(code, entry.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
return entries
|
|
||||||
.map(entry => {
|
|
||||||
const code = String(entry.item?.code || '').trim()
|
|
||||||
const name = String(entry.item?.name || '').trim()
|
|
||||||
if (!code || !name) return null
|
|
||||||
const standardFactor =
|
|
||||||
typeof entry.item?.defCoe === 'number' && Number.isFinite(entry.item.defCoe)
|
|
||||||
? entry.item.defCoe
|
|
||||||
: null
|
|
||||||
return {
|
|
||||||
id: entry.id,
|
|
||||||
code,
|
|
||||||
name,
|
|
||||||
standardFactor,
|
|
||||||
budgetValue: standardFactor,
|
|
||||||
remark: '',
|
|
||||||
path: buildCodePath(code, entry.id, codeIdMap)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter((item): item is FactorPersistRow => Boolean(item))
|
|
||||||
}
|
|
||||||
|
|
||||||
export const initializeProjectFactorStates = async (
|
|
||||||
kvStore: KvStoreLike,
|
|
||||||
industry: string,
|
|
||||||
consultCategoryFactorKey: string,
|
|
||||||
majorFactorKey: string
|
|
||||||
) => {
|
|
||||||
const industryType = getIndustryTypeValue(industry)
|
|
||||||
const consultEntries = getServiceDictEntries()
|
|
||||||
.map(({ id, item }) => ({ id, item: item as DictItemLite }))
|
|
||||||
.filter(({ item }) => {
|
|
||||||
if (item.notshowByzxflxs === true) return false
|
|
||||||
return isIndustryEnabledByType(item as Record<string, unknown>, industryType)
|
|
||||||
})
|
|
||||||
|
|
||||||
const majorEntries = getMajorDictEntries()
|
|
||||||
.map(({ id, item }) => ({ id, item: item as DictItemLite }))
|
|
||||||
.filter(({ id, item }) => item.notshowByzxflxs !== true && isMajorIdInIndustryScope(id, industry))
|
|
||||||
|
|
||||||
const consultPayload: FactorPersistState = {
|
|
||||||
detailRows: buildFactorRowsFromEntries(consultEntries)
|
|
||||||
}
|
|
||||||
const majorPayload: FactorPersistState = {
|
|
||||||
detailRows: buildFactorRowsFromEntries(majorEntries)
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
kvStore.setItem(consultCategoryFactorKey, consultPayload),
|
|
||||||
kvStore.setItem(majorFactorKey, majorPayload)
|
|
||||||
])
|
|
||||||
}
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
export type WorkspaceMode = 'home' | 'project' | 'quick'
|
|
||||||
|
|
||||||
export const HOME_TAB_ID = 'HomeView'
|
|
||||||
export const PROJECT_TAB_ID = 'ProjectCalcView'
|
|
||||||
export const QUICK_TAB_ID = 'QuickCalcView'
|
|
||||||
export const LEGACY_PROJECT_TAB_ID = 'XmView'
|
|
||||||
export const FIXED_WORKSPACE_TAB_IDS = [PROJECT_TAB_ID, QUICK_TAB_ID] as const
|
|
||||||
|
|
||||||
export const WORKSPACE_MODE_STORAGE_KEY = 'jgjs-workspace-mode-v1'
|
|
||||||
|
|
||||||
export const QUICK_CONTRACT_ID = 'quick-contract-default'
|
|
||||||
export const QUICK_CONTRACT_META_KEY = 'quick-contract-meta-v1'
|
|
||||||
export const QUICK_CONTRACT_FALLBACK_NAME = '快速计算合同'
|
|
||||||
export const QUICK_CONTRACT_TAB_ID = `contract-${QUICK_CONTRACT_ID}`
|
|
||||||
|
|
||||||
export const QUICK_PROJECT_INFO_KEY = 'quick-xm-base-info-v1'
|
|
||||||
export const QUICK_PROJECT_SCALE_KEY = 'quick-xm-info-v3'
|
|
||||||
export const QUICK_CONSULT_CATEGORY_FACTOR_KEY = 'quick-xm-consult-category-factor-v1'
|
|
||||||
export const QUICK_MAJOR_FACTOR_KEY = 'quick-xm-major-factor-v1'
|
|
||||||
|
|
||||||
export interface QuickContractMeta {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
updatedAt: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const normalizeWorkspaceMode = (value: unknown): WorkspaceMode => {
|
|
||||||
if (value === 'project' || value === 'quick' || value === 'home') return value
|
|
||||||
return 'home'
|
|
||||||
}
|
|
||||||
|
|
||||||
export const readWorkspaceMode = (): WorkspaceMode => {
|
|
||||||
try {
|
|
||||||
return normalizeWorkspaceMode(window.localStorage.getItem(WORKSPACE_MODE_STORAGE_KEY))
|
|
||||||
} catch {
|
|
||||||
return 'home'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const writeWorkspaceMode = (mode: WorkspaceMode) => {
|
|
||||||
try {
|
|
||||||
window.localStorage.setItem(WORKSPACE_MODE_STORAGE_KEY, normalizeWorkspaceMode(mode))
|
|
||||||
} catch {
|
|
||||||
// 忽略只读或隐私模式下的写入失败。
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createDefaultQuickContractMeta = (): QuickContractMeta => ({
|
|
||||||
id: QUICK_CONTRACT_ID,
|
|
||||||
name: QUICK_CONTRACT_FALLBACK_NAME,
|
|
||||||
updatedAt: new Date().toISOString()
|
|
||||||
})
|
|
||||||
@ -123,19 +123,18 @@ export default (config?: PiniaStorageConfig) => {
|
|||||||
{ detached: true }
|
{ detached: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
hydrating = true
|
|
||||||
void lf.getItem<Record<string, unknown>>(key)
|
void lf.getItem<Record<string, unknown>>(key)
|
||||||
.then(state => {
|
.then(state => {
|
||||||
if (!state || typeof state !== 'object') return
|
if (!state || typeof state !== 'object') return
|
||||||
// 若在异步hydrate返回前,store已被用户修改(如removeTab),不再回填旧缓存覆盖当前状态。
|
// 若在异步hydrate返回前,store已被用户修改(如removeTab),不再回填旧缓存覆盖当前状态。
|
||||||
if (userMutatedBeforeHydrate) return
|
if (userMutatedBeforeHydrate) return
|
||||||
|
hydrating = true
|
||||||
context.store.$patch(state as any)
|
context.store.$patch(state as any)
|
||||||
|
hydrating = false
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('pinia hydrate failed:', error)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
hydrating = false
|
hydrating = false
|
||||||
|
console.error('pinia hydrate failed:', error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { HOME_TAB_ID, PROJECT_TAB_ID, QUICK_CONTRACT_TAB_ID } from '@/lib/workspace'
|
|
||||||
|
|
||||||
export interface TabItem<TProps = Record<string, unknown>> {
|
export interface TabItem<TProps = Record<string, unknown>> {
|
||||||
id: string
|
id: string
|
||||||
@ -9,14 +8,14 @@ export interface TabItem<TProps = Record<string, unknown>> {
|
|||||||
props?: TProps
|
props?: TProps
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const HOME_TAB_ID = 'XmView'
|
||||||
const DEFAULT_TAB: TabItem = {
|
const DEFAULT_TAB: TabItem = {
|
||||||
id: HOME_TAB_ID,
|
id: HOME_TAB_ID,
|
||||||
title: '首页',
|
title: '项目卡片',
|
||||||
componentName: HOME_TAB_ID
|
componentName: HOME_TAB_ID
|
||||||
}
|
}
|
||||||
|
|
||||||
const createDefaultTabs = (): TabItem[] => [{ ...DEFAULT_TAB }]
|
const createDefaultTabs = (): TabItem[] => [{ ...DEFAULT_TAB }]
|
||||||
const PROTECTED_TAB_ID_SET = new Set<string>([HOME_TAB_ID, QUICK_CONTRACT_TAB_ID])
|
|
||||||
|
|
||||||
export const useTabStore = defineStore(
|
export const useTabStore = defineStore(
|
||||||
'tabs',
|
'tabs',
|
||||||
@ -37,11 +36,6 @@ export const useTabStore = defineStore(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const enterWorkspace = (config: TabItem) => {
|
|
||||||
tabs.value = [{ ...config }]
|
|
||||||
activeTabId.value = config.id
|
|
||||||
}
|
|
||||||
|
|
||||||
const openTab = (config: TabItem) => {
|
const openTab = (config: TabItem) => {
|
||||||
if (!tabs.value.some(tab => tab.id === config.id)) {
|
if (!tabs.value.some(tab => tab.id === config.id)) {
|
||||||
tabs.value = [...tabs.value, config]
|
tabs.value = [...tabs.value, config]
|
||||||
@ -50,7 +44,8 @@ export const useTabStore = defineStore(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const removeTab = (id: string) => {
|
const removeTab = (id: string) => {
|
||||||
if (PROTECTED_TAB_ID_SET.has(id)) return
|
// 首页标签固定保留,不允许关闭。
|
||||||
|
if (id === HOME_TAB_ID) return
|
||||||
|
|
||||||
const index = tabs.value.findIndex(tab => tab.id === id)
|
const index = tabs.value.findIndex(tab => tab.id === id)
|
||||||
if (index < 0) return
|
if (index < 0) return
|
||||||
@ -69,27 +64,26 @@ export const useTabStore = defineStore(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const closeAllTabs = () => {
|
const closeAllTabs = () => {
|
||||||
const protectedTabs = tabs.value.filter(tab => PROTECTED_TAB_ID_SET.has(tab.id))
|
tabs.value = createDefaultTabs()
|
||||||
tabs.value = protectedTabs.length > 0 ? protectedTabs : createDefaultTabs()
|
activeTabId.value = HOME_TAB_ID
|
||||||
activeTabId.value = tabs.value[0]?.id ?? HOME_TAB_ID
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const closeLeftTabs = (targetId: string) => {
|
const closeLeftTabs = (targetId: string) => {
|
||||||
const targetIndex = tabs.value.findIndex(tab => tab.id === targetId)
|
const targetIndex = tabs.value.findIndex(tab => tab.id === targetId)
|
||||||
if (targetIndex < 0) return
|
if (targetIndex < 0) return
|
||||||
tabs.value = tabs.value.filter((tab, index) => PROTECTED_TAB_ID_SET.has(tab.id) || index >= targetIndex)
|
tabs.value = tabs.value.filter((tab, index) => tab.id === HOME_TAB_ID || index >= targetIndex)
|
||||||
ensureActiveValid()
|
ensureActiveValid()
|
||||||
}
|
}
|
||||||
|
|
||||||
const closeRightTabs = (targetId: string) => {
|
const closeRightTabs = (targetId: string) => {
|
||||||
const targetIndex = tabs.value.findIndex(tab => tab.id === targetId)
|
const targetIndex = tabs.value.findIndex(tab => tab.id === targetId)
|
||||||
if (targetIndex < 0) return
|
if (targetIndex < 0) return
|
||||||
tabs.value = tabs.value.filter((tab, index) => PROTECTED_TAB_ID_SET.has(tab.id) || index <= targetIndex)
|
tabs.value = tabs.value.filter((tab, index) => tab.id === HOME_TAB_ID || index <= targetIndex)
|
||||||
ensureActiveValid()
|
ensureActiveValid()
|
||||||
}
|
}
|
||||||
|
|
||||||
const closeOtherTabs = (targetId: string) => {
|
const closeOtherTabs = (targetId: string) => {
|
||||||
tabs.value = tabs.value.filter(tab => PROTECTED_TAB_ID_SET.has(tab.id) || tab.id === targetId)
|
tabs.value = tabs.value.filter(tab => tab.id === HOME_TAB_ID || tab.id === targetId)
|
||||||
ensureHomeTab()
|
ensureHomeTab()
|
||||||
activeTabId.value = tabs.value.some(tab => tab.id === targetId) ? targetId : HOME_TAB_ID
|
activeTabId.value = tabs.value.some(tab => tab.id === targetId) ? targetId : HOME_TAB_ID
|
||||||
}
|
}
|
||||||
@ -102,7 +96,6 @@ export const useTabStore = defineStore(
|
|||||||
return {
|
return {
|
||||||
tabs,
|
tabs,
|
||||||
activeTabId,
|
activeTabId,
|
||||||
enterWorkspace,
|
|
||||||
openTab,
|
openTab,
|
||||||
removeTab,
|
removeTab,
|
||||||
closeAllTabs,
|
closeAllTabs,
|
||||||
|
|||||||
@ -328,13 +328,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
|||||||
return state[method] || null
|
return state[method] || null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author: wintsa
|
|
||||||
* @Date: 2026-03-13
|
|
||||||
* @LastEditors: wintsa
|
|
||||||
* @Description: 获取咨询服务计价方法状态
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
const getServicePricingMethodState = <TRow = unknown>(
|
const getServicePricingMethodState = <TRow = unknown>(
|
||||||
contractIdRaw: string | number,
|
contractIdRaw: string | number,
|
||||||
serviceIdRaw: string | number,
|
serviceIdRaw: string | number,
|
||||||
@ -346,13 +339,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
|||||||
return (servicePricingStates.value[contractId]?.[serviceId]?.[method] as ServicePricingMethodState<TRow> | undefined) || null
|
return (servicePricingStates.value[contractId]?.[serviceId]?.[method] as ServicePricingMethodState<TRow> | undefined) || null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author: wintsa
|
|
||||||
* @Date: 2026-03-13
|
|
||||||
* @LastEditors: wintsa
|
|
||||||
* @Description: 设置咨询服务计价方法状态并同步版本
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
const setServicePricingMethodState = <TRow = unknown>(
|
const setServicePricingMethodState = <TRow = unknown>(
|
||||||
contractIdRaw: string | number,
|
contractIdRaw: string | number,
|
||||||
serviceIdRaw: string | number,
|
serviceIdRaw: string | number,
|
||||||
@ -393,13 +379,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author: wintsa
|
|
||||||
* @Date: 2026-03-13
|
|
||||||
* @LastEditors: wintsa
|
|
||||||
* @Description: 从缓存或持久化存储加载咨询服务计价方法状态
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
const loadServicePricingMethodState = async <TRow = unknown>(
|
const loadServicePricingMethodState = async <TRow = unknown>(
|
||||||
contractIdRaw: string | number,
|
contractIdRaw: string | number,
|
||||||
serviceIdRaw: string | number,
|
serviceIdRaw: string | number,
|
||||||
@ -425,13 +404,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
|||||||
return getServicePricingMethodState<TRow>(contractId, serviceId, method)
|
return getServicePricingMethodState<TRow>(contractId, serviceId, method)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author: wintsa
|
|
||||||
* @Date: 2026-03-13
|
|
||||||
* @LastEditors: wintsa
|
|
||||||
* @Description: 删除单个咨询服务计价方法状态
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
const removeServicePricingMethodState = (
|
const removeServicePricingMethodState = (
|
||||||
contractIdRaw: string | number,
|
contractIdRaw: string | number,
|
||||||
serviceIdRaw: string | number,
|
serviceIdRaw: string | number,
|
||||||
@ -450,13 +422,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
|||||||
return had
|
return had
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author: wintsa
|
|
||||||
* @Date: 2026-03-13
|
|
||||||
* @LastEditors: wintsa
|
|
||||||
* @Description: 获取单个咨询服务计价方法存储键
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
const getServicePricingStorageKey = (
|
const getServicePricingStorageKey = (
|
||||||
contractIdRaw: string | number,
|
contractIdRaw: string | number,
|
||||||
serviceIdRaw: string | number,
|
serviceIdRaw: string | number,
|
||||||
@ -468,13 +433,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
|||||||
return serviceMethodDbKeyOf(contractId, serviceId, method)
|
return serviceMethodDbKeyOf(contractId, serviceId, method)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author: wintsa
|
|
||||||
* @Date: 2026-03-13
|
|
||||||
* @LastEditors: wintsa
|
|
||||||
* @Description: 获取咨询服务全部计价方法存储键
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
const getServicePricingStorageKeys = (contractIdRaw: string | number, serviceIdRaw: string | number) => {
|
const getServicePricingStorageKeys = (contractIdRaw: string | number, serviceIdRaw: string | number) => {
|
||||||
const contractId = toKey(contractIdRaw)
|
const contractId = toKey(contractIdRaw)
|
||||||
const serviceId = toServiceKey(serviceIdRaw)
|
const serviceId = toServiceKey(serviceIdRaw)
|
||||||
@ -484,13 +442,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author: wintsa
|
|
||||||
* @Date: 2026-03-13
|
|
||||||
* @LastEditors: wintsa
|
|
||||||
* @Description: 删除咨询服务全部计价方法状态
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
const removeAllServicePricingMethodStates = (contractIdRaw: string | number, serviceIdRaw: string | number) => {
|
const removeAllServicePricingMethodStates = (contractIdRaw: string | number, serviceIdRaw: string | number) => {
|
||||||
let changed = false
|
let changed = false
|
||||||
for (const method of Object.keys(METHOD_STORAGE_PREFIX_MAP) as ServicePricingMethod[]) {
|
for (const method of Object.keys(METHOD_STORAGE_PREFIX_MAP) as ServicePricingMethod[]) {
|
||||||
@ -499,26 +450,12 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
|||||||
return changed
|
return changed
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author: wintsa
|
|
||||||
* @Date: 2026-03-13
|
|
||||||
* @LastEditors: wintsa
|
|
||||||
* @Description: 获取合同附加费用主表状态
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
const getHtFeeMainState = <TRow = unknown>(mainStorageKeyRaw: string | number): HtFeeMainState<TRow> | null => {
|
const getHtFeeMainState = <TRow = unknown>(mainStorageKeyRaw: string | number): HtFeeMainState<TRow> | null => {
|
||||||
const mainStorageKey = toKey(mainStorageKeyRaw)
|
const mainStorageKey = toKey(mainStorageKeyRaw)
|
||||||
if (!mainStorageKey) return null
|
if (!mainStorageKey) return null
|
||||||
return (htFeeMainStates.value[mainStorageKey] as HtFeeMainState<TRow> | undefined) || null
|
return (htFeeMainStates.value[mainStorageKey] as HtFeeMainState<TRow> | undefined) || null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author: wintsa
|
|
||||||
* @Date: 2026-03-13
|
|
||||||
* @LastEditors: wintsa
|
|
||||||
* @Description: 设置合同附加费用主表状态并同步版本
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
const setHtFeeMainState = <TRow = unknown>(
|
const setHtFeeMainState = <TRow = unknown>(
|
||||||
mainStorageKeyRaw: string | number,
|
mainStorageKeyRaw: string | number,
|
||||||
payload: Partial<HtFeeMainState<TRow>> | null | undefined,
|
payload: Partial<HtFeeMainState<TRow>> | null | undefined,
|
||||||
@ -557,18 +494,10 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author: wintsa
|
|
||||||
* @Date: 2026-03-13
|
|
||||||
* @LastEditors: wintsa
|
|
||||||
* @Description: 从缓存或持久化存储加载合同附加费用主表状态
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
const loadHtFeeMainState = async <TRow = unknown>(
|
const loadHtFeeMainState = async <TRow = unknown>(
|
||||||
mainStorageKeyRaw: string | number,
|
mainStorageKeyRaw: string | number,
|
||||||
force = false
|
force = false
|
||||||
): Promise<HtFeeMainState<TRow> | null> => {
|
): Promise<HtFeeMainState<TRow> | null> => {
|
||||||
|
|
||||||
const mainStorageKey = toKey(mainStorageKeyRaw)
|
const mainStorageKey = toKey(mainStorageKeyRaw)
|
||||||
if (!mainStorageKey) return null
|
if (!mainStorageKey) return null
|
||||||
if (!force) {
|
if (!force) {
|
||||||
@ -584,13 +513,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
|||||||
return getHtFeeMainState<TRow>(mainStorageKey)
|
return getHtFeeMainState<TRow>(mainStorageKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author: wintsa
|
|
||||||
* @Date: 2026-03-13
|
|
||||||
* @LastEditors: wintsa
|
|
||||||
* @Description: 删除合同附加费用主表状态
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
const removeHtFeeMainState = (mainStorageKeyRaw: string | number) =>
|
const removeHtFeeMainState = (mainStorageKeyRaw: string | number) =>
|
||||||
setHtFeeMainState(mainStorageKeyRaw, null)
|
setHtFeeMainState(mainStorageKeyRaw, null)
|
||||||
|
|
||||||
@ -607,13 +529,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
|||||||
return htFeeMethodStates.value[mainStorageKey][rowId]
|
return htFeeMethodStates.value[mainStorageKey][rowId]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author: wintsa
|
|
||||||
* @Date: 2026-03-13
|
|
||||||
* @LastEditors: wintsa
|
|
||||||
* @Description: 获取合同附加费用子方法存储键
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
const getHtFeeMethodStorageKey = (
|
const getHtFeeMethodStorageKey = (
|
||||||
mainStorageKeyRaw: string | number,
|
mainStorageKeyRaw: string | number,
|
||||||
rowIdRaw: string | number,
|
rowIdRaw: string | number,
|
||||||
@ -625,13 +540,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
|||||||
return `${mainStorageKey}-${rowId}-${method}`
|
return `${mainStorageKey}-${rowId}-${method}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author: wintsa
|
|
||||||
* @Date: 2026-03-13
|
|
||||||
* @LastEditors: wintsa
|
|
||||||
* @Description: 获取合同附加费用子方法状态
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
const getHtFeeMethodState = <TPayload = HtFeeMethodPayload>(
|
const getHtFeeMethodState = <TPayload = HtFeeMethodPayload>(
|
||||||
mainStorageKeyRaw: string | number,
|
mainStorageKeyRaw: string | number,
|
||||||
rowIdRaw: string | number,
|
rowIdRaw: string | number,
|
||||||
@ -644,13 +552,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
|||||||
return value == null ? null : (cloneAny(value) as TPayload)
|
return value == null ? null : (cloneAny(value) as TPayload)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author: wintsa
|
|
||||||
* @Date: 2026-03-13
|
|
||||||
* @LastEditors: wintsa
|
|
||||||
* @Description: 设置合同附加费用子方法状态并同步版本
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
const setHtFeeMethodState = <TPayload = HtFeeMethodPayload>(
|
const setHtFeeMethodState = <TPayload = HtFeeMethodPayload>(
|
||||||
mainStorageKeyRaw: string | number,
|
mainStorageKeyRaw: string | number,
|
||||||
rowIdRaw: string | number,
|
rowIdRaw: string | number,
|
||||||
@ -663,7 +564,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
|||||||
) => {
|
) => {
|
||||||
const mainStorageKey = toKey(mainStorageKeyRaw)
|
const mainStorageKey = toKey(mainStorageKeyRaw)
|
||||||
const rowId = toKey(rowIdRaw)
|
const rowId = toKey(rowIdRaw)
|
||||||
|
|
||||||
if (!mainStorageKey || !rowId) return false
|
if (!mainStorageKey || !rowId) return false
|
||||||
const storageKey = getHtFeeMethodStorageKey(mainStorageKey, rowId, method)
|
const storageKey = getHtFeeMethodStorageKey(mainStorageKey, rowId, method)
|
||||||
if (!storageKey) return false
|
if (!storageKey) return false
|
||||||
@ -705,13 +605,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author: wintsa
|
|
||||||
* @Date: 2026-03-13
|
|
||||||
* @LastEditors: wintsa
|
|
||||||
* @Description: 从缓存或持久化存储加载合同附加费用子方法状态
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
const loadHtFeeMethodState = async <TPayload = HtFeeMethodPayload>(
|
const loadHtFeeMethodState = async <TPayload = HtFeeMethodPayload>(
|
||||||
mainStorageKeyRaw: string | number,
|
mainStorageKeyRaw: string | number,
|
||||||
rowIdRaw: string | number,
|
rowIdRaw: string | number,
|
||||||
@ -735,26 +628,12 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
|||||||
return getHtFeeMethodState<TPayload>(mainStorageKey, rowId, method)
|
return getHtFeeMethodState<TPayload>(mainStorageKey, rowId, method)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author: wintsa
|
|
||||||
* @Date: 2026-03-13
|
|
||||||
* @LastEditors: wintsa
|
|
||||||
* @Description: 删除合同附加费用子方法状态
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
const removeHtFeeMethodState = (
|
const removeHtFeeMethodState = (
|
||||||
mainStorageKeyRaw: string | number,
|
mainStorageKeyRaw: string | number,
|
||||||
rowIdRaw: string | number,
|
rowIdRaw: string | number,
|
||||||
method: HtFeeMethodType
|
method: HtFeeMethodType
|
||||||
) => setHtFeeMethodState(mainStorageKeyRaw, rowIdRaw, method, null)
|
) => setHtFeeMethodState(mainStorageKeyRaw, rowIdRaw, method, null)
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author: wintsa
|
|
||||||
* @Date: 2026-03-13
|
|
||||||
* @LastEditors: wintsa
|
|
||||||
* @Description: 按通用键获取状态
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
const getKeyState = <T = unknown>(keyRaw: string | number): T | null => {
|
const getKeyState = <T = unknown>(keyRaw: string | number): T | null => {
|
||||||
const key = toKey(keyRaw)
|
const key = toKey(keyRaw)
|
||||||
if (!key) return null
|
if (!key) return null
|
||||||
@ -785,13 +664,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
|||||||
return cloneAny(keyedStates.value[key] as T)
|
return cloneAny(keyedStates.value[key] as T)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author: wintsa
|
|
||||||
* @Date: 2026-03-13
|
|
||||||
* @LastEditors: wintsa
|
|
||||||
* @Description: 按通用键从缓存或持久化存储加载状态
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
const loadKeyState = async <T = unknown>(keyRaw: string | number, force = false): Promise<T | null> => {
|
const loadKeyState = async <T = unknown>(keyRaw: string | number, force = false): Promise<T | null> => {
|
||||||
const key = toKey(keyRaw)
|
const key = toKey(keyRaw)
|
||||||
if (!key) return null
|
if (!key) return null
|
||||||
@ -853,13 +725,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author: wintsa
|
|
||||||
* @Date: 2026-03-13
|
|
||||||
* @LastEditors: wintsa
|
|
||||||
* @Description: 按通用键设置状态并同步版本
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
const setKeyState = <T = unknown>(
|
const setKeyState = <T = unknown>(
|
||||||
keyRaw: string | number,
|
keyRaw: string | number,
|
||||||
value: T,
|
value: T,
|
||||||
@ -884,7 +749,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
|||||||
{ force: true, syncKeyState: false }
|
{ force: true, syncKeyState: false }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const htMethodMeta = parseHtFeeMethodStorageKey(key)
|
const htMethodMeta = parseHtFeeMethodStorageKey(key)
|
||||||
if (htMethodMeta) {
|
if (htMethodMeta) {
|
||||||
setHtFeeMethodState(
|
setHtFeeMethodState(
|
||||||
@ -905,13 +769,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author: wintsa
|
|
||||||
* @Date: 2026-03-13
|
|
||||||
* @LastEditors: wintsa
|
|
||||||
* @Description: 按通用键删除状态
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
const removeKeyState = (keyRaw: string | number) => {
|
const removeKeyState = (keyRaw: string | number) => {
|
||||||
const key = toKey(keyRaw)
|
const key = toKey(keyRaw)
|
||||||
if (!key) return false
|
if (!key) return false
|
||||||
@ -938,26 +795,12 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
|||||||
return hadValue
|
return hadValue
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author: wintsa
|
|
||||||
* @Date: 2026-03-13
|
|
||||||
* @LastEditors: wintsa
|
|
||||||
* @Description: 获取指定键的版本号
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
const getKeyVersion = (keyRaw: string | number) => {
|
const getKeyVersion = (keyRaw: string | number) => {
|
||||||
const key = toKey(keyRaw)
|
const key = toKey(keyRaw)
|
||||||
if (!key) return 0
|
if (!key) return 0
|
||||||
return keyVersions.value[key] || 0
|
return keyVersions.value[key] || 0
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author: wintsa
|
|
||||||
* @Date: 2026-03-13
|
|
||||||
* @LastEditors: wintsa
|
|
||||||
* @Description: 获取合同咨询服务主表状态
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
const getContractState = (contractIdRaw: string | number) => {
|
const getContractState = (contractIdRaw: string | number) => {
|
||||||
const contractId = toKey(contractIdRaw)
|
const contractId = toKey(contractIdRaw)
|
||||||
if (!contractId) return null
|
if (!contractId) return null
|
||||||
@ -965,13 +808,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
|||||||
return data ? cloneState(data) : null
|
return data ? cloneState(data) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author: wintsa
|
|
||||||
* @Date: 2026-03-13
|
|
||||||
* @LastEditors: wintsa
|
|
||||||
* @Description: 从缓存或持久化存储加载合同咨询服务主表状态
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
const loadContract = async (contractIdRaw: string | number, force = false) => {
|
const loadContract = async (contractIdRaw: string | number, force = false) => {
|
||||||
const contractId = toKey(contractIdRaw)
|
const contractId = toKey(contractIdRaw)
|
||||||
if (!contractId) return null
|
if (!contractId) return null
|
||||||
@ -1007,13 +843,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author: wintsa
|
|
||||||
* @Date: 2026-03-13
|
|
||||||
* @LastEditors: wintsa
|
|
||||||
* @Description: 设置合同咨询服务主表状态
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
const setContractState = async (contractIdRaw: string | number, state: ZxFwState) => {
|
const setContractState = async (contractIdRaw: string | number, state: ZxFwState) => {
|
||||||
const contractId = toKey(contractIdRaw)
|
const contractId = toKey(contractIdRaw)
|
||||||
if (!contractId) return false
|
if (!contractId) return false
|
||||||
@ -1026,13 +855,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author: wintsa
|
|
||||||
* @Date: 2026-03-13
|
|
||||||
* @LastEditors: wintsa
|
|
||||||
* @Description: 更新合同咨询服务行的计价汇总字段
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
const updatePricingField = async (params: {
|
const updatePricingField = async (params: {
|
||||||
contractId: string
|
contractId: string
|
||||||
serviceId: string | number
|
serviceId: string | number
|
||||||
@ -1078,13 +900,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author: wintsa
|
|
||||||
* @Date: 2026-03-13
|
|
||||||
* @LastEditors: wintsa
|
|
||||||
* @Description: 获取合同咨询服务基础小计
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
const getBaseSubtotal = (contractIdRaw: string | number): number | null => {
|
const getBaseSubtotal = (contractIdRaw: string | number): number | null => {
|
||||||
const contractId = toKey(contractIdRaw)
|
const contractId = toKey(contractIdRaw)
|
||||||
if (!contractId) return null
|
if (!contractId) return null
|
||||||
@ -1105,13 +920,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
|||||||
return hasValid ? round3(sum) : null
|
return hasValid ? round3(sum) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author: wintsa
|
|
||||||
* @Date: 2026-03-13
|
|
||||||
* @LastEditors: wintsa
|
|
||||||
* @Description: 删除合同关联的咨询服务、附加费用和键状态数据
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
const removeContractData = (contractIdRaw: string | number) => {
|
const removeContractData = (contractIdRaw: string | number) => {
|
||||||
const contractId = toKey(contractIdRaw)
|
const contractId = toKey(contractIdRaw)
|
||||||
if (!contractId) return false
|
if (!contractId) return false
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user