From ed6a2a907d68b0f48bb1a8af5d55b79317452b40 Mon Sep 17 00:00:00 2001 From: YANG JIANKUAN Date: Wed, 4 Nov 2026 23:46:07 +0800 Subject: [PATCH] =?UTF-8?q?feat(project):=20=E9=A1=B9=E7=9B=AE=E5=9F=BA?= =?UTF-8?q?=E6=9C=AC=E4=BF=A1=E6=81=AF=E4=BF=AE=E6=94=B9=E5=92=8C=E5=88=86?= =?UTF-8?q?=E5=8C=85=E9=80=89=E6=8B=A9=EF=BC=88FR-M2-03=E3=80=8104?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 字段修改:人工值优先、撤销恢复 AI 值、按字段校验(金额可写「286万」,按元存)、 冗余列同步、审计留痕、只读项目 409;关键日期显示改后的投标截止时间 - 分包:增删改、选包(至少一个)、一键「不分包」;删包时条目归为通用,删到一个不剩回到不分包 - 清单列表 lot_scope 默认只列通用 + 所投的包,响应带 hidden_count - 检查记下当时用的项目依据(截止时间、限价、所投的包、法律体系):报告按它写, 之后改了信息检查结果接口给 basis_changed,查标页提示重新检查 - 前端:概览标签页(项目要点、采购包、关键日期)、字段修改对话框、选包页和手工调整; 解析完识别到多个包时进选包页;读标页清单范围和所属采购包;查标页写明查哪几个包 - 决策 PI1–PI7;方案和落地记录 docs/architecture/project-info-plan.md Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 3 +- apps/web/src/components/FieldDialog.vue | 166 +++++++++ apps/web/src/components/ItemDialog.vue | 15 +- apps/web/src/components/ProjectHead.vue | 5 +- apps/web/src/lib/keys.ts | 1 + apps/web/src/lib/labels.ts | 53 +++ apps/web/src/lib/queries.ts | 23 +- apps/web/src/pages/BidCheckPage.vue | 4 + apps/web/src/pages/ChecklistPage.vue | 31 +- apps/web/src/pages/LotsPage.vue | 369 +++++++++++++++++++++ apps/web/src/pages/OverviewPage.vue | 267 +++++++++++++++ apps/web/src/pages/ParsePage.vue | 4 +- apps/web/src/pages/ResultsPage.vue | 6 + apps/web/src/router.ts | 12 + docs/architecture/api-design.md | 4 +- docs/architecture/openapi.yaml | 56 +++- docs/architecture/project-info-plan.md | 128 +++++++ docs/decisions.md | 16 + packages/api-client/src/schema.d.ts | 80 ++++- server/src/bid_doctor/check/runner.py | 3 +- server/src/bid_doctor/check/service.py | 8 + server/src/bid_doctor/checklist/api.py | 9 +- server/src/bid_doctor/checklist/service.py | 1 + server/src/bid_doctor/project/api.py | 94 +++++- server/src/bid_doctor/project/edit.py | 293 ++++++++++++++++ server/src/bid_doctor/project/service.py | 43 ++- server/src/bid_doctor/project/workbench.py | 15 +- server/src/bid_doctor/report/service.py | 8 +- server/tests/test_project_info.py | 160 +++++++++ 29 files changed, 1834 insertions(+), 43 deletions(-) create mode 100644 apps/web/src/components/FieldDialog.vue create mode 100644 apps/web/src/pages/LotsPage.vue create mode 100644 apps/web/src/pages/OverviewPage.vue create mode 100644 docs/architecture/project-info-plan.md create mode 100644 server/src/bid_doctor/project/edit.py create mode 100644 server/tests/test_project_info.py diff --git a/AGENTS.md b/AGENTS.md index a52a34f..d720fa6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -221,4 +221,5 @@ samples/ - 2026-11-04:按用户决定(E2)先修留出集暴露的 5 处(响应方式识别、偏离栏按「是不是明确满足」判断、另报价 / 附加条件按结构判断、空白栏、异体字),规则集 2026.11-r3。开发集不退化(召回 75%、规矩投标文件问题数不变);留出集参考数字召回 54% → 69%、规矩投标文件问题 1134 → 82(已看过,正式成绩仍以冻结代码那次为准)。见 `docs/architecture/check-eval.md` §12。 - 2026-11-04:查标第 5 步检查报告:导出 → 后台用 Gotenberg 生成 A4 PDF(首页覆盖范围和免责声明、问题明细、需人工核对附录,页眉 logo、页脚免责一句和页码)→ 加密归档,可重复下载;前端加「报告」标签页(选项、导出、内嵌预览、历次下载)。补了项目状态流转(递交 / 撤销递交 / 归档 / 撤销归档,致命未处理也能递交,递交后只读,都留痕)。产品可以从上传招标文件一路走到导出报告、标记递交。见 `docs/architecture/m3-plan.md` 进度。 - 2026-11-04:查标第 4 步资质库和资质匹配(`docs/architecture/qualification-plan.md`,QL1–QL7 全部按建议确认)。资质库页可增删改、挂证书扫描件(企业级密钥加密),到期按进行中项目的投标截止日提醒;查标按证书类别词表(16 类,产品证书、人员证书不进匹配)核对有效期、等级、有没有覆盖,投标文字层按清单项定级,只有资质库记录的降一级、出处显示「依据:资质库记录」,规则集 2026.11-r4。开发集不退化;派生的资质情况表上 D12 3 / 3、D18 2 / 2、规矩的资质表 0 误报,但表和词表是对着同一批样本写的,泛化要等国动真实文件(`check-eval.md` §13)。 -- 下一步:国动真实投标文件(用户晚些提供,到时先读标、请用户看清单,再查标一次;请用户按公司真实证书录资质库);其余可做:第 6 步前端补齐(键盘操作、批量处理、判断有误反馈、日期选择器的日历弹层)、M6 剩下的 P0(额度流水、套餐开通、数据删除、运营后台)。 +- 2026-11-04:项目基本信息修改和分包选择(FR-M2-03、04,`docs/architecture/project-info-plan.md`,PI1–PI7 全部按建议确认)。新增「概览」标签页(项目要点可改、带出处、可恢复 AI 值;采购包;关键日期)和选包页(手工调整、一键「不分包」);清单默认只列通用 + 所投的包;检查记下当时用的截止时间、限价、所投的包、法律体系,报告按它写,之后改了信息查标页提示重新检查。样本里分包识别对的只有 S10(S06 把分项认成 5 个包,S01、S08 单包写了包号),改进分包抽取记进读标专项。 +- 下一步:国动真实投标文件(用户晚些提供,到时先读标、请用户看清单,再查标一次;请用户按公司真实证书录资质库);其余可做:补遗追加(FR-M2-10,含补遗带来的新值确认)、第 6 步前端补齐(键盘操作、批量处理、判断有误反馈、日期选择器的日历弹层、概览页的检查记录和文件卡)、M6 剩下的 P0(额度流水、套餐开通、数据删除、运营后台)。 diff --git a/apps/web/src/components/FieldDialog.vue b/apps/web/src/components/FieldDialog.vue new file mode 100644 index 0000000..32ed190 --- /dev/null +++ b/apps/web/src/components/FieldDialog.vue @@ -0,0 +1,166 @@ + + + + + diff --git a/apps/web/src/components/ItemDialog.vue b/apps/web/src/components/ItemDialog.vue index 226bc48..fdd0860 100644 --- a/apps/web/src/components/ItemDialog.vue +++ b/apps/web/src/components/ItemDialog.vue @@ -12,13 +12,14 @@ import { computed, ref, watch } from "vue"; import { errorTitle, isApiError } from "@/lib/api"; import type { ItemFields } from "@/lib/checklistOps"; import { category, CATEGORY_ORDER, IMPORTANCE_MARK, IMPORTANCE_NAME } from "@/lib/labels"; -import type { ChecklistItem } from "@/lib/queries"; +import type { ChecklistItem, Lot } from "@/lib/queries"; type Mode = "edit" | "accept" | "create"; type Cat = ChecklistItem["categories"][number]; type Imp = ChecklistItem["importance"]; -const props = defineProps<{ mode: Mode; item?: ChecklistItem | null; defaultCategory?: Cat }>(); +/** lots:项目的分包(不含默认包);有 2 个及以上时可以改条目归属哪个包(FR-M2-04) */ +const props = defineProps<{ mode: Mode; item?: ChecklistItem | null; defaultCategory?: Cat; lots?: Lot[] }>(); const open = defineModel("open", { required: true }); const emit = defineEmits<{ /** 提交:changes 只含改过的字段;ack = 人工确认、无出处 */ @@ -32,6 +33,7 @@ const cats = ref([]); const imp = ref("normal"); const evidence = ref(""); const reqText = ref(""); +const lotId = ref(null); const ack = ref(false); const busy = ref(false); const err = ref(""); @@ -46,6 +48,7 @@ watch(open, (v) => { imp.value = i?.importance ?? "normal"; evidence.value = i?.evidence_required ?? ""; reqText.value = i?.requirement_text ?? ""; + lotId.value = i?.lot_id ?? null; ack.value = false; err.value = titleErr.value = catErr.value = ""; }); @@ -75,6 +78,7 @@ function changes(): ItemFields { if (!i || imp.value !== i.importance) out.importance = imp.value; const ev = evidence.value.trim() || null; if (ev !== (i?.evidence_required ?? null)) out.evidence_required = ev; + if ((props.lots?.length ?? 0) > 1 && lotId.value !== (i?.lot_id ?? null)) out.lot_id = lotId.value; const rt = reqText.value.trim() || null; if (reqEditable.value && rt !== (i?.requirement_text ?? null)) out.requirement_text = rt; return out; @@ -129,6 +133,13 @@ function submit() { ★ 条款不满足就会被判无效,已同时归在「无效条款」 +
+ + +
diff --git a/apps/web/src/components/ProjectHead.vue b/apps/web/src/components/ProjectHead.vue index 38ddf10..51b6407 100644 --- a/apps/web/src/components/ProjectHead.vue +++ b/apps/web/src/components/ProjectHead.vue @@ -1,6 +1,6 @@ + + + + diff --git a/apps/web/src/pages/OverviewPage.vue b/apps/web/src/pages/OverviewPage.vue new file mode 100644 index 0000000..9e489a9 --- /dev/null +++ b/apps/web/src/pages/OverviewPage.vue @@ -0,0 +1,267 @@ + + + + + diff --git a/apps/web/src/pages/ParsePage.vue b/apps/web/src/pages/ParsePage.vue index 0798845..f43768c 100644 --- a/apps/web/src/pages/ParsePage.vue +++ b/apps/web/src/pages/ParsePage.vue @@ -190,7 +190,9 @@ const result = computed(() => j.value?.result ?? null); 回工作台,完成后通知我