diff --git a/.codex-import-smoke.zw b/.codex-import-smoke.zw
new file mode 100644
index 0000000..8e1be18
--- /dev/null
+++ b/.codex-import-smoke.zw
@@ -0,0 +1,2 @@
+JGJSZW�#�Wg�tK_���G�����;��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���B����������M8��PG.d�������gP>fl��:��+q�l�
+�:���ՠ#T����Z�8]��X�(�(��WV���gI\?�ۣs�Q
�k� X�6鴦?אi+\p�cL�X;����ҹ�w,�����q2$��ą��<^����A� ���Sc�\>rp��-�A����p7F�@8jco��	�H��ڱ�a�9��� ���5�u��^d��a-̡.%���N�~�δIݳt��I�VnpI�ikc�J>vM��p<��u��pԄ*Ww�N��U
�O]ٟa��~8�Tm	n�JJ��a������\�������
�w�S!ǓO��2��(p���mZ
\ No newline at end of file
diff --git a/.codex-roundtrip.zw b/.codex-roundtrip.zw
new file mode 100644
index 0000000..9dd9a99
Binary files /dev/null and b/.codex-roundtrip.zw differ
diff --git a/.serena/.gitignore b/.serena/.gitignore
new file mode 100644
index 0000000..2e510af
--- /dev/null
+++ b/.serena/.gitignore
@@ -0,0 +1,2 @@
+/cache
+/project.local.yml
diff --git a/.serena/project.yml b/.serena/project.yml
new file mode 100644
index 0000000..ca9d6f5
--- /dev/null
+++ b/.serena/project.yml
@@ -0,0 +1,136 @@
+# 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: []
diff --git a/AGENTS.md b/AGENTS.md
index 7c06ddf..0c26dc3 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,19 +1,387 @@
-# AGENTS.md — Encoding & Chinese Safety Rules
+# AGENTS.md — Codex 工作操作手册
 
-## Absolute rules (must follow)
-1. Never corrupt non-ASCII text (Chinese, emoji, etc.). Preserve exact Unicode characters.
-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.
-4. When reading/writing files via scripts/tools, always use UTF-8 explicitly (no platform default encoding).
+本文件面向 Codex AI，定义其职责与操作规范。
 
-## Windows / PowerShell rules
-- If you need to run PowerShell, force UTF-8 output/input:
-  - Use: `[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()`
-  - Prefer `Set-Content -Encoding utf8` / `Out-File -Encoding utf8`
-- Avoid commands that may round-trip through ANSI/CP936/CP1252 without explicit encoding.
+## 0. 角色定位与职责边界
 
-## Workflow
-- Before editing: inspect the target lines only.
-- Apply changes as a patch (line-level edits), not full-file regeneration.
-- After editing: verify the edited lines still show correct Chinese.
-- If uncertain: stop and ask rather than guessing and corrupting text.
\ No newline at end of file
+| instruction | notes |
+| --- | --- |
+| Codex 负责任务规划、代码编写、文档生成、上下文收集、测试验证、质量审查等全流程 | 保持全栈能力 |
+| 职责范围：需求分析、技术方案设计、任务规划、代码实现、测试执行、质量验证、文档编写、工具使用、深度推理分析 | 承担完整开发生命周期 |
+| 工作模式：接收用户指令 → 深度思考（sequential-thinking） → 规划任务（shrimp-task-manager） → 执行实现 → 自我验证 → 交付成果 | 自主闭环流程 |
+| 决策权：自主决策技术方案、实现路径、质量标准，仅在真正需要用户输入时才询问 | 最大化自主性 |
+| 工具使用：所有工具可用（Read、Edit、Write、Bash、Grep、Glob、sequential-thinking、shrimp-task-manager、exa、code-index等），无使用限制 | 完整工具访问权限 |
+| 核心约束：标准化生态复用置于首位，禁止任何额外自研组件，所有安全性设计必须立刻删除并拒绝新增 | 强制执行 |
+
+## 1. 工具能力总览
+
+### 1.1 内置工具
+
+| 工具 | 作用 | 启用/审批要点 | 参考 |
+| --- | --- | --- | --- |
+| 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次）
+
+**判断原则**：
+- 如果不在"极少数例外"清单中 → 自动执行
+- 如有疑问 → 自动执行（而非询问）
+- 宁可执行后修复，也不要频繁打断工作流程
+
+---
+
+**协作原则总结**：
+- 我规划，我决策
+- 我观察，我判断
+- 我执行，我验证
+- 遇疑问，评估后决策或询问用户
\ No newline at end of file
diff --git a/codex.exe b/codex.exe
new file mode 100644
index 0000000..32fefcc
Binary files /dev/null and b/codex.exe differ
diff --git a/et --hard 9c11604 b/et --hard 9c11604
new file mode 100644
index 0000000..96b020f
--- /dev/null
+++ b/et --hard 9c11604	
@@ -0,0 +1,54 @@
+[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
diff --git a/home-entry-after.png b/home-entry-after.png
new file mode 100644
index 0000000..0a1f1f5
Binary files /dev/null and b/home-entry-after.png differ
diff --git a/home-entry-before.png b/home-entry-before.png
new file mode 100644
index 0000000..8dde8e0
Binary files /dev/null and b/home-entry-before.png differ
diff --git a/src/components/common/HtFeeMethodGrid.vue b/src/components/common/HtFeeMethodGrid.vue
index 187d026..3a54bd3 100644
--- a/src/components/common/HtFeeMethodGrid.vue
+++ b/src/components/common/HtFeeMethodGrid.vue
@@ -113,6 +113,7 @@ const getRowSubtotal = (row: FeeMethodRow | null | undefined) => {
   return round3(toFinite(row.rateFee) + toFinite(row.hourlyFee) + toFinite(row.quantityUnitPriceFee))
 }
 const toFiniteUnknown = (value: unknown): number | null => {
+  if (value == null || value === '') return null
   const numeric = Number(value)
   return Number.isFinite(numeric) ? numeric : null
 }
@@ -332,12 +333,14 @@ const mergeWithStoredRows = (rowsFromDb: unknown): FeeMethodRow[] => {
   )
   const rows = sourceRows.map(item => {
     const row = item as Partial<FeeMethodRow> & LegacyFeeRow
+
     return {
       id: typeof row.id === 'string' && row.id ? row.id : createRowId(),
       name:
         typeof row.name === 'string'
           ? row.name
           : (typeof row.feeItem === 'string' ? row.feeItem : ''),
+
       rateFee: typeof row.rateFee === 'number' ? row.rateFee : null,
       hourlyFee: typeof row.hourlyFee === 'number' ? row.hourlyFee : null,
       quantityUnitPriceFee:
diff --git a/src/components/common/xmCommonAgGrid.vue b/src/components/common/xmCommonAgGrid.vue
index 43e6606..c7f84fa 100644
--- a/src/components/common/xmCommonAgGrid.vue
+++ b/src/components/common/xmCommonAgGrid.vue
@@ -40,7 +40,7 @@ interface XmBaseInfoState {
   projectIndustry?: string
 }
 
-const BASE_INFO_KEY = 'xm-base-info-v1'
+const XM_SCALE_FLUSH_EVENT = 'jgjs:xm-scale-flush-request'
 type MajorLite = { code: string; name: string; hasCost?: boolean; hasArea?: boolean }
 const kvStore = useKvStore()
 
@@ -198,7 +198,7 @@ const applyPinnedTotalAmount = (
 const loadFromIndexedDB = async (api: GridApi<DetailRow>) => {
   try {
     const [baseInfo, contractData] = await Promise.all([
-      kvStore.getItem<XmBaseInfoState>(BASE_INFO_KEY),
+      kvStore.getItem<XmBaseInfoState>(props.baseInfoKey || 'xm-base-info-v1'),
       kvStore.getItem<XmScaleState>(props.dbKey)
     ])
 
@@ -296,6 +296,7 @@ const props = defineProps<{
   title: string
   dbKey: string
   xmInfoKey?: string | null
+  baseInfoKey?: string
 }>()
 
 let persistTimer: ReturnType<typeof setTimeout> | null = null
@@ -444,11 +445,29 @@ const saveToIndexedDB = async () => {
       hide: Boolean(row.hide),
       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 = {
       detailRows: [...leafRows, ...buildGroupRows(leafRows)]
     }
     payload.roughCalcEnabled = roughCalcEnabled.value
-    payload.totalAmount = pinnedTopRowData.value[0].amount
+    payload.totalAmount = normalizedTotalAmount
     await kvStore.setItem(props.dbKey, payload)
   } catch (error) {
     console.error('saveToIndexedDB failed:', error)
@@ -462,6 +481,18 @@ const schedulePersist = () => {
   }, 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) => {
   for (const row of detailRows.value) {
     row.hide = hidden
@@ -566,9 +597,14 @@ const syncPinnedTotalForNormalMode = () => {
 
 onBeforeUnmount(() => {
   if (persistTimer) clearTimeout(persistTimer)
+  window.removeEventListener(XM_SCALE_FLUSH_EVENT, handleFlushPersistRequest as EventListener)
   gridApi.value = null
   void saveToIndexedDB()
 })
+
+onMounted(() => {
+  window.addEventListener(XM_SCALE_FLUSH_EVENT, handleFlushPersistRequest as EventListener)
+})
 </script>
 
 <template>
@@ -580,7 +616,7 @@ onBeforeUnmount(() => {
           {{ props.title }}
         </h3>
         <div class="flex items-center gap-2">
-          <span class=" text-xs text-muted-foreground">粗略计算</span>
+          <span class=" text-xs text-muted-foreground">简要计算</span>
           <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"
             :modelValue="roughCalcEnabled" @update:modelValue="onRoughCalcSwitch">
diff --git a/src/components/views/HomeEntryView.vue b/src/components/views/HomeEntryView.vue
new file mode 100644
index 0000000..cf6d7b9
--- /dev/null
+++ b/src/components/views/HomeEntryView.vue
@@ -0,0 +1,466 @@
+<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>
diff --git a/src/components/views/Ht.vue b/src/components/views/Ht.vue
index 9fdfb42..8297a4b 100644
--- a/src/components/views/Ht.vue
+++ b/src/components/views/Ht.vue
@@ -45,9 +45,24 @@ interface DataEntry {
 interface ContractSegmentPackage {
   version: number
   exportedAt: string
-  projectIndustry: string
+  packageType?: 'contract-segments'
+  project?: {
+    industry: string
+  }
+  storage?: {
+    localforageEntries: DataEntry[]
+  }
   contracts: ContractItem[]
-  localforageEntries: DataEntry[]
+  projectIndustry?: string
+  localforageEntries?: DataEntry[]
+  pinia?: {
+    zxFwPricing?: {
+      contracts?: Record<string, unknown>
+      servicePricingStates?: Record<string, unknown>
+      htFeeMainStates?: Record<string, unknown>
+      htFeeMethodStates?: Record<string, unknown>
+    }
+  }
   piniaState?: {
     zxFwPricing?: {
       contracts?: Record<string, unknown>
@@ -99,7 +114,7 @@ interface QuantityMethodStateLike {
 
 const STORAGE_KEY = 'ht-card-v1'
 const CONTRACT_SEGMENT_FILE_EXTENSION = '.htzw'
-const CONTRACT_SEGMENT_VERSION = 2
+const CONTRACT_SEGMENT_VERSION = 3
 const CONTRACT_KEY_PREFIX = 'ht-info-v3-'
 const SERVICE_KEY_PREFIX = 'zxFW-'
 const CONTRACT_CONSULT_FACTOR_KEY_PREFIX = 'ht-consult-category-factor-v1-'
@@ -274,6 +289,7 @@ const getCurrentProjectIndustry = async (): Promise<string> => {
 }
 
 const toFiniteNumber = (value: unknown): number | null => {
+  if (value == null || value === '') return null
   const num = Number(value)
   return Number.isFinite(num) ? num : null
 }
@@ -607,6 +623,15 @@ 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> =>
   Boolean(value && typeof value === 'object' && !Array.isArray(value))
 
@@ -799,11 +824,16 @@ const exportSelectedContracts = async () => {
     const now = new Date()
     const payload: ContractSegmentPackage = {
       version: CONTRACT_SEGMENT_VERSION,
+      packageType: 'contract-segments',
       exportedAt: now.toISOString(),
-      projectIndustry,
+      project: {
+        industry: projectIndustry
+      },
       contracts: selectedContracts,
-      localforageEntries,
-      piniaState: {
+      storage: {
+        localforageEntries
+      },
+      pinia: {
         zxFwPricing: piniaPayload
       }
     }
@@ -845,16 +875,17 @@ const importContractSegments = async (event: Event) => {
     if (!isContractSegmentPackage(payload)) {
       throw new Error('INVALID_CONTRACT_SEGMENT_PAYLOAD')
     }
+    const normalizedPackage = normalizeContractSegmentPackage(payload)
 
     const currentProjectIndustry = await getCurrentProjectIndustry()
     if (!currentProjectIndustry) {
       throw new Error('CURRENT_PROJECT_INDUSTRY_MISSING')
     }
-    if (typeof payload.projectIndustry !== 'string' || !payload.projectIndustry.trim()) {
+    if (!normalizedPackage.projectIndustry) {
       throw new Error('IMPORT_PACKAGE_INDUSTRY_MISSING')
     }
-    if (payload.projectIndustry.trim() !== currentProjectIndustry) {
-      throw new Error(`PROJECT_INDUSTRY_MISMATCH:${payload.projectIndustry.trim()}:${currentProjectIndustry}`)
+    if (normalizedPackage.projectIndustry !== currentProjectIndustry) {
+      throw new Error(`PROJECT_INDUSTRY_MISMATCH:${normalizedPackage.projectIndustry}:${currentProjectIndustry}`)
     }
 
     const importedContracts = normalizeContractsFromPayload(payload.contracts)
@@ -862,7 +893,7 @@ const importContractSegments = async (event: Event) => {
       throw new Error('EMPTY_CONTRACTS')
     }
 
-    const importedEntries = normalizeDataEntries(payload.localforageEntries)
+    const importedEntries = normalizedPackage.localforageEntries
     const usedIds = new Set(contracts.value.map(item => item.id))
     const oldToNewIdMap = new Map<string, string>()
     const nextContracts: ContractItem[] = importedContracts.map((item, index) => {
@@ -889,7 +920,7 @@ const importContractSegments = async (event: Event) => {
     })
 
     await Promise.all(rewrittenEntries.map(entry => kvStore.setItem(entry.key, entry.value)))
-    await applyImportedContractPiniaPayload(payload.piniaState, oldToNewIdMap)
+    await applyImportedContractPiniaPayload(normalizedPackage.piniaState, oldToNewIdMap)
 
     contracts.value = [...contracts.value, ...nextContracts]
     await saveContracts()
diff --git a/src/components/views/HtConsultCategoryFactor.vue b/src/components/views/HtConsultCategoryFactor.vue
index e83bc5d..640ba3c 100644
--- a/src/components/views/HtConsultCategoryFactor.vue
+++ b/src/components/views/HtConsultCategoryFactor.vue
@@ -6,6 +6,8 @@ import { useKvStore } from '@/pinia/kv'
 
 const props = defineProps<{
   contractId: string
+  projectInfoKey?: string
+  parentStorageKey?: string
 }>()
 
 interface XmBaseInfoState {
@@ -20,13 +22,13 @@ type ServiceItem = {
   notshowByzxflxs?: boolean
 }
 
-const PROJECT_INFO_KEY = 'xm-base-info-v1'
+const PROJECT_INFO_KEY = computed(() => props.projectInfoKey || 'xm-base-info-v1')
 const projectIndustry = ref('')
 const kvStore = useKvStore()
 
 const loadProjectIndustry = async () => {
   try {
-    const data = await kvStore.getItem<XmBaseInfoState>(PROJECT_INFO_KEY)
+    const data = await kvStore.getItem<XmBaseInfoState>(PROJECT_INFO_KEY.value)
     projectIndustry.value =
       typeof data?.projectIndustry === 'string' ? data.projectIndustry.trim() : ''
   } catch (error) {
@@ -57,7 +59,7 @@ onActivated(() => {
   <XmFactorGrid
     title="咨询分类系数明细"
     :storage-key="`ht-consult-category-factor-v1-${props.contractId}`"
-    parent-storage-key="xm-consult-category-factor-v1"
+    :parent-storage-key="props.parentStorageKey || 'xm-consult-category-factor-v1'"
     :dict="filteredServiceDict"
     :disable-budget-edit-when-standard-null="true"
     :exclude-notshow-by-zxflxs="true"
diff --git a/src/components/views/HtMajorFactor.vue b/src/components/views/HtMajorFactor.vue
index 2a4d84e..1690c91 100644
--- a/src/components/views/HtMajorFactor.vue
+++ b/src/components/views/HtMajorFactor.vue
@@ -18,15 +18,17 @@ type MajorItem = {
 
 const props = defineProps<{
   contractId: string
+  projectInfoKey?: string
+  parentStorageKey?: string
 }>()
 
-const PROJECT_INFO_KEY = 'xm-base-info-v1'
+const PROJECT_INFO_KEY = computed(() => props.projectInfoKey || 'xm-base-info-v1')
 const projectIndustry = ref('')
 const kvStore = useKvStore()
 
 const loadProjectIndustry = async () => {
   try {
-    const data = await kvStore.getItem<XmBaseInfoState>(PROJECT_INFO_KEY)
+    const data = await kvStore.getItem<XmBaseInfoState>(PROJECT_INFO_KEY.value)
     projectIndustry.value =
       typeof data?.projectIndustry === 'string' ? data.projectIndustry.trim() : ''
   } catch (error) {
@@ -57,7 +59,7 @@ onActivated(() => {
   <XmFactorGrid
     title="工程专业系数明细"
     :storage-key="`ht-major-factor-v1-${props.contractId}`"
-    parent-storage-key="xm-major-factor-v1"
+    :parent-storage-key="props.parentStorageKey || 'xm-major-factor-v1'"
     :dict="filteredMajorDict"
     :disable-budget-edit-when-standard-null="true"
     :exclude-notshow-by-zxflxs="true"
diff --git a/src/components/views/ProjectWorkspaceView.vue b/src/components/views/ProjectWorkspaceView.vue
new file mode 100644
index 0000000..f17f060
--- /dev/null
+++ b/src/components/views/ProjectWorkspaceView.vue
@@ -0,0 +1,17 @@
+<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>
diff --git a/src/components/views/QuickCalcView.vue b/src/components/views/QuickCalcView.vue
new file mode 100644
index 0000000..fa84aba
--- /dev/null
+++ b/src/components/views/QuickCalcView.vue
@@ -0,0 +1,413 @@
+<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>
diff --git a/src/components/views/ZxFwView.vue b/src/components/views/ZxFwView.vue
index b205493..bae7559 100644
--- a/src/components/views/ZxFwView.vue
+++ b/src/components/views/ZxFwView.vue
@@ -28,6 +28,7 @@ const props = defineProps<{
   serviceId: string|number
   fwName:string
   type?: ServiceMethodType
+  projectInfoKey?: string
 }>()
 
 interface PricingCategoryItem {
@@ -64,7 +65,11 @@ const createPricingPane = (name: string) =>
           }
         })
 
-        return () => h(AsyncPricingView, { contractId: props.contractId, serviceId: props.serviceId })
+        return () => h(AsyncPricingView, {
+          contractId: props.contractId,
+          serviceId: props.serviceId,
+          projectInfoKey: props.projectInfoKey
+        })
       }
     })
   )
diff --git a/src/components/views/htCard.vue b/src/components/views/htCard.vue
index e7e94c6..d4fc88f 100644
--- a/src/components/views/htCard.vue
+++ b/src/components/views/htCard.vue
@@ -4,6 +4,7 @@
     scene="ht-tab"
     :title="`合同段：${contractName}`"
     :subtitle="`合同段ID：${contractId}`"
+    :meta-text="`合同段预算金额：${formatBudgetAmount(contractBudget)}`"
     :copy-text="contractId"
     :storage-key="`project-active-cat-${contractId}`"
     default-category="info"
@@ -12,14 +13,170 @@
 </template>
 
 <script setup lang="ts">
-import { markRaw, defineAsyncComponent, defineComponent, h, type Component } from 'vue';
+import { computed, markRaw, defineAsyncComponent, defineComponent, h, onActivated, onBeforeUnmount, onMounted, ref, watch, type Component } from 'vue';
 import TypeLine from '@/layout/typeLine.vue';
+import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
+import { roundTo } from '@/lib/decimal'
+import { formatThousands } from '@/lib/numberFormat'
 
 // 1. 完善 Props 类型 + 添加校验（可选但推荐）
 const props = defineProps<{
   contractId: string; // 合同ID（必传）
   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 结构）
 interface XmCategoryItem {
@@ -39,7 +196,11 @@ const htView = markRaw(
           console.error('加载 htInfo 组件失败：', err);
         }
       });
-      return () => h(AsyncHtInfo, { contractId: props.contractId });
+      return () => h(AsyncHtInfo, {
+        contractId: props.contractId,
+        projectScaleKey: props.projectScaleKey,
+        projectInfoKey: props.projectInfoKey
+      });
     }
   })
 );
@@ -54,7 +215,11 @@ const zxfwView = markRaw(
           console.error('加载 zxFw 组件失败：', err);
         }
       });
-      return () => h(AsyncZxFw, { contractId: props.contractId, contractName: props.contractName });
+      return () => h(AsyncZxFw, {
+        contractId: props.contractId,
+        contractName: props.contractName,
+        projectInfoKey: props.projectInfoKey
+      });
     }
   })
 );
@@ -69,7 +234,11 @@ const consultCategoryFactorView = markRaw(
           console.error('加载 HtConsultCategoryFactor 组件失败：', err);
         }
       });
-      return () => h(AsyncHtConsultCategoryFactor, { contractId: props.contractId });
+      return () => h(AsyncHtConsultCategoryFactor, {
+        contractId: props.contractId,
+        projectInfoKey: props.projectInfoKey,
+        parentStorageKey: props.projectConsultCategoryFactorKey
+      });
     }
   })
 );
@@ -84,7 +253,11 @@ const majorFactorView = markRaw(
           console.error('加载 HtMajorFactor 组件失败：', err);
         }
       });
-      return () => h(AsyncHtMajorFactor, { contractId: props.contractId });
+      return () => h(AsyncHtMajorFactor, {
+        contractId: props.contractId,
+        projectInfoKey: props.projectInfoKey,
+        parentStorageKey: props.projectMajorFactorKey
+      });
     }
   })
 );
@@ -130,4 +303,21 @@ const xmCategories: XmCategoryItem[] = [
   { 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>
diff --git a/src/components/views/htInfo.vue b/src/components/views/htInfo.vue
index 3a06cbd..7deeae0 100644
--- a/src/components/views/htInfo.vue
+++ b/src/components/views/htInfo.vue
@@ -5,13 +5,19 @@ import CommonAgGrid from '@/components/common/xmCommonAgGrid.vue'
 
 const props = defineProps<{
   contractId: string
+  projectScaleKey?: string | null
+  projectInfoKey?: string
 }>()
 const DB_KEY = computed(() => `ht-info-v3-${props.contractId}`)
-const XM_DB_KEY = 'xm-info-v3'
+const XM_DB_KEY = computed(() => {
+  if (props.projectScaleKey === null) return undefined
+  return props.projectScaleKey || 'xm-info-v3'
+})
+const BASE_INFO_KEY = computed(() => props.projectInfoKey || 'xm-base-info-v1')
 
 
 </script>
 
 <template>
-  <CommonAgGrid title="合同规模明细" :dbKey="DB_KEY" :xmInfoKey="XM_DB_KEY"/>
+  <CommonAgGrid title="合同规模明细" :dbKey="DB_KEY" :xmInfoKey="XM_DB_KEY" :base-info-key="BASE_INFO_KEY"/>
 </template>
diff --git a/src/components/views/info.vue b/src/components/views/info.vue
index 5a22445..55664ea 100644
--- a/src/components/views/info.vue
+++ b/src/components/views/info.vue
@@ -2,12 +2,7 @@
 import { parseDate } from '@internationalized/date'
 import { onMounted, ref, watch } from 'vue'
 import {
-  getMajorDictEntries,
-  getServiceDictEntries,
-  getIndustryTypeValue,
   industryTypeList,
-  isIndustryEnabledByType,
-  isMajorIdInIndustryScope
 } from '@/sql'
 import { useKvStore } from '@/pinia/kv'
 import { Calendar as CalendarIcon, CircleHelp } from 'lucide-vue-next'
@@ -45,31 +40,10 @@ interface XmInfoState {
 }
 
 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 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 INDUSTRY_HINT_TEXT = '变更需要重置后重新选择'
-const PROJECT_INIT_CHANGED_EVENT = 'xm-project-init-changed'
 const getTodayDateString = () => {
   const now = new Date()
   const year = String(now.getFullYear())
@@ -79,8 +53,6 @@ const getTodayDateString = () => {
 }
 
 const isProjectInitialized = ref(false)
-const showCreateDialog = ref(false)
-const pendingIndustry = ref('')
 
 const projectName = ref('')
 const projectIndustry = ref('')
@@ -128,76 +100,6 @@ const majorParentCodeSet = new Set(majorParentNodes.map(item => item.id))
 const DEFAULT_PROJECT_INDUSTRY = majorParentNodes[0]?.id || ''
 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 () => {
   try {
     const payload: XmInfoState = {
@@ -268,38 +170,6 @@ 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(
   [projectIndustry, projectName, preparedBy, reviewedBy, preparedCompany, preparedDate],
   schedulePersist
@@ -315,15 +185,9 @@ onMounted(async () => {
   <div class="space-y-6 h-full">
     <div
       v-if="!isProjectInitialized"
-      class=" bg-card p-10 h-full flex items-center justify-center"
+      class="rounded-xl border bg-card p-10 h-full flex items-center justify-center text-sm text-muted-foreground"
     >
-      <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 v-else class="rounded-xl border bg-card p-4 shadow-sm shrink-0 md:p-5">
@@ -516,44 +380,6 @@ onMounted(async () => {
         </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>
   </TooltipProvider>
 </template>
diff --git a/src/components/views/pricingView/InvestmentScalePricingPane.vue b/src/components/views/pricingView/InvestmentScalePricingPane.vue
index baea255..953c67b 100644
--- a/src/components/views/pricingView/InvestmentScalePricingPane.vue
+++ b/src/components/views/pricingView/InvestmentScalePricingPane.vue
@@ -92,6 +92,7 @@ interface ServiceLite {
 const props = defineProps<{
   contractId: string,
   serviceId: string | number
+  projectInfoKey?: string
 }>()
 const zxFwPricingStore = useZxFwPricingStore()
 const kvStore = useKvStore()
@@ -99,7 +100,7 @@ const DB_KEY = computed(() => `tzGMF-${props.contractId}-${props.serviceId}`)
 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_MAJOR_FACTOR_KEY = computed(() => `ht-major-factor-v1-${props.contractId}`)
-const BASE_INFO_KEY = 'xm-base-info-v1'
+const BASE_INFO_KEY = computed(() => props.projectInfoKey || 'xm-base-info-v1')
 const activeIndustryCode = ref('')
 const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:'
 const PRICING_FORCE_DEFAULT_PREFIX = 'pricing-force-default:'
@@ -1149,7 +1150,7 @@ const applyProjectCountChange = async (nextValue: unknown) => {
 
 const loadFromIndexedDB = async () => {
   try {
-    const baseInfo = await kvStore.getItem<XmBaseInfoState>(BASE_INFO_KEY)
+    const baseInfo = await kvStore.getItem<XmBaseInfoState>(BASE_INFO_KEY.value)
     activeIndustryCode.value =
       typeof baseInfo?.projectIndustry === 'string' ? baseInfo.projectIndustry.trim() : ''
     projectCount.value = 1
@@ -1214,7 +1215,7 @@ const loadFromIndexedDB = async () => {
 
 const importContractData = async () => {
   try {
-    const baseInfo = await kvStore.getItem<XmBaseInfoState>(BASE_INFO_KEY)
+    const baseInfo = await kvStore.getItem<XmBaseInfoState>(BASE_INFO_KEY.value)
     activeIndustryCode.value =
       typeof baseInfo?.projectIndustry === 'string' ? baseInfo.projectIndustry.trim() : ''
 
diff --git a/src/components/views/pricingView/LandScalePricingPane.vue b/src/components/views/pricingView/LandScalePricingPane.vue
index 0761337..217b53a 100644
--- a/src/components/views/pricingView/LandScalePricingPane.vue
+++ b/src/components/views/pricingView/LandScalePricingPane.vue
@@ -92,6 +92,7 @@ interface ServiceLite {
 const props = defineProps<{
   contractId: string,
   serviceId: string | number
+  projectInfoKey?: string
 }>()
 const zxFwPricingStore = useZxFwPricingStore()
 const kvStore = useKvStore()
@@ -99,7 +100,7 @@ const DB_KEY = computed(() => `ydGMF-${props.contractId}-${props.serviceId}`)
 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_MAJOR_FACTOR_KEY = computed(() => `ht-major-factor-v1-${props.contractId}`)
-const BASE_INFO_KEY = 'xm-base-info-v1'
+const BASE_INFO_KEY = computed(() => props.projectInfoKey || 'xm-base-info-v1')
 const activeIndustryCode = ref('')
 const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:'
 const PRICING_FORCE_DEFAULT_PREFIX = 'pricing-force-default:'
@@ -997,7 +998,7 @@ const applyProjectCountChange = async (nextValue: unknown) => {
 
 const loadFromIndexedDB = async () => {
   try {
-    const baseInfo = await kvStore.getItem<XmBaseInfoState>(BASE_INFO_KEY)
+    const baseInfo = await kvStore.getItem<XmBaseInfoState>(BASE_INFO_KEY.value)
     activeIndustryCode.value =
       typeof baseInfo?.projectIndustry === 'string' ? baseInfo.projectIndustry.trim() : ''
     projectCount.value = 1
@@ -1049,7 +1050,7 @@ const loadFromIndexedDB = async () => {
 
 const importContractData = async () => {
   try {
-    const baseInfo = await kvStore.getItem<XmBaseInfoState>(BASE_INFO_KEY)
+    const baseInfo = await kvStore.getItem<XmBaseInfoState>(BASE_INFO_KEY.value)
     activeIndustryCode.value =
       typeof baseInfo?.projectIndustry === 'string' ? baseInfo.projectIndustry.trim() : ''
 
diff --git a/src/components/views/xmCard.vue b/src/components/views/xmCard.vue
index 3885488..c472fbb 100644
--- a/src/components/views/xmCard.vue
+++ b/src/components/views/xmCard.vue
@@ -3,17 +3,14 @@
     scene="xm-tab"
     title=""
     storage-key="project-active-cat"
-    default-category="info"
+    default-category="scale-info"
     :categories="xmCategories"
   />
 </template>
 
 <script setup lang="ts">
-import { computed, defineAsyncComponent, markRaw, onBeforeUnmount, onMounted, ref } from 'vue'
+import { defineAsyncComponent, markRaw } from '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 htView = markRaw(defineAsyncComponent(() => import('@/components/views/Ht.vue')))
 const consultCategoryFactorView = markRaw(
@@ -23,49 +20,10 @@ const majorFactorView = markRaw(
   defineAsyncComponent(() => import('@/components/views/XmMajorFactor.vue'))
 )
 
-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 },
+const xmCategories = [
   { key: 'scale-info', label: '规模信息', component: scaleInfoView },
   { 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>
diff --git a/src/components/views/zxFw.vue b/src/components/views/zxFw.vue
index cd328b4..f603eb0 100644
--- a/src/components/views/zxFw.vue
+++ b/src/components/views/zxFw.vue
@@ -74,11 +74,12 @@ interface ServiceMethodType {
 const props = defineProps<{
   contractId: string
   contractName?: string
+  projectInfoKey?: string
 }>()
 const tabStore = useTabStore()
 const zxFwPricingStore = useZxFwPricingStore()
 const kvStore = useKvStore()
-const PROJECT_INFO_KEY = 'xm-base-info-v1'
+const PROJECT_INFO_KEY = computed(() => props.projectInfoKey || 'xm-base-info-v1')
 const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:'
 const PRICING_FORCE_DEFAULT_PREFIX = 'pricing-force-default:'
 const PRICING_CLEAR_SKIP_TTL_MS = 5000
@@ -500,7 +501,8 @@ const openEditTab = (row: DetailRow) => {
       contractName: props.contractName || '',
       serviceId: row.id,
       fwName: row.code + row.name,
-      type: serviceType ? { ...serviceType } : undefined
+      type: serviceType ? { ...serviceType } : undefined,
+      projectInfoKey: props.projectInfoKey
     }
   })
 }
@@ -1098,7 +1100,7 @@ const initializeContractState = async () => {
 
 const loadProjectIndustry = async () => {
   try {
-    const data = await kvStore.getItem<XmBaseInfoState>(PROJECT_INFO_KEY)
+    const data = await kvStore.getItem<XmBaseInfoState>(PROJECT_INFO_KEY.value)
     projectIndustry.value =
       typeof data?.projectIndustry === 'string' ? data.projectIndustry.trim() : ''
   } catch (error) {
diff --git a/src/layout/tab.vue b/src/layout/tab.vue
index b407d20..33c673d 100644
--- a/src/layout/tab.vue
+++ b/src/layout/tab.vue
@@ -27,8 +27,28 @@ import {
   ToastViewport,
 } from 'reka-ui'
 import { decodeZwArchive, encodeZwArchive, ZW_FILE_EXTENSION } from '@/lib/zwArchive'
-import { addNumbers, roundTo } from '@/lib/decimal'
+import { addNumbers, roundTo, toDecimal } from '@/lib/decimal'
+import { getBenchmarkBudgetSplitByScale, getScaleBudgetFeeSplit } from '@/lib/pricingScaleFee'
 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 {
   key: string
@@ -40,15 +60,29 @@ interface ForageStoreSnapshot {
   entries: DataEntry[]
 }
 
-interface DataPackage {
-  version: number
-  exportedAt: string
+interface DataPackageStorage {
   localStorage: DataEntry[]
   sessionStorage: DataEntry[]
   localforageDefault: DataEntry[]
   localforageStores?: ForageStoreSnapshot[]
 }
 
+interface DataPackageWorkspace {
+  mode: WorkspaceMode
+}
+
+interface DataPackage {
+  version: number
+  exportedAt: string
+  packageType?: 'workspace-snapshot'
+  workspace?: DataPackageWorkspace
+  storage?: DataPackageStorage
+  localStorage?: DataEntry[]
+  sessionStorage?: DataEntry[]
+  localforageDefault?: DataEntry[]
+  localforageStores?: ForageStoreSnapshot[]
+}
+
 interface UserGuideStep {
   title: string
   description: string
@@ -85,6 +119,16 @@ interface ContractCardItem {
   order?: number
 }
 
+interface ReportWorkspaceConfig {
+  mode: WorkspaceMode
+  projectInfoKey: string
+  projectScaleKey: string
+  consultCategoryFactorKey: string
+  majorFactorKey: string
+  contractCardsKey?: string
+  quickContractKey?: string
+}
+
 interface ZxFwRowLike {
   id: string
   process?: unknown
@@ -107,6 +151,8 @@ interface ScaleMethodRowLike extends ScaleRowLike {
   benchmarkBudget?: unknown
   benchmarkBudgetBasic?: unknown
   benchmarkBudgetOptional?: unknown
+  benchmarkBudgetBasicChecked?: unknown
+  benchmarkBudgetOptionalChecked?: unknown
   budgetFee?: unknown
   budgetFeeBasic?: unknown
   budgetFeeOptional?: unknown
@@ -139,6 +185,7 @@ interface QuantityMethodRowLike {
 
 interface WorkloadMethodRowLike {
   id: string
+  conversion?: unknown
   budgetAdoptedUnitPrice?: unknown
   workload?: unknown
   basicFee?: unknown
@@ -343,17 +390,32 @@ interface ExportReserve {
 }
 
 interface ExportReportPayload {
+  version: number
+  reportType: 'budget-report'
+  mode: WorkspaceMode
   name: string
   writer: string
   reviewer: string
   date: string
   industry: number
   fee: number
-  scaleCost: number
+  scaleCost: number | null
   scale: ExportScaleRow[]
   serviceCoes: ExportServiceCoe[]
   majorCoes: ExportMajorCoe[]
   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'
@@ -364,6 +426,7 @@ const MAJOR_FACTOR_DB_KEY = 'xm-major-factor-v1'
 const PINIA_PERSIST_DB_NAME = 'DB'
 const PINIA_PERSIST_BASE_STORE_NAME = 'pinia'
 const PINIA_PERSIST_STORE_IDS = ['tabs', 'zxFwPricing', 'kv'] as const
+const XM_SCALE_FLUSH_EVENT = 'jgjs:xm-scale-flush-request'
 const userGuideSteps: UserGuideStep[] = [
   {
     title: '欢迎使用',
@@ -440,7 +503,9 @@ const userGuideSteps: UserGuideStep[] = [
 ]
 
 const componentMap: Record<string, any> = {
-  XmView: markRaw(defineAsyncComponent(() => import('@/components/views/xmCard.vue'))),
+  [HOME_TAB_ID]: markRaw(defineAsyncComponent(() => import('@/components/views/HomeEntryView.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'))),
   ZxFwView: markRaw(defineAsyncComponent(() => import('@/components/views/ZxFwView.vue'))),
   HtFeeMethodTypeLineView: markRaw(defineAsyncComponent(() => import('@/components/views/HtFeeMethodTypeLineView.vue'))),
@@ -449,13 +514,15 @@ const componentMap: Record<string, any> = {
 const tabStore = useTabStore()
 const zxFwPricingStore = useZxFwPricingStore()
 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 tabContextX = ref(0)
 const tabContextY = ref(0)
-const contextTabId = ref<string>('XmView')
+const contextTabId = ref<string>(HOME_TAB_ID)
 const tabContextRef = ref<HTMLElement | null>(null)
 
 const dataMenuOpen = ref(false)
@@ -491,8 +558,13 @@ const tabsModel = computed({
 })
 
 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 => t.id !== 'XmView'))
+const hasClosableTabs = computed(() => tabStore.tabs.some(t => isTabClosable(t.id)))
 const activeGuideStep = computed(
   () => userGuideSteps[userGuideStepIndex.value] || userGuideSteps[0]
 )
@@ -501,14 +573,14 @@ const isLastGuideStep = computed(() => userGuideStepIndex.value >= userGuideStep
 const guideProgressText = computed(() => `${userGuideStepIndex.value + 1} / ${userGuideSteps.length}`)
 const canCloseLeft = computed(() => {
   if (contextTabIndex.value <= 0) return false
-  return tabStore.tabs.slice(0, contextTabIndex.value).some(t => t.id !== 'XmView')
+  return tabStore.tabs.slice(0, contextTabIndex.value).some(t => isTabClosable(t.id))
 })
 const canCloseRight = computed(() => {
   if (contextTabIndex.value < 0) return false
-  return tabStore.tabs.slice(contextTabIndex.value + 1).some(t => t.id !== 'XmView')
+  return tabStore.tabs.slice(contextTabIndex.value + 1).some(t => isTabClosable(t.id))
 })
 const canCloseOther = computed(() =>
-  tabStore.tabs.some(t => t.id !== 'XmView' && t.id !== contextTabId.value)
+  tabStore.tabs.some(t => isTabClosable(t.id) && t.id !== contextTabId.value)
 )
 
 const closeMenus = () => {
@@ -516,6 +588,79 @@ const closeMenus = () => {
   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 = () => {
   if (!reportExportToastTimer) return
   clearTimeout(reportExportToastTimer)
@@ -564,9 +709,9 @@ const hasNonDefaultTabState = () => {
     if (!raw) return false
     const parsed = JSON.parse(raw) as { tabs?: Array<{ id?: string }>; activeTabId?: string }
     const tabs = Array.isArray(parsed?.tabs) ? parsed.tabs : []
-    const hasCustomTabs = tabs.some(item => item?.id && item.id !== 'XmView')
+    const hasCustomTabs = tabs.some(item => item?.id && isTabClosable(item.id))
     const activeTabId = typeof parsed?.activeTabId === 'string' ? parsed.activeTabId : ''
-    return hasCustomTabs || (activeTabId !== '' && activeTabId !== 'XmView')
+    return hasCustomTabs || (activeTabId !== '' && isTabClosable(activeTabId))
   } catch (error) {
     console.error('parse tabs cache failed:', error)
     return false
@@ -574,6 +719,7 @@ const hasNonDefaultTabState = () => {
 }
 
 const shouldAutoOpenGuide = async () => {
+  if (readWorkspaceMode() === 'home') return false
   if (hasGuideCompleted()) return false
   if (hasNonDefaultTabState()) return false
   try {
@@ -672,7 +818,7 @@ const runTabMenuAction = (action: 'all' | 'left' | 'right' | 'other') => {
 const canMoveTab = (event: any) => {
   const draggedId = event?.draggedContext?.element?.id
   const targetIndex = event?.relatedContext?.index
-  if (draggedId === 'XmView') return false
+  if (protectedTabIdSet.has(draggedId)) return false
   if (typeof targetIndex === 'number' && targetIndex === 0) return false
   return true
 }
@@ -889,6 +1035,27 @@ 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 keys = await store.keys()
   const entries: DataEntry[] = []
@@ -954,12 +1121,80 @@ const formatExportTimestamp = (date: Date): string => {
   return `${yyyy}${mm}${dd}-${hh}${mi}`
 }
 
-const getExportProjectName = (entries: DataEntry[]): string => {
+const getExportProjectName = (entries: DataEntry[], forageStores: ForageStoreSnapshot[] = []): string => {
   const target =
     entries.find(item => item.key === PROJECT_INFO_DB_KEY) ||
     entries.find(item => item.key === LEGACY_PROJECT_DB_KEY)
   const data = (target?.value || {}) as XmInfoLike
-  return typeof data.projectName === 'string' ? sanitizeFileNamePart(data.projectName) : '造价项目'
+  if (typeof data.projectName === 'string' && data.projectName.trim()) {
+    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 => {
@@ -985,6 +1220,74 @@ const sumNumbers = (values: Array<number | null | undefined>): number =>
 const isNonEmptyString = (value: unknown): value is string =>
   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 match = /^task-(\d+)-\d+$/.exec(value)
   return match ? toSafeInteger(match[1]) : null
@@ -1063,18 +1366,6 @@ const toExportScaleRows = (rows: ScaleRowLike[] | undefined): ExportScaleRow[] =
     .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 => {
   if (!Array.isArray(rows)) return null
   let hasTotalValue = false
@@ -1083,10 +1374,11 @@ const buildMethod1 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod1 | n
       const major = toSafeInteger(row.id)
       if (major == null) return null
       const cost = toFiniteNumber(row.amount)
-      const basicFee = toFiniteNumber(row.benchmarkBudget)
-      const basicFeeBasic = toFiniteNumber(row.benchmarkBudgetBasic)
-      const basicFeeOptional = toFiniteNumber(row.benchmarkBudgetOptional)
-      const fee = toFiniteNumber(row.budgetFee)
+      const computed = resolveScaleMethodComputedValues(row, 'cost')
+      const basicFee = computed.benchmarkBudget
+      const basicFeeBasic = computed.benchmarkBudgetBasic
+      const basicFeeOptional = computed.benchmarkBudgetOptional
+      const fee = computed.budgetFee
       if (basicFee != null || fee != null) hasTotalValue = true
       const remark = typeof row.remark === 'string' ? row.remark : ''
       const hasValue =
@@ -1101,9 +1393,9 @@ const buildMethod1 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod1 | n
         major,
         cost: cost ?? 0,
         basicFee: basicFee ?? 0,
-        basicFormula: typeof row.basicFormula === 'string' ? row.basicFormula : '',
+        basicFormula: computed.basicFormula,
         basicFee_basic: basicFeeBasic ?? 0,
-        optionalFormula: typeof row.optionalFormula === 'string' ? row.optionalFormula : '',
+        optionalFormula: computed.optionalFormula,
         basicFee_optional: basicFeeOptional ?? 0,
         serviceCoe: toFiniteNumberOrZero(row.consultCategoryFactor),
         majorCoe: toFiniteNumberOrZero(row.majorFactor),
@@ -1134,10 +1426,11 @@ const buildMethod2 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod2 | n
       const major = toSafeInteger(row.id)
       if (major == null) return null
       const area = toFiniteNumber(row.landArea)
-      const basicFee = toFiniteNumber(row.benchmarkBudget)
-      const basicFeeBasic = toFiniteNumber(row.benchmarkBudgetBasic)
-      const basicFeeOptional = toFiniteNumber(row.benchmarkBudgetOptional)
-      const fee = toFiniteNumber(row.budgetFee)
+      const computed = resolveScaleMethodComputedValues(row, 'area')
+      const basicFee = computed.benchmarkBudget
+      const basicFeeBasic = computed.benchmarkBudgetBasic
+      const basicFeeOptional = computed.benchmarkBudgetOptional
+      const fee = computed.budgetFee
       if (basicFee != null || fee != null) hasTotalValue = true
       const remark = typeof row.remark === 'string' ? row.remark : ''
       const hasValue =
@@ -1152,9 +1445,9 @@ const buildMethod2 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod2 | n
         major,
         area: area ?? 0,
         basicFee: basicFee ?? 0,
-        basicFormula: typeof row.basicFormula === 'string' ? row.basicFormula : '',
+        basicFormula: computed.basicFormula,
         basicFee_basic: basicFeeBasic ?? 0,
-        optionalFormula: typeof row.optionalFormula === 'string' ? row.optionalFormula : '',
+        optionalFormula: computed.optionalFormula,
         basicFee_optional: basicFeeOptional ?? 0,
         serviceCoe: toFiniteNumberOrZero(row.consultCategoryFactor),
         majorCoe: toFiniteNumberOrZero(row.majorFactor),
@@ -1183,10 +1476,10 @@ const buildMethod3 = (rows: WorkloadMethodRowLike[] | undefined): ExportMethod3
   const det = rows
     .map(row => {
       const task = getTaskIdFromRowId(row.id)
-      if (task == null || row.basicFee == null) return null
+      if (task == null) return null
       const amount = toFiniteNumber(row.workload)
-      const basicFee = toFiniteNumber(row.basicFee)
-      const fee = toFiniteNumber(row.serviceFee)
+      const basicFee = toFiniteNumber(row.basicFee) ?? calcWorkloadBasicFeeFromRow(row)
+      const fee = toFiniteNumber(row.serviceFee) ?? calcWorkloadServiceFeeFromRow(row, basicFee)
       if (fee != null) hasTotalValue = true
       const remark = typeof row.remark === 'string' ? row.remark : ''
       const hasValue = amount != null || basicFee != null || fee != null || isNonEmptyString(remark)
@@ -1217,10 +1510,10 @@ const buildMethod4 = (rows: HourlyMethodRowLike[] | undefined): ExportMethod4 |
   const det = rows
     .map(row => {
       const expert = getExpertIdFromRowId(row.id)
-      if (expert == null || row.serviceBudget == null) return null
+      if (expert == null) return null
       const personNum = toFiniteNumber(row.personnelCount)
       const workDay = toFiniteNumber(row.workdayCount)
-      const fee = toFiniteNumber(row.serviceBudget)
+      const fee = toFiniteNumber(row.serviceBudget) ?? calcHourlyServiceFeeFromRow(row)
       if (fee != null) hasTotalValue = true
       const remark = typeof row.remark === 'string' ? row.remark : ''
       const hasValue = personNum != null || workDay != null || fee != null || isNonEmptyString(remark)
@@ -1416,18 +1709,20 @@ const buildReserveExport = async (contractId: string): Promise<ExportReserve | n
 }
 
 const buildExportReportPayload = async (): Promise<ExportReportPayload> => {
-  const [projectInfoRaw, projectScaleRaw, consultCategoryFactorRaw, majorFactorRaw, contractCardsRaw] = await Promise.all([
-    kvStore.getItem<XmInfoLike>(PROJECT_INFO_DB_KEY),
-    kvStore.getItem<XmInfoStorageLike>(LEGACY_PROJECT_DB_KEY),
-    kvStore.getItem<DetailRowsStorageLike<FactorRowLike>>(CONSULT_CATEGORY_FACTOR_DB_KEY),
-    kvStore.getItem<DetailRowsStorageLike<FactorRowLike>>(MAJOR_FACTOR_DB_KEY),
-    kvStore.getItem<ContractCardItem[]>('ht-card-v1')
+  const config = getReportWorkspaceConfig()
+  const [projectInfoRaw, projectScaleRaw, consultCategoryFactorRaw, majorFactorRaw, contractCardsRaw, quickContractRaw] = await Promise.all([
+    kvStore.getItem<XmInfoLike>(config.projectInfoKey),
+    kvStore.getItem<XmInfoStorageLike>(config.projectScaleKey),
+    kvStore.getItem<DetailRowsStorageLike<FactorRowLike>>(config.consultCategoryFactorKey),
+    kvStore.getItem<DetailRowsStorageLike<FactorRowLike>>(config.majorFactorKey),
+    config.contractCardsKey ? kvStore.getItem<ContractCardItem[]>(config.contractCardsKey) : Promise.resolve(null),
+    config.quickContractKey ? kvStore.getItem<QuickContractMeta>(config.quickContractKey) : Promise.resolve(null)
   ])
 
   const projectInfo = projectInfoRaw || {}
   const projectScaleSource = projectScaleRaw || {}
   const projectScale = projectScaleSource.roughCalcEnabled ? [] : toExportScaleRows(projectScaleSource.detailRows)
-  const projectScaleCost = toFiniteNumber(projectScaleSource.totalAmount) ?? sumLeafScaleCost(projectScaleSource.detailRows)
+  const projectScaleCost = toFiniteNumber(projectScaleSource.totalAmount)
   projectScale.push({
     major: -1, cost: projectScaleCost,
     area: null
@@ -1435,13 +1730,21 @@ const buildExportReportPayload = async (): Promise<ExportReportPayload> => {
 
   const projectServiceCoes = buildProjectServiceCoes(consultCategoryFactorRaw?.detailRows)
   const projectMajorCoes = buildProjectMajorCoes(majorFactorRaw?.detailRows)
-  const projectName = isNonEmptyString(projectInfo.projectName) ? projectInfo.projectName.trim() : '造价项目'
+  const quickContract = normalizeQuickContractMeta(quickContractRaw)
+  const projectName =
+    config.mode === 'quick'
+      ? `${quickContract.name}-快速计算`
+      : (isNonEmptyString(projectInfo.projectName) ? projectInfo.projectName.trim() : '造价项目')
   const writer = isNonEmptyString(projectInfo.preparedBy) ? projectInfo.preparedBy.trim() : ''
   const reviewer = isNonEmptyString(projectInfo.reviewedBy) ? projectInfo.reviewedBy.trim() : ''
   const date = isNonEmptyString(projectInfo.preparedDate) ? projectInfo.preparedDate.trim() : ''
   const industry = mapIndustryCodeToExportIndustry(projectInfo.projectIndustry)
 
-  const contractCards = (Array.isArray(contractCardsRaw) ? contractCardsRaw : [])
+  const contractCards = (
+    config.mode === 'quick'
+      ? [{ id: quickContract.id, name: quickContract.name, order: 0 }]
+      : (Array.isArray(contractCardsRaw) ? contractCardsRaw : [])
+  )
     .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))
 
@@ -1576,6 +1879,9 @@ const buildExportReportPayload = async (): Promise<ExportReportPayload> => {
   }
 
   return {
+    version: 2,
+    reportType: 'budget-report',
+    mode: config.mode,
     name: projectName,
     writer,
     reviewer,
@@ -1586,7 +1892,19 @@ const buildExportReportPayload = async (): Promise<ExportReportPayload> => {
     scale: projectScale,
     serviceCoes: projectServiceCoes,
     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
+    }
   }
 }
 
@@ -1602,12 +1920,18 @@ const exportData = async () => {
       }))
     )
     const payload: DataPackage = {
-      version: 2,
+      version: 3,
+      packageType: 'workspace-snapshot',
       exportedAt: now.toISOString(),
-      localStorage: readWebStorage(localStorage),
-      sessionStorage: readWebStorage(sessionStorage),
-      localforageDefault: await readForage(localforage),
-      localforageStores: piniaForageStores
+      workspace: {
+        mode: resolveWorkspaceModeForExport()
+      },
+      storage: {
+        localStorage: readWebStorage(localStorage),
+        sessionStorage: readWebStorage(sessionStorage),
+        localforageDefault: await readForage(localforage),
+        localforageStores: piniaForageStores
+      }
     }
 
     const content = await encodeZwArchive(payload)
@@ -1617,7 +1941,7 @@ const exportData = async () => {
     const url = URL.createObjectURL(blob)
     const link = document.createElement('a')
     link.href = url
-    const projectName = getExportProjectName(payload.localforageDefault)
+    const projectName = getExportProjectName(payload.storage?.localforageDefault || [], piniaForageStores)
     const timestamp = formatExportTimestamp(now)
     link.download = `${projectName}-${timestamp}${ZW_FILE_EXTENSION}`
     document.body.appendChild(link)
@@ -1635,12 +1959,12 @@ const exportData = async () => {
 const exportReport = async () => {
   try {
     showReportExportProgress(10, '正在准备报表导出...')
+    await requestXmScalePersistFlush()
     const now = new Date()
     showReportExportProgress(40, '正在汇总报表数据...')
     const payload = await buildExportReportPayload()
     showReportExportProgress(80, '正在生成并写出报表文件...')
     const fileName = `${sanitizeFileNamePart(payload.name)}-报表-${formatExportTimestamp(now)}`
-    console.log(payload)
     await exportFile(fileName, payload)
     finishReportExportProgress(true, '报表导出完成')
   } catch (error) {
@@ -1654,9 +1978,7 @@ const triggerImport = () => {
   importFileRef.value?.click()
 }
 
-const importData = async (event: Event) => {
-  const input = event.target as HTMLInputElement
-  const file = input.files?.[0]
+const handleSelectedImportFile = async (file: File) => {
   if (!file) return
 
   try {
@@ -1665,12 +1987,34 @@ const importData = async (event: Event) => {
     }
     const buffer = await file.arrayBuffer()
     const payload = await decodeZwArchive<DataPackage>(buffer)
-    pendingImportPayload.value = payload
+    const normalizedStorage = normalizeDataPackageStorage(payload)
+    pendingImportPayload.value = {
+      ...payload,
+      workspace: normalizeDataPackageWorkspace(payload),
+      storage: normalizedStorage
+    }
     pendingImportFileName.value = file.name
     importConfirmOpen.value = true
   } catch (error) {
     console.error('import failed:', error)
     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 {
     input.value = ''
   }
@@ -1686,10 +2030,12 @@ const confirmImportOverride = async () => {
   const payload = pendingImportPayload.value
   if (!payload) return
   try {
-    writeWebStorage(localStorage, normalizeEntries(payload.localStorage))
-    writeWebStorage(sessionStorage, normalizeEntries(payload.sessionStorage))
-    await writeForage(localforage, normalizeEntries(payload.localforageDefault))
-    const piniaSnapshots = normalizeForageStoreSnapshots(payload.localforageStores)
+    const normalizedStorage = normalizeDataPackageStorage(payload)
+    const normalizedWorkspace = normalizeDataPackageWorkspace(payload)
+    writeWebStorage(localStorage, normalizedStorage.localStorage)
+    writeWebStorage(sessionStorage, normalizedStorage.sessionStorage)
+    await writeForage(localforage, normalizedStorage.localforageDefault)
+    const piniaSnapshots = normalizeForageStoreSnapshots(normalizedStorage.localforageStores)
     const snapshotMap = new Map(piniaSnapshots.map(item => [item.storeName, item.entries]))
     await Promise.all(
       getPiniaPersistStores().map(async ({ storeName, store }) => {
@@ -1714,6 +2060,7 @@ const confirmImportOverride = async () => {
     } else {
       tabStore.resetTabs()
     }
+    normalizeTabStoreState()
 
     const zxFwPricingState = readPersistedState('zxFwPricing')
     if (zxFwPricingState) {
@@ -1730,6 +2077,7 @@ const confirmImportOverride = async () => {
       zxFwPricingStore.$persistNow?.(),
       kvStore.$persistNow?.()
     ])
+    writeWorkspaceMode(normalizedWorkspace.mode)
     dataMenuOpen.value = false
     window.location.reload()
   } catch (error) {
@@ -1761,8 +2109,10 @@ const handleReset = async () => {
 }
 
 onMounted(() => {
+  normalizeTabStoreState()
   window.addEventListener('mousedown', handleGlobalMouseDown)
   window.addEventListener('keydown', handleGlobalKeyDown)
+  window.addEventListener('home-import-selected', handleHomeImportSelected as EventListener)
   window.addEventListener('resize', scheduleUpdateTabTitleOverflow)
   void nextTick(() => {
     bindTabStripScroll()
@@ -1782,6 +2132,7 @@ onBeforeUnmount(() => {
   clearReportExportToastTimer()
   window.removeEventListener('mousedown', handleGlobalMouseDown)
   window.removeEventListener('keydown', handleGlobalKeyDown)
+  window.removeEventListener('home-import-selected', handleHomeImportSelected as EventListener)
   window.removeEventListener('resize', scheduleUpdateTabTitleOverflow)
   if (tabStripViewportEl) {
     tabStripViewportEl.removeEventListener('scroll', handleTabStripScroll)
@@ -1834,8 +2185,15 @@ watch(
   <ToastProvider>
   <TooltipProvider>
     <div class="flex flex-col w-full h-screen bg-background overflow-hidden">
-      <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">
-        <div class="flex min-w-0 items-start gap-1 h-full" @mouseenter="isTabStripHover = true"
+      <div
+        v-if="!isHomeOnlyView"
+        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">
           <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',
@@ -1857,7 +2215,7 @@ watch(
                     tabStore.activeTabId === tab.id && !isTabDragging
                       ? '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',
-                    tab.id !== 'XmView' ? 'cursor-move' : ''
+                    isTabClosable(tab.id) ? 'cursor-move' : ''
                   ]">
                   <TooltipRoot>
                     <TooltipTrigger as-child>
@@ -1868,7 +2226,7 @@ watch(
                     <TooltipContent v-if="tabTitleOverflowMap[tab.id]" side="bottom">{{ tab.title }}</TooltipContent>
                   </TooltipRoot>
 
-                  <Button v-if="tab.id !== 'XmView'" variant="ghost" size="icon"
+                  <Button v-if="isTabClosable(tab.id)" 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"
                     @click.stop="tabStore.removeTab(tab.id)">
                     <X class="h-3 w-3" />
@@ -1885,14 +2243,18 @@ watch(
           </button>
         </div>
         <div class="flex shrink-0 self-center items-center gap-1">
-          <div ref="dataMenuRef" class="relative shrink-0">
-            <Button variant="outline" size="sm"
+          <div v-if="showWorkspaceActions" ref="dataMenuRef" class="relative shrink-0">
+            <Button
+              variant="outline"
+              size="sm"
               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" />
               导入/导出
             </Button>
-            <div v-if="dataMenuOpen"
+            <div
+              v-if="dataMenuOpen"
               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"
                 @click="triggerImport">
@@ -1907,16 +2269,22 @@ watch(
                 导出报表
               </button>
             </div>
-            <input ref="importFileRef" type="file" accept=".zw" class="hidden" @change="importData" />
           </div>
 
-          <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)">
+          <input ref="importFileRef" type="file" accept=".zw" class="hidden" @change="importData" />
+
+          <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" />
             使用引导
           </Button>
 
-          <AlertDialogRoot>
+          <AlertDialogRoot v-if="showWorkspaceActions">
             <AlertDialogTrigger as-child>
               <Button variant="destructive" size="sm"
                 class="h-9 min-h-9 shrink-0 px-3 py-0 text-sm leading-none cursor-pointer">
diff --git a/src/layout/typeLine.vue b/src/layout/typeLine.vue
index 68039a4..65687b7 100644
--- a/src/layout/typeLine.vue
+++ b/src/layout/typeLine.vue
@@ -25,6 +25,7 @@ const props = withDefaults(
     scene?: string
     title?: string
     subtitle?: string
+    metaText?: string
     copyText?: string
     categories: TypeLineCategory[]
     storageKey?: string
@@ -34,6 +35,7 @@ const props = withDefaults(
     scene: 'default',
     title: '配置',
     subtitle: '',
+    metaText: '',
     copyText: '',
     storageKey: '',
     defaultCategory: ''
@@ -201,7 +203,7 @@ useMotionValueEvent(
   <TooltipProvider>
   <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 v-if="props.title || props.subtitle" class="space-y-1">
+      <div v-if="props.title || props.subtitle || props.metaText" class="space-y-1">
         <TooltipRoot>
           <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">
@@ -224,6 +226,9 @@ useMotionValueEvent(
             {{ copyBtnText }}
           </Button>
         </div>
+        <div v-if="props.metaText" class="text-xs leading-5 text-muted-foreground">
+          {{ props.metaText }}
+        </div>
       </div>
 
       <div class="flex flex-col gap-10 relative">
diff --git a/src/lib/projectWorkspace.ts b/src/lib/projectWorkspace.ts
new file mode 100644
index 0000000..dd2d2ab
--- /dev/null
+++ b/src/lib/projectWorkspace.ts
@@ -0,0 +1,112 @@
+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)
+  ])
+}
diff --git a/src/lib/workspace.ts b/src/lib/workspace.ts
new file mode 100644
index 0000000..e83419d
--- /dev/null
+++ b/src/lib/workspace.ts
@@ -0,0 +1,52 @@
+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()
+})
diff --git a/src/pinia/Plugin/indexdb.ts b/src/pinia/Plugin/indexdb.ts
index 6cfa828..9ac40dd 100644
--- a/src/pinia/Plugin/indexdb.ts
+++ b/src/pinia/Plugin/indexdb.ts
@@ -123,18 +123,19 @@ export default (config?: PiniaStorageConfig) => {
       { detached: true }
     )
 
+    hydrating = true
     void lf.getItem<Record<string, unknown>>(key)
       .then(state => {
         if (!state || typeof state !== 'object') return
         // 若在异步hydrate返回前，store已被用户修改（如removeTab），不再回填旧缓存覆盖当前状态。
         if (userMutatedBeforeHydrate) return
-        hydrating = true
         context.store.$patch(state as any)
-        hydrating = false
       })
       .catch(error => {
-        hydrating = false
         console.error('pinia hydrate failed:', error)
       })
+      .finally(() => {
+        hydrating = false
+      })
   }
 }
diff --git a/src/pinia/tab.ts b/src/pinia/tab.ts
index 71e350f..a66f9b3 100644
--- a/src/pinia/tab.ts
+++ b/src/pinia/tab.ts
@@ -1,5 +1,6 @@
 import { defineStore } from 'pinia'
 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>> {
   id: string
@@ -8,14 +9,14 @@ export interface TabItem<TProps = Record<string, unknown>> {
   props?: TProps
 }
 
-const HOME_TAB_ID = 'XmView'
 const DEFAULT_TAB: TabItem = {
   id: HOME_TAB_ID,
-  title: '项目卡片',
+  title: '首页',
   componentName: HOME_TAB_ID
 }
 
 const createDefaultTabs = (): TabItem[] => [{ ...DEFAULT_TAB }]
+const PROTECTED_TAB_ID_SET = new Set<string>([HOME_TAB_ID, QUICK_CONTRACT_TAB_ID])
 
 export const useTabStore = defineStore(
   'tabs',
@@ -36,6 +37,11 @@ export const useTabStore = defineStore(
       }
     }
 
+    const enterWorkspace = (config: TabItem) => {
+      tabs.value = [{ ...config }]
+      activeTabId.value = config.id
+    }
+
     const openTab = (config: TabItem) => {
       if (!tabs.value.some(tab => tab.id === config.id)) {
         tabs.value = [...tabs.value, config]
@@ -44,8 +50,7 @@ export const useTabStore = defineStore(
     }
 
     const removeTab = (id: string) => {
-      // 首页标签固定保留，不允许关闭。
-      if (id === HOME_TAB_ID) return
+      if (PROTECTED_TAB_ID_SET.has(id)) return
 
       const index = tabs.value.findIndex(tab => tab.id === id)
       if (index < 0) return
@@ -64,26 +69,27 @@ export const useTabStore = defineStore(
     }
 
     const closeAllTabs = () => {
-      tabs.value = createDefaultTabs()
-      activeTabId.value = HOME_TAB_ID
+      const protectedTabs = tabs.value.filter(tab => PROTECTED_TAB_ID_SET.has(tab.id))
+      tabs.value = protectedTabs.length > 0 ? protectedTabs : createDefaultTabs()
+      activeTabId.value = tabs.value[0]?.id ?? HOME_TAB_ID
     }
 
     const closeLeftTabs = (targetId: string) => {
       const targetIndex = tabs.value.findIndex(tab => tab.id === targetId)
       if (targetIndex < 0) return
-      tabs.value = tabs.value.filter((tab, index) => tab.id === HOME_TAB_ID || index >= targetIndex)
+      tabs.value = tabs.value.filter((tab, index) => PROTECTED_TAB_ID_SET.has(tab.id) || index >= targetIndex)
       ensureActiveValid()
     }
 
     const closeRightTabs = (targetId: string) => {
       const targetIndex = tabs.value.findIndex(tab => tab.id === targetId)
       if (targetIndex < 0) return
-      tabs.value = tabs.value.filter((tab, index) => tab.id === HOME_TAB_ID || index <= targetIndex)
+      tabs.value = tabs.value.filter((tab, index) => PROTECTED_TAB_ID_SET.has(tab.id) || index <= targetIndex)
       ensureActiveValid()
     }
 
     const closeOtherTabs = (targetId: string) => {
-      tabs.value = tabs.value.filter(tab => tab.id === HOME_TAB_ID || tab.id === targetId)
+      tabs.value = tabs.value.filter(tab => PROTECTED_TAB_ID_SET.has(tab.id) || tab.id === targetId)
       ensureHomeTab()
       activeTabId.value = tabs.value.some(tab => tab.id === targetId) ? targetId : HOME_TAB_ID
     }
@@ -96,6 +102,7 @@ export const useTabStore = defineStore(
     return {
       tabs,
       activeTabId,
+      enterWorkspace,
       openTab,
       removeTab,
       closeAllTabs,
diff --git a/src/pinia/zxFwPricing.ts b/src/pinia/zxFwPricing.ts
index 1b1f97d..bc255b9 100644
--- a/src/pinia/zxFwPricing.ts
+++ b/src/pinia/zxFwPricing.ts
@@ -328,6 +328,13 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
     return state[method] || null
   }
 
+  /**
+   * @Author: wintsa
+   * @Date: 2026-03-13
+   * @LastEditors: wintsa
+   * @Description: 获取咨询服务计价方法状态
+   * @returns {*}
+   */
   const getServicePricingMethodState = <TRow = unknown>(
     contractIdRaw: string | number,
     serviceIdRaw: string | number,
@@ -339,6 +346,13 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
     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>(
     contractIdRaw: string | number,
     serviceIdRaw: string | number,
@@ -379,6 +393,13 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
     return true
   }
 
+  /**
+   * @Author: wintsa
+   * @Date: 2026-03-13
+   * @LastEditors: wintsa
+   * @Description: 从缓存或持久化存储加载咨询服务计价方法状态
+   * @returns {*}
+   */
   const loadServicePricingMethodState = async <TRow = unknown>(
     contractIdRaw: string | number,
     serviceIdRaw: string | number,
@@ -404,6 +425,13 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
     return getServicePricingMethodState<TRow>(contractId, serviceId, method)
   }
 
+  /**
+   * @Author: wintsa
+   * @Date: 2026-03-13
+   * @LastEditors: wintsa
+   * @Description: 删除单个咨询服务计价方法状态
+   * @returns {*}
+   */
   const removeServicePricingMethodState = (
     contractIdRaw: string | number,
     serviceIdRaw: string | number,
@@ -422,6 +450,13 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
     return had
   }
 
+  /**
+   * @Author: wintsa
+   * @Date: 2026-03-13
+   * @LastEditors: wintsa
+   * @Description: 获取单个咨询服务计价方法存储键
+   * @returns {*}
+   */
   const getServicePricingStorageKey = (
     contractIdRaw: string | number,
     serviceIdRaw: string | number,
@@ -433,6 +468,13 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
     return serviceMethodDbKeyOf(contractId, serviceId, method)
   }
 
+  /**
+   * @Author: wintsa
+   * @Date: 2026-03-13
+   * @LastEditors: wintsa
+   * @Description: 获取咨询服务全部计价方法存储键
+   * @returns {*}
+   */
   const getServicePricingStorageKeys = (contractIdRaw: string | number, serviceIdRaw: string | number) => {
     const contractId = toKey(contractIdRaw)
     const serviceId = toServiceKey(serviceIdRaw)
@@ -442,6 +484,13 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
     )
   }
 
+  /**
+   * @Author: wintsa
+   * @Date: 2026-03-13
+   * @LastEditors: wintsa
+   * @Description: 删除咨询服务全部计价方法状态
+   * @returns {*}
+   */
   const removeAllServicePricingMethodStates = (contractIdRaw: string | number, serviceIdRaw: string | number) => {
     let changed = false
     for (const method of Object.keys(METHOD_STORAGE_PREFIX_MAP) as ServicePricingMethod[]) {
@@ -450,12 +499,26 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
     return changed
   }
 
+  /**
+   * @Author: wintsa
+   * @Date: 2026-03-13
+   * @LastEditors: wintsa
+   * @Description: 获取合同附加费用主表状态
+   * @returns {*}
+   */
   const getHtFeeMainState = <TRow = unknown>(mainStorageKeyRaw: string | number): HtFeeMainState<TRow> | null => {
     const mainStorageKey = toKey(mainStorageKeyRaw)
     if (!mainStorageKey) return null
     return (htFeeMainStates.value[mainStorageKey] as HtFeeMainState<TRow> | undefined) || null
   }
 
+  /**
+   * @Author: wintsa
+   * @Date: 2026-03-13
+   * @LastEditors: wintsa
+   * @Description: 设置合同附加费用主表状态并同步版本
+   * @returns {*}
+   */
   const setHtFeeMainState = <TRow = unknown>(
     mainStorageKeyRaw: string | number,
     payload: Partial<HtFeeMainState<TRow>> | null | undefined,
@@ -494,10 +557,18 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
     return true
   }
 
+  /**
+   * @Author: wintsa
+   * @Date: 2026-03-13
+   * @LastEditors: wintsa
+   * @Description: 从缓存或持久化存储加载合同附加费用主表状态
+   * @returns {*}
+   */
   const loadHtFeeMainState = async <TRow = unknown>(
     mainStorageKeyRaw: string | number,
     force = false
   ): Promise<HtFeeMainState<TRow> | null> => {
+    
     const mainStorageKey = toKey(mainStorageKeyRaw)
     if (!mainStorageKey) return null
     if (!force) {
@@ -513,6 +584,13 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
     return getHtFeeMainState<TRow>(mainStorageKey)
   }
 
+  /**
+   * @Author: wintsa
+   * @Date: 2026-03-13
+   * @LastEditors: wintsa
+   * @Description: 删除合同附加费用主表状态
+   * @returns {*}
+   */
   const removeHtFeeMainState = (mainStorageKeyRaw: string | number) =>
     setHtFeeMainState(mainStorageKeyRaw, null)
 
@@ -529,6 +607,13 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
     return htFeeMethodStates.value[mainStorageKey][rowId]
   }
 
+  /**
+   * @Author: wintsa
+   * @Date: 2026-03-13
+   * @LastEditors: wintsa
+   * @Description: 获取合同附加费用子方法存储键
+   * @returns {*}
+   */
   const getHtFeeMethodStorageKey = (
     mainStorageKeyRaw: string | number,
     rowIdRaw: string | number,
@@ -540,6 +625,13 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
     return `${mainStorageKey}-${rowId}-${method}`
   }
 
+  /**
+   * @Author: wintsa
+   * @Date: 2026-03-13
+   * @LastEditors: wintsa
+   * @Description: 获取合同附加费用子方法状态
+   * @returns {*}
+   */
   const getHtFeeMethodState = <TPayload = HtFeeMethodPayload>(
     mainStorageKeyRaw: string | number,
     rowIdRaw: string | number,
@@ -552,6 +644,13 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
     return value == null ? null : (cloneAny(value) as TPayload)
   }
 
+  /**
+   * @Author: wintsa
+   * @Date: 2026-03-13
+   * @LastEditors: wintsa
+   * @Description: 设置合同附加费用子方法状态并同步版本
+   * @returns {*}
+   */
   const setHtFeeMethodState = <TPayload = HtFeeMethodPayload>(
     mainStorageKeyRaw: string | number,
     rowIdRaw: string | number,
@@ -564,6 +663,7 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
   ) => {
     const mainStorageKey = toKey(mainStorageKeyRaw)
     const rowId = toKey(rowIdRaw)
+    
     if (!mainStorageKey || !rowId) return false
     const storageKey = getHtFeeMethodStorageKey(mainStorageKey, rowId, method)
     if (!storageKey) return false
@@ -605,6 +705,13 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
     return true
   }
 
+  /**
+   * @Author: wintsa
+   * @Date: 2026-03-13
+   * @LastEditors: wintsa
+   * @Description: 从缓存或持久化存储加载合同附加费用子方法状态
+   * @returns {*}
+   */
   const loadHtFeeMethodState = async <TPayload = HtFeeMethodPayload>(
     mainStorageKeyRaw: string | number,
     rowIdRaw: string | number,
@@ -628,12 +735,26 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
     return getHtFeeMethodState<TPayload>(mainStorageKey, rowId, method)
   }
 
+  /**
+   * @Author: wintsa
+   * @Date: 2026-03-13
+   * @LastEditors: wintsa
+   * @Description: 删除合同附加费用子方法状态
+   * @returns {*}
+   */
   const removeHtFeeMethodState = (
     mainStorageKeyRaw: string | number,
     rowIdRaw: string | number,
     method: HtFeeMethodType
   ) => 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 key = toKey(keyRaw)
     if (!key) return null
@@ -664,6 +785,13 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
     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 key = toKey(keyRaw)
     if (!key) return null
@@ -725,6 +853,13 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
     }
   }
 
+  /**
+   * @Author: wintsa
+   * @Date: 2026-03-13
+   * @LastEditors: wintsa
+   * @Description: 按通用键设置状态并同步版本
+   * @returns {*}
+   */
   const setKeyState = <T = unknown>(
     keyRaw: string | number,
     value: T,
@@ -749,6 +884,7 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
         { force: true, syncKeyState: false }
       )
     }
+
     const htMethodMeta = parseHtFeeMethodStorageKey(key)
     if (htMethodMeta) {
       setHtFeeMethodState(
@@ -769,6 +905,13 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
     return true
   }
 
+  /**
+   * @Author: wintsa
+   * @Date: 2026-03-13
+   * @LastEditors: wintsa
+   * @Description: 按通用键删除状态
+   * @returns {*}
+   */
   const removeKeyState = (keyRaw: string | number) => {
     const key = toKey(keyRaw)
     if (!key) return false
@@ -795,12 +938,26 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
     return hadValue
   }
 
+  /**
+   * @Author: wintsa
+   * @Date: 2026-03-13
+   * @LastEditors: wintsa
+   * @Description: 获取指定键的版本号
+   * @returns {*}
+   */
   const getKeyVersion = (keyRaw: string | number) => {
     const key = toKey(keyRaw)
     if (!key) return 0
     return keyVersions.value[key] || 0
   }
 
+  /**
+   * @Author: wintsa
+   * @Date: 2026-03-13
+   * @LastEditors: wintsa
+   * @Description: 获取合同咨询服务主表状态
+   * @returns {*}
+   */
   const getContractState = (contractIdRaw: string | number) => {
     const contractId = toKey(contractIdRaw)
     if (!contractId) return null
@@ -808,6 +965,13 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
     return data ? cloneState(data) : null
   }
 
+  /**
+   * @Author: wintsa
+   * @Date: 2026-03-13
+   * @LastEditors: wintsa
+   * @Description: 从缓存或持久化存储加载合同咨询服务主表状态
+   * @returns {*}
+   */
   const loadContract = async (contractIdRaw: string | number, force = false) => {
     const contractId = toKey(contractIdRaw)
     if (!contractId) return null
@@ -843,6 +1007,13 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
     }
   }
 
+  /**
+   * @Author: wintsa
+   * @Date: 2026-03-13
+   * @LastEditors: wintsa
+   * @Description: 设置合同咨询服务主表状态
+   * @returns {*}
+   */
   const setContractState = async (contractIdRaw: string | number, state: ZxFwState) => {
     const contractId = toKey(contractIdRaw)
     if (!contractId) return false
@@ -855,6 +1026,13 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
     return true
   }
 
+  /**
+   * @Author: wintsa
+   * @Date: 2026-03-13
+   * @LastEditors: wintsa
+   * @Description: 更新合同咨询服务行的计价汇总字段
+   * @returns {*}
+   */
   const updatePricingField = async (params: {
     contractId: string
     serviceId: string | number
@@ -900,6 +1078,13 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
     return true
   }
 
+  /**
+   * @Author: wintsa
+   * @Date: 2026-03-13
+   * @LastEditors: wintsa
+   * @Description: 获取合同咨询服务基础小计
+   * @returns {*}
+   */
   const getBaseSubtotal = (contractIdRaw: string | number): number | null => {
     const contractId = toKey(contractIdRaw)
     if (!contractId) return null
@@ -920,6 +1105,13 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
     return hasValid ? round3(sum) : null
   }
 
+  /**
+   * @Author: wintsa
+   * @Date: 2026-03-13
+   * @LastEditors: wintsa
+   * @Description: 删除合同关联的咨询服务、附加费用和键状态数据
+   * @returns {*}
+   */
   const removeContractData = (contractIdRaw: string | number) => {
     const contractId = toKey(contractIdRaw)
     if (!contractId) return false
diff --git a/src/sql.ts b/src/sql.ts
index e6d0ed4..2862a66 100644
--- a/src/sql.ts
+++ b/src/sql.ts
@@ -1114,16 +1114,18 @@ async function generateTemplate(data) {
                 }
             });
             let endRows = 1;
-            cusInsertRowFunc(ci.services.length + 3 + endRows, [sheet_1.getRow(3)], sheet_1, (targetRow) => {
-                targetRow.getCell(1).value = ci.services.length + endRows;
-                targetRow.getCell(2).value = '';
-                targetRow.getCell(3).value = '基本、可选工作小计';
-                targetRow.getCell(4).value = numberFormatter(m1Sum, 2);
-                targetRow.getCell(5).value = numberFormatter(m2Sum, 2);
-                targetRow.getCell(6).value = numberFormatter(m3Sum, 2);
-                targetRow.getCell(7).value = numberFormatter(m4Sum, 2);
-                targetRow.getCell(8).value = numberFormatter(serviceSum, 2);
-            });
+            if (ci.services.length) {
+                cusInsertRowFunc(ci.services.length + 3 + endRows, [sheet_1.getRow(3)], sheet_1, (targetRow) => {
+                    targetRow.getCell(1).value = ci.services.length + endRows;
+                    targetRow.getCell(2).value = '';
+                    targetRow.getCell(3).value = '基本、可选工作小计';
+                    targetRow.getCell(4).value = numberFormatter(m1Sum, 2);
+                    targetRow.getCell(5).value = numberFormatter(m2Sum, 2);
+                    targetRow.getCell(6).value = numberFormatter(m3Sum, 2);
+                    targetRow.getCell(7).value = numberFormatter(m4Sum, 2);
+                    targetRow.getCell(8).value = numberFormatter(serviceSum, 2);
+                });
+            }
             if (ci.addtional) {
                 endRows++;
                 cusInsertRowFunc(ci.services.length + 3 + endRows, [sheet_1.getRow(3)], sheet_1, (targetRow) => {
