优化目录

This commit is contained in:
wintsa 2026-03-17 12:05:22 +08:00
parent 4e343cccf1
commit 2b18fe9991
46 changed files with 1061 additions and 1308 deletions

View File

@ -0,0 +1,15 @@
{
"permissions": {
"allow": [
"Skill(update-config)",
"Bash(mv views/Ht.vue views/htCard.vue views/htInfo.vue views/HtAdditionalWorkFee.vue views/HtConsultCategoryFactor.vue views/HtFeeRateMethodForm.vue views/HtMajorFactor.vue views/HtReserveFee.vue ht/)",
"Bash(mv views/xmCard.vue views/xmInfo.vue views/info.vue views/XmMajorFactor.vue views/XmConsultCategoryFactor.vue xm/)",
"Bash(mv views/pricingView/HourlyPricingPane.vue views/pricingView/InvestmentScalePricingPane.vue views/pricingView/LandScalePricingPane.vue views/pricingView/WorkloadPricingPane.vue pricing/)",
"Bash(mv common/HourlyFeeGrid.vue common/HtFeeGrid.vue common/HtFeeMethodGrid.vue common/MethodUnavailableNotice.vue common/XmFactorGrid.vue common/xmCommonAgGrid.vue views/ServiceCheckboxSelector.vue views/WorkContentGrid.vue shared/)",
"Bash(rmdir views/pricingView common)",
"Bash(cd:*)",
"Bash(bun run:*)",
"Bash(grep:*)"
]
}
}

View File

@ -1,2 +0,0 @@
JGJSZW ó¦#ďWgťtK_ł…çGşŚ<C59F>±Ů;ÚćwëfÂ)ĺA»·u:퇼!ĺP÷YĽâ2Z[B[ßş ĐĺÂmF’ó¤Ďé«Sśs;˙eQóqdÚđú˘;gÖĆfí}DFÚđć`Ë˙ń4ĹĎŃĽ I{íÁKş¶Ź1źÍN˙źrŰ|Á[jČ„ů<>(YĹX®†ĎĚ9?š2ĘH×ä˙v^ł0“ídL‡Č6gNvˇÁ˛ÉZ~?™ámq^#ĘŽI†m ,HŐî˘ŔÁu˛?[ÓÚ.ż ­şPďß5<C39F>¶ÍBÚůŚĎ×ďđŠM8Ö¬PG.d‰<64>ä®čý„îgP>flŁ<6C>: <0C>˝+qűl†
Ŕ:˝<CB9D>Ő #Túź®ŇZ<C587>8]®ŕXň°(č(÷<>WV±ŮÖgI\?ŰŁs­Q …k<11> Xô6é´¦?×<>i+\p cLÔX;˛žŁäŇąňw,Áď§Ăýčq2$ňßÄ…č×< ^“„ÓďA´ “ŁŢSc\>rpŐŽ-óA®ýüŚp7FÂ@8jcoĘ HčîÚ±Ďa«9ł‚— ©ŹŐ5<C590> Š^dÜČa-̡.%Ł˙ »Nü~ťÎ´IÝłt¤îŤI„VnpI°ikcđJ>vMŘá˘p<ŕčuć•ĂpÔ„*WwčNŁă‡U <14>O]Ůźaý<61>~8ČTm n™JJ°Ĺa”łĚßů´\Á™Ž‹ł™Ý żwÄS!Ç“Ożď2śč(pźíŔmZ

Binary file not shown.

2
.serena/.gitignore vendored
View File

@ -1,2 +0,0 @@
/cache
/project.local.yml

View File

@ -1,136 +0,0 @@
# the name by which the project can be referenced within Serena
project_name: "JGJS2026"
# list of languages for which language servers are started; choose from:
# al bash clojure cpp csharp
# csharp_omnisharp dart elixir elm erlang
# fortran fsharp go groovy haskell
# java julia kotlin lua markdown
# matlab nix pascal perl php
# php_phpactor powershell python python_jedi r
# rego ruby ruby_solargraph rust scala
# swift terraform toml typescript typescript_vts
# vue yaml zig
# (This list may be outdated. For the current list, see values of Language enum here:
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py
# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
# Note:
# - For C, use cpp
# - For JavaScript, use typescript
# - For Free Pascal/Lazarus, use pascal
# Special requirements:
# Some languages require additional setup/installations.
# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers
# When using multiple languages, the first language server that supports a given file will be used for that file.
# The first language is the default language and the respective language server will be used as a fallback.
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
languages:
- vue
- typescript
# the encoding used by text files in the project
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
encoding: "utf-8"
# line ending convention to use when writing source files.
# Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default)
# This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings.
line_ending:
# The language backend to use for this project.
# If not set, the global setting from serena_config.yml is used.
# Valid values: LSP, JetBrains
# Note: the backend is fixed at startup. If a project with a different backend
# is activated post-init, an error will be returned.
language_backend:
# whether to use project's .gitignore files to ignore files
ignore_all_files_in_gitignore: true
# list of additional paths to ignore in this project.
# Same syntax as gitignore, so you can use * and **.
# Note: global ignored_paths from serena_config.yml are also applied additively.
ignored_paths: []
# whether the project is in read-only mode
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
# Added on 2025-04-18
read_only: false
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
# Below is the complete list of tools for convenience.
# To make sure you have the latest list of tools, and to view their descriptions,
# execute `uv run scripts/print_tool_overview.py`.
#
# * `activate_project`: Activates a project by name.
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
# * `create_text_file`: Creates/overwrites a file in the project directory.
# * `delete_lines`: Deletes a range of lines within a file.
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
# * `execute_shell_command`: Executes a shell command.
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
# * `initial_instructions`: Gets the initial instructions for the current project.
# Should only be used in settings where the system prompt cannot be set,
# e.g. in clients you have no control over, like Claude Desktop.
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
# * `insert_at_line`: Inserts content at a given line in a file.
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
# * `list_memories`: Lists memories in Serena's project-specific memory store.
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
# * `read_file`: Reads a file within the project directory.
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
# * `remove_project`: Removes a project from the Serena configuration.
# * `replace_lines`: Replaces a range of lines within a file with new content.
# * `replace_symbol_body`: Replaces the full definition of a symbol.
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
# * `search_for_pattern`: Performs a search for a pattern in the project.
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
# * `switch_modes`: Activates modes by providing a list of their names
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
excluded_tools: []
# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default)
included_optional_tools: []
# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
# This cannot be combined with non-empty excluded_tools or included_optional_tools.
fixed_tools: []
# list of mode names to that are always to be included in the set of active modes
# The full set of modes to be activated is base_modes + default_modes.
# If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply.
# Otherwise, this setting overrides the global configuration.
# Set this to [] to disable base modes for this project.
# Set this to a list of mode names to always include the respective modes for this project.
base_modes:
# list of mode names that are to be activated by default.
# The full set of modes to be activated is base_modes + default_modes.
# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply.
# Otherwise, this overrides the setting from the global configuration (serena_config.yml).
# This setting can, in turn, be overridden by CLI parameters (--mode).
default_modes:
# initial prompt for the project. It will always be given to the LLM upon activating the project
# (contrary to the memories, which are loaded on demand).
initial_prompt: ""
# time budget (seconds) per tool call for the retrieval of additional symbol information
# such as docstrings or parameter information.
# This overrides the corresponding setting in the global configuration; see the documentation there.
# If null or missing, use the setting from the global configuration.
symbol_info_budget:
# list of regex patterns which, when matched, mark a memory entry as readonly.
# Extends the list from the global configuration, merging the two lists.
read_only_memory_patterns: []

383
AGENTS.md
View File

@ -1,383 +0,0 @@
# AGENTS.md — Codex 工作操作手册
本文件面向 Codex AI定义其职责与操作规范。
## 0. 角色定位与职责边界
| 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 约束与最佳实践。 | **新项目启动或者有新依赖强制使用**。用于初始化依赖知识库。 ||
| update_planplan 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次
**判断原则**
- 如果不在"极少数例外"清单中 → 自动执行
- 如有疑问 → 自动执行(而非询问)
- 宁可执行后修复,也不要频繁打断工作流程
---
**协作原则总结**
- 我规划,我决策
- 我观察,我判断
- 我执行,我验证
- 遇疑问,评估后决策或询问用户

115
CLAUDE.md Normal file
View File

@ -0,0 +1,115 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
# 任何项目都务必遵守的规则(极其重要!!!)
## Communication
- 永远使用简体中文进行思考和对话
## Documentation
- 编写 .md 文档时,也要用中文
- 正式文档写到项目的 docs/ 目录下
- 用于讨论和评审的计划、方案等文档,写到项目的 discuss/ 目录下
## Code Architecture
- 编写代码的硬性指标,包括以下原则:
1对于 Python、JavaScript、TypeScript 等动态语言,尽可能确保每个代码文件不要超过 500 行
3每层文件夹中的文件尽可能不超过 8 个。如有超过,需要规划为多层子文件夹
- 除了硬性指标以外,还需要时刻关注优雅的架构设计,避免出现以下可能侵蚀我们代码质量的「坏味道」:
1僵化 (Rigidity): 系统难以变更,任何微小的改动都会引发一连串的连锁修改。
2冗余 (Redundancy): 同样的代码逻辑在多处重复出现,导致维护困难且容易产生不一致。
3循环依赖 (Circular Dependency): 两个或多个模块互相纠缠,形成无法解耦的“死结”,导致难以测试与复用。
4脆弱性 (Fragility): 对代码一处的修改,导致了系统中其他看似无关部分功能的意外损坏。
5晦涩性 (Obscurity): 代码意图不明,结构混乱,导致阅读者难以理解其功能和设计。
6数据泥团 (Data Clump): 多个数据项总是一起出现在不同方法的参数中,暗示着它们应该被组合成一个独立的对象。
7不必要的复杂性 (Needless Complexity): 用“杀牛刀”去解决“杀鸡”的问题,过度设计使系统变得臃肿且难以理解。
- 【非常重要!!】无论是你自己编写代码,还是阅读或审核他人代码时,都要严格遵守上述硬性指标,以及时刻关注优雅的架构设计。
- 【非常重要!!】无论何时,一旦你识别出那些可能侵蚀我们代码质量的「坏味道」,都应当立即询问用户是否需要优化,并给出合理的优化建议。
## Commands
- **Dev server**: `bun run dev` (uses `bunx --bun vite`)
- **Build**: `bun run build` (type-checks then bundles via Vite)
- **Type check only**: `bun run type-check`
- **Preview build**: `bun run preview`
No test runner is configured. Use Playwright for UI automation when needed (see AGENTS.md).
## Architecture Overview
This is a **browser-only Vue 3 SPA** — no backend, no SSR. All data is persisted client-side via IndexedDB (through localforage). The domain is **交通建设项目工程造价咨询** (transport infrastructure cost-consulting fee calculation for road, railway, and waterway projects).
### App Entry & Routing
There is no Vue Router. Navigation is tab-based, managed entirely by `useTabStore` (`src/pinia/tab.ts`). `src/App.vue` shows either `HomeEntryView` (first-launch onboarding) or the main `Tab` layout depending on `tabStore.hasCompletedSetup`.
The `Tab` layout (`src/layout/tab.vue`) renders the active tab's component by name using `defineAsyncComponent` with a map of component names → import paths.
### Workspace Modes
Defined in `src/lib/workspace.ts`. Three modes: `home`, `project`, `quick`. The current mode is persisted to `localStorage` under key `jgjs-workspace-mode-v1`. Two fixed/protected tab IDs exist: `ProjectCalcView` and `QuickCalcView` (and the quick-contract tab), which cannot be closed.
### State & Persistence
- **Pinia** is used for all state management with `pinia-plugin-persistedstate`.
- The persistence plugin is customized at `src/pinia/Plugin/indexdb` — it stores pinia state in IndexedDB (not localStorage), using localforage with `mode: 'multiple'` (each store in its own IndexedDB store named `pinia`).
- `useTabStore` (`src/pinia/tab.ts`): tab list, active tab, setup flag. Persisted.
- `useZxFwPricingStore` (`src/pinia/zxFwPricing.ts`): the core domain store. Manages contract-level 咨询服务 (consulting service) pricing state, per-service pricing method states, and contract extra-fee states. Uses a generic key/value layer (`getKeyState`/`setKeyState`/`loadKeyState`) backed by `useKvStore`.
- `useKvStore` (`src/pinia/kv.ts`): raw key-value access to IndexedDB via localforage.
### Data Layer (`src/sql.ts`)
Despite the name, there is no SQL database. `src/sql.ts` is a large static data file containing:
- `industryTypeList`: road / railway / waterway
- `majorList`: engineering specialties (E1E4 codes) with coefficient ranges
- `serviceList`: consulting service types (D1D5 codes) with pricing method flags
- `additionalWorkList`: extra work item definitions
- `exportFile`: Excel export logic using ExcelJS
All fee calculation formulas and coefficient tables live here.
### Lib Utilities (`src/lib/`)
- `decimal.ts` / `number.ts` / `numberFormat.ts`: safe arithmetic using `decimal.js`, thousand-separator formatting
- `zwArchive.ts`: AES-GCM encryption/decryption for `.zw` save files (import/export format). The key is derived from a fixed seed via SHA-256. File magic bytes: `JGJSZW`.
- `workspace.ts`: workspace mode constants and storage helpers
- `diyAgGridOptions.ts`: shared AG Grid configuration defaults
- `pricingScaleFee.ts` / `pricingMethodTotals.ts`: fee calculation helpers
- `projectWorkspace.ts` / `xmFactorDefaults.ts`: project-level workspace helpers
- `zxFwPricingSync.ts`: syncs pricing store state to/from IndexedDB on demand
### Views (`src/components/views/`)
Key views:
- `HomeEntryView.vue`: onboarding screen shown on first launch; dispatches `home-import-selected` DOM event on completion
- `ProjectWorkspaceView.vue` / `xmCard.vue`: project card workspace (项目卡片)
- `QuickCalcView.vue`: quick calculation mode
- `ZxFwView.vue`: 咨询服务 (consulting services) grid — the primary fee input view
- `Ht.vue` / `htCard.vue`: contract (合同) views
- `HtFeeMethodTypeLineView.vue`: per-service fee method line (rate/hourly/quantity-unit-price)
- `WorkContentGrid.vue`: AG Grid-based work content table
### AG Grid
AG Grid Enterprise is used throughout. Modules are registered once globally in `src/main.ts`. A license key is set there. Shared grid option defaults live in `src/lib/diyAgGridOptions.ts`.
### UI Components
Reka UI (headless component library) + Tailwind CSS v4 + `lucide-vue-next` icons + `@iconify/vue`. Shared UI primitives are in `src/components/ui/`. Tailwind is integrated as a Vite plugin (`@tailwindcss/vite`).
### Build
Vite 8 with rolldown. Output goes to `dist/` with `base: './'` (relative paths — important for local file:// deployment). Code-splitting separates `ag-grid`, `vue/pinia`, and `reka-ui` into distinct vendor chunks.
### Code Conventions
- All code comments are in Chinese.
- Composition API (`<script setup>`) everywhere.
- No Vue Router — use `useTabStore.openTab()` to navigate.
- Numeric calculations always go through `src/lib/decimal.ts` helpers to avoid floating-point errors.
- Storage keys follow patterns like `zxFW-{contractId}`, `tzGMF-{contractId}-{serviceId}`, `htExtraFee-{contractId}-{feeType}`.

0
XUQIU.md Normal file
View File

View File

@ -1,54 +0,0 @@
d3695c8 (HEAD -> main, origin/main, origin/HEAD) HEAD@{0}: reset: moving to HEAD^
9c11604 HEAD@{1}: checkout: moving from 9c11604ba744feb874018575a6a679700971e548 to main
9c11604 HEAD@{2}: checkout: moving from main to 9c11604ba744feb874018575a6a679700971e548
9c11604 HEAD@{3}: reset: moving to 9c11604ba744feb874018575a6a679700971e548
9c11604 HEAD@{4}: commit: 首页
d3695c8 (HEAD -> main, origin/main, origin/HEAD) HEAD@{5}: pull -f: Fast-forward
1c600e6 HEAD@{6}: commit: fix
f4f6e5c HEAD@{7}: commit: final
398fca9 HEAD@{8}: pull: Fast-forward
f4c768d HEAD@{9}: commit: fix
cd10760 HEAD@{10}: commit: 1
0f71fff HEAD@{11}: commit: fix
3d26b0b HEAD@{12}: commit: fix,去掉大部分indexdb的逻辑
9a045cf HEAD@{13}: commit: 大改使用pinia传值indexdb做持久化
3ad7bae HEAD@{14}: commit: 调整存储的逻辑
bbc8777 HEAD@{15}: commit: fix
5614e31 HEAD@{16}: commit: 修复bug
5bb6609 HEAD@{17}: commit: fix bug
1910f15 HEAD@{18}: pull: Fast-forward
2a2c0fe HEAD@{19}: commit: 1
f79e8e0 HEAD@{20}: commit: merge
ab310b4 HEAD@{21}: commit: 1
d1dda7f HEAD@{22}: pull: Fast-forward
8a15587 HEAD@{23}: reset: moving to HEAD
8a15587 HEAD@{24}: commit: 备份
fc26a87 HEAD@{25}: commit: 系数字段修改
21d3f03 HEAD@{26}: pull: Fast-forward
303f54b HEAD@{27}: commit: if
043e1fc HEAD@{28}: commit: fix
ad4e9cd HEAD@{29}: commit: fix someone
c482faa HEAD@{30}: commit: fix
626513b HEAD@{31}: commit: fix
d8f8b62 HEAD@{32}: commit: fix
75f293f HEAD@{33}: commit: '20260305修复bug'
53c1b2c HEAD@{34}: commit: 1
75d5066 HEAD@{35}: commit: 1
e4a2b53 HEAD@{36}: commit: 1
42fd6e4 HEAD@{37}: commit: 重构
33913c2 HEAD@{38}: commit: 1
62546bc HEAD@{39}: commit: 1
a10359f HEAD@{40}: commit: 优化
3950057 HEAD@{41}: commit: fix
757de9a HEAD@{42}: commit: 1
ea6a244 HEAD@{43}: commit: fix
13b03e0 HEAD@{44}: commit: 完成大部分
e97707a HEAD@{45}: commit: fix
9849801 HEAD@{46}: commit: fix all
badf131 HEAD@{47}: commit: fix
57a2029 HEAD@{48}: commit: fix 拖动流畅度
37f4a99 HEAD@{49}: commit: fix bug
1609f19 HEAD@{50}: commit: fix more
5734cfa HEAD@{51}: commit: fix more
f121aa2 HEAD@{52}: commit: fix all
6ba08da HEAD@{53}: clone: from https://git.zwgczx.com/zwgczx/JGJS2026.git

View File

@ -1,7 +1,30 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useTabStore } from '@/pinia/tab'
import HomeEntryView from '@/components/views/HomeEntryView.vue'
import Tab from '@/layout/tab.vue'
import { waitForHydration } from '@/pinia/Plugin/indexdb'
const tabStore = useTabStore()
const isReady = ref(false)
const showHomeEntry = computed(() => !tabStore.hasCompletedSetup)
const handleImportComplete = () => {
tabStore.hasCompletedSetup = true
}
onMounted(() => {
window.addEventListener('home-import-selected', handleImportComplete)
waitForHydration('tabs').then(() => {
isReady.value = true
})
})
</script>
<template>
<Tab />
<template v-if="isReady">
<HomeEntryView v-if="showHomeEntry" />
<Tab v-else />
</template>
</template>

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { computed } from 'vue'
import HtFeeMethodGrid from '@/components/common/HtFeeMethodGrid.vue'
import HtFeeMethodGrid from '@/components/shared/HtFeeMethodGrid.vue'
import { additionalWorkList } from '@/sql'
const props = defineProps<{

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, onActivated, onMounted, ref } from 'vue'
import { getServiceDictEntries, isIndustryEnabledByType, getIndustryTypeValue } from '@/sql'
import XmFactorGrid from '@/components/common/XmFactorGrid.vue'
import XmFactorGrid from '@/components/shared/XmFactorGrid.vue'
import { useKvStore } from '@/pinia/kv'
const props = defineProps<{

View File

@ -148,7 +148,7 @@ const saveForm = async (force = false) => {
}
const applyRateInput = () => {
const next = parseNumberOrNull(rateInput.value, { sanitize: true, precision: 3 })
const next = parseNumberOrNull(rateInput.value, { sanitize: true, precision: 2 })
rate.value = next
rateInput.value = next == null ? '' : String(next)
}
@ -209,8 +209,8 @@ onBeforeUnmount(() => {
</label>
<label class="space-y-1.5">
<div class="text-xs text-muted-foreground">费率可编辑三位小数</div>
<input v-model="rateInput" type="text" inputmode="decimal" placeholder="请输入费率,建议0.01 ~ 0.05"
<div class="text-xs text-muted-foreground">费率%</div>
<input v-model="rateInput" type="text" inputmode="decimal" placeholder="请输入费率,建议1 ~ 5"
class="h-9 w-full rounded-md border bg-background px-3 text-sm text-foreground outline-none focus:ring-2 focus:ring-primary/30"
@blur="applyRateInput" />
</label>

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, onActivated, onMounted, ref } from 'vue'
import { getMajorDictEntries, isMajorIdInIndustryScope } from '@/sql'
import XmFactorGrid from '@/components/common/XmFactorGrid.vue'
import XmFactorGrid from '@/components/shared/XmFactorGrid.vue'
import { useKvStore } from '@/pinia/kv'
interface XmBaseInfoState {

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { computed } from 'vue'
import HtFeeMethodGrid from '@/components/common/HtFeeMethodGrid.vue'
import HtFeeMethodGrid from '@/components/shared/HtFeeMethodGrid.vue'
import { reserveList } from '@/sql'
const props = defineProps<{

View File

@ -180,7 +180,14 @@ const scheduleRefreshContractBudget = () => {
// 2. TS categories
interface XmCategoryItem {
key: string;
key:
| 'info'
| 'consult-category-factor'
| 'major-factor'
| 'work-grid'
| 'contract'
| 'additional-work-fee'
| 'reserve-fee';
label: string;
component: Component; // Vue
}
@ -191,7 +198,7 @@ const htView = markRaw(
name: 'HtInfoWithProps',
setup() {
const AsyncHtInfo = defineAsyncComponent({
loader: () => import('@/components/views/htInfo.vue'),
loader: () => import('@/components/ht/htInfo.vue'),
onError: (err) => {
console.error('加载 htInfo 组件失败:', err);
}
@ -210,7 +217,7 @@ const zxfwView = markRaw(
name: 'ZxFwWithProps',
setup() {
const AsyncZxFw = defineAsyncComponent({
loader: () => import('@/components/views/zxFw.vue'),
loader: () => import('@/components/ht/zxFw.vue'),
onError: (err) => {
console.error('加载 zxFw 组件失败:', err);
}
@ -229,7 +236,7 @@ const consultCategoryFactorView = markRaw(
name: 'HtConsultCategoryFactorWithProps',
setup() {
const AsyncHtConsultCategoryFactor = defineAsyncComponent({
loader: () => import('@/components/views/HtConsultCategoryFactor.vue'),
loader: () => import('@/components/ht/HtConsultCategoryFactor.vue'),
onError: (err) => {
console.error('加载 HtConsultCategoryFactor 组件失败:', err);
}
@ -248,7 +255,7 @@ const majorFactorView = markRaw(
name: 'HtMajorFactorWithProps',
setup() {
const AsyncHtMajorFactor = defineAsyncComponent({
loader: () => import('@/components/views/HtMajorFactor.vue'),
loader: () => import('@/components/ht/HtMajorFactor.vue'),
onError: (err) => {
console.error('加载 HtMajorFactor 组件失败:', err);
}
@ -262,12 +269,14 @@ const majorFactorView = markRaw(
})
);
const additionalWorkFeeView = markRaw(
defineComponent({
name: 'HtAdditionalWorkFeeWithProps',
setup() {
const AsyncHtAdditionalWorkFee = defineAsyncComponent({
loader: () => import('@/components/views/HtAdditionalWorkFee.vue'),
loader: () => import('@/components/ht/HtAdditionalWorkFee.vue'),
onError: (err) => {
console.error('加载 HtAdditionalWorkFee 组件失败:', err);
}
@ -282,7 +291,7 @@ const reserveFeeView = markRaw(
name: 'HtReserveFeeWithProps',
setup() {
const AsyncHtReserveFee = defineAsyncComponent({
loader: () => import('@/components/views/HtReserveFee.vue'),
loader: () => import('@/components/ht/HtReserveFee.vue'),
onError: (err) => {
console.error('加载 HtReserveFee 组件失败:', err);
}
@ -297,11 +306,11 @@ const xmCategories: XmCategoryItem[] = [
{ key: 'info', label: '规模信息', component: htView },
{ key: 'consult-category-factor', label: '咨询分类系数', component: consultCategoryFactorView },
{ key: 'major-factor', label: '工程专业系数', component: majorFactorView },
{ key: 'contract', label: '咨询服务', component: zxfwView },
{ key: 'contract', label: '咨询服务', component: zxfwView },
{ key: 'additional-work-fee', label: '附加工作费', component: additionalWorkFeeView },
{ key: 'reserve-fee', label: '预备费', component: reserveFeeView },
];
watch(budgetRefreshSignature, (next, prev) => {

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { computed } from 'vue'
import CommonAgGrid from '@/components/common/xmCommonAgGrid.vue'
import CommonAgGrid from '@/components/shared/xmCommonAgGrid.vue'
const props = defineProps<{

View File

@ -32,7 +32,7 @@ import { getServiceDictEntries, isIndustryEnabledByType, getIndustryTypeValue }
import { useTabStore } from '@/pinia/tab'
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
import { useKvStore } from '@/pinia/kv'
import ServiceCheckboxSelector from '@/components/views/ServiceCheckboxSelector.vue'
import ServiceCheckboxSelector from '@/components/shared/ServiceCheckboxSelector.vue'
interface ServiceItem {
id: string

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { computed } from 'vue'
import HourlyFeeGrid from '@/components/common/HourlyFeeGrid.vue'
import HourlyFeeGrid from '@/components/shared/HourlyFeeGrid.vue'
const props = defineProps<{
contractId: string

View File

@ -10,7 +10,7 @@ import { parseNumberOrNull } from '@/lib/number'
import { syncPricingTotalToZxFw } from '@/lib/zxFwPricingSync'
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
import { loadConsultCategoryFactorMap } from '@/lib/xmFactorDefaults'
import MethodUnavailableNotice from '@/components/common/MethodUnavailableNotice.vue'
import MethodUnavailableNotice from '@/components/shared/MethodUnavailableNotice.vue'
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale';

View File

@ -0,0 +1,320 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { AgGridVue } from 'ag-grid-vue3'
import type {
CellValueChangedEvent,
ColDef,
GridApi,
GridReadyEvent,
ICellRendererParams,
ValueFormatterParams
} from 'ag-grid-community'
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
import { Button } from '@/components/ui/button'
import { myTheme } from '@/lib/diyAgGridOptions'
import { workList } from '@/sql'
import type { WorkType } from '@/sql'
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
interface WorkContentRow {
id: string
content: string
type: WorkType
remark: string
checked: boolean
custom: boolean
path: string[]
}
interface WorkContentState {
detailRows: WorkContentRow[]
}
// dictMode: 'service' serviceIdworkList'additional' serviceid=-1'none'
const props = withDefaults(defineProps<{
title?: string
storageKey: string
serviceId?: number | string
dictMode?: 'service' | 'additional' | 'none'
}>(), {
title: '工作内容',
dictMode: 'none'
})
const emit = defineEmits<{
checkedChange: [value: string[]]
}>()
const zxFwPricingStore = useZxFwPricingStore()
const gridApi = ref<GridApi<WorkContentRow> | null>(null)
const rowData = ref<WorkContentRow[]>([])
const TYPE_LABEL_MAP: Record<number, WorkType> = {
0: '基本工作',
1: '可选工作',
2: '日常顾问',
3: '专项顾问',
4: '附加工作'
}
const buildDefaultRowsFromDict = (): WorkContentRow[] => {
const rows: WorkContentRow[] = []
const entries = Object.values(workList) as Array<{ text: string; serviceid: number; order: number; type: number }>
let filtered: typeof entries
if (props.dictMode === 'service') {
const sid = Number(props.serviceId)
filtered = entries.filter(e => e.serviceid === sid)
} else if (props.dictMode === 'additional') {
filtered = entries.filter(e => e.serviceid === -1)
} else {
return []
}
filtered.sort((a, b) => a.order - b.order)
for (const entry of filtered) {
const content = String(entry.text || '').trim()
if (!content) continue
const typeLabel = TYPE_LABEL_MAP[entry.type] ?? '基本工作'
rows.push({
id: `dict-${entry.order}`,
content,
type: typeLabel,
remark: '',
checked: false,
custom: false,
path: [typeLabel, content]
})
}
return rows
}
const checkedIds = computed(() =>
rowData.value.filter(item => item.checked).map(item => item.id)
)
//
const selectedTexts = computed(() =>
rowData.value
.filter(item => item.custom || item.checked)
.map(item => item.content)
.filter(Boolean)
)
defineExpose({ selectedTexts })
const emitCheckedChange = () => {
emit('checkedChange', [...checkedIds.value])
}
const saveToStore = () => {
const payload: WorkContentState = {
detailRows: rowData.value.map(item => ({ ...item }))
}
zxFwPricingStore.setKeyState(props.storageKey, payload)
emitCheckedChange()
}
const loadFromStore = async () => {
const state = await zxFwPricingStore.loadKeyState<WorkContentState>(props.storageKey)
if (Array.isArray(state?.detailRows) && state.detailRows.length > 0) {
rowData.value = state.detailRows.map(item => ({
...item,
type: item.custom ? '自定义' : (item.type || '基本工作'),
path: Array.isArray(item.path) && item.path.length ? item.path : ['自定义', item.content || '未命名']
})) as WorkContentRow[]
} else {
rowData.value = buildDefaultRowsFromDict()
saveToStore()
}
emitCheckedChange()
}
const handleCheckedToggle = (id: string, checked: boolean) => {
const target = rowData.value.find(item => item.id === id)
if (!target) return
target.checked = checked
gridApi.value?.refreshCells({ force: true })
saveToStore()
}
const contentCellRenderer = (params: ICellRendererParams<WorkContentRow>) => {
const data = params.data
if (!data) return ''
const wrapper = document.createElement('div')
wrapper.className = 'work-content-cell'
// checkbox placeholder
if (data.custom) {
const label = document.createElement('span')
if (!data.content) {
label.className = 'work-content-placeholder'
label.textContent = '点击输入工作内容'
} else {
label.className = 'work-content-text'
label.textContent = data.content
}
wrapper.appendChild(label)
return wrapper
}
const checkbox = document.createElement('input')
checkbox.type = 'checkbox'
checkbox.className = 'work-content-check'
checkbox.checked = Boolean(data.checked)
checkbox.addEventListener('change', () => {
handleCheckedToggle(data.id, checkbox.checked)
})
const label = document.createElement('span')
label.className = 'work-content-text'
label.textContent = String(data.content || '')
wrapper.appendChild(checkbox)
wrapper.appendChild(label)
return wrapper
}
const columnDefs: ColDef<WorkContentRow>[] = [
{
headerName: '序号',
minWidth: 60,
width: 70,
pinned: 'left',
suppressMovable: true,
editable: false,
valueGetter: params => (params.node?.rowIndex ?? 0) + 1
},
{
headerName: '工作内容',
field: 'content',
minWidth: 320,
flex: 2,
editable: params => Boolean(params.data?.custom),
cellClass: params => (params.data?.custom ? 'editable-cell-line' : ''),
valueParser: params => String(params.newValue || '').trim(),
cellRenderer: contentCellRenderer
},
{
headerName: '工作类型',
field: 'type',
minWidth: 100,
width: 120,
editable: false,
valueFormatter: (params: ValueFormatterParams<WorkContentRow>) => String(params.value || '')
},
{
headerName: '备注',
field: 'remark',
minWidth: 180,
flex: 1.2,
editable: true,
cellEditor: 'agLargeTextCellEditor',
wrapText: true,
autoHeight: true,
cellStyle: { whiteSpace: 'normal', lineHeight: '1.4' },
cellClass: 'editable-cell-line remark-wrap-cell',
cellClassRules: {
'editable-cell-empty': params => params.value == null || params.value === ''
},
valueFormatter: params => params.value || '点击输入'
}
]
const addCustomRow = () => {
const ts = Date.now()
rowData.value.push({
id: `custom-${ts}`,
content: '',
type: '自定义' as WorkType,
remark: '',
checked: false,
custom: true,
path: ['自定义', `自定义-${ts}`]
})
saveToStore()
}
const onGridReady = (event: GridReadyEvent<WorkContentRow>) => {
gridApi.value = event.api
}
const onCellValueChanged = (event: CellValueChangedEvent<WorkContentRow>) => {
const row = event.data
if (!row) return
if (event.colDef.field === 'content' && row.custom) {
row.path = ['自定义', row.content || `自定义-${row.id}`]
}
if (event.colDef.field === 'type' && row.custom) {
row.type = '自定义'
}
saveToStore()
}
onMounted(() => {
void loadFromStore()
})
onBeforeUnmount(() => {
saveToStore()
})
</script>
<template>
<div class="h-full min-h-0 xmMx">
<div class="h-full min-h-0 rounded-2xl border border-border/60 bg-card/90 shadow-sm backdrop-blur-sm">
<div class="flex items-center justify-between border-b border-border/60 px-4 py-3">
<h3 class="text-sm font-semibold text-foreground">{{ props.title }}</h3>
<Button type="button" size="sm" variant="outline" class="cursor-pointer" @click="addCustomRow">
添加自定义内容
</Button>
</div>
<div class="ag-theme-quartz h-[calc(100%-56px)] min-h-0 w-full">
<AgGridVue
:style="{ height: '100%' }"
:rowData="rowData"
:columnDefs="columnDefs"
:theme="myTheme"
:getRowId="(params: { data: WorkContentRow }) => params.data.id"
:animateRows="true"
:localeText="AG_GRID_LOCALE_CN"
:tooltipShowDelay="500"
:singleClickEdit="true"
:stopEditingWhenCellsLoseFocus="true"
:defaultColDef="{ resizable: true, sortable: false, filter: false }"
:suppressColumnVirtualisation="false"
:suppressRowVirtualisation="false"
@grid-ready="onGridReady"
@cell-value-changed="onCellValueChanged"
/>
</div>
</div>
</div>
</template>
<style scoped>
:deep(.work-content-placeholder) {
color: #94a3b8;
font-style: italic;
min-width: 0;
flex: 1;
}
:deep(.work-content-cell) {
display: flex;
width: 100%;
align-items: center;
justify-content: space-between;
gap: 8px;
}
:deep(.work-content-text) {
min-width: 0;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
:deep(.work-content-check) {
width: 14px;
height: 14px;
cursor: pointer;
}
</style>

View File

@ -4,7 +4,15 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
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 {
Calculator,
Check,
ChevronDown,
Download,
FolderKanban,
X,
Zap
} from 'lucide-vue-next'
import { industryTypeList } from '@/sql'
import { initializeProjectFactorStates } from '@/lib/projectWorkspace'
import {
@ -57,7 +65,7 @@ interface ProjectInfoState {
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 DEFAULT_PROJECT_NAME = 'xxx 造价咨询服务'
const PROJECT_INIT_CHANGED_EVENT = 'xm-project-init-changed'
const tabStore = useTabStore()
@ -69,6 +77,10 @@ const quickDialogOpen = ref(false)
const quickIndustry = ref(String(industryTypeList[0]?.id || ''))
const quickContractName = ref(QUICK_CONTRACT_FALLBACK_NAME)
const quickSubmitting = ref(false)
const homeImportInputRef = ref<HTMLInputElement | null>(null)
const projectIconAvailable = ref(false)
const quickIconAvailable = ref(false)
const importIconAvailable = ref(false)
const getTodayDateString = () => {
const now = new Date()
@ -83,8 +95,9 @@ const enterProjectCalc = () => {
tabStore.enterWorkspace({
id: PROJECT_TAB_ID,
title: '项目计算',
componentName: PROJECT_TAB_ID
componentName: 'XmView'
})
tabStore.hasCompletedSetup = true
}
const loadProjectDefaults = async () => {
@ -187,7 +200,6 @@ const confirmQuickCalc = async () => {
QUICK_MAJOR_FACTOR_KEY
)
writeWorkspaceMode('quick')
tabStore.enterWorkspace({
id: `contract-${QUICK_CONTRACT_ID}`,
title: contractName,
@ -201,6 +213,7 @@ const confirmQuickCalc = async () => {
projectMajorFactorKey: QUICK_MAJOR_FACTOR_KEY
}
})
tabStore.hasCompletedSetup = true
} finally {
quickSubmitting.value = false
quickDialogOpen.value = false
@ -219,6 +232,10 @@ const handleHomeImportChange = (event: Event) => {
input.value = ''
}
const openHomeImport = () => {
homeImportInputRef.value?.click()
}
onMounted(() => {
void loadProjectDefaults()
void loadQuickDefaults()
@ -226,104 +243,133 @@ onMounted(() => {
</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"
<input ref="homeImportInputRef" type="file" accept=".zw" class="sr-only" @change="handleHomeImportChange" />
<div class="flex min-h-full items-center justify-center px-4 py-8 lg:py-10">
<div class="w-full max-w-[1240px]">
<div class="grid items-stretch gap-6 lg:grid-cols-[320px_1fr]">
<div
class="relative overflow-hidden rounded-2xl bg-[linear-gradient(145deg,#041c5f_0%,#0b2e86_50%,#354cc7_100%)] p-8 text-white shadow-[0_24px_60px_rgba(11,46,134,0.45)]"
>
<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>
<div class="pointer-events-none absolute -right-20 -top-16 h-56 w-56 rounded-full bg-white/12 blur-2xl" />
<div
class="pointer-events-none absolute inset-0 bg-[repeating-linear-gradient(140deg,rgba(255,255,255,0.06)_0,rgba(255,255,255,0.06)_1px,transparent_1px,transparent_20px)]"
/>
<div class="relative inline-flex h-12 w-12 items-center justify-center rounded-xl bg-white/15 ring-1 ring-white/35">
<Calculator class="h-6 w-6" />
</div>
<h2 class="relative mt-10 text-3xl font-semibold leading-tight tracking-tight">智能预算一键生成-></h2>
<p class="relative mt-3 text-base text-blue-100">助力规范高效落地</p>
</div>
<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>
<div class="flex flex-col justify-center">
<div class="text-center">
<h1 class="text-3xl font-semibold tracking-tight text-slate-900">计算入口</h1>
<p class="mt-2 text-sm text-slate-600">支持三种计算方式项目计算 · 单项速算 · 导入数据</p>
</div>
<div class="mt-6 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
<Card
role="button"
tabindex="0"
class="group flex cursor-pointer flex-col rounded-2xl border border-slate-200/80 bg-white/95 p-5 shadow-[0_10px_30px_rgba(15,23,42,0.08)] backdrop-blur-sm transition-all duration-200 hover:-translate-y-1 hover:shadow-[0_20px_45px_rgba(15,23,42,0.16)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-200"
@click="openProjectCalc"
@keydown.enter.prevent="openProjectCalc"
@keydown.space.prevent="openProjectCalc"
>
<CardHeader class="space-y-3 p-0">
<div>
<CardTitle class="text-2xl font-semibold text-slate-900">项目预算</CardTitle>
<CardDescription class="mt-2 text-sm leading-6 text-slate-500">
适用多合同段项目级整体计算
<br>
操作支持当前项目及合同段卡片和流程线可导出/导入完整项目数据
</CardDescription>
</div>
</CardHeader>
<CardContent class="mt-auto flex flex-col items-center p-0 pt-8">
<div
class="mt-4 inline-flex h-16 w-16 items-center justify-center rounded-full border border-blue-100 bg-[radial-gradient(circle_at_30%_30%,#ffffff,#eef4ff)] shadow-[0_10px_24px_rgba(27,76,176,0.18)]"
>
<img
v-if="projectIconAvailable"
:src="'/image_0.png'"
alt="项目预算"
class="h-8 w-8 object-contain"
@error="projectIconAvailable = false"
>
<FolderKanban v-else class="h-7 w-7 text-blue-700" />
</div>
</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>
<Card
role="button"
tabindex="0"
class="group flex cursor-pointer flex-col rounded-2xl border border-slate-200/80 bg-white/95 p-5 shadow-[0_10px_30px_rgba(15,23,42,0.08)] backdrop-blur-sm transition-all duration-200 hover:-translate-y-1 hover:shadow-[0_20px_45px_rgba(15,23,42,0.16)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-200"
@click="openQuickCalcDialog"
@keydown.enter.prevent="openQuickCalcDialog"
@keydown.space.prevent="openQuickCalcDialog"
>
<CardHeader class="space-y-3 p-0">
<div>
<CardTitle class="text-2xl font-semibold text-slate-900">单项速算</CardTitle>
<CardDescription class="mt-2 text-sm leading-6 text-slate-500">
适用单合同段预算单项试算
<br>
操作选择行业与咨询类型输入基数秒出结果
</CardDescription>
</div>
</CardHeader>
<CardContent class="mt-auto flex flex-col items-center p-0 pt-8">
<div
class="mt-4 inline-flex h-16 w-16 items-center justify-center rounded-full border border-blue-100 bg-[radial-gradient(circle_at_30%_30%,#ffffff,#eef4ff)] shadow-[0_10px_24px_rgba(27,76,176,0.18)]"
>
<img
v-if="quickIconAvailable"
:src="'/image_1.png'"
alt="单项速算"
class="h-8 w-8 object-contain"
@error="quickIconAvailable = false"
>
<Zap v-else class="h-7 w-7 text-amber-500" />
</div>
</CardContent>
</Card>
<Card
role="button"
tabindex="0"
class="group flex cursor-pointer flex-col rounded-2xl border border-slate-200/80 bg-white/95 p-5 shadow-[0_10px_30px_rgba(15,23,42,0.08)] backdrop-blur-sm transition-all duration-200 hover:-translate-y-1 hover:shadow-[0_20px_45px_rgba(15,23,42,0.16)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-200"
@click="openHomeImport"
@keydown.enter.prevent="openHomeImport"
@keydown.space.prevent="openHomeImport"
>
<CardHeader class="space-y-3 p-0">
<div>
<CardTitle class="text-2xl font-semibold text-slate-900">导入数据</CardTitle>
<CardDescription class="mt-2 text-sm leading-6 text-slate-500">
适用续未完成工作跨设备恢复数据
<br>
操作导入".zw"数据包快速恢复项目计算状态
</CardDescription>
</div>
</CardHeader>
<CardContent class="mt-auto flex flex-col items-center p-0 pt-8">
<div
class="mt-4 inline-flex h-16 w-16 items-center justify-center rounded-full border border-blue-100 bg-[radial-gradient(circle_at_30%_30%,#ffffff,#eef4ff)] shadow-[0_10px_24px_rgba(27,76,176,0.18)]"
>
<img
v-if="importIconAvailable"
:src="'/image_2.png'"
alt="导入数据"
class="h-8 w-8 object-contain"
@error="importIconAvailable = false"
>
<Download v-else class="h-7 w-7 text-cyan-600" />
</div>
</CardContent>
</Card>
</div>
</div>
</div>
</div>
</div>
@ -398,8 +444,8 @@ onMounted(() => {
<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>
<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" />
@ -447,18 +493,16 @@ onMounted(() => {
<input
v-model="quickContractName"
type="text"
maxlength="40"
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 placeholder:text-slate-400 hover:border-border focus-visible:ring-2 focus-visible:ring-slate-300"
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 :disabled="quickSubmitting || !quickContractName" @click="confirmQuickCalc">
{{ quickSubmitting ? '进入中...' : '进入计算' }}
</Button>
</div>
</div>

View File

@ -5,17 +5,17 @@
:subtitle="`合同ID${contractIdText}`"
:copy-text="contractIdText"
:storage-key="activeTypeStorageKey"
default-category="rate-fee"
default-category="work-content"
:categories="categories"
/>
</template>
<script setup lang="ts">
import { computed, defineComponent, h, markRaw, type Component } from 'vue'
import { computed, defineComponent, h, markRaw, defineAsyncComponent, type Component } from 'vue'
import TypeLine from '@/layout/typeLine.vue'
import HtFeeGrid from '@/components/common/HtFeeGrid.vue'
import HtFeeRateMethodForm from '@/components/views/HtFeeRateMethodForm.vue'
import HourlyFeeGrid from '@/components/common/HourlyFeeGrid.vue'
import HtFeeGrid from '@/components/shared/HtFeeGrid.vue'
import HtFeeRateMethodForm from '@/components/ht/HtFeeRateMethodForm.vue'
import HourlyFeeGrid from '@/components/shared/HourlyFeeGrid.vue'
interface TypeLineCategoryItem {
key: string
@ -92,9 +92,43 @@ const hourlyFeePane = markRaw(
})
)
const categories: TypeLineCategoryItem[] = [
{ key: 'rate-fee', label: '费率计取', component: rateFeePane },
{ key: 'hourly-fee', label: '工时法', component: hourlyFeePane },
{ key: 'quantity-unit-price-fee', label: '数量单价', component: quantityUnitPricePane }
]
const isAdditionalWork = computed(() => props.sourceTitle === '附加工作费')
const isReserveFee = computed(() => props.sourceTitle === '预备费')
const showWorkContent = computed(() => {
if (isReserveFee.value) return false
if (isAdditionalWork.value) return props.rowName === '咨询服务协调工作'
return true
})
const workContentPane = markRaw(
defineComponent({
name: 'WorkContentPane',
setup() {
const AsyncWorkContentGrid = defineAsyncComponent({
loader: () => import('@/components/shared/WorkContentGrid.vue'),
onError: err => {
console.error('加载 WorkContentGrid 组件失败:', err)
}
})
return () => h(AsyncWorkContentGrid, {
title: '工作内容',
storageKey: `work-content-${props.storageKey}-${props.rowId}`,
dictMode: isAdditionalWork.value ? 'additional' : 'none'
})
}
})
)
const categories = computed<TypeLineCategoryItem[]>(() => {
const base: TypeLineCategoryItem[] = [
{ key: 'rate-fee', label: '费率计取', component: rateFeePane },
{ key: 'hourly-fee', label: '工时法', component: hourlyFeePane },
{ key: 'quantity-unit-price-fee', label: '数量单价', component: quantityUnitPricePane },
]
if (showWorkContent.value) {
base.push({ key: 'work-content', label: '工作内容', component: workContentPane })
}
return base
})
</script>

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { onActivated, onMounted } from 'vue'
import XmCard from '@/components/views/xmCard.vue'
import XmCard from '@/components/xm/xmCard.vue'
import { writeWorkspaceMode } from '@/lib/workspace'
onMounted(() => {

View File

@ -13,7 +13,7 @@
<script setup lang="ts">
import { computed, defineAsyncComponent, defineComponent, h, markRaw, type Component } from 'vue'
import TypeLine from '@/layout/typeLine.vue'
import MethodUnavailableNotice from '@/components/common/MethodUnavailableNotice.vue'
import MethodUnavailableNotice from '@/components/shared/MethodUnavailableNotice.vue'
interface ServiceMethodType {
scale?: boolean | null
@ -59,7 +59,7 @@ const createPricingPane = (name: string) =>
name,
setup() {
const AsyncPricingView = defineAsyncComponent({
loader: () => import(`@/components/views/pricingView/${name}.vue`),
loader: () => import(`@/components/pricing/${name}.vue`),
onError: err => {
console.error('加载 PricingMethodView 组件失败:', err)
}
@ -89,6 +89,27 @@ const landScaleView = createPricingPane('LandScalePricingPane')
const workloadView = createPricingPane('WorkloadPricingPane')
const hourlyView = createPricingPane('HourlyPricingPane')
const workContentPane = markRaw(
defineComponent({
name: 'WorkContentPane',
setup() {
const AsyncWorkContentGrid = defineAsyncComponent({
loader: () => import('@/components/shared/WorkContentGrid.vue'),
onError: err => {
console.error('加载 WorkContentGrid 组件失败:', err)
}
})
return () => h(AsyncWorkContentGrid, {
title: '工作内容',
storageKey: `work-content-${props.contractId}-${props.serviceId}`,
serviceId: props.serviceId,
dictMode: 'service'
})
}
})
)
const investmentScaleUnavailableView = createMethodUnavailablePane(
'该服务不适用投资规模法',
'当前服务未启用规模法,投资规模法不可编辑。'
@ -107,6 +128,7 @@ const hourlyUnavailableView = createMethodUnavailablePane(
)
const pricingCategories = computed<PricingCategoryItem[]>(() => [
{
key: 'investment-scale-method',
label: '投资规模法',
@ -126,14 +148,15 @@ const pricingCategories = computed<PricingCategoryItem[]>(() => [
key: 'hourly-method',
label: '工时法',
component: methodAvailability.value.hourly ? hourlyView : hourlyUnavailableView
}
},
{
key: 'work-content',
label: '工作内容',
component: workContentPane
},
])
const defaultCategory = computed(() => {
if (methodAvailability.value.investmentScale) return 'investment-scale-method'
if (methodAvailability.value.landScale) return 'land-scale-method'
if (methodAvailability.value.workload) return 'workload-method'
if (methodAvailability.value.hourly) return 'hourly-method'
return 'investment-scale-method'
return 'work-content'
})
</script>

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, onActivated, onMounted, ref } from 'vue'
import { getServiceDictEntries, isIndustryEnabledByType, getIndustryTypeValue } from '@/sql'
import XmFactorGrid from '@/components/common/XmFactorGrid.vue'
import XmFactorGrid from '@/components/shared/XmFactorGrid.vue'
import { useKvStore } from '@/pinia/kv'
interface XmBaseInfoState {

View File

@ -1,8 +1,8 @@
<script setup lang="ts">
import { computed, onActivated, onMounted, ref } from 'vue'
import { getMajorDictEntries, isMajorIdInIndustryScope } from '@/sql'
import XmFactorGrid from '@/components/common/XmFactorGrid.vue'
import MethodUnavailableNotice from '@/components/common/MethodUnavailableNotice.vue'
import XmFactorGrid from '@/components/shared/XmFactorGrid.vue'
import MethodUnavailableNotice from '@/components/shared/MethodUnavailableNotice.vue'
import { useKvStore } from '@/pinia/kv'
interface XmBaseInfoState {

View File

@ -37,12 +37,15 @@ interface XmInfoState {
reviewedBy?: string
preparedCompany?: string
preparedDate?: string
overview?: string
desc?: string
}
type MajorParentNode = { id: string; name: string }
const DB_KEY = 'xm-base-info-v1'
const DEFAULT_PROJECT_NAME = 'xxx造价咨询服务'
const DEFAULT_DESC = '在履行造价咨询服务时宜根据咨询服务质量情况分级确定相应的处罚金额。其中考评得分在大于及等于85和小于90分时处罚金额为预算费用的10%其中考评得分在大于及等于80和小于85分时处罚金额为预算费用的20%其中考评得分在大于及等于75和小于80分时处罚金额为预算费用的30%其中考评得分在大于及等于70和小于75分时处罚金额为预算费用的40%其中考评得分小于70分时处罚金额为预算费用的50%以上。'
const INDUSTRY_HINT_TEXT = '变更需要重置后重新选择'
const getTodayDateString = () => {
const now = new Date()
@ -61,6 +64,8 @@ const reviewedBy = ref('')
const preparedCompany = ref('')
const preparedDate = ref(getTodayDateString())
const preparedDatePickerValue = ref<any>(undefined)
const overview = ref('')
const desc = ref(DEFAULT_DESC)
const normalizeDateString = (value: unknown): string => {
if (typeof value !== 'string') return ''
@ -108,7 +113,9 @@ const saveToIndexedDB = async () => {
preparedBy: preparedBy.value,
reviewedBy: reviewedBy.value,
preparedCompany: preparedCompany.value,
preparedDate: preparedDate.value
preparedDate: preparedDate.value,
overview: overview.value,
desc: desc.value
}
await kvStore.setItem(DB_KEY, payload)
} catch (error) {
@ -131,6 +138,8 @@ const loadFromIndexedDB = async () => {
reviewedBy.value = typeof data.reviewedBy === 'string' ? data.reviewedBy : ''
preparedCompany.value = typeof data.preparedCompany === 'string' ? data.preparedCompany : ''
preparedDate.value = normalizeDateString(data.preparedDate) || getTodayDateString()
overview.value = typeof data.overview === 'string' ? data.overview : ''
desc.value = typeof data.desc === 'string' && data.desc.trim() ? data.desc : DEFAULT_DESC
syncPreparedDatePickerFromString()
return
}
@ -142,6 +151,8 @@ const loadFromIndexedDB = async () => {
reviewedBy.value = ''
preparedCompany.value = ''
preparedDate.value = getTodayDateString()
overview.value = ''
desc.value = DEFAULT_DESC
syncPreparedDatePickerFromString()
} catch (error) {
console.error('loadFromIndexedDB failed:', error)
@ -151,6 +162,8 @@ const loadFromIndexedDB = async () => {
reviewedBy.value = ''
preparedCompany.value = ''
preparedDate.value = getTodayDateString()
overview.value = ''
desc.value = DEFAULT_DESC
syncPreparedDatePickerFromString()
}
}
@ -171,7 +184,7 @@ const handleProjectNameBlur = () => {
}
watch(
[projectIndustry, projectName, preparedBy, reviewedBy, preparedCompany, preparedDate],
[projectIndustry, projectName, preparedBy, reviewedBy, preparedCompany, preparedDate, overview, desc],
schedulePersist
)
@ -378,6 +391,24 @@ onMounted(async () => {
</DatePickerContent>
</DatePickerRoot>
</div>
<div class="md:col-span-2 xl:col-span-4">
<label class="block text-sm font-medium text-foreground">项目概况</label>
<textarea
v-model="overview"
rows="3"
placeholder="请输入项目概况"
class="mt-2 w-full rounded-lg border bg-background px-4 py-2 text-sm outline-none ring-offset-background shadow-sm transition placeholder:text-muted-foreground/70 focus-visible:border-primary/60 focus-visible:ring-2 focus-visible:ring-ring resize-none"
/>
</div>
<div class="md:col-span-2 xl:col-span-4">
<label class="block text-sm font-medium text-foreground">备注</label>
<textarea
v-model="desc"
rows="4"
class="mt-2 w-full rounded-lg border bg-background px-4 py-2 text-sm outline-none ring-offset-background shadow-sm transition placeholder:text-muted-foreground/70 focus-visible:border-primary/60 focus-visible:ring-2 focus-visible:ring-ring resize-none"
/>
</div>
</div>
</div>
</div>

View File

@ -3,7 +3,7 @@
scene="xm-tab"
title=""
storage-key="project-active-cat"
default-category="scale-info"
default-category="info"
:categories="xmCategories"
/>
</template>
@ -11,16 +11,18 @@
<script setup lang="ts">
import { defineAsyncComponent, markRaw } from 'vue'
import TypeLine from '@/layout/typeLine.vue'
const scaleInfoView = markRaw(defineAsyncComponent(() => import('@/components/views/xmInfo.vue')))
const htView = markRaw(defineAsyncComponent(() => import('@/components/views/Ht.vue')))
const infoView = markRaw(defineAsyncComponent(() => import('@/components/xm/info.vue')))
const scaleInfoView = markRaw(defineAsyncComponent(() => import('@/components/xm/xmInfo.vue')))
const htView = markRaw(defineAsyncComponent(() => import('@/components/ht/Ht.vue')))
const consultCategoryFactorView = markRaw(
defineAsyncComponent(() => import('@/components/views/XmConsultCategoryFactor.vue'))
defineAsyncComponent(() => import('@/components/xm/XmConsultCategoryFactor.vue'))
)
const majorFactorView = markRaw(
defineAsyncComponent(() => import('@/components/views/XmMajorFactor.vue'))
defineAsyncComponent(() => import('@/components/xm/XmMajorFactor.vue'))
)
const xmCategories = [
{ key: 'info', label: '基础信息', component: infoView },
{ key: 'scale-info', label: '规模信息', component: scaleInfoView },
{ key: 'consult-category-factor', label: '咨询分类系数', component: consultCategoryFactorView },
{ key: 'major-factor', label: '工程专业系数', component: majorFactorView },

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import CommonAgGrid from '@/components/common/xmCommonAgGrid.vue'
import CommonAgGrid from '@/components/shared/xmCommonAgGrid.vue'
const DB_KEY = 'xm-info-v3'

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,5 @@
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'
@ -37,6 +36,13 @@ export const readWorkspaceMode = (): WorkspaceMode => {
}
}
export const createDefaultQuickContractMeta = (): QuickContractMeta => ({
id: QUICK_CONTRACT_ID,
name: QUICK_CONTRACT_FALLBACK_NAME,
updatedAt: new Date().toISOString()
})
export const writeWorkspaceMode = (mode: WorkspaceMode) => {
try {
window.localStorage.setItem(WORKSPACE_MODE_STORAGE_KEY, normalizeWorkspaceMode(mode))
@ -44,9 +50,3 @@ export const writeWorkspaceMode = (mode: WorkspaceMode) => {
// 忽略只读或隐私模式下的写入失败。
}
}
export const createDefaultQuickContractMeta = (): QuickContractMeta => ({
id: QUICK_CONTRACT_ID,
name: QUICK_CONTRACT_FALLBACK_NAME,
updatedAt: new Date().toISOString()
})

View File

@ -63,6 +63,18 @@ const trimStringValuesDeep = (value: unknown): unknown => {
return value
}
const hydratePromises = new Map<string, Promise<void>>()
export const waitForHydration = (storeId: string): Promise<void> => {
const existing = hydratePromises.get(storeId)
if (existing) return existing
// store 尚未注册,返回一个会在注册后 resolve 的 Promise
let resolve!: () => void
const p = new Promise<void>(r => { resolve = r })
hydratePromises.set(storeId, p)
return p
}
export default (config?: PiniaStorageConfig) => {
const finalConfig = { ...baseConfig, ...(config || {}) }
const { mode = 'single', debounce = 300, ...forageConfig } = finalConfig
@ -123,6 +135,11 @@ export default (config?: PiniaStorageConfig) => {
{ detached: true }
)
// 注册或获取当前 store 的水合 Promise
let resolveHydration!: () => void
const hydrateP = new Promise<void>(r => { resolveHydration = r })
hydratePromises.set(storeId, hydrateP)
hydrating = true
void lf.getItem<Record<string, unknown>>(key)
.then(state => {
@ -136,6 +153,7 @@ export default (config?: PiniaStorageConfig) => {
})
.finally(() => {
hydrating = false
resolveHydration()
})
}
}

View File

@ -1,6 +1,6 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { HOME_TAB_ID, PROJECT_TAB_ID, QUICK_CONTRACT_TAB_ID } from '@/lib/workspace'
import { PROJECT_TAB_ID, QUICK_CONTRACT_TAB_ID } from '@/lib/workspace'
export interface TabItem<TProps = Record<string, unknown>> {
id: string
@ -10,22 +10,23 @@ export interface TabItem<TProps = Record<string, unknown>> {
}
const DEFAULT_TAB: TabItem = {
id: HOME_TAB_ID,
title: '首页',
componentName: HOME_TAB_ID
id: PROJECT_TAB_ID,
title: '项目卡片',
componentName: 'XmView'
}
const createDefaultTabs = (): TabItem[] => [{ ...DEFAULT_TAB }]
const PROTECTED_TAB_ID_SET = new Set<string>([HOME_TAB_ID, QUICK_CONTRACT_TAB_ID])
const createDefaultTabs = (): TabItem[] => [{...DEFAULT_TAB}]
const PROTECTED_TAB_ID_SET = new Set<string>([ PROJECT_TAB_ID,QUICK_CONTRACT_TAB_ID])
export const useTabStore = defineStore(
'tabs',
() => {
const tabs = ref<TabItem[]>(createDefaultTabs())
const activeTabId = ref(HOME_TAB_ID)
const activeTabId = ref()
const hasCompletedSetup = ref(false)
const ensureHomeTab = () => {
if (tabs.value.some(tab => tab.id === HOME_TAB_ID)) return
if (tabs.value.some(tab => tab.id === PROJECT_TAB_ID)) return
tabs.value = [...createDefaultTabs(), ...tabs.value]
}
@ -33,7 +34,7 @@ export const useTabStore = defineStore(
ensureHomeTab()
if (tabs.value.length === 0) tabs.value = createDefaultTabs()
if (!tabs.value.some(tab => tab.id === activeTabId.value)) {
activeTabId.value = tabs.value[0]?.id ?? HOME_TAB_ID
activeTabId.value = tabs.value[0]?.id ?? PROJECT_TAB_ID
}
}
@ -61,7 +62,7 @@ export const useTabStore = defineStore(
if (wasActive) {
const fallbackIndex = Math.max(0, Math.min(index - 1, tabs.value.length - 1))
activeTabId.value = tabs.value[fallbackIndex]?.id ?? HOME_TAB_ID
activeTabId.value = tabs.value[fallbackIndex]?.id ?? PROJECT_TAB_ID
return
}
@ -71,7 +72,7 @@ export const useTabStore = defineStore(
const closeAllTabs = () => {
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
activeTabId.value = tabs.value[0]?.id ?? PROJECT_TAB_ID
}
const closeLeftTabs = (targetId: string) => {
@ -91,17 +92,19 @@ export const useTabStore = defineStore(
const closeOtherTabs = (targetId: string) => {
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
activeTabId.value = tabs.value.some(tab => tab.id === targetId) ? targetId : PROJECT_TAB_ID
}
const resetTabs = () => {
tabs.value = createDefaultTabs()
activeTabId.value = HOME_TAB_ID
activeTabId.value = PROJECT_TAB_ID
hasCompletedSetup.value = false
}
return {
tabs,
activeTabId,
hasCompletedSetup,
enterWorkspace,
openTab,
removeTab,

View File

@ -183,6 +183,158 @@ export const reserveList = [
name: '预备费'
}
]
//工作内容词典
export const workList = {
0: { text: '完成投资估算文件及主要技术经济指标,完成与相关单位核对投资估算', serviceid: 6, order: 1, type: 0 },
1: { text: '根据项目建议书或可行性研究报告的变化,动态调整投资估算和技术经济指标,并对比各环节投资估算变化,完成项目建议书阶段到可行性研究阶段的量、价对比及原因分析', serviceid: 6, order: 2, type: 0 },
2: { text: '参加项目建议书或可行性研究的技术论证会议、评审会议和与确定投资估算相关的会议', serviceid: 6, order: 3, type: 0 },
3: { text: '对比类似项目的技术经济指标,分析技术经济指标差异原因,提供造价差异分析报告', serviceid: 6, order: 4, type: 1 },
4: { text: '根据造价差异分析结果,提出调整工程方案的咨询意见供委托人或关联单位决策', serviceid: 6, order: 5, type: 1 },
5: { text: '依据投资估算和项目建设计划,编制项目资金计划,供委托人决策', serviceid: 6, order: 6, type: 1 },
6: { text: '完成设计概算文件及主要技术经济指标,完成与相关单位核对设计概算', serviceid: 7, order: 7, type: 0 },
7: { text: '依据各版初步设计动态调整设计概算及经济指标,并对比各版设计概算结果,完成可行性研究阶段到初步设计阶段的造价对比,提供造价差异分析报告', serviceid: 7, order: 8, type: 0 },
8: { text: '参加初步设计的技术论证会议、评审会议和与设计概算相关的会议', serviceid: 7, order: 9, type: 0 },
9: { text: '对比类似项目的技术经济指标,分析技术经济指标差异原因,提供造价差异分析报告', serviceid: 7, order: 10, type: 1 },
10: { text: '根据造价差异分析结果,提出调整工程设计方案的咨询意见供委托人或关联单位决策', serviceid: 7, order: 11, type: 1 },
11: { text: '依据设计概算和项目建设计划,编制或调整项目资金计划,供委托人决策', serviceid: 7, order: 12, type: 1 },
12: { text: '完成施工图预算文件及主要技术经济指标,完成与相关单位核对施工图预算', serviceid: 8, order: 13, type: 0 },
13: { text: '依据各版施工图动态调整施工图预算,并对比各版施工图预算结果,完成施工图预算与设计概算的造价对比(如有必要应完成与投资估算的造价对比),提供造价差异分析报告', serviceid: 8, order: 14, type: 0 },
14: { text: '参加施工图设计的技术论证会议、评审会议和与施工图预算相关的会议', serviceid: 8, order: 15, type: 0 },
15: { text: '对比类似项目的技术经济指标,分析技术经济指标差异原因,提供造价差异分析报告', serviceid: 8, order: 16, type: 1 },
16: { text: '根据造价差异分析结果,提出调整工程设计方案的咨询意见供委托人或关联单位决策', serviceid: 8, order: 17, type: 1 },
17: { text: '依据施工图预算和项目建设计划,编制或调整项目资金计划,供委托人决策', serviceid: 8, order: 18, type: 1 },
18: { text: '分析专项设计方案的经济合理性,提出咨询意见供委托人决策', serviceid: 8, order: 19, type: 1 },
19: { text: '分析施工组织设计和交通组织方案的经济合理性,提出咨询意见供委托人决策', serviceid: 8, order: 20, type: 1 },
20: { text: '完成招标工程量清单及工程量清单预算,与相关单位核对工程量清单及工程量清单预算,并完成修改与调整', serviceid: 9, order: 21, type: 0 },
21: { text: '对比工程量清单预算和施工图预算,提供造价差异分析报告', serviceid: 9, order: 22, type: 0 },
22: { text: '审核计量与支付条款,对招标文件和合同文件涉及合同价款、工程变更费用咨询、结算费用等与造价相关条款提供咨询意见', serviceid: 9, order: 23, type: 0 },
23: { text: '协助招标人完成招标答疑涉及的造价问题的解答与回复', serviceid: 9, order: 24, type: 0 },
24: { text: '参加招投标阶段与造价确定相关的会议', serviceid: 9, order: 25, type: 0 },
25: { text: '对比中标价与工程量清单预算,并分析差异原因,协助招标人完成不平衡报价调整', serviceid: 9, order: 26, type: 1 },
26: { text: '参与合同谈判,协助招标人合同谈判过程中相关造价分析', serviceid: 9, order: 27, type: 1 },
27: { text: '清理施工承包合同费用、征地拆迁费用、各项价差、其他类费用及费用依据、计算资料等', serviceid: 10, order: 28, type: 0 },
28: { text: '清理征地拆迁费用,包括清理建设项目的用地、青苗补偿、房屋拆迁等费用,清理管线迁改、改路、改河、改沟以及构筑物迁改等费用,土地复垦费用及征拆各项规费的清理等', serviceid: 10, order: 29, type: 0 },
29: { text: '清理材料价差,包括调整甲供材料设备价差、政策性文件规定的其他材料价差等', serviceid: 10, order: 30, type: 0 },
30: { text: '清理政策性调整费用', serviceid: 10, order: 31, type: 0 },
31: { text: '清理基本预备费和招标降造费,包括分析降造费的使用情况', serviceid: 10, order: 32, type: 0 },
32: { text: '对未进行初步验收的剩余工程及投资情况进行清理', serviceid: 10, order: 33, type: 0 },
33: { text: '梳理和清理批复的概算费用,分析费用变化的原因', serviceid: 10, order: 34, type: 0 },
34: { text: '根据收集的资料和费用分析结果,编制清理概算文件,包括清理概算申请报告、相关批复文件、设计图纸、施工组织设计、合同、验工计价、设计变更和新增工程等支撑性资料、审计报告(如有)、第三方审价报告及其他相关依据', serviceid: 10, order: 35, type: 0 },
35: { text: '招标图纸与实际施工图纸进行验核,核实工程数量,针对分析概算费用变化的原因', serviceid: 10, order: 36, type: 1 },
36: { text: '参加与清理概算相关的会议,协助委托人完成清理概算文件预评审工作', serviceid: 10, order: 37, type: 0 },
37: { text: '完成工程变更费用的测算,或提出性价比更优的替代方案、材料等意见', serviceid: 11, order: 38, type: 0 },
38: { text: '完成新增预算单价或合同单价的计算与核定', serviceid: 11, order: 39, type: 0 },
39: { text: '按施工图预算或合同清单方式完成工程变更费用的计算', serviceid: 11, order: 40, type: 0 },
40: { text: '完成过程结算,包括建立过程结算多维度清单体系,如:项目清单、工程量清单、分项清单;划分过程结算清单单元;依据经确认的实际完工工程量完成计价计费,完成过程结算文件', serviceid: 11, order: 41, type: 0 },
41: { text: '完成价格波动费用的计算或审核', serviceid: 11, order: 42, type: 0 },
42: { text: '完成索赔与补偿费用的计算或审核', serviceid: 11, order: 43, type: 0 },
43: { text: '完成结算文件及统计技术经济指标,与相关单位核对合同(工程)结算', serviceid: 11, order: 44, type: 0 },
44: { text: '完成合同、变更和结算三环节费用的对比,分析合同费用各环节变化原因,提供对比报表和分析报告', serviceid: 11, order: 45, type: 0 },
45: { text: '参加与过程结算、工程变更费用确定和合同(工程)结算相关的会议', serviceid: 11, order: 46, type: 0 },
46: { text: '现场勘查与测量实际完工工程量', serviceid: 11, order: 47, type: 1 },
47: { text: '复核竣工图纸数量与结算数量的差异,提交数量差异报告', serviceid: 11, order: 48, type: 1 },
48: { text: '协助委托人对项目工程变更费用审批情况进行清理,编制交工验收造价文件', serviceid: 11, order: 49, type: 1 },
49: { text: '协助委托人处理工期索赔、造价费用等费用的争议与纠纷、仲裁与诉讼', serviceid: 11, order: 50, type: 1 },
50: { text: '编制项目竣工决算文件', serviceid: 13, order: 51, type: 0 },
51: { text: '编制竣工决算备案送审文件', serviceid: 13, order: 52, type: 0 },
52: { text: '参与竣工决算相关的会议', serviceid: 13, order: 53, type: 0 },
53: { text: '根据审计意见和决算备案意见调整竣工决算文件', serviceid: 13, order: 54, type: 0 },
54: { text: '协助委托人编制建设项目造价执行情况报告', serviceid: 13, order: 55, type: 1 },
55: { text: '对比类似项目的技术经济指标,分析技术经济指标差异原因,提供造价差异分析报告', serviceid: 13, order: 56, type: 1 },
56: { text: '根据造价差异分析结果,提出调整工程方案的咨询意见供委托人或关联单位决策', serviceid: 13, order: 57, type: 1 },
57: { text: '协助委托人指导参建单位完成与决算相关辅助文件的编制', serviceid: 13, order: 58, type: 1 },
58: { text: '协助委托人开展项目造价管理及投资效益后评估', serviceid: 13, order: 59, type: 1 },
59: { text: '定期向委托人介绍并解读国家及地方最新发布的关于造价管理有关的法律法规、政策文件和技术标准等信息,针对风险控制方面,提供专业的造价管理建议及实施对策', serviceid: 15, order: 60, type: 2 },
60: { text: '为委托人在日常建设、运营及经营管理中遇到的造价编制与审核问题,提供专业咨询建议', serviceid: 15, order: 61, type: 2 },
61: { text: '对委托人起草的工程勘察设计、工程监理、施工、物资采购和技术服务等方面的合同与协议文本,从造价控制与投资风险角度提供审阅与修改建议', serviceid: 15, order: 62, type: 2 },
62: { text: '审查各类合同履行情况时,对识别出的潜在漏洞或风险,提出相应的修改建议或补救措施', serviceid: 15, order: 63, type: 2 },
63: { text: '对委托人提供的造价管理相关的制度,进行审阅与修改,或提供专业咨询建议', serviceid: 15, order: 64, type: 2 },
64: { text: '针对具体建设工程项目的重大投资决策,出具独立的造价专业意见;或根据委托人的要求, 对造价争议事项提供专业依据并出具顾问报告', serviceid: 15, order: 65, type: 3 },
65: { text: '为委托人起草与工程建设相关的合同、协议等文书,包括且不限于工程勘察设计、监理、施 工、采购及技术服务等领域', serviceid: 15, order: 66, type: 3 },
66: { text: '独立对各类合同的履行情况进行审查,识别潜在漏洞或风险,并提出系统性的修改建议或补 救方案', serviceid: 15, order: 67, type: 3 },
67: { text: '为委托人制订或修订与造价管理相关的制度', serviceid: 15, order: 68, type: 3 },
68: { text: '受托处理涉及委托人的造价纠纷、仲裁及诉讼案件,提供专业意见与支持,并出具咨询顾问 报告', serviceid: 15, order: 69, type: 3 },
69: { text: '组织并完成调研工作,包括制定调研计划、实施调研和撰写调研报告', serviceid: 16, order: 70, type: 0 },
70: { text: '完成各阶段文件的起草、修订、调整与校对工作;整理并分析各阶段的 反馈意见、完成反馈意见的采纳情况,编制必要的采纳情况报告或说明;完成工作报告', serviceid: 16, order: 71, type: 0 },
71: { text: '完成评审汇报材料,编写报告;整理并汇总评审意见、复核评审意见,完成评审 意见的采纳及反馈工作', serviceid: 16, order: 72, type: 0 },
72: { text: '负责评审会议的全过程组织工作, 包括准备会议材料、发出通知、组织召开并主持评审会。', serviceid: 16, order: 73, type: 1 },
73: { text: '组织并完成调研工作,包括制定调研计划、实施调研和撰写调研报告', serviceid: 17, order: 74, type: 0 },
74: { text: '完成工作大纲和编写大纲;开展专项研究工作,完成各阶段研究报告的起 草、修订、调整与校对工作;分研究阶段进行技术报告的编写、修改、调整以及相应的报审 报批工作,包括大纲与成果的评审及验收等;整理并分析各阶段的反馈意见、完成反馈意见 的采纳情况,编制必要的采纳情况报告或说明;编制工作报告', serviceid: 17, order: 75, type: 0 },
75: { text: '确定测试与验证项目,完成测试与验证工作,完成测试与验证报告', serviceid: 17, order: 76, type: 0 },
76: { text: '完成评审与验收所需汇报材料,编写报告;整理并汇总评审意见、复核评审意见,完成评审意见的采纳及反馈工作', serviceid: 17, order: 77, type: 0 },
77: { text: '编写培训与宣贯相关的材料,组织研究成果的宣贯与推广工作', serviceid: 17, order: 78, type: 1 },
78: { text: '负责评审与验收会议的全过程组织工作,包括准备会议材料、发出通知、 组织召开并主持评审与验收会议', serviceid: 17, order: 79, type: 1 },
79: { text: '组织并完成调研工作,包括制定调研计划、实施调研和撰写调研报告', serviceid: 18, order: 80, type: 0 },
80: { text: '制定工作计划并完成编制大纲,编制大纲包括工作大纲和编写大纲', serviceid: 18, order: 81, type: 0 },
81: { text: '完成数据采集与测定工作,编制相应的数据采集与测定报告', serviceid: 18, order: 82, type: 0 },
82: { text: '完成数据整理与分析工作,编制相应的数据分析报告,整理支撑性资料', serviceid: 18, order: 83, type: 0 },
83: { text: '完成定额文本的起草、修改及调整工作;完成各阶段研究报告的起草、修订、 调整与校对工作,编制必要的采纳情况报告或说明;编制工作报告或报批报告', serviceid: 18, order: 84, type: 0 },
84: { text: '确定测试与验证项目,完成测试与验证工作,完成测试与验证报告', serviceid: 18, order: 85, type: 0 },
85: { text: '完成评审与验收所需汇报材料,编写报告;整理并汇总评审意见、复核评 审意见,完成评审意见的采纳及反馈工作', serviceid: 18, order: 86, type: 0 },
86: { text: '编写培训与宣贯相关的材料,组织定额成果的培训与宣贯工作', serviceid: 18, order: 87, type: 1 },
87: { text: '负责评审与验收会议的全过程组织工作,包括准备会议材料、发出通知、 组织召开并主持评审与验收会议', serviceid: 18, order: 88, type: 1 },
88: { text: '开展各类造价信息的搜集、筛选与整理工作', serviceid: 19, order: 89, type: 0 },
89: { text: '对整理后的信息与数据进行对比与分析', serviceid: 19, order: 90, type: 0 },
90: { text: '依据分析结果,对特定价格或费用进行评估与论证,编制咨询报告', serviceid: 19, order: 91, type: 0 },
91: { text: '预测未来特定时期的价格或费用,编制预测报告', serviceid: 19, order: 92, type: 1 },
92: { text: '研究价格或费用的长期变动规律,编制趋势分析报告', serviceid: 19, order: 93, type: 1 },
93: { text: '根据委托人的目标,确定鉴定工作范围', serviceid: 20, order: 94, type: 0 },
94: { text: '收集与复核鉴定资料', serviceid: 20, order: 95, type: 0 },
95: { text: '组织或参与必要的现场勘查工作', serviceid: 20, order: 96, type: 0 },
96: { text: '工程量的计算与工程费用的计价或估价', serviceid: 20, order: 97, type: 0 },
97: { text: '对争议焦点进行鉴定,提出解决争议的意见', serviceid: 20, order: 98, type: 0 },
98: { text: '回复各方当事人的质证意见', serviceid: 20, order: 99, type: 0 },
99: { text: '编制并出具造价鉴定报告', serviceid: 20, order: 100, type: 0 },
100: { text: '组织或参与现场数据复测或特殊鉴定', serviceid: 20, order: 101, type: 1 },
101: { text: '与当事人相关方进行必要的造价数据核对', serviceid: 20, order: 102, type: 1 },
102: { text: '出庭就鉴定报告及相关专业问题接受质证', serviceid: 20, order: 103, type: 1 },
103: { text: '编制影响工程成本的资源要素清单', serviceid: 21, order: 104, type: 0 },
104: { text: '调查人工、材料、设备和施工机械的市场供应情况,主要是价格情况', serviceid: 21, order: 105, type: 0 },
105: { text: '测算、分析、计算工程成本费用,结合其他相关因素,编制并出具工程成本测算报告', serviceid: 21, order: 106, type: 0 },
106: { text: '参加与工程成本测算相关的会议', serviceid: 21, order: 107, type: 0 },
107: { text: '审核及分析资源配置、施工措施、施工管理方案的经济合理性', serviceid: 21, order: 108, type: 1 },
108: { text: '对比工程成本与投资估算、设计概算、施工图预算、工程量清单预算、合同价,并分析费用差异,提交分析报告', serviceid: 21, order: 109, type: 1 },
109: { text: '编制影响工程成本的资源要素清单或成本组成科目', serviceid: 22, order: 110, type: 0 },
110: { text: '收集人工、材料、设备、施工机械、措施、税费等实际发生的费用', serviceid: 22, order: 111, type: 0 },
111: { text: '统计与汇总、拆解与合并、对比与分析工程成本各项费用,编制并出具工程成本核算报告', serviceid: 22, order: 112, type: 0 },
112: { text: '参加与工程成本核算相关的会议', serviceid: 22, order: 113, type: 0 },
113: { text: '分析实际工程成本与测算工程成本差异的合理性,提交分析报告', serviceid: 22, order: 114, type: 1 },
114: { text: '归纳与总结实际工程成本与测算工程成本的差异原因,提出改进建议', serviceid: 22, order: 115, type: 1 },
115: { text: '依据本项目在招标阶段确认的设计图纸工程量,结合工程量清单计量支付规则,对设计工程量进行复核,包括构件工程量、明细表工程量和汇总表工程量,同时与相关方核对工程量', serviceid: 23, order: 116, type: 0 },
116: { text: '依据核对后确认的设计图纸数量,细化与合并招标工程量清单或合同工程量清单,建立各维度清单间的数据链接,与相关单位完成核对与确认', serviceid: 23, order: 117, type: 0 },
117: { text: '依据确定的招标工程量清单或合同工程量清单,拆解与合并相应的清单费用,与相关单位完成核对与确认', serviceid: 23, order: 118, type: 1 },
118: { text: '现场勘查与测量现场实施工程量', serviceid: 23, order: 119, type: 1 },
119: { text: '完成设计图纸所载数量与复核后的数量进行对比与分析,提供对比分析报告', serviceid: 23, order: 120, type: 1 },
120: { text: '参加计算工程量相关的会议', serviceid: 23, order: 121, type: 0 },
121: { text: '协助委托人处理涉及工程数量的争议、纠纷、仲裁或诉讼事务', serviceid: 23, order: 122, type: 1 },
122: { text: '完成工程变更费用的测算', serviceid: 24, order: 123, type: 0 },
123: { text: '完成新增预算单价或合同单价的计算与核定', serviceid: 24, order: 124, type: 0 },
124: { text: '按施工图预算或合同清单方式完成工程变更费用的计算', serviceid: 24, order: 125, type: 0 },
125: { text: '完成价格波动引起的价差费用的计算', serviceid: 24, order: 126, type: 1 },
126: { text: '完成索赔与补偿费用的计算,如加速施工费用、暂停施工补偿等', serviceid: 24, order: 127, type: 1 },
127: { text: '协助委托人处理涉及工程变更费用的争议与纠纷、仲裁与诉讼', serviceid: 24, order: 128, type: 1 },
128: { text: '分析、论证及评估影响投资估算的主要因素', serviceid: 25, order: 129, type: 0 },
129: { text: '根据分析论证结果,完成调整后投资估算文件及主要技术经济指标,与相关单位核对数据', serviceid: 25, order: 130, type: 0 },
130: { text: '完成调整后投资估算与原批复估算的对比及原因分析', serviceid: 25, order: 131, type: 0 },
131: { text: '出席与投资估算调整工作相关的会议', serviceid: 25, order: 132, type: 0 },
132: { text: '协助调规报告编制单位完成报告的造价专业部分的内容', serviceid: 25, order: 133, type: 1 },
133: { text: '分析、论证及评估影响设计概算的主要因素', serviceid: 26, order: 134, type: 0 },
134: { text: '根据分析论证结果,编制调整后设计概算文件及主要技术经济指标,与相关单位核对数据', serviceid: 26, order: 135, type: 0 },
135: { text: '完成调整后概算与原批复概算的对比及原因分析,完成调整概算报告', serviceid: 26, order: 136, type: 0 },
136: { text: '出席与概算调整工作相关的会议', serviceid: 26, order: 137, type: 0 },
137: { text: '协助委托人开展项目预估决算费用的测算、估算工作', serviceid: 26, order: 138, type: 1 },
138: { text: '检查相关单位对工程造价管理法律法规、规章制度以及工程造价依据的执行情况', serviceid: 27, order: 139, type: 0 },
139: { text: '检查各阶段造价文件编制、审查(批)或备案以及对批复意见的落实情况', serviceid: 27, order: 140, type: 0 },
140: { text: '检查造价管理台账和计量支付制度的建立与执行、造价全过程管理与控制情况', serviceid: 27, order: 141, type: 0 },
141: { text: '检查工程变更管理情况', serviceid: 27, order: 142, type: 0 },
142: { text: '检查项目造价信息的收集、分析及报送情况', serviceid: 27, order: 143, type: 0 },
143: { text: '检查项目从业单位的造价人员执业情况', serviceid: 27, order: 144, type: 0 },
144: { text: '协助委托人进行现场核查、资料抽检和台账复核工作', serviceid: 27, order: 145, type: 0 },
145: { text: '协助委托人整理检查结果和起草检查报告等工作', serviceid: 27, order: 146, type: 0 },
146: { text: '作为造价咨询服务总体协调单位,依据造价技术标准的具体条款或委托方的个性化需求,进一步细化各项工作的具体要求,检查其他服务单位的造价文件的组成完整性、电子文件格式是否符合要求、电子版与纸质版是否对应、造价文件报表的规范性', serviceid: -1, order: 147, type: 4 },
147: { text: '作为造价咨询服务总体协调单位,负责总体协调其他咨询人或专家团队的工作,确保各方在项目服务中的沟通顺畅,监控造价咨询服务的进展情况,确保各咨询人按时完成工作', serviceid: -1, order: 148, type: 4 },
}
let costScaleCal = [
{ code: 'C1-1', staLine: 0, endLine: 100, basic: { staPrice: 0, rate: 0.01 }, optional: { staPrice: 0, rate: 0.002 } },
{ code: 'C1-2', staLine: 100, endLine: 300, basic: { staPrice: 10000, rate: 0.008 }, optional: { staPrice: 2000, rate: 0.0016 } },
@ -217,6 +369,8 @@ let areaScaleCal = [
export type WorkType = '基本工作' | '可选工作' | '日常顾问' | '专项顾问' | '附加工作' | '自定义'
export type IndustryType = (typeof industryTypeList)[number]['type']
type DictItem = Record<string, any>
type DictEntry = { id: string; rawId: string; item: DictItem }

View File

@ -1 +1 @@
{"root":["./src/main.ts","./src/sql.ts","./src/components/ui/button/index.ts","./src/components/ui/card/index.ts","./src/components/ui/scroll-area/index.ts","./src/components/ui/tooltip/index.ts","./src/lib/decimal.ts","./src/lib/diyaggridoptions.ts","./src/lib/number.ts","./src/lib/numberformat.ts","./src/lib/pricingmethodtotals.ts","./src/lib/pricingscalefee.ts","./src/lib/utils.ts","./src/lib/xmfactordefaults.ts","./src/lib/zwarchive.ts","./src/lib/zxfwpricingsync.ts","./src/pinia/kv.ts","./src/pinia/tab.ts","./src/pinia/zxfwpricing.ts","./src/pinia/plugin/indexdb.ts","./src/pinia/plugin/types.d.ts","./src/app.vue","./src/components/common/hourlyfeegrid.vue","./src/components/common/htfeegrid.vue","./src/components/common/htfeemethodgrid.vue","./src/components/common/methodunavailablenotice.vue","./src/components/common/xmfactorgrid.vue","./src/components/common/xmcommonaggrid.vue","./src/components/ui/button/button.vue","./src/components/ui/card/card.vue","./src/components/ui/card/cardaction.vue","./src/components/ui/card/cardcontent.vue","./src/components/ui/card/carddescription.vue","./src/components/ui/card/cardfooter.vue","./src/components/ui/card/cardheader.vue","./src/components/ui/card/cardtitle.vue","./src/components/ui/scroll-area/scrollarea.vue","./src/components/ui/scroll-area/scrollbar.vue","./src/components/ui/tooltip/tooltipcontent.vue","./src/components/views/ht.vue","./src/components/views/htadditionalworkfee.vue","./src/components/views/htconsultcategoryfactor.vue","./src/components/views/htfeemethodtypelineview.vue","./src/components/views/htfeeratemethodform.vue","./src/components/views/htmajorfactor.vue","./src/components/views/htreservefee.vue","./src/components/views/servicecheckboxselector.vue","./src/components/views/xmconsultcategoryfactor.vue","./src/components/views/xmmajorfactor.vue","./src/components/views/zxfwview.vue","./src/components/views/htcard.vue","./src/components/views/htinfo.vue","./src/components/views/info.vue","./src/components/views/xmcard.vue","./src/components/views/xminfo.vue","./src/components/views/zxfw.vue","./src/components/views/pricingview/hourlypricingpane.vue","./src/components/views/pricingview/investmentscalepricingpane.vue","./src/components/views/pricingview/landscalepricingpane.vue","./src/components/views/pricingview/workloadpricingpane.vue","./src/layout/tab.vue","./src/layout/typeline.vue"],"version":"5.9.3"}
{"root":["./src/main.ts","./src/sql.ts","./src/components/ui/button/index.ts","./src/components/ui/card/index.ts","./src/components/ui/scroll-area/index.ts","./src/components/ui/tooltip/index.ts","./src/lib/decimal.ts","./src/lib/diyaggridoptions.ts","./src/lib/number.ts","./src/lib/numberformat.ts","./src/lib/pricingmethodtotals.ts","./src/lib/pricingscalefee.ts","./src/lib/projectworkspace.ts","./src/lib/utils.ts","./src/lib/workspace.ts","./src/lib/xmfactordefaults.ts","./src/lib/zwarchive.ts","./src/lib/zxfwpricingsync.ts","./src/pinia/kv.ts","./src/pinia/tab.ts","./src/pinia/zxfwpricing.ts","./src/pinia/plugin/indexdb.ts","./src/pinia/plugin/types.d.ts","./src/app.vue","./src/components/common/hourlyfeegrid.vue","./src/components/common/htfeegrid.vue","./src/components/common/htfeemethodgrid.vue","./src/components/common/methodunavailablenotice.vue","./src/components/common/xmfactorgrid.vue","./src/components/common/xmcommonaggrid.vue","./src/components/ui/button/button.vue","./src/components/ui/card/card.vue","./src/components/ui/card/cardaction.vue","./src/components/ui/card/cardcontent.vue","./src/components/ui/card/carddescription.vue","./src/components/ui/card/cardfooter.vue","./src/components/ui/card/cardheader.vue","./src/components/ui/card/cardtitle.vue","./src/components/ui/scroll-area/scrollarea.vue","./src/components/ui/scroll-area/scrollbar.vue","./src/components/ui/tooltip/tooltipcontent.vue","./src/components/views/homeentryview.vue","./src/components/views/ht.vue","./src/components/views/htadditionalworkfee.vue","./src/components/views/htconsultcategoryfactor.vue","./src/components/views/htfeemethodtypelineview.vue","./src/components/views/htfeeratemethodform.vue","./src/components/views/htmajorfactor.vue","./src/components/views/htreservefee.vue","./src/components/views/projectworkspaceview.vue","./src/components/views/quickcalcview.vue","./src/components/views/servicecheckboxselector.vue","./src/components/views/workcontentgrid.vue","./src/components/views/xmconsultcategoryfactor.vue","./src/components/views/xmmajorfactor.vue","./src/components/views/zxfwview.vue","./src/components/views/htcard.vue","./src/components/views/htinfo.vue","./src/components/views/info.vue","./src/components/views/xmcard.vue","./src/components/views/xminfo.vue","./src/components/views/zxfw.vue","./src/components/views/pricingview/hourlypricingpane.vue","./src/components/views/pricingview/investmentscalepricingpane.vue","./src/components/views/pricingview/landscalepricingpane.vue","./src/components/views/pricingview/workloadpricingpane.vue","./src/layout/tab.vue","./src/layout/typeline.vue"],"version":"5.9.3"}