diff --git a/AGENTS.md b/AGENTS.md index c8a6e3b..db1fbc6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -223,4 +223,5 @@ samples/ - 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)。 - 2026-11-04:项目基本信息修改和分包选择(FR-M2-03、04,`docs/architecture/project-info-plan.md`,PI1–PI7 全部按建议确认)。新增「概览」标签页(项目要点可改、带出处、可恢复 AI 值;采购包;关键日期)和选包页(手工调整、一键「不分包」);清单默认只列通用 + 所投的包;检查记下当时用的截止时间、限价、所投的包、法律体系,报告按它写,之后改了信息查标页提示重新检查。样本里分包识别对的只有 S10(S06 把分项认成 5 个包,S01、S08 单包写了包号),改进分包抽取记进读标专项。 - 2026-11-05:补遗追加(FR-M2-10,`docs/architecture/addendum-plan.md`,AD1–AD6 全部按建议确认)。招标文件解析后可以追加补遗 / 澄清(单独解析、扣 1 次),新条目标「补遗 N」进正式清单、清单回到草稿;同一种日期时间不同的旧条目标「已被补遗 N 更新」,项目字段的新值在概览「采用 / 不采用」,采用截止时间时日期条目一起确认,没确认前查标用旧值、检查预览提示。补遗不改项目名称、编号、采购人等(更正公告标题会被抽成项目名称)。真实模型对虚构更正公告抽得对;还没有真实补遗样本。 +- 2026-11-05:读标、查标全流程走查(浏览器从注册走到导出报告)。修了走查发现的问题:原生下拉换成按规范 §7.21 的 `SelectBox`(Reka UI,和搜索框同高,长选项不再挤压签名);关键日期去重;报告页致命数改读项目的实时值;★ 只是「重要」的文件清单项也挂到 ★ 规则上;检查范围文案过期;报告出处不重复文件名(模板 2026.11-v3)。查标结果页按原型补齐:本次处理的留在原位、J / K / O / 1 / 2 / 3 / X / U 键盘、「提示」批量确认、「判断有误?」反馈、覆盖范围默认收起、导出报告入口;项目头改为「就绪 / 未就绪」,致命数挂在查标标签上。 - 下一步:国动真实投标文件(用户晚些提供,到时先读标、请用户看清单,再查标一次;请用户按公司真实证书录资质库;有更正公告一起要);其余可做:第 6 步前端补齐(键盘操作、批量处理、判断有误反馈、日期选择器的日历弹层、概览页的检查记录和文件卡)、M6 剩下的 P0(额度流水、套餐开通、数据删除、运营后台)。 diff --git a/apps/web/src/components/ChecklistItemRow.vue b/apps/web/src/components/ChecklistItemRow.vue index 5f97a15..4dbfc9a 100644 --- a/apps/web/src/components/ChecklistItemRow.vue +++ b/apps/web/src/components/ChecklistItemRow.vue @@ -106,6 +106,12 @@ const facts = computed(() => specFacts(props.item.spec)); margin: var(--sp-1) 0 0; font-size: var(--fs-caption); } +.evid .muted { + flex: none; /* 标签不跟着长文本一起被挤成竖排 */ +} +.t { + overflow-wrap: anywhere; /* 型号、参数这类没有空格的长串 */ +} .cites .cap { margin: var(--sp-1) 0 0; } diff --git a/apps/web/src/components/FieldDialog.vue b/apps/web/src/components/FieldDialog.vue index 32ed190..3180700 100644 --- a/apps/web/src/components/FieldDialog.vue +++ b/apps/web/src/components/FieldDialog.vue @@ -7,6 +7,7 @@ import { unwrap, type Schemas } from "@bid-doctor/api-client"; import { X } from "@lucide/vue"; import { DialogClose, DialogContent, DialogOverlay, DialogPortal, DialogRoot, DialogTitle } from "reka-ui"; import { computed, ref, watch } from "vue"; +import SelectBox from "@/components/SelectBox.vue"; import { api, errorTitle, isApiError } from "@/lib/api"; import { BOOL_FIELDS, FIELD_LABEL, fieldText, LEGAL_REGIME, MONEY_FIELDS, parseDay } from "@/lib/labels"; import type { ProjectField } from "@/lib/queries"; @@ -104,9 +105,12 @@ async function save(restore = false) {
- + 决定查标用「投标无效」还是「否决投标」的口径
diff --git a/apps/web/src/components/ItemDialog.vue b/apps/web/src/components/ItemDialog.vue index fdd0860..e681d60 100644 --- a/apps/web/src/components/ItemDialog.vue +++ b/apps/web/src/components/ItemDialog.vue @@ -9,6 +9,7 @@ import { X } from "@lucide/vue"; import { DialogClose, DialogContent, DialogOverlay, DialogPortal, DialogRoot, DialogTitle } from "reka-ui"; import { computed, ref, watch } from "vue"; +import SelectBox from "@/components/SelectBox.vue"; import { errorTitle, isApiError } from "@/lib/api"; import type { ItemFields } from "@/lib/checklistOps"; import { category, CATEGORY_ORDER, IMPORTANCE_MARK, IMPORTANCE_NAME } from "@/lib/labels"; @@ -135,10 +136,11 @@ function submit() {
- +
diff --git a/apps/web/src/components/ProjectHead.vue b/apps/web/src/components/ProjectHead.vue index 51b6407..1da7f0e 100644 --- a/apps/web/src/components/ProjectHead.vue +++ b/apps/web/src/components/ProjectHead.vue @@ -89,8 +89,8 @@ const checkTo = computed(() =>
就绪
- 致命问题已处理 - 致命{{ p.readiness.open_fatal_count }} + 就绪 + 未就绪
@@ -98,7 +98,7 @@ const checkTo = computed(() => diff --git a/apps/web/src/components/QualificationDialog.vue b/apps/web/src/components/QualificationDialog.vue index afe3c93..be7a5d4 100644 --- a/apps/web/src/components/QualificationDialog.vue +++ b/apps/web/src/components/QualificationDialog.vue @@ -9,6 +9,7 @@ import { unwrap, uploadFile } from "@bid-doctor/api-client"; import { X } from "@lucide/vue"; import { DialogClose, DialogContent, DialogOverlay, DialogPortal, DialogRoot, DialogTitle } from "reka-ui"; import { computed, ref, watch } from "vue"; +import SelectBox from "@/components/SelectBox.vue"; import { api, apiOptions, errorTitle } from "@/lib/api"; import { extOf, fmtMonthDay, fmtSize, parseDay, QUALIFICATION_KIND, QUALIFICATION_KIND_ORDER } from "@/lib/labels"; import type { Qualification } from "@/lib/queries"; @@ -25,6 +26,7 @@ const open = defineModel("open", { required: true }); const emit = defineEmits<{ saved: [q: Qualification, how: "created" | "updated" | "files"] }>(); type Kind = Qualification["kind"]; +const KIND_OPTIONS = QUALIFICATION_KIND_ORDER.map((k) => ({ value: k, label: QUALIFICATION_KIND[k] })); const name = ref(""); const kind = ref("certificate"); const level = ref(""); @@ -203,9 +205,12 @@ async function removeFile(fileId: string) {
- +
diff --git a/apps/web/src/components/SelectBox.vue b/apps/web/src/components/SelectBox.vue new file mode 100644 index 0000000..3d68584 --- /dev/null +++ b/apps/web/src/components/SelectBox.vue @@ -0,0 +1,51 @@ + + + diff --git a/apps/web/src/pages/BidCheckPage.vue b/apps/web/src/pages/BidCheckPage.vue index 874df2f..026b2d3 100644 --- a/apps/web/src/pages/BidCheckPage.vue +++ b/apps/web/src/pages/BidCheckPage.vue @@ -6,11 +6,12 @@ * - 扣点说明写在按钮旁边(红线 9),来自 check-preview;发起时带 expected_charge,和服务端算的不一致时不扣点(API4)。 */ import { ApiError, unwrap, uploadFile, type Schemas } from "@bid-doctor/api-client"; -import { Info, Upload } from "@lucide/vue"; +import { Info, Plus, Upload } from "@lucide/vue"; import { useQueryClient } from "@tanstack/vue-query"; import { computed, ref } from "vue"; import { RouterLink, useRouter } from "vue-router"; import ProjectHead from "@/components/ProjectHead.vue"; +import SelectBox from "@/components/SelectBox.vue"; import { api, apiOptions, errorTitle, isApiError } from "@/lib/api"; import { useCrumbs } from "@/lib/crumbs"; import { qk } from "@/lib/keys"; @@ -33,7 +34,16 @@ useCrumbs(() => [{ label: "工作台", to: { name: "workbench" } }, { label: p.v const vols = computed(() => pv.value?.bid_documents ?? []); /* ---------- 上传 ---------- */ +const KIND_OPTIONS = VOLUME_KINDS.map((k) => ({ value: k, label: VOLUME_KIND[k] })); const kind = ref("whole"); +/** 已经有分册时,新增分册的表单收起来,点「添加分册」再展开;默认选还没用过的类型 */ +const adding = ref(false); +const showNew = computed(() => !vols.value.length || adding.value || !!uploading.value); +function startAdd() { + const used = new Set(vols.value.map((v) => v.volume_kind)); + kind.value = VOLUME_KINDS.find((k) => k !== "whole" && !used.has(k)) ?? "other"; + adding.value = true; +} const volName = ref(""); const replacing = ref(null); const uploading = ref<{ name: string; ratio: number } | null>(null); @@ -58,6 +68,7 @@ async function upload(file: File) { ); volName.value = ""; replacing.value = null; + adding.value = false; await qc.invalidateQueries({ queryKey: qk.checkPreview(props.projectId) }); void qc.invalidateQueries({ queryKey: qk.projects }); } catch (e) { @@ -155,12 +166,19 @@ async function start() {
+ + +
@@ -194,16 +213,17 @@ async function start() {

这一版还不查

@@ -246,9 +266,12 @@ async function start() { display: flex; flex-direction: column; gap: var(--sp-4); - padding-top: var(--sp-5); + padding-top: var(--sp-2); padding-bottom: var(--sp-6); } +.add-vol { + align-self: flex-start; +} .file-row .name { display: flex; align-items: center; @@ -278,6 +301,7 @@ async function start() { } .scope legend { grid-column: 1 / -1; + padding: 0; font-weight: var(--fw-bold); margin-bottom: var(--sp-2); } diff --git a/apps/web/src/pages/ChecklistPage.vue b/apps/web/src/pages/ChecklistPage.vue index 413e79f..0843fd6 100644 --- a/apps/web/src/pages/ChecklistPage.vue +++ b/apps/web/src/pages/ChecklistPage.vue @@ -19,6 +19,7 @@ import HistoryDialog from "@/components/HistoryDialog.vue"; import ItemDialog from "@/components/ItemDialog.vue"; import PdfViewer, { type ViewerTarget } from "@/components/PdfViewer.vue"; import ProjectHead from "@/components/ProjectHead.vue"; +import SelectBox from "@/components/SelectBox.vue"; import { api, errorTitle } from "@/lib/api"; import { useChecklistOps, useDeletedItems, type ItemFields } from "@/lib/checklistOps"; import { groupCitations, type CitationGroup } from "@/lib/citations"; @@ -152,6 +153,10 @@ const openGroup = (g: CitationGroup) => openCitation(g.head, g.tail); /* ---------- 响应方式(C12):招标文件要求逐条响应,还是只列偏离、没列出的视为接受;查标按它判没找到响应算不算问题 ---------- */ const qc = useQueryClient(); const MODE_CATEGORY: Record = { technical: "技术要求", commercial: "商务要求" }; +const MODE_OPTIONS = [ + { value: "explicit", label: "逐条响应" }, + { value: "implicit", label: "只列偏离,没列出的视为接受" }, +]; const MODE_STATUS: Record = { unknown: "招标文件没写明,按逐条响应处理", conflict: "招标文件两处写法不一致,按逐条响应处理" }; const modeBusy = ref(""); async function setMode(category: "technical" | "commercial", mode: "explicit" | "implicit") { @@ -357,17 +362,15 @@ onBeforeUnmount(() => window.removeEventListener("keydown", onKey)); 响应方式
- - + @update:model-value="(v) => v !== m.mode && setMode(m.category, v as 'explicit' | 'implicit')" + /> + {{ m.set_by?.name }}改过 {{ MODE_STATUS[m.status] }}
@@ -413,10 +416,15 @@ onBeforeUnmount(() => window.removeEventListener("keydown", onKey)); - + @@ -539,8 +547,10 @@ onBeforeUnmount(() => window.removeEventListener("keydown", onKey));