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) {
-
+ (text = v ?? '')"
+ />
决定查标用「投标无效」还是「否决投标」的口径
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) {
-
+ (kind = v as Kind)"
+ />
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 @@
+
+
+
+ emit('update:modelValue', dec(String(v)))">
+
+ {{ current?.label ?? "" }}
+
+
+
+
+
+
+
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() {
+
+
+
-
+ (kind = v as Schemas['VolumeKind'])"
+ />
@@ -185,6 +203,7 @@ async function start() {
{{ uploading ? `正在上传 ${uploading.name}(${Math.round(uploading.ratio * 100)}%)` : "拖入或点击上传分册" }}
Word / PDF;电子投标客户端加密后的文件读不了,请上传加密前的稿子
+
!{{ upErr }}
这一版还不查
- - 分项 / 单价上限、分项和采购需求逐项对照、格式文件固定文字被改、声明函、资质有效期:还没有上线
+ - 分项报价表和采购需求清单逐项对照、格式文件固定文字被改、授权代表和签字人一致、声明函填报、签章有无、低价说明、跨页的响应表、证书主体和投标人是否一致:还没有上线
- 签章真伪(以后也只看有无,不鉴定真伪)
- - 份数、密封、CA 加密、递交方式(文件里看不出来)
+ - 份数、密封、CA 加密、递交方式:文件里看不出来,列入「需人工核对」,递交前自己核对
- 报价是否合理、该报多少,评分高低
@@ -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));
-
+ (source = v ?? '')"
+ />
@@ -539,8 +547,10 @@ onBeforeUnmount(() => window.removeEventListener("keydown", onKey));